Tricky aggregation Oracle 11G - sql

I am using Oracle 11G.
Here is my data in table ClassGrades:
ID Name APlusCount TotalStudents PctAplus
0 All 44 95 46.31
1 Grade1A 13 24 54.16
2 Grade1B 11 25 44.00
3 Grade1C 8 23 34.78
4 Grade1D 12 23 52.17
The data (APlusCount,TotalStudents) for ID 0 is the sum of data for all classes.
I want to calculate how each class compares to other classes except itself.
Example:
Take Grade1A that has PctAplus = 54.16.
I want to add all values for Grade1B,Grade1C and Grade1D which is;
((Sum of APlusCount for Grade 1B,1C,1D)/(Sum of TotalStudents for Grade 1B,1C,1D))*100
=(31/71)*100=> 43.66%
So Grade1A (54.16%) is doing much better when compared to its peers (43.66%)
I want to calculate Peers collective percentage for each Grade.
How do I do this?

Another approach might be to leverage the All record for totals (self cross join as mentioned in the comments), i.e.,
WITH g1 AS (
SELECT apluscount, totalstudents
FROM grades
WHERE name = 'All'
)
SELECT g.name, 100*(g1.apluscount - g.apluscount)/(g1.totalstudents - g.totalstudents)
FROM grades g, g1
WHERE g.name != 'All';
However I think that #Wernfried's solution is better as it doesn't depend on the existence of an All record.
UPDATE
Alternately, one could use an aggregate along with a GROUP BY in the WITH statement:
WITH g1 AS (
SELECT SUM(apluscount) AS apluscount, SUM(totalstudents) AS totalstudents
FROM grades
WHERE name != 'All'
)
SELECT g.name, 100*(g1.apluscount - g.apluscount)/(g1.totalstudents - g.totalstudents)
FROM grades g, g1
WHERE g.name != 'All';
Hope this helps. Again, the solution using window functions is probably the best, however.

I don't know how to deal with "All" record but for the others this is an approach:
select Name,
100*(sum(APlusCount) over () - APlusCount) /
(sum(TotalStudents) over () - TotalStudents) as result
from grades
where name <> 'All';
NAME RESULT
=================================
Grade1A 43.661971830986
Grade1B 47.142857142857
Grade1C 50
Grade1D 44.444444444444
See example in SQL Fiddle

Related

Two distinct aggregate queries combined: possible? (EF Core 5, SQL Server)

I've got a table like this:
id (PK)
name
course
date
grade
1
Jane
french
2016-11-20
20
3
CARL
french
2015-09-02
30
4
Anna
french
2016-11-20
25
5
JON
french
2016-09-02
56
6
Linda
english
2016-09-02
22
7
TIM
english
2016-11-20
23
8
JON
english
2016-11-20
44
and I'm wondering if it's possible to answer the following two questions with a single query:
What is the average grade when grouping by course, across all enrolled students in all dates per the course? (this seems more straightforward, as roughly, group on several columns and then take the average of the 'grade' column).
What is the average number of students that enroll in each course by date? Phrased differently, "in 'English,' since two students were enrolled on 11-20, and one student was enrolled on 09-02, the average number of enrolled students per class date is 1.5.
Is it possible to answer both of these questions with a single query? Or, is it such that because I'd be "grouping by" different columns, I can't achieve this in a single query?
I'm using EF Core 5.0, backed by a SQL Server database, so it would be slick if I could achieve this using the LINQ groupby operator, but if not, I don't mind writing straight SQL if it's more efficient.
It is going to be somewhat more difficult to combine these, and it's pretty much impossible to do in EF/Linq. I would say it's only worth it if querying the table twice is massively inefficient. Otherwise just do them in either EF or as a single T-SQL batch of two queries.
The first query could easily be answered in T-SQL
SELECT
c.Course,
AVG(c.Grade * 1.0) AvgGrade
FROM Course c
GROUP BY
c.Course;
And in Linq
from c in Course
group by c.Course into g
select new {
Course: g.Key,
AvgGrade: g.Avg(c2 => c2.Grade * 1.0)
}
Likewise, the other query
SELECT
c.Course,
AVG(Count * 1.0) AvgCount
FROM (
SELECT
c.Course,
COUNT(*) Count
FROM Course c
GROUP BY
c.Course,
c.Date
) c
GROUP BY
c.Course;
And in Linq
from c in Course
group by new {c.Course, c.Date} into g
select new {
Course: g.Key.Course,
Count: g.Count()
} into c2
group by c2.Course into g2
select new {
Course: g2.Key,
AvgCount: g2.Average(c3 => c3.Count * 1.0)
}
To combine this, you can use the following:
This is made more complicated by the fact that there is already aggregation, and you can't (mathematically) do averages over averages. So you need to use SUM / COUNT and nest the grouping.
SELECT
c.Course,
AVG(Count * 1.0) AvgCount,
SUM(c.TotalGrades) * 1.0 / SUM(Count) AvgGrade
FROM (
SELECT
c.Course,
COUNT(*) Count,
SUM(c.Grade) TotalGrades
FROM Course c
GROUP BY
c.Course,
c.Date
) c
GROUP BY
c.Course;
And in Linq
from c in Course
group by new {c.Course, c.Date} into g
select new {
Course: g.Key.Course,
Count: g.Count(),
TotalGrades: g.Sum(c3 => c3.Grade)
} into c2
group by c2.Course into g2
select new {
Course: g2.Key,
AvgCount: g2.Average(c3 => c3.Count * 1.0).
AvgGrade: g2.Sum(c3 => c3.TotalGrades) * 1.0 / g2.Sum(c3 => c3.Count)
}
db<>fiddle

Oracle SQL Count function

I am hoping someone can advise on the below please?
I have some code (below), it is pulling the data I need with no issues. I have been trying (in vain) to add a COUNT function in here somewhere. The output I am looking for would be a count of how many orders are assigned to each agent. I tried a few diffent things based on other questions but can't seem to get it correct. I think I am placing the COUNT 'Agent' statement and the GROUP BY in the wrong place. Please can someone advise? (I am using Oracle SQL Developer).
select
n.ordernum as "Order",
h.employee as "Name"
from ordermgmt n, orderheader h
where h.ordernum = n.ordernum
and h.employee_group IN ('ORDER.MGMT')
and h.employee is NOT NULL
and n.percentcomplete = '0'
and h.order_status !='CLOSED'
Output I am looking for would be, for example:
Name Orders Assigned
Bob 3
Peter 6
John 2
Thank you in advance
Name
Total
49
49
49
49
49
John
4
John
4
John
4
John
4
Peter
2
Peter
2
Bob
3
Bob
3
Bob
3
for example. so there are 49 blank rows summed up as 49 in the Total column. I did not add the full 49 blank columns to save space
Would be easier with sample data and expected output, but maybe you are looking for something like this
select
n.ordernum as "Order",
h.employee as "Name",
count(*) over (partition by h.employee) as OrdersAssigned
from ordermgmt n, orderheader h
where h.ordernum = n.ordernum
and h.employee_group IN ('ORDER.MGMT')
and h.employee is NOT NULL
and n.percentcomplete = '0'
and h.order_status !='CLOSED'
The use of COUNT (as other aggregate functions) is simple.
If you want to add an aggregate function, please group all scalar fields in the GROUP BY clause.
So, in the SELECT you can manage field1, field2, count(1) and so on but you must add in group by (after where conditions) field1, field2
Try this:
select
h.employee as "Name",
count(1) as "total"
from ordermgmt n, orderheader h
where h.ordernum = n.ordernum
and h.employee_group IN ('ORDER.MGMT')
and h.employee is NOT NULL
and n.percentcomplete = '0'
and h.order_status !='CLOSED'
GROUP BY h.employee

Select row with shortest string in one column if there are duplicates in another column?

Let's say I have a database with rows like this
ID PNR NAME
1 35 Television
2 35 Television, flat screen
3 35 Television, CRT
4 87 Hat
5 99 Cup
6 99 Cup, small
I want to select each individual type of item (television, hat, cup) - but for the ones that have multiple entries in PNR I only want to select the one with the shortest NAME. So the result set would be
ID PNR NAME
1 35 Television
4 87 Hat
5 99 Cup
How would I construct such a query using SQLite? Is it even possible, or do I need to do this filtering in the application code?
Since SQLite 3.7.11, you can use MIN() or MAX() to select a row in a group:
SELECT ID,
PNR,
Name,
min(length(Name))
FROM MyTable
GROUP BY PNR;
You can use MIN(length(name))-aggregate function to find out the minimum length of several names; the slightly tricky thing is to get corresponding ID and NAME into the result. The following query should work:
select mt1.ID, mt1.PNR, mt1.Name
from MyTable mt1 inner join (
select pnr, min(length(Name)) as minlength
from MyTable group by pnr) mt2
on mt1.pnr = mt2.pnr and length(mt1.Name) = mt2.minlength

SQL query that returns a subset of table plus column containing count for whole table

I have one table named PEOPLE (Sex, Age, Weight) listed here:
Sex Age Weight
M 10 81
F 21 146
M 32 179
F 40 129
F 58 133
I would like to have the following data returned (Sex, Age, Weight, Count(*) as SexCount from PEOPLE where Age < 35):
Sex Age Weight SexCount
M 10 81 2
F 21 146 3
M 32 179 2
I have found answers that work if I want to return all the people in the table (count without group).
But I have not found an answer if I want SexCount to include the total count from the whole table...and not just the total count from my returned subset. In other words, I want my returned data to just include people who are less than 35 years old, but I want the SexCount to include the count for all people in the table regardless of age.
Anyone know a query that will return the data I want from the table example above? I am using Oracle if it makes a difference.
I tried using this SQL phrase in my query:
COUNT(*) OVER(PARTITION BY Sex) as SexCount
But it only counted the number that were in my query results, and not in the whole table as I require (and explained above). Thanks.
You're looking for a single column on your output that is built using different criteria than the rest of the group. No matter your SQL system, you will need to invoke a second record-set.
Thankfully, since you're not really looking for an aggregate query, this can be done with a single subquery on your FROM list.
SELECT P.Sex, P.Age, P.Weight, T.SexCount
FROM PEOPLE as P
INNER JOIN (SELECT Sex, COUNT(*) As SexCount FROM PEOPLE GROUP BY Sex) AS T
ON P.Sex = T.Sex
WHERE P.Age < 35;

Calculating Grades in SQL

Very simply, I need to find student grades using SQL.
If, for example, I have following table that define grades
Marks (int)
Grade (Char)
and the data like this:
Marks | Grade
__90 | A+
__80 | A
__70 | A-
__60 | B
__50 | C
__40 | D
Okay, having said that, if I have a student that gained marks 73, how do I calculate her grade using above gradings in SQL.
Thank you so much...
You want the highest value below or equal to your value, substitue 73 for your value...
select top 1 Grade from TableName where Mark <= 73 order by Mark desc
Assuming your GradeCutoff table is created with something like:
CREATE TABLE GradeCutoff
( mark int
, grade char(3)
)
and you want to check #studentMark
SELECT grade
FROM GradeCutoff
WHERE mark =
( SELECT max(mark)
FROM gradeCutoff
WHERE #studentMark >= mark
)
;
Note: you may also have to add a (0, 'E') row in your cutoff table.
I think you should define a UDF for this which takes student markes as parameter and returns the grade according to the table given.
Then you can get grade for any student from student table as -
select studentID, getGrade(studentMarks) from student
Here getGrade(studentMarks) is UDF and studentMarks is column in student table with marks (for eg: in your case it is 73)
HINT: You need to use CASE construct in the UDF to get the grade.
SELECT Grade FROM 'table name here' WHERE student_mark <= 79 AND student_mark >= 70 - in order to be more specific I would need to see the actual layout of the tables. But, something to that affect would work.
If the marks are actually regular multiples of 10, you could look into SQLs MOD function
Since I didn't find any way to do it using MySQL, I had to do some PHP programming to achieve the result. The goal is to get the closest value from gradings.
Okay suppose, we have the grades as defined in my question.
$MarksObt = 73 <- Marks obtained by the student:
Step 1: Get grades from mysql ordered by Marks in ASC order (ASC order is important):
SELECT marks, grade FROM gradings ORDER BY marks
Step 2: Create an array variable "$MinGrades". Loop through the result of above query. Suppose MySQL results are stored in "$Gradings" array. Now on each iteration, do the following:
Subtract the $Grading['marks'] From $MarksObt
If result is greater than or equal to 0, add the result to "$MinGrades" array
Step 3: When loop ends, the "$MinGrades" array's first element will be the closest value ... DONE
Below is the PHP code that implements the above:
$MinGrades = array();
foreach($Gradings as $Key=>$Grading){
$Subtract = $MarksObt - $Grading['marks'];
if( $Subtract >= 0 )
array_push($MinGrades, array($Key=>$Subtract))
}
$GradeKey = key($MinGrades[0]); // Get key of first element in the array
print $Gradings[$GradeKey]['grade'];
If you have some better approach, please mention here.
Thanks for your contribution...