Home Functions ROW_NUMBER()
SQL Window Function

ROW_NUMBER()

ROW_NUMBER() is a window function that assigns a unique sequential integer to each row within a partition, based on the ordering defined in its OVER() clause.

MySQLPostgreSQLSQL ServerSQLite
Returns: A BIGINT: a unique sequential integer starting at 1 for each row within its partition, following the ORDER BY inside OVER().

Syntax

ROW_NUMBER() OVER (ORDER BY sort_expression)ROW_NUMBER() OVER (PARTITION BY group_expression ORDER BY sort_expression)
ParameterTypeRequiredDescription
(no arguments) none yes ROW_NUMBER() takes no arguments; the parentheses are always empty. All of its behaviour comes from the OVER() clause.
PARTITION BY group_expression column or expression no Restarts the numbering at 1 for each distinct group. Without it, the whole result is one partition.
ORDER BY sort_expression column or expression yes (in practice) Defines the order in which rows are numbered inside each partition. Omitting it makes the sequence meaningless.

How it works

ROW_NUMBER() is a ranking window function. For every row it returns a whole number, starting at 1, that reflects the row position within its partition according to the ORDER BY inside the OVER() clause. Unlike an aggregate, it does not collapse rows: every input row keeps its own output row and simply gains a number.

The key property of ROW_NUMBER() is that it never ties. Even when two rows have identical ordering values, they still receive different numbers, and the tie is broken arbitrarily unless you add a further column to the ORDER BY. This is what separates it from RANK() and DENSE_RANK(), which deliberately give tied rows the same value.

Add PARTITION BY and the counter resets to 1 at the start of each group, which is what makes ROW_NUMBER() the standard tool for top-N-per-group queries, deduplication and stable pagination. It is available in MySQL 8.0 and later, PostgreSQL, SQL Server and SQLite 3.25 and later. Older MySQL 5.x and pre-3.25 SQLite do not support window functions at all.

Examples

Number every row in a result set

SELECT ROW_NUMBER() OVER (ORDER BY created_at) AS rn,
       id, created_at
FROM orders
ORDER BY created_at;
Result
rn | id | created_at
---+----+-----------
 1 |  7 | 2026-01-02
 2 |  3 | 2026-01-02
 3 |  9 | 2026-01-05

Number rows within each group (PARTITION BY)

-- Restart the counter for each customer
SELECT customer_id, id, amount,
       ROW_NUMBER() OVER (
         PARTITION BY customer_id
         ORDER BY amount DESC) AS rn
FROM orders
ORDER BY customer_id, rn;
Result
customer_id | id | amount | rn
------------+----+--------+---
        101 | 12 |    900 |  1
        101 |  4 |    350 |  2
        102 | 18 |    500 |  1

Top-N per group (keep the highest order per customer)

-- ROW_NUMBER() cannot go in WHERE, so wrap it in a CTE
WITH ranked AS (
  SELECT customer_id, id, amount,
         ROW_NUMBER() OVER (
           PARTITION BY customer_id
           ORDER BY amount DESC) AS rn
  FROM orders
)
SELECT customer_id, id, amount
FROM ranked
WHERE rn = 1;
Result
customer_id | id | amount
------------+----+-------
        101 | 12 |    900
        102 | 18 |    500
        103 | 22 |    740

Deduplicate: keep the first row of each group

-- Keep one row per email, the most recently updated one
WITH dedup AS (
  SELECT id, email, updated_at,
         ROW_NUMBER() OVER (
           PARTITION BY email
           ORDER BY updated_at DESC) AS rn
  FROM customers
)
SELECT id, email, updated_at
FROM dedup
WHERE rn = 1;
Result
id | email           | updated_at
---+-----------------+-----------
 5 | ali@example.com | 2026-06-30
 8 | sara@shop.com   | 2026-07-01

Keyset-friendly pagination

-- Number rows in a stable order, then slice a page
WITH numbered AS (
  SELECT id, title,
         ROW_NUMBER() OVER (ORDER BY created_at, id) AS rn
  FROM articles
)
SELECT id, title
FROM numbered
WHERE rn BETWEEN 21 AND 30
ORDER BY rn;
Result
id  | title
----+------------------
 41 | Indexing basics
 42 | Joins explained
 43 | Window functions

Common mistakes

Wrong
-- No ORDER BY: the numbering is meaningless
-- and can differ on every run
SELECT id,
       ROW_NUMBER() OVER () AS rn
FROM orders;
Right
-- Always give OVER() an ORDER BY so the
-- sequence is defined and repeatable
SELECT id,
       ROW_NUMBER() OVER (ORDER BY created_at) AS rn
FROM orders;

ROW_NUMBER() only makes sense with an ORDER BY inside OVER(). Without one, the database is free to number rows in any order, so the result is not deterministic.

Wrong
-- Window functions are not allowed in WHERE
SELECT customer_id, id, amount
FROM orders
WHERE ROW_NUMBER() OVER (
        PARTITION BY customer_id
        ORDER BY amount DESC) = 1;
Right
-- Compute it in a subquery or CTE, then filter
WITH ranked AS (
  SELECT customer_id, id, amount,
         ROW_NUMBER() OVER (
           PARTITION BY customer_id
           ORDER BY amount DESC) AS rn
  FROM orders
)
SELECT customer_id, id, amount
FROM ranked
WHERE rn = 1;

WHERE runs before window functions are evaluated, so you cannot reference ROW_NUMBER() there. Wrap the query in a CTE or subquery and filter on the alias.

Wrong
-- ORDER BY has ties, so rows with the same amount
-- get numbered in an unpredictable order
SELECT id, amount,
       ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn
FROM orders;
Right
-- Add a unique tiebreaker for a stable result
SELECT id, amount,
       ROW_NUMBER() OVER (
         ORDER BY amount DESC, id) AS rn
FROM orders;

When the ORDER BY can tie, ROW_NUMBER() still assigns different numbers but picks the order arbitrarily. Add a unique column such as the primary key so the numbering is deterministic.

Performance

ROW_NUMBER() requires the engine to sort each partition according to the ORDER BY inside OVER(). On a large table with no supporting index this sort dominates the cost, exactly like an ORDER BY on the whole result set.

An index whose columns match the PARTITION BY keys followed by the ORDER BY keys lets the optimiser read rows already in order and skip the sort entirely. For example, a partition on customer_id ordered by amount DESC is served well by an index on (customer_id, amount).

For top-N-per-group queries the CTE plus WHERE rn = 1 pattern still numbers every row before filtering. If N is tiny and the group count is huge, a correlated subquery or a lateral join can sometimes be cheaper, but ROW_NUMBER() is usually the clearest and fast enough choice. See the window functions guide for more patterns.

Interview questions

What is the difference between ROW_NUMBER(), RANK() and DENSE_RANK()?

ROW_NUMBER() always gives a unique number, so tied rows still differ. RANK() gives tied rows the same rank and then leaves a gap (1, 1, 3). DENSE_RANK() gives tied rows the same rank with no gap (1, 1, 2).

How do you select the top row per group, for example the highest paid employee in each department?

Number the rows with ROW_NUMBER() partitioned by the group and ordered by the value, then keep the rows where the number is 1. Because you cannot filter a window function in WHERE, do it in a CTE or subquery.

WITH ranked AS (
  SELECT department_id, name, salary,
         ROW_NUMBER() OVER (
           PARTITION BY department_id
           ORDER BY salary DESC) AS rn
  FROM employees
)
SELECT department_id, name, salary
FROM ranked
WHERE rn = 1;

How would you remove duplicate rows but keep one copy of each?

Assign a ROW_NUMBER() partitioned by the columns that define a duplicate, ordered so the row you want to keep is number 1. Keep rn = 1, or delete the rows where rn > 1.

WITH dedup AS (
  SELECT id,
         ROW_NUMBER() OVER (
           PARTITION BY email
           ORDER BY updated_at DESC) AS rn
  FROM customers
)
DELETE FROM customers
WHERE id IN (SELECT id FROM dedup WHERE rn > 1);

Why can you not put ROW_NUMBER() in a WHERE clause?

Window functions are evaluated after WHERE and GROUP BY, near the end of query processing. So at the point WHERE runs, the row number does not exist yet. You must compute it in a subquery or CTE and filter on the result.

Does ROW_NUMBER() need an ORDER BY, and what happens without one?

In practice yes. Without an ORDER BY inside OVER(), the numbers are assigned in an unspecified order and can change between runs, which makes the result non-deterministic and usually useless.

Master SQL, one function at a time

Browse the full SQL functions library, or learn the fundamentals with our free, structured courses.