How to rank rows by grouping without using cursors with SQL Server
On occasion, you need to group and rank rows in consecutive order without any gap. The usual way is to create a temporary table and to use cursors in Transact-SQL to increment and reset the counters.
SQL Server 2005 introduced the function DENSE_RANK that does it all.
Data Used
- USE sql911;
- go
- SELECT EMPNO AS 'Emp #',
- FIRSTNME AS 'First Name',
- LASTNAME AS 'Last Name',
- isnull(WORKDEPT,'**') AS 'Dept',
- isnull(PHONENO,'**') AS 'Local',
- HIREDATE AS 'Hired',
- JOB AS 'Position'
- FROM emps
- ORDER BY lastname, firstnme;
- go
Emp # First Name Last Name Dept Local Hired Position ------ ------------ --------------- ---- ----- ----------------------- -------- 000150 BRUCE ADAMSON ** ** 1987-02-12 00:00:00.000 DESIGNER 000200 DAVID BROWN D11 4501 1981-03-03 00:00:00.000 DESIGNER 000050 JOHN GEYER E01 6789 1964-08-17 00:00:00.000 MANAGER 000340 JASON GOUNOT E21 5698 1962-05-05 00:00:00.000 FIELDREP 000010 CHRISTINE HAAS A00 3978 1980-01-01 00:00:00.000 PRES 000090 EILEEN# HENDERSON E11 5498 1985-08-15 00:00:00.000 MANAGER 000230 JAMES JEFFERSON D21 2094 1981-11-21 00:00:00.000 CLERK 000260 SYBIL\ JOHNSON D21 8953 1990-09-11 00:00:00.000 CLERK 000210 WILLIAM* JONES D11 0942 1994-04-11 00:00:00.000 DESIGNER 000030 SALLY! KWAN C01 4738 1990-04-05 00:00:00.000 MANAGER 000330 WING LEE E21 2103 1991-02-23 00:00:00.000 FIELDREP 000110 VINCENZO. LUCCHESSI A00 3490 1973-05-16 00:00:00.000 SALESREP 000220 JENNIFER] LUTZ ** ** 1983-08-29 00:00:00.000 DESIGNER 000240 SALVATORE[ MARINO D21 3780 1994-12-05 00:00:00.000 CLERK 000320 RAMLAL MEHTA ** 9990 1980-07-07 00:00:00.000 FIELDREP 000140 HEATHER NICHOLLS C01 1793 1991-12-15 00:00:00.000 ANALYST 000120 SEAN O CONNELL A00 2167 1978-12-05 00:00:00.000 CLERK 000290 JOHN PARKER E11 4502 1995-05-30 00:00:00.000 OPERATOR 000270 MARIA PEREZ D21 9001 1995-09-30 00:00:00.000 CLERK 000160 ELIZABETH PIANKA D11 3782 1992-10-11 00:00:00.000 DESIGNER 000070 EVA PULASKI D21 7831 1995-09-30 00:00:00.000 MANAGER 000130 DOLORES, QUINTANA C01 4578 1986-07-28 00:00:00.000 ANALYST 000280 ETHEL/ SCHNEIDER E11 8997 1982-03-24 00:00:00.000 OPERATOR 000180 MARILYN SCOUTTEN D11 1682 1988-07-07 00:00:00.000 DESIGNER 000310 MAUDE SETRIGHT E11 3332 1979-09-12 00:00:00.000 OPERATOR 000250 DANIEL SMITH D21 0961 1984-10-30 00:00:00.000 CLERK 000300 PHILIP SMITH E11 2095 1987-06-19 00:00:00.000 OPERATOR 000100 THEODORE SPENSER ** ** 1995-06-19 00:00:00.000 MANAGER 000060 IRVING@ STERN D11 6423 1988-09-14 00:00:00.000 MANAGER 000020 MICHAEL THOMPSON ** 3476 1988-10-10 00:00:00.000 MANAGER 000190 JAMES& WALKER D11 2986 1989-07-26 00:00:00.000 DESIGNER 000170 MASATOSHI% YOSHIMURA D11 2890 1993-09-15 00:00:00.000 DESIGNER (32 row(s) affected)
Ranking
- USE sql911;
- go
- SELECT CAST(ROW_NUMBER() OVER (ORDER BY lastname, firstnme) AS VARCHAR(8)) AS 'Row #',
- CAST(dense_rank() OVER (ORDER BY workdept) AS VARCHAR(8)) AS '# Dept',
- EMPNO AS 'Emp#',
- FIRSTNME AS 'First Name',
- LASTNAME AS 'Last Name',
- isnull(WORKDEPT,'**') AS 'Dept',
- isnull(PHONENO,'**') AS 'Local',
- HIREDATE AS 'Hired',
- JOB AS 'Position'
- FROM emps
- ORDER BY workdept, lastname, firstnme;
- go
Row # # Dept Emp# First Name Last Name Dept Local Hired Position -------- -------- ------ ------------ --------------- ---- ----- ----------------------- -------- 1 1 000150 BRUCE ADAMSON ** ** 1987-02-12 00:00:00.000 DESIGNER 13 1 000220 JENNIFER] LUTZ ** ** 1983-08-29 00:00:00.000 DESIGNER 15 1 000320 RAMLAL MEHTA ** 9990 1980-07-07 00:00:00.000 FIELDREP 28 1 000100 THEODORE SPENSER ** ** 1995-06-19 00:00:00.000 MANAGER 30 1 000020 MICHAEL THOMPSON ** 3476 1988-10-10 00:00:00.000 MANAGER 5 2 000010 CHRISTINE HAAS A00 3978 1980-01-01 00:00:00.000 PRES 12 2 000110 VINCENZO. LUCCHESSI A00 3490 1973-05-16 00:00:00.000 SALESREP 17 2 000120 SEAN O CONNELL A00 2167 1978-12-05 00:00:00.000 CLERK 10 3 000030 SALLY! KWAN C01 4738 1990-04-05 00:00:00.000 MANAGER 16 3 000140 HEATHER NICHOLLS C01 1793 1991-12-15 00:00:00.000 ANALYST 22 3 000130 DOLORES, QUINTANA C01 4578 1986-07-28 00:00:00.000 ANALYST 2 4 000200 DAVID BROWN D11 4501 1981-03-03 00:00:00.000 DESIGNER 9 4 000210 WILLIAM* JONES D11 0942 1994-04-11 00:00:00.000 DESIGNER 20 4 000160 ELIZABETH PIANKA D11 3782 1992-10-11 00:00:00.000 DESIGNER 24 4 000180 MARILYN SCOUTTEN D11 1682 1988-07-07 00:00:00.000 DESIGNER 29 4 000060 IRVING@ STERN D11 6423 1988-09-14 00:00:00.000 MANAGER 31 4 000190 JAMES& WALKER D11 2986 1989-07-26 00:00:00.000 DESIGNER 32 4 000170 MASATOSHI% YOSHIMURA D11 2890 1993-09-15 00:00:00.000 DESIGNER 7 5 000230 JAMES JEFFERSON D21 2094 1981-11-21 00:00:00.000 CLERK 8 5 000260 SYBIL\ JOHNSON D21 8953 1990-09-11 00:00:00.000 CLERK 14 5 000240 SALVATORE[ MARINO D21 3780 1994-12-05 00:00:00.000 CLERK 19 5 000270 MARIA PEREZ D21 9001 1995-09-30 00:00:00.000 CLERK 21 5 000070 EVA PULASKI D21 7831 1995-09-30 00:00:00.000 MANAGER 26 5 000250 DANIEL SMITH D21 0961 1984-10-30 00:00:00.000 CLERK 3 6 000050 JOHN GEYER E01 6789 1964-08-17 00:00:00.000 MANAGER 6 7 000090 EILEEN# HENDERSON E11 5498 1985-08-15 00:00:00.000 MANAGER 18 7 000290 JOHN PARKER E11 4502 1995-05-30 00:00:00.000 OPERATOR 23 7 000280 ETHEL/ SCHNEIDER E11 8997 1982-03-24 00:00:00.000 OPERATOR 25 7 000310 MAUDE SETRIGHT E11 3332 1979-09-12 00:00:00.000 OPERATOR 27 7 000300 PHILIP SMITH E11 2095 1987-06-19 00:00:00.000 OPERATOR 4 8 000340 JASON GOUNOT E21 5698 1962-05-05 00:00:00.000 FIELDREP 11 8 000330 WING LEE E21 2103 1991-02-23 00:00:00.000 FIELDREP (32 row(s) affected)
- The DENSE_RANK() function does not have any gap, all the numbers are always consecutive.
- If 2 rows have the same value of the OVER, they will have the same DENSE_RANK(), the following row will have DENSE_RANK() + 1* and NOT DENSE_RANK() + Number of duplicates.
- The OVER order does not have to be the same as the order of the ORDER BY clause. The DENSE_RANK, it will generate it's own sort. You can see the showplan for the details.
- The OVER order should be the same as the order of the ORDER BY clause. It's lot more efficient, especially for large tables.
- The OVER order should be covered by some index, especially if the report is a large one and used often.
- Note that the order for the ROW_NUMBER does not have to match either the ORDER BY clause or the DENSE_RANK, [in this case: an alphabetical sort for the employee names.]
Showplan
- SET showplan_text ON;
- go
- SELECT CAST(ROW_NUMBER() OVER (ORDER BY lastname, firstnme) AS VARCHAR(8)) AS 'Row #',
- CAST(dense_rank() OVER (ORDER BY workdept) AS VARCHAR(8)) AS '# Dept',
- EMPNO AS 'Emp#',
- FIRSTNME AS 'First Name',
- LASTNAME AS 'Last Name',
- isnull(WORKDEPT,'**') AS 'Dept',
- isnull(PHONENO,'**') AS 'Local',
- HIREDATE AS 'Hired',
- JOB AS 'Position'
- FROM emps
- ORDER BY workdept, lastname, firstnme;
- go
StmtText
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
select cast(ROW_NUMBER() OVER (ORDER BY lastname, firstnme) as varchar(8)) AS 'Row #',
cast(dense_rank() over (order by workdept) as varchar(8)) as '# Dept',
EMPNO as 'Emp#',
FIRSTNME as 'First Name',
LASTNAME as 'Last N
(1 row(s) affected)
StmtText
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|--Sort(ORDER BY:([sql911].[dbo].[Emps].[WORKDEPT] ASC, [sql911].[dbo].[Emps].[LASTNAME] ASC, [sql911].[dbo].[Emps].[FIRSTNME] ASC))
|--Compute Scalar(DEFINE:([Expr1006]=CONVERT(varchar(8),[Expr1004],0), [Expr1007]=CONVERT(varchar(8),[Expr1005],0), [Expr1008]=isnull([sql911].[dbo].[Emps].[WORKDEPT],'** '), [Expr1009]=isnull([sql911].[dbo].[Emps].[PHONENO],'** ')))
|--Sequence Project(DEFINE:([Expr1005]=dense_rank))
|--Segment
|--Segment
|--Sort(ORDER BY:([sql911].[dbo].[Emps].[WORKDEPT] ASC))
|--Sequence Project(DEFINE:([Expr1004]=row_number))
|--Compute Scalar(DEFINE:([Expr1011]=(1)))
|--Segment
|--Sort(ORDER BY:([sql911].[dbo].[Emps].[LASTNAME] ASC, [sql911].[dbo].[Emps].[FIRSTNME] ASC))
|--Table Scan(OBJECT:([sql911].[dbo].[Emps]))
(11 row(s) affected)
- The graphical vue of the SHOWPLAN is much more detailed, you can actually see the IO cost, the CPU cost, the number of rows...
- The plan is evaluated from the last item: Table Scan(OBJECT:([sql911].[dbo].[Emps])) to the first item: Sort(ORDER BY:([sql911].[dbo].[Emps].[WORKDEPT] ASC, [sql911].[dbo].[Emps].[LASTNAME] ASC, [sql911].[dbo].[Emps].[FIRSTNME] ASC))
- You can clearly see how SQL Server does all the sorts at each stage.

