Reputation: 23
Suppose I have a couple tables for storing a student's test marks, like so
tests (
testId INT,
proctoredDate DATE,
totalMark INT,
weight FLOAT,
CONSTRAINT test_id_primary_key PRIMARY KEY(testId),
...
)
student_test_marks (
studentId INT,
testId INT,
awardedMark INT,
CONSTRAINT student_test_composite_key PRIMARY KEY(studentId, testId),
...
)
I'm trying to determine which student in a given month earned the highest cummulative mark. So the process is to sum all the test marks and weights grouped by each student in the specified month and return only the maximum sum. I already have a working query but I don't think it's very efficient.
SELECT studentId
FROM student_test_marks INNER JOIN tests
ON student_test_marks.testId = tests.testId
GROUP BY studentId, EXTRACT(MONTH FROM proctoredDate)
HAVING EXTRACT(MONTH FROM proctoredDate) = 4 AND
SUM((awardedMark / totalMark) * weight) IN (
SELECT MAX(SUM((awardedMark / totalMark) * weight))
FROM student_test_marks INNER JOIN tests
ON student_test_marks.testId = test.testId
GROUP BY studentId, EXTRACT(MONTH FROM proctoredDate)
HAVING EXTRACT(MONTH FROM proctoredDate) = 4
)
It does work but I have a feeling its not optimal since the query joins, groups and filters twice. I considered ordering by the Sum column of the group and then fetching only the first row but that feels clunky.
Is there a better way to solve this?
Upvotes: 1
Views: 27
Reputation: 1288
with tests as (
select 1 testid, date '2019-01-10' proctoredDate, 100 totalmark from dual union all
select 2 testid, date '2019-02-22' proctoredDate, 100 totalmark from dual union all
select 3 testid, date '2019-03-14' proctoredDate, 100 totalmark from dual union all
select 4 testid, date '2019-04-19' proctoredDate, 100 totalmark from dual),
student_test_marks as (
select 1 studentid, 1 testid, 50 awardedmark from dual union all
select 2 studentid, 1 testid, 30 awardedmark from dual union all
select 3 studentid, 1 testid, 70 awardedmark from dual union all
select 4 studentid, 1 testid, 60 awardedmark from dual union all
select 1 studentid, 2 testid, 30 awardedmark from dual union all
select 2 studentid, 2 testid, 40 awardedmark from dual union all
select 3 studentid, 2 testid, 50 awardedmark from dual union all
select 4 studentid, 2 testid, 70 awardedmark from dual union all
select 1 studentid, 3 testid, 70 awardedmark from dual union all
select 2 studentid, 3 testid, 60 awardedmark from dual union all
select 2 studentid, 4 testid, 30 awardedmark from dual union all
select 1 studentid, 4 testid, 40 awardedmark from dual union all
select 2 studentid, 4 testid, 50 awardedmark from dual
)
select
mn,
max(studentid) keep (dense_rank last order by sumawarded) studentid,
max(sumawarded) max_sumawarded
from
(select trunc(t.proctoredDate, 'month') mn, s.studentid, sum(awardedmark) sumawarded
from tests t
inner join student_test_marks s on t.testId = s.testId
group by trunc(t.proctoredDate, 'month'), s.studentid
)
group by mn;
MN STUDENTID MAX_SUMAWARDED
------------------- ---------- --------------
2019-01-01 00:00:00 3 70
2019-02-01 00:00:00 4 70
2019-03-01 00:00:00 1 70
2019-04-01 00:00:00 2 80
Upvotes: 0
Reputation: 1269763
I think you just want to use analytic functions:
SELECT m.*
FROM (SELECT TRUNC(proctoredDate, 'MON') as yyyymm,
studentId, SUM((awardedMark / totalMark) * weight) as weighted_mark,
RANK() OVER (PARTITION BY TRUNC(proctoredDate, 'MON')
ORDER BY SUM((awardedMark / totalMark) * weight)
) as seqnum
FROM student_test_marks stm INNER JOIN
tests t
ON stm.testId = t.testId
GROUP BY TRUNC(proctoredDate, 'MON'), studentId
) m
WHERE seqnum = 1;
Upvotes: 1