Average of Sum minus Minimum - sql

I have an SQL statement that grabs the grades of different activity types (Homework, Quiz, etc), and if there's a drop lowest for that type, it drops, else, it remains. The errors are below as well as the SQL Code.
SELECT Student.firstName, Student.lastName, 'Grades' =
CASE
WHEN Grades.activityType = 'Homework' THEN
CASE WHEN Policy.drop_hw = 1 THEN
(AVG(SUM(Grades.grade) - MIN(Grades.grade))) * (Policy.homework / 100)
ELSE
(AVG(Grades.grade) * (Policy.homework / 100))
END
END, Course.courseNum, Course.sectNum, Grades.activityType
FROM ...
Here are the errors I'm getting:
- Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
- Column 'Policy.drop_hw' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.

Look into analytical functions. (SO question, Oracle documentation).
Something like this:
AVG(Grades.grade) OVER (PARTITION BY Grades.student_id) AS avg_of_grades
and:
(AVG(SUM(Grades.grade) - MIN(Grades.grade))) OVER (PARTITION BY Grades.student_id) AS avg_grades_with_drop
Set the partitioning with whatever makes sense in your case; we can't tell since you omitted the FROM ... in your example.
You can then use those column aliases in any calculations inside your CASE statement.

If you only need to drop one lowest grade (in case of ties)
SELECT student_id, AVG(grade)
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY student_id ORDER BY grade) rn
FROM my_tables
)
WHERE NOT (drop_hw = 1 AND rn = 1)
GROUP BY
student_id
If you need to drop all lowest grades:
SELECT student_id, AVG(grade)
FROM (
SELECT *, MIN(grade) OVER (PARTITION BY student_id) mingrade
FROM my_tables
)
WHERE NOT (drop_hw = 1 AND grade = mingrade)
GROUP BY
student_id

The sum-operator gives one result (per group). The min-operator, too. So over what should the avg-operator aggregate?

Related

How do I grab each student’s 3rd max assignment mark in each subject

I am trying to write an sql that will allow me select each student’s 3rd best assignment mark in each subject. I have tried with the query below but it isn't working for me. I will be grateful to get some answers. I am getting an error [Code: 0, SQL State: 21000] ERROR: more than one row returned by a subquery used as an expression.
This is the table structure Students , Courses(Id) , bridging table called StudentsCourses(ID, StudentID,CourseID) and then assignment table which has StudentsCourse(FK) and Grade
select max(Assignments.Grade)
from Assignments
where grade < (select max(Assignments.Grade)
from Assignments
where grade < (select max(Assignments.Grade)
from Assignments
group by Assignments.StudentCourseID))
You can use window functions:
select *
from (
select a.*, row_number() over(partition by student_id, subject_id order by grade desc)
from assignments a
) a
where rn = 3
Your question is a bit unclear about the structure of table assignments. This assumes that a student is identified by student_id and a subject by subject_id - you many need to ajust that to your actual column names.
Use row_number():
select a.*
from (select a.*,
row_number() over (partition by student_id, StudentCourseID order by grade desc) as seqnum
from assignments a
) a
where seqnum = 3;
Note: If all the assignments have the same value, this will return the highest value.
If you want the third highest distinct score, then use dense_rank() instead of row_number().

Use Rank() over a pseudo Column Name

I have a table with columns:
StudentName
Marks1
Marks2
from which I need to perform a query that will calculate the average of two marks and rank the rows from highest average to least.
I executed the following query:
SELECT
*,
(SELECT AVG(c) FROM (VALUES(Marks1),(Marks2)) T (c)) AS Average,
RANK() OVER (ORDER BY Average DESC) AS Position
from Marks;
But that gives an error:
Average is an Invalid Column Name.
How do I fix this? How do I give a query to perform Rank() over Average.
You can't reference a column by its alias in the SELECT; the only place you can reference its alias is in the ORDER BY clause.
What you can do, however, is move the subquery to the FROM, and then you can reference the column returned in your (outer) SELECT:
SELECT M.*,--List your columns here, don't use *
A.Average,
RANK() OVER (ORDER BY A.Average DESC) AS Position
FROM Marks M
CROSS APPLY(SELECT AVG(Mark) AS Average FROM (VALUES(Marks1),(Marks2)) V(Mark) ) A;
You should just use the average of the two marks inlined in the outer query:
SELECT *, RANK() OVER (ORDER BY (Marks1 + Marks2) / 2 DESC) AS Position
FROM Marks
ORDER BY (Marks1 + Marks2) / 2 DESC;

Rank Over Partition By in Oracle SQL (Oracle 11g)

I have 4 columns in a table
Company Part Number
Manufacturer Part Number
Order Number
Part Receipt Date
Ex.
I just want to return one record based on the maximum Part Receipt Date which would be the first row in the table (The one with Part Receipt date 03/31/2015).
I tried
RANK() OVER (PARTITION BY Company Part Number,Manufacturer Part Number
ORDER BY Part Receipt Date DESC,Order Number DESC) = 1
at the end of the WHERE statement and this did not work.
This would seem to do what you want:
select t.*
from (select t.*
from t
order by partreceiptdate desc
) t
where rownum = 1;
Analytic functions like rank() are available in the SELECT clause, they can't be invoked directly in a WHERE clause. To use rank() the way you want it, you must declare it in a subquery and then use it in the WHERE clause in the outer query. Something like this:
select company_part_number, manufacturer_part_number, order_number, part_receipt_date
from ( select t.*, rank() over (partition by... order by...) as rnk
from your_table t
)
where rnk = 1
Note also that you can't have a column name like company part number (with spaces in it) - at least not unless they are enclosed in double-quotes, which is a very poor practice, best avoided.

Trying to figure out how to join these queries

I have a table named grades. A column named Students, Practical, Written. I am trying to figure out the top 5 students by total score on the test. Here are the queries that I have not sure how to join them correctly. I am using oracle 11g.
This get's me the total sums from each student:
SELECT Student, Practical, Written, (Practical+Written) AS SumColumn
FROM Grades;
This gets the top 5 students:
SELECT Student
FROM ( SELECT Student,
, DENSE_RANK() OVER (ORDER BY Score DESC) as Score_dr
FROM Grades )
WHERE Student_dr <= 5
order by Student_dr;
The approach I prefer is data-centric, rather than row-position centric:
SELECT g.Student, g.Practical, g.Written, (g.Practical+g.Written) AS SumColumn
FROM Grades g
LEFT JOIN Grades g2 on g2.Practical+g2.Written > g.Practical+g.Written
GROUP BY g.Student, g.Practical, g.Written, (g.Practical+g.Written) AS SumColumn
HAVING COUNT(*) < 5
ORDER BY g.Practical+g.Written DESC
This works by joining with all students that have greater scores, then using a HAVING clause to filter out those that have less than 5 with a greater score - giving you the top 5.
The left join is needed to return the top scorer(s), which have no other students with greater scores to join to.
Ties are all returned, leading to more than 5 rows in the case of a tie for 5th.
By not using row position logic, which varies from darabase to database, this query is also completely portable.
Note that the ORDER BY is optional.
With Oracle's PLSQL you can do:
SELECT score.Student, Practical, Written, (Practical+Written) as SumColumn
FROM ( SELECT Student, DENSE_RANK() OVER (ORDER BY Score DESC) as Score_dr
FROM VOTES ) as score, students
WHERE score.score_dr <= 5
and score.Student = students.Student
order by score.Score_dr;
You can easily include the projection of the first query in the sub-query of the second.
SELECT Student
, Practical
, Written
, tot_score
FROM (
SELECT Student
, Practical
, Written
, (Practical+Written) AS tot_score
, DENSE_RANK() OVER (ORDER BY (Practical+Written) DESC) as Score_dr
FROM Grades
)
WHERE Student_dr <= 5
order by Student_dr;
One virtue of analytic functions is that we can just use them in any query. This distinguishes them from aggregate functions, where we need to include all non-aggregate columns in the GROUP BY clause (at least with Oracle).

sql query finding most often level appear

I have a table Student in SQL Server with these columns:
[ID], [Age], [Level]
I want the query that returns each age value that appears in Students, and finds the level value that appears most often. For example, if there are more 'a' level students aged 18 than 'b' or 'c' it should print the pair (18, a).
I am new to SQL Server and I want a simple answer with nested query.
You can do this using window functions:
select t.*
from (select age, level, count(*) as cnt,
row_number() over (partition by age order by count(*) desc) as seqnum
from student s
group by age, level
) t
where seqnum = 1;
The inner query aggregates the data to count the number of levels for each age. The row_number() enumerates these for each age (the partition by with the largest first). The where clause then chooses the highest values.
In the case of ties, this returns just one of the values. If you want all of them, use rank() instead of row_number().
One more option with ROW_NUMBER ranking function in the ORDER BY clause. WITH TIES used when you want to return two or more rows that tie for last place in the limited results set.
SELECT TOP 1 WITH TIES age, level
FROM dbo.Student
GROUP BY age, level
ORDER BY ROW_NUMBER() OVER(PARTITION BY age ORDER BY COUNT(*) DESC)
Or the second version of the query using amount each pair of age and level, and max values of count pair age and level per age.
SELECT *
FROM (
SELECT age, level, COUNT(*) AS cnt,
MAX(COUNT(*)) OVER(PARTITION BY age) AS mCnt
FROM dbo.Student
GROUP BY age, level
)x
WHERE x.cnt = x.mCnt
Demo on SQLFiddle
Another option but will require later version of sql-server:
;WITH x AS
(
SELECT age,
level,
occurrences = COUNT(*)
FROM Student
GROUP BY age,
level
)
SELECT *
FROM x x
WHERE EXISTS (
SELECT *
FROM x y
WHERE x.occurrences > y.occurrences
)
I realise it doesn't quite answer the question as it only returns the age/level combinations where there are more than one level for the age.
Maybe someone can help to amend it so it includes the single level ages aswell in the result set: http://sqlfiddle.com/#!3/d597b/9
with combinations as (
select age, level, count(*) occurrences
from Student
group by age, level
)
select age, level
from combinations c
where occurrences = (select max(occurrences)
from combinations
where age = c.age)
This finds every age and level combination in the Students table and counts the number of occurrences of each level.
Then, for each age/level combination, find the one whose occurrences are the highest for that age/level combination. Return the age and level for that row.
This has the advantage of not being tied to SQL Server - it's vanilla SQL. However, a window function like Gordon pointed out may perform better on SQL Server.