SQL Puzzle #1

Background

I had to write a semi-interesting SQL query this past week and thought it might make for a fun SQL puzzle (for very small values of "fun").

I'm working on a bio-tech business intelligence application, but I simplified things way down to two tables in a SQL Server 2000 database: Student and Grade. The student may have any number of grades, and may of course may get the same grade several times.

StudentID FirstName LastName
1 Bill Smith
2 Bobby Brown
3 Derek Zoolander
GradeID StudentID Grade
1 2 61
2 3 98
3 1 87
... ... ...
18 1 58
19 2 82
20 3 68

 

Task

Prepare a report that shows the top ten grades for each student. You must show exactly ten rows for each student, so if they have less than ten grades you should show a null. Remember that you need to handle the case where the same student gets the same score more than once (e.g.- in my results below, check out Bobby Brown's top two grades are both 82).

Row StudentID FirstName LastName Grade
1 1 Bill Smith 94
2 1 Bill Smith 87
3 1 Bill Smith 82
4 1 Bill Smith 67
5 1 Bill Smith 62
6 1 Bill Smith 58
7 1 Bill Smith NULL
8 1 Bill Smith NULL
9 1 Bill Smith NULL
10 1 Bill Smith NULL
1 2 Bobby Brown 82
2 2 Bobby Brown 82
3 2 Bobby Brown 72
7 3 Derek Zoolander 53
8 3 Derek Zoolander NULL
9 3 Derek Zoolander NULL
10 3 Derek Zoolander NULL

My answer will not use cursors and will run on standard SQL 2000 T-SQL, but if you'd like to submit answers using the SQL 2005 ranking functions, go for it. Post your answers as comments; my answer will be in the next post.


Here's a SQL script with some sample data. You'll need to create a "test" database to run this. 

 

use [test]
go

if exists (select * from information_schema.tables where table_name = 'Grade') drop table Grade
if exists (select * from information_schema.tables where table_name = 'Student') drop table student

create table Student(
StudentID
int identity(1,1) primary key,
FirstName
varchar(50),
LastName
varchar(50)
)

insert into student(firstname, lastname) values ('Bill','Smith')
insert into student(firstname, lastname) values ('Bobby','Brown')
insert into student(firstname, lastname) values ('Derek','Zoolander')

create table Grade(
GradeID
int identity(1,1),
StudentID
int,
Grade
float
)
ALTER TABLE Grade WITH CHECK ADD CONSTRAINT FK_Grade_Student FOREIGN KEY([StudentID]) REFERENCES Student(StudentID)

declare @counter int
declare @grade int
set @counter = 0
while @counter < 20
begin
set @counter = @counter + 1
set @grade = cast(50*rand(cast(cast(newid() as binary(8)) as int)) as int) + 50
insert into Grade (StudentID,Grade) values (@counter % 3 + 1,@grade)
end
select * from Grade
go
Published Thursday, November 02, 2006 12:05 AM by Jon Galloway
Filed under: ,

Comments

# SQL Puzzle #1 - Answer

Here's mysolution to the puzzle in my previous post. If you haven't seen the challenge, go back and read

Thursday, November 02, 2006 3:22 AM by Jon Galloway

# re: SQL Puzzle #1

The answer would be very simple, if you wouldn't have the rule to have always 10 rows, even when less than 10 grades are known.

Because you can do something like:

SELECT S.StudentID, S.FirstName, S.LastName, SG.Grade,  FROM

STUDENT S LEFT JOIN (SELECT TOP 10 * FROM GRADE G WHERE S.StudentID = G.StudentID ORDER BY Grade DESC) SG

ORDER BY S.StudentID ASC

(not tested, might be full of bugs)

You see, the ordering number in the list and the empty rows to make it to 10 rows, is not a set problem, but a data consumer's problem, so you shouldn't solve it in SQL but in the code consuming the data.

Thursday, November 02, 2006 6:15 AM by Frans Bouma

# re: SQL Puzzle #1

Sorry, Frans - My puzzle, my rules. You flunk! ;-)

Thursday, November 02, 2006 2:39 PM by Jon Galloway

# re: SQL Puzzle #1

This seems to work for me:

select

d.studentid

,s.firstname

,s.lastname

,g.grade

from (

select

g.gradeid

,s.studentid

,null as rank

from

grade g

join student s

on s.studentid=g.studentid

where

gradeid in (select top 10 gradeid from grade where studentid=g.studentid order by grade desc)

union select null, studentid,1 from student s where 1 > (select count(*) from grade t where s.studentid=t.studentid)

union select null, studentid,2 from student s where 2 > (select count(*) from grade t where s.studentid=t.studentid)

union select null, studentid,3 from student s where 3 > (select count(*) from grade t where s.studentid=t.studentid)

union select null, studentid,4 from student s where 4 > (select count(*) from grade t where s.studentid=t.studentid)

union select null, studentid,5 from student s where 5 > (select count(*) from grade t where s.studentid=t.studentid)

union select null, studentid,6 from student s where 6 > (select count(*) from grade t where s.studentid=t.studentid)

union select null, studentid,7 from student s where 7 > (select count(*) from grade t where s.studentid=t.studentid)

union select null, studentid,8 from student s where 8 > (select count(*) from grade t where s.studentid=t.studentid)

union select null, studentid,9 from student s where 9 > (select count(*) from grade t where s.studentid=t.studentid)

union select null, studentid,10 from student s where 10 > (select count(*) from grade t where s.studentid=t.studentid)

) d

left join grade g

on d.gradeid=g.gradeid

left join student s

on d.studentid=s.studentid

order by

 studentid asc, grade desc

Thursday, November 02, 2006 5:11 PM by Scott Wilson

# re: SQL Puzzle #1

Nice, Scott!

Thursday, November 02, 2006 6:34 PM by Jon Galloway

# re: SQL Puzzle #1

@Scott - After thinking about it a bit, I still like my solution better. They're comparable in terms of the amount of code, but mine's more dynamic. If I needed to show the top 100 grades for each student, I'd only have to change one character; you'd have more work to do. You can generate the SQL, but it's still more work.

Thursday, November 02, 2006 7:58 PM by Jon Galloway

# re: SQL Puzzle #1

SQL Server 2005 Rulez :)

SELECT *

FROM [dbo].[Student] s

CROSS APPLY (

SELECT TOP 10 *

FROM (

SELECT g.[Grade] FROM [dbo].[Grade] g WHERE g.[StudentID] = s.[StudentID]

UNION ALL SELECT NULL

UNION ALL SELECT NULL

UNION ALL SELECT NULL

UNION ALL SELECT NULL

UNION ALL SELECT NULL

UNION ALL SELECT NULL

UNION ALL SELECT NULL

UNION ALL SELECT NULL

UNION ALL SELECT NULL

UNION ALL SELECT NULL

) g1

ORDER BY g1.[Grade] DESC

) g2

Friday, November 03, 2006 9:23 AM by Sacode

# re: SQL Puzzle #1

Here is my attempt.  I did not have time to complete the row number aspect.  Also, you will have to change the Top "X" variable in two places since I use a sub select....  

DECLARE @TopNumber int

SET @TopNumber = 10

IF OBJECT_ID('tempdb..#tmpFill') IS NOT NULL drop table #tmpFill

CREATE TABLE #tmpFill

(

FillID int IDENTITY(1,1),

JoinCol int DEFAULT 1

)

-- Fill X records tmp table

DECLARE @CurrCnt int

SET @CurrCnt = 1

WHILE @CurrCnt <= @TopNumber

BEGIN

INSERT INTO #tmpFill(JoinCol) VALUES (1)

SET @CurrCnt = @CurrCnt + 1

END

IF OBJECT_ID('tempdb..#tmpGrades') IS NOT NULL drop table #tmpGrades

CREATE TABLE #tmpGrades

(

GradeID int IDENTITY(1,1),

StudentID int,

Grade float,

JoinCol int DEFAULT 1

)

-- Insert real values

INSERT INTO #tmpGrades (StudentID, Grade)

SELECT StudentID, Grade FROM Grade

-- Insert X number of values for each student

INSERT INTO #tmpGrades (StudentID, Grade)

SELECT StudentID, NULL

FROM Student s INNER JOIN #tmpFill tf ON 1 = tf.JoinCol

DECLARE @RowCnt int

SET @RowCnt = 0

SELECT

s.StudentID, s.FirstName, s.LastName, g.Grade

FROM

Student s INNER JOIN #tmpGrades g ON s.StudentID = g.StudentID

WHERE

g.GradeID IN ( SELECT TOP 10 GradeID FROM #tmpGrades innerGrades WHERE innerGrades.StudentID = s.StudentID)

ORDER BY g.StudentID, Grade Desc

DROP TABLE #tmpFill

DROP TABLE #tmpGrades

Friday, November 03, 2006 11:26 AM by Dan

# re: SQL Puzzle #1

Jon,

I agree that your solution is far more maintainable, and that is a major advantage for taking that approach.  But, I often favor performance over maintainability when coding.  I checked the three above approaches against a larger recordset (120 grades, 14 students) to get a feel for their performance in SQL Profiler.  Here were my results on SQL 2005:

First Run:

Jon: 71ms, 1249 reads, 47 cpu

Me: 15ms, 1327 reads, 16 cpu

sacode: 7ms, 52 reads, 15 cpu

Second Run: (comparable results each subsequent time)

Jon: 41ms, 1203 reads, 32 cpu

Me: 16ms, 1327 reads, 15 cpu

sacode: 1ms, 44 reads, 0 cpu

wow.  I think sacode kicked our butts.  And the principle on that approach - unioning nulls w/ no subqueries then selecting top 10 in the outer query - was definitely optimum (I was kicking kicking my own rear for not thinking of that).  And, I think the only SQL 2005 feature used (Cross Apply) could easily be represented by some creative joins in SQL 2000 (would probably reduce performance slightly though)

Cool puzzle, I'd love to see more in the future.

Scott

Friday, November 03, 2006 12:39 PM by Scott Wilson

# re: SQL Puzzle #1

**REALLY LATE ENTRY**

Modifying Sacode's salution just a bit:

select *

from dbo.student s

cross apply

(

select top 10 *

from

(

select g.grade

from dbo.grade g

where g.stid = s.stid

union all

select null

from dbo.grade g

where g.stid = s.stid

) g1

order by g1.grade desc

) g2

Wednesday, April 11, 2007 10:04 PM by Bill Dunn

# re: SQL Puzzle #1

select a.studentid,a.firstname,a.lastname,b.grade

      from student a cross join (select 1 as a union select 2 as a union select 3 as a union select 4 as a union select 5 as a union select 6 as a union select 7 as a union select 8 as a  union select 9 as a  union select 10 as a) x

left outer join

(select a.studentid,a.gradeid,a.grade,count(b.grade) as grade_rank

from   grade a, grade b

where  a.studentid = b.studentid

and    a.grade>= b.grade

group by a.studentid,a.gradeid,a.grade) b

on a.studentid = b.studentid

and x.a = b.grade_rank

order by a.firstname,a.lastname, b.grade desc

Tuesday, June 05, 2007 6:57 AM by Senthil

Leave a Comment

(required) 
(required) 
(optional)
(required)