Home Functions NTILE()
SQL Window Function

NTILE()

NTILE() is a window function that divides the ordered rows into n roughly equal buckets and labels each row with its bucket number from 1 to n.

MySQLPostgreSQLSQL ServerSQLite
Returns: A positive integer bucket number from 1 to n. Every row in the window gets exactly one bucket label.

Syntax

NTILE(n) OVER (ORDER BY sort_expression)NTILE(n) OVER (PARTITION BY group_expression ORDER BY sort_expression)
ParameterTypeRequiredDescription
n positive integer yes The number of buckets to split the ordered rows into. Must be greater than 0.
ORDER BY clause yes Defines the order of the rows before they are divided into buckets. NTILE has no meaning without it.
PARTITION BY clause no Restarts the bucketing for each group, so every partition is split into its own n buckets.

How it works

NTILE(n) takes the rows in a window, puts them in the order you specify, and splits them into n contiguous buckets of as-equal-as-possible size. Each row is stamped with the number of the bucket it landed in, from 1 for the first bucket up to n for the last. It is the standard tool for building quartiles, deciles and other equal-size groupings.

The most common uses are NTILE(4) for quartiles, NTILE(10) for deciles, and NTILE(100) for percentiles, but any value works: NTILE(3) splits a customer list into three tiers. Unlike RANK(), which reacts to ties in the value, NTILE only cares about position, so it fills buckets purely by row order.

When the row count does not divide evenly by n, NTILE puts the extra rows in the earlier buckets. If 10 rows are split into 3 buckets, the sizes are 4, 3 and 3, not 3, 3, 4. This is defined behaviour, not a bug, and it is the same across every engine. Like all window functions, NTILE requires an OVER() clause and needs MySQL 8.0+ or SQLite 3.25+; PostgreSQL and SQL Server have supported it for years.

Examples

Quartiles of order value

SELECT id, amount,
       NTILE(4) OVER (ORDER BY amount) AS quartile
FROM orders;
Result
id | amount | quartile
---+--------+---------
 5 |     12 |        1
 3 |     40 |        1
 8 |     75 |        2
 1 |     90 |        3
 7 |    150 |        4

Deciles of test scores

-- split students into 10 equal bands, band 1 = lowest
SELECT name, score,
       NTILE(10) OVER (ORDER BY score DESC) AS decile
FROM results;
Result
name  | score | decile
------+-------+-------
 Sara |    99 |      1
 Omar |    95 |      1
 Lina |    88 |      2
 Ali  |    71 |      5
 Noor |    44 |     10

Split customers into 3 tiers

SELECT name, total_spent,
       CASE NTILE(3) OVER (ORDER BY total_spent DESC)
            WHEN 1 THEN 'gold'
            WHEN 2 THEN 'silver'
            ELSE 'bronze'
       END AS tier
FROM customers;
Result
name  | total_spent | tier
------+-------------+-------
 Omar |        9800 | gold
 Sara |        6100 | gold
 Lina |        4200 | silver
 Ali  |        3000 | silver
 Noor |         900 | bronze

Bucket within each group with PARTITION BY

-- quartiles computed separately per country
SELECT country, name, revenue,
       NTILE(4) OVER (PARTITION BY country
                      ORDER BY revenue DESC) AS q
FROM sales;
Result
country | name | revenue | q
--------+------+---------+---
 UAE    | A    |    5000 | 1
 UAE    | B    |    3000 | 2
 UAE    | C    |    1000 | 3
 KSA    | D    |    8000 | 1
 KSA    | E    |    2000 | 2

Uneven split shows extra rows in early buckets

-- 5 rows into 2 buckets -> sizes 3 and 2
SELECT n,
       NTILE(2) OVER (ORDER BY n) AS bucket
FROM (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3
      UNION SELECT 4 UNION SELECT 5) t;
Result
n | bucket
--+-------
1 |      1
2 |      1
3 |      1
4 |      2
5 |      2

Common mistakes

Wrong
-- Expecting perfectly equal buckets
-- 10 rows / 3 does NOT give 3, 3, 4
SELECT id,
       NTILE(3) OVER (ORDER BY id) AS bucket
FROM ten_rows;  -- sizes are 4, 3, 3
Right
-- This is correct behaviour: earlier buckets get
-- the extra rows first. Do not fight it.
SELECT id,
       NTILE(3) OVER (ORDER BY id) AS bucket
FROM ten_rows;  -- buckets: 4 rows, 3 rows, 3 rows

Uneven buckets are expected. When the row count is not a multiple of n, NTILE gives the earlier buckets one extra row each. This is defined and consistent across engines, so plan for slightly larger low-numbered buckets rather than treating it as an error.

Wrong
-- Missing ORDER BY: the result is meaningless
SELECT id,
       NTILE(4) OVER () AS quartile
FROM orders;
Right
SELECT id,
       NTILE(4) OVER (ORDER BY amount) AS quartile
FROM orders;

NTILE needs an ORDER BY inside OVER(). Without it the row order is undefined, so the bucket numbers are arbitrary and can change between runs. Always order by the value you are bucketing on.

Wrong
-- Treating the bucket number as a real percentile value
SELECT amount,
       NTILE(100) OVER (ORDER BY amount) AS pct
FROM orders;  -- pct is a bucket label, not a cutoff
Right
-- For an actual percentile value use a percentile function
SELECT PERCENTILE_CONT(0.9)
         WITHIN GROUP (ORDER BY amount) AS p90
FROM orders;

Do not confuse NTILE buckets with exact percentile values. NTILE(100) returns a group number from 1 to 100, not the amount at the 90th percentile. Use PERCENTILE_CONT or PERCENTILE_DISC when you need the actual cutoff value.

Performance

NTILE() requires the engine to order the rows in the window before it can assign buckets, so its cost is dominated by that sort. If an index already provides the rows in the ORDER BY order, the optimiser can skip the sort and the function becomes very cheap.

With PARTITION BY the sort happens once per partition, which is usually cheaper than one giant sort. Keeping the partition and order columns covered by an index lets the database stream rows straight into the buckets instead of spilling a large sort to disk.

NTILE only needs to know the total row count per partition and each row position, so it does not carry the heavy state of a running aggregate. On large result sets the practical limit is memory for the sort; see the indexing guide for how ordered scans avoid it.

Interview questions

What does NTILE(n) do?

It divides the ordered rows of a window into n roughly equal buckets and labels each row with its bucket number from 1 to n. It is the standard way to build quartiles, deciles and percentile groups.

What happens when the number of rows does not divide evenly by n?

The earlier buckets each receive one extra row. For example 10 rows into 3 buckets gives bucket sizes of 4, 3 and 3. This is defined behaviour and is consistent across MySQL, PostgreSQL, SQL Server and SQLite.

How is NTILE different from RANK and ROW_NUMBER?

ROW_NUMBER() gives every row a unique sequential number, and RANK() gives tied values the same rank. NTILE ignores tie handling entirely and instead groups rows into n equal-size buckets by position, returning a bucket label rather than a per-row rank.

How would you split customers into quartiles by spend?

Use NTILE(4) ordered by the spend column. Bucket 1 through 4 then represents the four quartiles. Order descending if you want bucket 1 to be the highest spenders.

SELECT name, total_spent,
       NTILE(4) OVER (ORDER BY total_spent DESC) AS quartile
FROM customers;

Does NTILE require an ORDER BY clause?

Yes. Without ORDER BY inside OVER() the row order is undefined, so the bucket assignment is arbitrary and can change between executions. NTILE is only meaningful on ordered rows.

Can NTILE give me the exact value at the 90th percentile?

No. NTILE returns a bucket number, not a value. For the actual cutoff value use a percentile function such as PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY amount).

Master SQL, one function at a time

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