Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SQL (PostgreSQL, MySQL, SQLite)  Module 8 – Window functions

ROW_NUMBER, RANK, DENSE_RANK and NTILE

How ROW_NUMBER, RANK and DENSE_RANK number tied rows, ranking within groups with PARTITION BY, and dealing rows into equal buckets with NTILE.

  • Intermediate
  • 30 minutes
  • Examples run with sql.js 1.14.2
  • By MySmartCoPilot
Free core, Pro depth: 11 of 30 minutes free
Free: Four numbers for the same eight offers · Ranking inside each branch · NTILE, PERCENTRANK and CUMEDIST · Your turn. With a pass: 2 in-depth parts and 3 interview answers, about 19 minutes.

What you will learn

  • Rank rows within partitions with ROW_NUMBER, RANK and DENSE_RANK
  • Choose the ranking function that treats ties the way a question needs
  • Remove duplicate rows with row numbers that a unique tie-breaker makes repeatable
  • Split rows into buckets with NTILE and place them with PERCENT_RANK and CUME_DIST

Before you start

What you’ll learn · Before you start

What you will learn

  • Rank rows within partitions with ROW_NUMBER, RANK and DENSE_RANK
  • Choose the ranking function that treats ties the way a question needs
  • Remove duplicate rows with row numbers that a unique tie-breaker makes repeatable
  • Split rows into buckets with NTILE and place them with PERCENT_RANK and CUME_DIST

Before you start

On this page

ROW_NUMBER, RANK, DENSE_RANK and NTILE are window functions that number the rows of each window in the order of its ORDER BY. They differ in what they do with ties, rows that are equal in that order. ROW_NUMBER gives every row a number of its own, 1, 2, 3 and so on, and puts tied rows in whatever order they arrive. RANK gives tied rows the same number and then skips, as in a race where two runners share first place and the next one is third. DENSE_RANK gives tied rows the same number without skipping. NTILE(n) does not rank at all: it deals the rows into n buckets of nearly equal size. So the choice depends on the question: one number per row, places with gaps after a tie, or a count of the distinct values ahead.

All four work the same way in PostgreSQL, MySQL and SQLite, and each example below runs on SQLite 3.49.1 inside this page.

Four numbers for the same eight offers

The examples rank the job offers made to the final-year students of one college, by branch.

India-specific

The packages are in lakh rupees per annum (LPA), the way Indian colleges report campus placement offers: 12.0 means ₹12,00,000 a year.

Here are the eight offers of the CSE branch, numbered four ways from the highest package down:

ROW_NUMBER, RANK, DENSE_RANK and NTILE(3) over the same rows SQL · four_rankings.sql
-- Placement offers of one college's final-year students, in three branches.
-- lpa: the package offered, in lakh rupees a year.
CREATE TABLE offers (
  student_id INTEGER PRIMARY KEY,
  student    TEXT NOT NULL,
  branch     TEXT NOT NULL,
  lpa        REAL NOT NULL
);
INSERT INTO offers VALUES
  (1,  'Aarav',  'CSE', 12.0),
  (2,  'Diya',   'CSE', 18.5),
  (3,  'Ishaan', 'CSE', 12.0),
  (4,  'Kavya',  'CSE',  9.5),
  (5,  'Nikhil', 'CSE', 12.0),
  (6,  'Riya',   'CSE', 18.5),
  (7,  'Sana',   'CSE',  7.0),
  (8,  'Tanvi',  'CSE',  9.5),
  (9,  'Arjun',  'ECE', 10.0),
  (10, 'Meher',  'ECE', 14.0),
  (11, 'Pooja',  'ECE', 10.0),
  (12, 'Rahul',  'ECE',  8.5),
  (13, 'Farah',  'ME',   7.5),
  (14, 'Gopal',  'ME',   9.0),
  (15, 'Harsh',  'ME',   6.0);

-- The eight CSE offers numbered four ways, from the highest package down.
SELECT student, lpa,
       row_number() OVER (ORDER BY lpa DESC) AS rn,
       rank()       OVER (ORDER BY lpa DESC) AS rnk,
       dense_rank() OVER (ORDER BY lpa DESC) AS dense,
       ntile(3)     OVER (ORDER BY lpa DESC) AS third
FROM offers
WHERE branch = 'CSE'
ORDER BY rn;

Output

┌─────────┬──────┬────┬─────┬───────┬───────┐
│ student │ lpa  │ rn │ rnk │ dense │ third │
├─────────┼──────┼────┼─────┼───────┼───────┤
│ Diya    │ 18.5 │ 1  │ 1   │ 1     │ 1     │
│ Riya    │ 18.5 │ 2  │ 1   │ 1     │ 1     │
│ Aarav   │ 12.0 │ 3  │ 3   │ 2     │ 1     │
│ Ishaan  │ 12.0 │ 4  │ 3   │ 2     │ 2     │
│ Nikhil  │ 12.0 │ 5  │ 3   │ 2     │ 2     │
│ Kavya   │ 9.5  │ 6  │ 6   │ 3     │ 2     │
│ Tanvi   │ 9.5  │ 7  │ 6   │ 3     │ 3     │
│ Sana    │ 7.0  │ 8  │ 8   │ 4     │ 3     │
└─────────┴──────┴────┴─────┴───────┴───────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < four_rankings.sql

Look at the three offers of 12.0, made to Aarav, Ishaan and Nikhil:

  • ROW_NUMBER numbered them 3, 4 and 5. Every row gets a different number, so the numbers count rows.
  • RANK gave all three the number 3, and the next package, 9.5, is ranked 6. Three students share third place, so 4 and 5 are skipped.
  • DENSE_RANK gave them 2, and the next package gets 3. Its numbers count the different packages, without gaps.
  • NTILE(3) dealt the eight rows into three buckets of 3, 3 and 2 rows. It ignores ties, so Aarav landed in the first bucket and Ishaan and Nikhil in the second, although all three have the same package.
Eight offers in four groups of equal packages: RANK numbers each group by the rows before it, DENSE_RANK by the groups before it.18.5: Diya, Riya2 rows, positions 1 and 2RANK 1 · DENSE_RANK 112.0: Aarav, Ishaan, Nikhil3 rows, positions 3 to 5RANK 3 · DENSE_RANK 29.5: Kavya, Tanvi2 rows, positions 6 and 7RANK 6 · DENSE_RANK 37.0: Sana1 row, position 8RANK 8 · DENSE_RANK 4ROW_NUMBER: the position, 1 to 8RANK: 1 + the rows before the groupDENSE_RANK: 1 + the groups before it2 rows before5 rows before7 rows before

RANK and DENSE_RANK number groups of peers, not single rows

Text description of the diagram

The diagram shows the eight CSE offers of the lesson, ordered from the highest package down, as four groups of peers: rows with the same package.

  1. The first group is the package 18.5, with Diya and Riya, in positions 1 and 2. No row comes before it, so RANK is 1 and DENSE_RANK is 1.
  2. The second group is the package 12.0, with Aarav, Ishaan and Nikhil, in positions 3 to 5. Two rows come before it, so RANK is 3; one group comes before it, so DENSE_RANK is 2.
  3. The third group is the package 9.5, with Kavya and Tanvi, in positions 6 and 7. Five rows come before it, so RANK is 6; two groups come before it, so DENSE_RANK is 3.
  4. The fourth group is the package 7.0, with Sana alone, in position 8. Seven rows come before it, so RANK is 8; three groups come before it, so DENSE_RANK is 4.

The rules at the bottom: ROW_NUMBER is the row's position, 1 to 8, different for every row. RANK is 1 plus the number of rows before the row's group. DENSE_RANK is 1 plus the number of groups before it.

Rows that are equal in the window’s ORDER BY are called peers. RANK is 1 plus the number of rows before the current row’s group of peers, and DENSE_RANK is 1 plus the number of peer groups before it. ROW_NUMBER has no rule for peers: Diya came before Riya here only because her row was read first. Unless a column breaks the tie, as in ORDER BY lpa DESC, student_id, the numbers of tied rows may change from one run of the same query to the next.

ROW_NUMBER, RANK and DENSE_RANK on the eight CSE offers
Criterion ROW_NUMBERRANKDENSE_RANK
The 12.0 offers 3, 4, 53, 3, 32, 2, 2
Next: 9.5 663
Highest number 8: the rows8 here4: the values
Ties share a number NoYesYes
When to choose One row per numberPlaces with gapsLevels without gaps

NTILE answers another question, so it is not in the table: it gives the bucket, 1 to n, and two tied rows can land in different buckets. Pick ROW_NUMBER when each number must belong to exactly one row, such as keeping one row per key; RANK for places that show how many rows are ahead; DENSE_RANK for levels, such as the second-highest package; and NTILE for groups of equal size, such as quartiles or deciles.

Ranking inside each branch

With PARTITION BY, each branch is numbered on its own, and the numbers start again at 1:

RANK and DENSE_RANK within each branch SQL · rank_in_branch.sql
-- Placement offers of one college's final-year students, in three branches.
-- lpa: the package offered, in lakh rupees a year.
CREATE TABLE offers (
  student_id INTEGER PRIMARY KEY,
  student    TEXT NOT NULL,
  branch     TEXT NOT NULL,
  lpa        REAL NOT NULL
);
INSERT INTO offers VALUES
  (1,  'Aarav',  'CSE', 12.0),
  (2,  'Diya',   'CSE', 18.5),
  (3,  'Ishaan', 'CSE', 12.0),
  (4,  'Kavya',  'CSE',  9.5),
  (5,  'Nikhil', 'CSE', 12.0),
  (6,  'Riya',   'CSE', 18.5),
  (7,  'Sana',   'CSE',  7.0),
  (8,  'Tanvi',  'CSE',  9.5),
  (9,  'Arjun',  'ECE', 10.0),
  (10, 'Meher',  'ECE', 14.0),
  (11, 'Pooja',  'ECE', 10.0),
  (12, 'Rahul',  'ECE',  8.5),
  (13, 'Farah',  'ME',   7.5),
  (14, 'Gopal',  'ME',   9.0),
  (15, 'Harsh',  'ME',   6.0);

-- PARTITION BY branch: every branch is ranked on its own.
SELECT branch, student, lpa,
       rank()       OVER (PARTITION BY branch ORDER BY lpa DESC) AS rnk,
       dense_rank() OVER (PARTITION BY branch ORDER BY lpa DESC) AS dense,
       count(*)     OVER (PARTITION BY branch)                   AS students
FROM offers
ORDER BY branch, dense, student;

Output

┌────────┬─────────┬──────┬─────┬───────┬──────────┐
│ branch │ student │ lpa  │ rnk │ dense │ students │
├────────┼─────────┼──────┼─────┼───────┼──────────┤
│ CSE    │ Diya    │ 18.5 │ 1   │ 1     │ 8        │
│ CSE    │ Riya    │ 18.5 │ 1   │ 1     │ 8        │
│ CSE    │ Aarav   │ 12.0 │ 3   │ 2     │ 8        │
│ CSE    │ Ishaan  │ 12.0 │ 3   │ 2     │ 8        │
│ CSE    │ Nikhil  │ 12.0 │ 3   │ 2     │ 8        │
│ CSE    │ Kavya   │ 9.5  │ 6   │ 3     │ 8        │
│ CSE    │ Tanvi   │ 9.5  │ 6   │ 3     │ 8        │
│ CSE    │ Sana    │ 7.0  │ 8   │ 4     │ 8        │
│ ECE    │ Meher   │ 14.0 │ 1   │ 1     │ 4        │
│ ECE    │ Arjun   │ 10.0 │ 2   │ 2     │ 4        │
│ ECE    │ Pooja   │ 10.0 │ 2   │ 2     │ 4        │
│ ECE    │ Rahul   │ 8.5  │ 4   │ 3     │ 4        │
│ ME     │ Gopal   │ 9.0  │ 1   │ 1     │ 3        │
│ ME     │ Farah   │ 7.5  │ 2   │ 2     │ 3        │
│ ME     │ Harsh   │ 6.0  │ 3   │ 3     │ 3        │
└────────┴─────────┴──────┴─────┴───────┴──────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < rank_in_branch.sql

In ECE, Arjun’s and Pooja’s offers of 10.0 share second place under both functions. Rahul’s 8.5 then comes fourth by RANK, because three students are ahead of him, and third by DENSE_RANK, because two different packages are. In ME there are no ties, so the two functions agree. The lesson on top-N per group in this module builds on such ranked lists.

NTILE, PERCENT_RANK and CUME_DIST

Three more functions place a row within its window instead of numbering it:

  • NTILE(n) deals the rows, in order, into n buckets whose sizes differ by at most one row.
  • PERCENT_RANK is a row’s RANK as a fraction, from 0 for the first group of peers up to at most 1.
  • CUME_DIST is the share of the window’s rows that come before the current row or tie with it.

NTILE(4) gives quartiles and NTILE(10) deciles. PERCENT_RANK and CUME_DIST give every peer the same value, while NTILE can put two peers in different buckets, and that difference changes who counts as the top 10 % of a list.

Descriptive Statistics Calculator Paste a list of numbers to see its quartiles and percentiles, and compare them with what NTILE and CUME_DIST return in SQL.

Your turn

Exercise · Easy · SQL

Places in each branch by CGPA, with ties sharing a place

The table students has one row per final-year student:

  • student_id: the student;
  • name: the student's name;
  • branch: 'CSE', 'ECE', 'ME' and so on;
  • cgpa: the cumulative grade point average, from 0 to 10, never NULL.

Return every student with four columns: branch, name, cgpa and place. place is the student's place within their own branch, from the highest CGPA down. Students with the same CGPA share a place, and the next CGPA gets the next place, with no gap: CGPAs of 9.1, 8.4, 8.4 and 7.9 get the places 1, 2, 2 and 3.

The order of the rows is not checked.

Starter code · places.sql

-- Each student's place within their branch, from the highest CGPA down.
-- So far every branch is ranked together, and a tie leaves a gap.
SELECT branch, name, cgpa,
       rank() OVER (ORDER BY cgpa DESC) AS place
FROM students;
The sample tests · tests.yaml
# Sample tests: each one builds a small students table, runs your query on it and compares the rows it returns (in any
# order).
tests:
  - name: two branches, with a tie in CSE
    setup: |
      CREATE TABLE students (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL, cgpa REAL NOT NULL);
      INSERT INTO students VALUES
        (1, 'Anaya', 'CSE', 8.4),
        (2, 'Bhavin', 'CSE', 9.1),
        (3, 'Chetan', 'CSE', 7.9),
        (4, 'Diksha', 'CSE', 8.4),
        (5, 'Eshan', 'ECE', 7.9),
        (6, 'Fiza', 'ECE', 8.6);
    expected:
      columns: [branch, name, cgpa, place]
      rows:
        - ['CSE', 'Bhavin', 9.1, 1]
        - ['CSE', 'Anaya', 8.4, 2]
        - ['CSE', 'Diksha', 8.4, 2]
        - ['CSE', 'Chetan', 7.9, 3]
        - ['ECE', 'Fiza', 8.6, 1]
        - ['ECE', 'Eshan', 7.9, 2]
  - name: a branch where everyone has the same CGPA, and a branch of one student
    setup: |
      CREATE TABLE students (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL, cgpa REAL NOT NULL);
      INSERT INTO students VALUES
        (1, 'Gauri', 'ME', 8.0),
        (2, 'Hemant', 'ME', 8.0),
        (3, 'Ilina', 'ME', 8.0),
        (4, 'Jatin', 'CIVIL', 6.5);
    expected:
      columns: [branch, name, cgpa, place]
      rows:
        - ['ME', 'Gauri', 8.0, 1]
        - ['ME', 'Hemant', 8.0, 1]
        - ['ME', 'Ilina', 8.0, 1]
        - ['CIVIL', 'Jatin', 6.5, 1]
  - name: two ties in a row, so that a gap would show
    setup: |
      CREATE TABLE students (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL, cgpa REAL NOT NULL);
      INSERT INTO students VALUES
        (1, 'Kabir', 'CSE', 9.5),
        (2, 'Lavanya', 'CSE', 9.5),
        (3, 'Madhav', 'CSE', 9.0),
        (4, 'Neha', 'CSE', 9.0),
        (5, 'Om', 'CSE', 8.2);
    expected:
      columns: [branch, name, cgpa, place]
      rows:
        - ['CSE', 'Kabir', 9.5, 1]
        - ['CSE', 'Lavanya', 9.5, 1]
        - ['CSE', 'Madhav', 9.0, 2]
        - ['CSE', 'Neha', 9.0, 2]
        - ['CSE', 'Om', 8.2, 3]
A hint

Each branch is ranked on its own, so the window needs PARTITION BY branch and an ORDER BY from the highest CGPA down. Of the ranking functions, one leaves a gap after a tie (1, 2, 2, 4) and one does not (1, 2, 2, 3): the places here have no gaps.

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 The values 50, 40, 40 and 30 are ranked from the highest down. What does rank() return for 30?

    Type a number.

    Show the answer to question 1

    Answer: 4

    Three rows come before 30: the 50 and the two tied 40s. RANK is 1 plus the number of rows before the row's group of peers, so 30 is ranked 4, and the number 3 is skipped after the tie.

  2. Question 2 of 4 For the same values, 50, 40, 40 and 30 from the highest down, what does dense_rank() return for 30?

    Type a number.

    Show the answer to question 2

    Answer: 3

    DENSE_RANK counts the groups of equal values, not the rows: 50 is 1, both 40s are 2, and 30 is the third distinct value, so it gets 3, without a gap.

  3. Question 3 of 4 Which function gives exactly one row the number 1 in every partition, whatever the ties?

    Choose one answer.

    Show the answer to question 3

    Answer: row_number()

    ROW_NUMBER gives every row of the partition its own number, so exactly one row is 1. RANK and DENSE_RANK give all rows tied for the top the number 1, and NTILE(4) puts about a quarter of the rows in bucket 1.

  4. Question 4 of 4 ntile(3) runs over a partition of 8 rows. How many rows go into buckets 1, 2 and 3?

    Choose one answer.

    Show the answer to question 4

    Answer: 3, 3 and 2

    8 ÷ 3 is 2 with 2 left over, so every bucket gets 2 rows and the first two buckets get one more each. The larger buckets always come first.

Ties and tie-breakers: numbering that stays put, reproduced Pro: opens with a Pro, Premium or Ultimate pass

A ROW_NUMBER over tied values is only as stable as the plan that produced it. This part renumbers tied rows without changing any data, fixes the numbering with a tie-breaker, and uses the fixed form to delete duplicate rows.

In this part:

  • An index that renumbers tied students
  • A unique tie-breaker that fixes the numbers
  • Deleting duplicates while keeping the latest row per key
  • Which function answers “the top three”

4 runnable examples (SQL) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Buckets and percentiles with NTILE, PERCENT_RANK and CUME_DIST, with numbers Pro: opens with a Pro, Premium or Ultimate pass

NTILE, PERCENT_RANK and CUME_DIST all say where a row sits in its window, and they disagree whenever rows tie. This part works out the bucket sizes and both fractions by hand, then gets three different answers to “the top 10 % of customers” from the same twenty rows.

In this part:

  • How NTILE shares out rows that do not divide evenly
  • PERCENT_RANK and CUME_DIST, worked by hand
  • The top 10 % of customers, three ways
  • The same functions in PostgreSQL, MySQL and SQLite

3 runnable examples (SQL) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    For the values 30, 20, 20 and 10, ranked from the highest down, what do ROW_NUMBER, RANK and DENSE_RANK return, and when would you use each?

    ROW_NUMBER gives 1, 2, 3 and 4: every row a number of its own, with the two 20s in no defined order unless a tie-breaker column is added to the ORDER BY. RANK gives 1, 2, 2 and 4: tied rows share a place and the next place is skipped, as in a race. DENSE_RANK gives 1, 2, 2 and 3: tied rows share a place and no number is skipped, so the numbers count the distinct values. Use ROW_NUMBER to pick exactly one row per number, for example one row per key when removing duplicates; RANK for places that must show how many rows are ahead; and DENSE_RANK for questions about distinct levels, such as the second-highest value.

  2. Fresher to mid

    Return the second-highest package of each branch. In CSE two students share the highest package, 18.5, and the next package is 12.0. What does your query return for CSE, and what should it return?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A nightly job marks each customer's first order with ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date) = 1. After a database upgrade, a few hundred customers have a different first order, although no order changed. What happened, and how do you make it impossible?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    Marketing wants the top 10 % of 1,000 customers by spend for a campaign, and spend is often tied at round amounts. Compare NTILE(10), PERCENT_RANK, CUME_DIST and a ROW_NUMBER cut, and choose one.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • ROW_NUMBER numbers every row, RANK gives ties the same place and skips after them, DENSE_RANK gives ties the same place without gaps, and NTILE(n) deals the rows into n buckets.
  • PARTITION BY ranks each group on its own; the numbers start again at 1 in every partition.
  • ROW_NUMBER and NTILE split ties in whatever order the rows arrive, so give them a unique tie-breaker such as the primary key.
  • PERCENT_RANK and CUME_DIST place a row as a fraction of its window and treat peers alike.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.