How does NTILE(4) distribute rows when the row count is not divisible by 4?
answer
- think row counts, not value ranges
- there is a remainder to place somewhere
- early buckets are the fat ones
- fewer rows than buckets is legal
- equal values need not share a bucket
basics
~20 sNTILE(n) splits the ordered partition into n buckets as evenly as possible: with R rows, the first R mod n buckets get one extra row each. Ten rows into four buckets gives sizes 3, 3, 2, 2.
solid answer
~40 s`NTILE(n)` labels each row of an ordered partition with a bucket number from 1 to n, dividing by **row position**, not by value. With R rows, let q be R divided by n and r the remainder: the first r buckets receive q+1 rows and the rest receive q, so the larger buckets always come first in the window's `ORDER BY` sequence. Ten rows into four buckets gives 3, 3, 2, 2. If R is smaller than n, the first R buckets get one row each and the remaining bucket numbers simply never appear — three rows with `NTILE(5)` yield 1, 2, 3. Because the split is positional, two rows with identical ordering values can land on opposite sides of a boundary, and which one crosses is arbitrary unless the ordering is unique.
code
sql · 5 lines-- 10 rows, 4 buckets -> sizes 3, 3, 2, 2
SELECT customer_id, total_spend,
NTILE(4) OVER (ORDER BY total_spend DESC) AS spend_quartile
FROM customer_totals;
-- bucket 1 = the three highest spendersgo deeper
Know that NTILE(n) tags each row with a bucket number from 1 to n and that bucket 1 is whichever end the window ORDER BY puts first. Remember the buckets differ by at most one row.
State the remainder rule exactly — the first R mod n buckets get one extra row — and explain that the cut is positional, so tied values can be split across a boundary.
Be ready for the small-partition and tied-value cases in real data, and to say plainly that equal bucket sizes and equal value ranges are mutually exclusive guarantees.
Own the definition question: publishing a quartile means committing to equal-count cohorts whose value boundaries drift with the population, which is a different metric from a fixed-threshold segment and should be chosen deliberately.
## What NTILE(n) does `NTILE(n)` takes the rows of a window partition in the window's `ORDER BY` sequence and tags each one with an integer from 1 to n, so that the buckets are as equal in **row count** as possible. It is the SQL spelling of "split this population into quartiles/deciles/percentiles". The argument n must be a positive integer. The key word is *count*. `NTILE` does not look at how far apart the values are; it counts rows and cuts. A bucket is a slice of the ordered sequence, not a range of values. ## The exact distribution rule Let R be the number of rows in the partition and n the number of buckets. Write R = q·n + r with 0 ≤ r < n. Then the first r buckets contain q+1 rows each, and the remaining n−r buckets contain q rows each. In other words, the extra rows go to the **lowest-numbered** buckets — the ones that come first in the window ordering. Examples: - R = 10, n = 4: q = 2, r = 2 → sizes 3, 3, 2, 2. - R = 12, n = 4: q = 3, r = 0 → sizes 3, 3, 3, 3. - R = 7, n = 3: q = 2, r = 1 → sizes 3, 2, 2. ## Fewer rows than buckets When R < n, q is 0 and r is R, so the first R buckets get one row each and the remaining buckets get zero. Three rows with `NTILE(5)` produce the bucket numbers 1, 2 and 3; buckets 4 and 5 exist only conceptually and never appear in the output. This is not an error, and it is a routine surprise when `NTILE(10)` is applied per partition and some partitions are small — a customer with four orders gets deciles 1 to 4 only. ## Ties straddle boundaries Because the cut is positional, `NTILE` gives no special treatment to rows that the `ORDER BY` cannot distinguish. If a boundary falls in the middle of a run of equal values, some of those rows land in bucket k and the rest in bucket k+1 — and which ones cross is exactly as arbitrary as `ROW_NUMBER()`'s tie handling. Adding a unique tiebreaker to the window `ORDER BY` makes the split reproducible across runs, but it does not make equal values share a bucket; nothing about `NTILE` can do that, because the bucket sizes are fixed by construction. That is the trade-off to state in an interview: `NTILE` guarantees near-equal bucket **sizes** and therefore cannot also guarantee that equal **values** share a bucket. If your requirement is the second one, `NTILE` is the wrong tool. ## Writing it ```sql SELECT customer_id, total_spend, NTILE(4) OVER (ORDER BY total_spend DESC) AS spend_quartile FROM customer_totals; ``` A `PARTITION BY` makes the bucketing independent per group, and the size rule then applies within each partition separately: ```sql SELECT region, customer_id, total_spend, NTILE(4) OVER (PARTITION BY region ORDER BY total_spend DESC) AS regional_quartile FROM customer_totals; ``` As with the other ranking functions, an `NTILE` result is determined by the partition and the window `ORDER BY` alone; there is no frame to specify. Some engines require an `ORDER BY` in the `OVER` clause for ranking functions while others accept its absence and then produce an arbitrary assignment — engines differ, so specify the ordering explicitly and the question never arises. ## Reading the output Bucket 1 is always the first slice in the window ordering. With `ORDER BY total_spend DESC`, bucket 1 is the highest spenders; with `ASC` it is the lowest. Reversing the sort silently inverts the meaning of every bucket label, which is a classic source of upside-down dashboards, so state the direction wherever the quartile is documented.
- What does NTILE(5) return for a partition holding only three rows?The bucket numbers 1, 2 and 3, one row each; buckets 4 and 5 never appear. With fewer rows than buckets the remainder rule puts one row in each of the first R buckets and leaves the rest empty. It is not an error, and it bites when NTILE is applied per partition and some partitions are small.
- Can NTILE guarantee that rows with identical ordering values share a bucket?No. The bucket sizes are fixed by row count, so a boundary can fall inside a run of equal values and split it. A unique tiebreaker in the window ORDER BY makes the split reproducible between runs, but it cannot keep peers together — that requirement needs value-based cut-points instead.
- How do you know whether bucket 1 is the top or the bottom of the population?From the window ORDER BY direction alone: bucket 1 is always the first slice in that ordering, so DESC makes it the highest values and ASC the lowest. Flipping the sort inverts every label, so record the direction alongside the metric definition.
saying these in an interview costs you the question
- Expects every NTILE bucket to hold exactly the same count
- Thinks the extra rows go to the last buckets
- Assumes equal values always land in one bucket
- Believes NTILE splits by value range, not row position
- Expects an error when rows are fewer than buckets