Verified by actually running this against SQLite — not hand-computed.
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT NOT NULL,
salary INTEGER NOT NULL
);
INSERT INTO employees (emp_id, name, department, salary) VALUES
(1, 'Alice Chen', 'Engineering', 95000),
(2, 'Bob Martinez', 'Engineering', 90000),
(3, 'Carol Nguyen', 'Engineering', 90000),
(4, 'Dave Okafor', 'Engineering', 88000),
(5, 'Eve Patel', 'Engineering', 85000),
(6, 'Frank Lee', 'Sales', 82000),
(7, 'Grace Kim', 'Sales', 82000),
(8, 'Heidi Wagner', 'Sales', 82000),
(9, 'Ivan Petrov', 'Sales', 79000),
(10, 'Judy Alvarez', 'Marketing', 91000),
(11, 'Mallory Singh', 'Marketing', 91000),
(12, 'Niaj Rahman', 'Marketing', 87000),
(13, 'Olivia Brooks', 'Marketing', 84000),
(14, 'Peggy Torres', 'Marketing', 84000),
(15, 'Sybil Costa', 'Marketing', 80000);
SELECT department, name, salary,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS row_num
FROM employees
ORDER BY department, salary DESC;
| department | name | salary | row_num |
|---|---|---|---|
| Engineering | Alice Chen | 95000 | 1 |
| Engineering | Bob Martinez | 90000 | 2 |
| Engineering | Carol Nguyen | 90000 | 3 |
| Engineering | Dave Okafor | 88000 | 4 |
| Engineering | Eve Patel | 85000 | 5 |
| Marketing | Judy Alvarez | 91000 | 1 |
| Marketing | Mallory Singh | 91000 | 2 |
| Marketing | Niaj Rahman | 87000 | 3 |
| Marketing | Olivia Brooks | 84000 | 4 |
| Marketing | Peggy Torres | 84000 | 5 |
| Marketing | Sybil Costa | 80000 | 6 |
| Sales | Frank Lee | 82000 | 1 |
| Sales | Grace Kim | 82000 | 2 |
| Sales | Heidi Wagner | 82000 | 3 |
| Sales | Ivan Petrov | 79000 | 4 |
Notice Bob and Carol — tied at 90000 — still get different numbers (2 and 3). ROW_NUMBER() doesn't know or care about the tie.
SELECT department, name, salary,
RANK() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS rnk
FROM employees
ORDER BY department, salary DESC;
| department | name | salary | rnk |
|---|---|---|---|
| Engineering | Alice Chen | 95000 | 1 |
| Engineering | Bob Martinez | 90000 | 2 |
| Engineering | Carol Nguyen | 90000 | 2 |
| Engineering | Dave Okafor | 88000 | 4 |
| Engineering | Eve Patel | 85000 | 5 |
| Marketing | Judy Alvarez | 91000 | 1 |
| Marketing | Mallory Singh | 91000 | 1 |
| Marketing | Niaj Rahman | 87000 | 3 |
| Marketing | Olivia Brooks | 84000 | 4 |
| Marketing | Peggy Torres | 84000 | 4 |
| Marketing | Sybil Costa | 80000 | 6 |
| Sales | Frank Lee | 82000 | 1 |
| Sales | Grace Kim | 82000 | 1 |
| Sales | Heidi Wagner | 82000 | 1 |
| Sales | Ivan Petrov | 79000 | 4 |
In Sales, three people tie for rank 1 — and the next rank is 4, not 2. RANK() leaves a gap the size of the tie group.
SELECT department, name, salary,
DENSE_RANK() OVER (
PARTITION BY department
ORDER BY salary DESC
) AS dense_rnk
FROM employees
ORDER BY department, salary DESC;
| department | name | salary | dense_rnk |
|---|---|---|---|
| Engineering | Alice Chen | 95000 | 1 |
| Engineering | Bob Martinez | 90000 | 2 |
| Engineering | Carol Nguyen | 90000 | 2 |
| Engineering | Dave Okafor | 88000 | 3 |
| Engineering | Eve Patel | 85000 | 4 |
| Marketing | Judy Alvarez | 91000 | 1 |
| Marketing | Mallory Singh | 91000 | 1 |
| Marketing | Niaj Rahman | 87000 | 2 |
| Marketing | Olivia Brooks | 84000 | 3 |
| Marketing | Peggy Torres | 84000 | 3 |
| Marketing | Sybil Costa | 80000 | 4 |
| Sales | Frank Lee | 82000 | 1 |
| Sales | Grace Kim | 82000 | 1 |
| Sales | Heidi Wagner | 82000 | 1 |
| Sales | Ivan Petrov | 79000 | 2 |
Same three-way tie in Sales — but the next rank is 2, not 4. No gaps, ever.
SELECT department, name, salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rnk
FROM employees
ORDER BY department, salary DESC;
| department | name | salary | row_num | rnk | dense_rnk |
|---|---|---|---|---|---|
| Engineering | Alice Chen | 95000 | 1 | 1 | 1 |
| Engineering | Bob Martinez | 90000 | 2 | 2 | 2 |
| Engineering | Carol Nguyen | 90000 | 3 | 2 | 2 |
| Engineering | Dave Okafor | 88000 | 4 | 4 | 3 |
| Engineering | Eve Patel | 85000 | 5 | 5 | 4 |
| Marketing | Judy Alvarez | 91000 | 1 | 1 | 1 |
| Marketing | Mallory Singh | 91000 | 2 | 1 | 1 |
| Marketing | Niaj Rahman | 87000 | 3 | 3 | 2 |
| Marketing | Olivia Brooks | 84000 | 4 | 4 | 3 |
| Marketing | Peggy Torres | 84000 | 5 | 4 | 3 |
| Marketing | Sybil Costa | 80000 | 6 | 6 | 4 |
| Sales | Frank Lee | 82000 | 1 | 1 | 1 |
| Sales | Grace Kim | 82000 | 2 | 1 | 1 |
| Sales | Heidi Wagner | 82000 | 3 | 1 | 1 |
| Sales | Ivan Petrov | 79000 | 4 | 4 | 2 |
Every row where the three columns diverge is a tie. Watch Marketing: two separate tie groups (91000 and 84000) — DENSE_RANK compresses both gaps, RANK leaves both.
| Function | Ties | After a tie | Typical use |
|---|---|---|---|
| ROW_NUMBER() | Always unique | Never skips (nothing to skip) | Dedup, pagination, "latest per key" |
| RANK() | Same rank | Skips by tie-group size | Competition-style ranking |
| DENSE_RANK() | Same rank | Never skips | Tiering, "top N distinct values" |