sqlpostgresqlntilewindow-functions

NTILE в SQL: как делить строки на квартили, децили и равные когорты

Как NTILE(n) делит упорядоченные строки на почти равные корзины, куда попадает остаток и почему это не то же самое, что WIDTH_BUCKET.

10 мин чтенияСправочникsql · postgresql · ntile · window-functions · analytics

В SQL часто нужно не просто отсортировать строки, а разделить их на несколько примерно равных групп.

Например:

  • выделить топ-25% клиентов по сумме покупок;
  • разбить сотрудников на зарплатные квартили;
  • разделить пользователей на 10 групп по активности;
  • получить децили по выручке;
  • построить ранговые сегменты для продуктовой аналитики.

Для таких задач есть оконная функция NTILE.

Она отвечает на вопрос:

Как разделить отсортированный список строк на n почти одинаковых по размеру групп?

Например:

NTILE(4)

делит строки на 4 группы.

Такие группы часто называют квартилями.

NTILE(10)

делит строки на 10 групп.

Такие группы называют децилями.

Важно: NTILE делит не диапазон значений, а сами строки.

То есть функция старается сделать так, чтобы в каждой корзине было примерно одинаковое количество строк.

Что делает NTILE простыми словами

Представим, что у нас есть 10 клиентов, отсортированных по сумме покупок от большего к меньшему.

client | spent
-------+------
A      | 9000
B      | 8000
C      | 7000
D      | 6000
E      | 5000
F      | 4000
G      | 3000
H      | 2000
I      | 1000
J      | 500

Если применить NTILE(4), SQL разделит эти 10 строк на 4 группы.

Так как 10 не делится на 4 ровно, группы будут такими:

bucket 1: 3 строки
bucket 2: 3 строки
bucket 3: 2 строки
bucket 4: 2 строки

Результат:

client | spent | quartile
-------+-------+---------
A      | 9000  | 1
B      | 8000  | 1
C      | 7000  | 1
D      | 6000  | 2
E      | 5000  | 2
F      | 4000  | 2
G      | 3000  | 3
H      | 2000  | 3
I      | 1000  | 4
J      | 500   | 4

Если сортировка идёт по убыванию:

ORDER BY spent DESC

то корзина 1 — это верхняя группа, то есть самые дорогие клиенты.

Если сортировка идёт по возрастанию:

ORDER BY spent ASC

то корзина 1 будет нижней группой, то есть самые маленькие значения.

Базовый синтаксис NTILE

NTILE — это оконная функция.

Базовый синтаксис:

NTILE(number_of_buckets) OVER (
  ORDER BY column
)

Например:

NTILE(4) OVER (ORDER BY spent DESC)

означает:

отсортируй строки по spent от большего к меньшему и раздели их на 4 примерно равные группы.

Пример с таблицей клиентов:

SELECT
  id,
  email,
  spent,
  NTILE(4) OVER (ORDER BY spent DESC) AS quartile
FROM customers;

Результат:

id | email         | spent | quartile
---+---------------+-------+---------
1  | anna@mail.com | 9000  | 1
2  | bob@mail.com  | 8000  | 1
3  | kate@mail.com | 7000  | 1
4  | tom@mail.com  | 6000  | 2

Здесь quartile показывает, в какую четверть отсортированного списка попала строка.

NTILE — это оконная функция

NTILE не схлопывает строки, как GROUP BY.

Он добавляет к каждой строке номер группы.

Например, обычный GROUP BY уменьшает количество строк:

SELECT
  country,
  SUM(amount) AS revenue
FROM orders
GROUP BY country;

А NTILE оставляет строки и добавляет к ним вычисленное значение:

SELECT
  id,
  email,
  spent,
  NTILE(4) OVER (ORDER BY spent DESC) AS quartile
FROM customers;

То есть NTILE работает «поверх» результата и присваивает каждой строке номер корзины.

Это удобно, когда нужно не потерять отдельные строки, а просто разметить их по сегментам.

Пример: квартили клиентов по сумме покупок

Допустим, у нас есть таблицы users и orders.

Нужно разбить пользователей на 4 группы по сумме оплаченных заказов.

Сначала посчитаем траты каждого пользователя, а потом применим NTILE.

WITH user_spending AS (
  SELECT
    u.id,
    u.email,
    SUM(o.amount) AS spent
  FROM users u
  JOIN orders o ON o.user_id = u.id
  WHERE o.status = 'paid'
  GROUP BY u.id, u.email
)
SELECT
  id,
  email,
  spent,
  NTILE(4) OVER (ORDER BY spent DESC, id) AS quartile
FROM user_spending
ORDER BY quartile, spent DESC;

Здесь:

SUM(o.amount) AS spent

считает сумму оплаченных заказов пользователя.

А:

NTILE(4) OVER (ORDER BY spent DESC, id)

делит пользователей на 4 группы по убыванию трат.

Корзина 1 — самые дорогие клиенты.

Корзина 4 — клиенты с самыми маленькими тратами.

Почему в ORDER BY добавлен id?

ORDER BY spent DESC, id

Потому что если у двух пользователей одинаковая сумма, нужен стабильный дополнительный порядок. Без него база может расположить равные строки по-разному, и граница между корзинами начнёт плавать.

Почему важен ORDER BY

NTILE полностью зависит от сортировки.

Например:

NTILE(4) OVER (ORDER BY spent DESC)

и:

NTILE(4) OVER (ORDER BY spent ASC)

дают разные результаты.

В первом случае первая корзина — самые большие значения.

Во втором случае первая корзина — самые маленькие значения.

Сравним:

ORDER BY spent DESC

spent | quartile
------+---------
9000  | 1
8000  | 1
7000  | 1
...
500   | 4

А теперь:

ORDER BY spent ASC

spent | quartile
------+---------
500   | 1
1000  | 1
2000  | 1
...
9000  | 4

Поэтому перед использованием NTILE нужно честно ответить:

Верхняя корзина должна означать большие значения или маленькие?

Для топ-клиентов обычно пишут:

ORDER BY spent DESC

Для самых быстрых запросов по latency можно писать:

ORDER BY response_time_ms ASC

Потому что меньшее время ответа лучше.

Одинаковые значения и плавающие границы

Есть важная ловушка.

Если несколько строк имеют одинаковое значение в сортировке, база не обязана каждый раз располагать их в одном и том же порядке.

Например:

user_id | spent
--------+------
1       | 1000
2       | 1000
3       | 1000
4       | 900

Если написать:

NTILE(2) OVER (ORDER BY spent DESC)

пользователи с одинаковым spent = 1000 могут оказаться на границе корзин.

Чтобы результат был стабильным, добавляйте тай-брейкер — дополнительную колонку для сортировки.

Например:

NTILE(2) OVER (ORDER BY spent DESC, user_id)

Теперь при одинаковом spent строки будут упорядочены по user_id.

Главное правило:

В ORDER BY для NTILE лучше добавлять уникальный тай-брейкер, если результат должен быть стабильным.

Как NTILE распределяет остаток

Если количество строк не делится на количество корзин ровно, лишние строки попадают в корзины с меньшими номерами.

Например, 10 строк и NTILE(4):

10 / 4 = 2 строки в каждую корзину
остаток = 2 строки

Значит первые две корзины получат по одной дополнительной строке.

Распределение:

bucket 1: 3 строки
bucket 2: 3 строки
bucket 3: 2 строки
bucket 4: 2 строки

Другие примеры:

10 строк, NTILE(4) -> 3, 3, 2, 2
11 строк, NTILE(4) -> 3, 3, 3, 2
7 строк,  NTILE(3) -> 3, 2, 2
5 строк,  NTILE(2) -> 3, 2

Это поведение важно учитывать, если вы ожидаете строго равные группы.

NTILE делает группы максимально равными, но если строки не делятся без остатка, часть групп будет на одну строку больше.

Если корзин больше, чем строк

Допустим, у нас 7 строк, а мы пишем:

NTILE(10)

Что будет?

PostgreSQL не создаст пустые строки для корзин 8, 9 и 10.

Он просто присвоит существующим строкам номера с 1 по 7.

Пример:

row | ntile
----+------
1   | 1
2   | 2
3   | 3
4   | 4
5   | 5
6   | 6
7   | 7

Корзины 8, 9 и 10 в результате не появятся.

Это важная грабля для отчётов.

Не стоит писать код, который ожидает, что после NTILE(10) в результате обязательно будут все номера от 1 до 10.

Если строк меньше, чем корзин, часть корзин будет отсутствовать.

NTILE не ищет естественные границы

NTILE делит строки по количеству, а не по значениям.

Например, есть суммы:

100
101
102
103
10000
20000
30000
40000

Если применить NTILE(4), каждая корзина получит примерно одинаковое число строк.

Но диапазоны значений внутри корзин могут быть очень разными.

quartile 1: 40000, 30000
quartile 2: 20000, 10000
quartile 3: 103, 102
quartile 4: 101, 100

Это не значит, что SQL нашёл «естественные» денежные диапазоны.

Он просто разделил отсортированный список на равные части.

Главная мысль:

NTILE делит строки, а не шкалу значений.

Если вам нужны диапазоны вроде:

0-1000
1000-5000
5000-10000
10000+

то NTILE не подходит. Там лучше использовать CASE или WIDTH_BUCKET.

Пример: децили активности пользователей

Допустим, мы хотим разделить пользователей на 10 групп по количеству действий.

Есть таблица events:

id | user_id | event_name | created_at
---+---------+------------+---------------------
1  | 1       | click      | 2026-06-01 10:00:00
2  | 1       | view       | 2026-06-01 10:01:00
3  | 2       | click      | 2026-06-01 10:02:00

Сначала считаем количество событий по пользователю, потом делим на децили.

WITH user_activity AS (
  SELECT
    user_id,
    COUNT(*) AS events_count
  FROM events
  GROUP BY user_id
)
SELECT
  user_id,
  events_count,
  NTILE(10) OVER (ORDER BY events_count DESC, user_id) AS activity_decile
FROM user_activity
ORDER BY activity_decile, events_count DESC;

Корзина 1 — самые активные пользователи.

Корзина 10 — наименее активные.

Такой подход полезен для продуктовой аналитики: можно сравнивать поведение верхних и нижних сегментов.

PARTITION BY: корзины внутри каждой группы

NTILE можно считать не по всей таблице, а отдельно внутри каждой группы.

Для этого используется PARTITION BY.

Например, нужно разбить клиентов на децили по сумме покупок внутри каждой страны.

Это важно, потому что страны могут сильно отличаться по уровню платежей.

WITH user_spending AS (
  SELECT
    u.id,
    u.email,
    u.country,
    SUM(o.amount) AS spent
  FROM users u
  JOIN orders o ON o.user_id = u.id
  WHERE o.status = 'paid'
  GROUP BY u.id, u.email, u.country
)
SELECT
  country,
  id,
  email,
  spent,
  NTILE(10) OVER (
    PARTITION BY country
    ORDER BY spent DESC, id
  ) AS country_decile
FROM user_spending
ORDER BY country, country_decile, spent DESC;

Здесь PARTITION BY country означает:

для каждой страны отдельно отсортируй пользователей и раздели их на 10 групп.

Пользователи из Vietnam соревнуются только с пользователями из Vietnam. Пользователи из Germany — только с пользователями из Germany.

Это честнее, чем смешивать рынки с разным масштабом в одну общую очередь.

Пример: зарплатные квартили внутри отдела

Ещё один классический пример — зарплаты внутри отдела.

SELECT
  id,
  name,
  dept,
  salary,
  NTILE(4) OVER (
    PARTITION BY dept
    ORDER BY salary DESC, id
  ) AS pay_quartile
FROM employees
ORDER BY dept, pay_quartile, salary DESC;

Что делает запрос:

  • отдельно берёт каждый отдел;
  • сортирует сотрудников по зарплате;
  • делит сотрудников отдела на 4 группы;
  • присваивает каждому номер квартиля.

Корзина 1 — верхняя четверть зарплат внутри отдела.

Это важно: сотрудник из маленького отдела не сравнивается с сотрудником из другого отдела. Каждый отдел получает собственную шкалу.

NTILE против RANK

RANK и NTILE часто используют рядом, но они отвечают на разные вопросы.

RANK показывает место строки в сортировке.

RANK() OVER (ORDER BY spent DESC)

Например:

user | spent | rank
-----+-------+-----
A    | 9000  | 1
B    | 8000  | 2
C    | 8000  | 2
D    | 7000  | 4

NTILE показывает номер группы, в которую попала строка.

NTILE(4) OVER (ORDER BY spent DESC)

Пример:

user | spent | quartile
-----+-------+---------
A    | 9000  | 1
B    | 8000  | 1
C    | 8000  | 2
D    | 7000  | 2

RANK не пытается сделать группы одинакового размера. NTILE как раз пытается разложить строки по равным корзинам.

Коротко:

RANK   -- какое место у строки
NTILE  -- в какую равную группу попала строка

NTILE против ROW_NUMBER

ROW_NUMBER просто нумерует строки по порядку.

ROW_NUMBER() OVER (ORDER BY spent DESC)

Результат:

user | spent | row_number
-----+-------+-----------
A    | 9000  | 1
B    | 8000  | 2
C    | 7000  | 3

А NTILE не даёт уникальный номер каждой строке. Он даёт номер корзины.

user | spent | quartile
-----+-------+---------
A    | 9000  | 1
B    | 8000  | 1
C    | 7000  | 1
D    | 6000  | 2

То есть несколько строк могут иметь один и тот же quartile.

ROW_NUMBER полезен для топ-N и порядковой нумерации.

NTILE полезен для сегментации на равные группы.

NTILE против PERCENT_RANK и CUME_DIST

Есть ещё оконные функции, которые работают с относительной позицией строки:

PERCENT_RANK()
CUME_DIST()

Они возвращают не номер корзины, а относительное положение строки в распределении.

Например:

SELECT
  id,
  spent,
  PERCENT_RANK() OVER (ORDER BY spent) AS percent_rank,
  CUME_DIST() OVER (ORDER BY spent) AS cume_dist
FROM customers;

PERCENT_RANK показывает относительный ранг строки.

CUME_DIST показывает долю строк, которые меньше или равны текущей строке по сортировке.

А NTILE(4) просто делит строки на 4 группы.

Коротко:

NTILE        -- номер корзины
PERCENT_RANK -- относительный ранг
CUME_DIST    -- накопленная доля распределения

Если вам нужны понятные группы вроде «квартиль 1, 2, 3, 4», чаще берут NTILE.

NTILE против WIDTH_BUCKET

NTILE часто путают с WIDTH_BUCKET.

Обе функции как будто делят данные на корзины, но делают это по-разному.

NTILE(n) делит отсортированные строки на n примерно равных групп.

То есть выравнивает количество строк.

WIDTH_BUCKET(value, min, max, n) делит числовой диапазон от min до max на n равных интервалов.

То есть выравнивает ширину диапазонов.

Пример WIDTH_BUCKET:

SELECT
  id,
  amount,
  WIDTH_BUCKET(amount, 0, 1000, 4) AS price_band
FROM orders;

Диапазон 0-1000 делится на 4 равные части:

1: 0-250
2: 250-500
3: 500-750
4: 750-1000

Количество строк внутри этих диапазонов может быть любым.

Например, почти все заказы могут попасть в первый диапазон, если большинство сумм маленькие.

А NTILE(4) постарается разложить строки по четырём группам примерно поровну.

Коротко:

NTILE        -- равное количество строк
WIDTH_BUCKET -- равная ширина числовых интервалов

Когда использовать NTILE

NTILE подходит, когда вы хотите получить равные по размеру сегменты.

Например:

  • топ-25% клиентов;
  • нижние 25% пользователей по активности;
  • 10 децилей по выручке;
  • 4 квартиля по зарплате;
  • равные когорты по количеству событий;
  • ранговые группы для анализа.

Пример:

WITH user_spending AS (
  SELECT
    u.id,
    u.email,
    SUM(o.amount) AS spent
  FROM users u
  JOIN orders o ON o.user_id = u.id
  WHERE o.status = 'paid'
  GROUP BY u.id, u.email
)
SELECT
  *,
  NTILE(4) OVER (ORDER BY spent DESC, id) AS quartile
FROM user_spending;

Так можно выделить верхнюю четверть клиентов для маркетинговой кампании.

Но важно помнить: верхняя четверть — это относительное понятие. В плохой месяц абсолютные суммы могут быть ниже, но кто-то всё равно окажется в первой корзине.

Когда NTILE не подходит

NTILE не подходит, если вам нужны фиксированные бизнес-пороги.

Например:

VIP: spent >= 10000
Middle: spent >= 1000 and spent < 10000
Low: spent < 1000

Для такой логики лучше использовать CASE.

SELECT
  id,
  email,
  spent,
  CASE
    WHEN spent >= 10000 THEN 'vip'
    WHEN spent >= 1000 THEN 'middle'
    ELSE 'low'
  END AS segment
FROM user_spending;

Почему не NTILE?

Потому что NTILE не знает ваших бизнес-порогов. Он просто делит строки на равные группы.

Если у всех пользователей маленькие суммы, NTILE(4) всё равно выделит верхнюю четверть. Но это не значит, что эти пользователи стали VIP по бизнес-смыслу.

NTILE и эксперименты

Иногда NTILE используют, чтобы разделить аудиторию на равные группы.

Например:

SELECT
  user_id,
  NTILE(2) OVER (ORDER BY user_id) AS group_id
FROM users;

Так можно получить две примерно равные группы.

Но важно: это не полноценная случайная рандомизация.

Если сортировка идёт по user_id, дате регистрации, выручке или активности, группы могут системно отличаться.

Например:

NTILE(2) OVER (ORDER BY spent DESC)

первая группа будет состоять из более дорогих клиентов, а вторая — из менее дорогих.

Такой результат нельзя использовать как честное A/B-разделение на контроль и тест.

Для экспериментов нужна случайная или специально контролируемая рандомизация.

NTILE создаёт ранговые сегменты, а не случайные группы.

Как подписать корзины

Числа 1, 2, 3, 4 не всегда удобно отдавать в отчёт.

Можно добавить понятные подписи.

Например, для квартилей:

WITH user_spending AS (
  SELECT
    u.id,
    u.email,
    SUM(o.amount) AS spent
  FROM users u
  JOIN orders o ON o.user_id = u.id
  WHERE o.status = 'paid'
  GROUP BY u.id, u.email
),
ranked AS (
  SELECT
    *,
    NTILE(4) OVER (ORDER BY spent DESC, id) AS quartile
  FROM user_spending
)
SELECT
  id,
  email,
  spent,
  quartile,
  CASE quartile
    WHEN 1 THEN 'top 25%'
    WHEN 2 THEN 'upper middle'
    WHEN 3 THEN 'lower middle'
    WHEN 4 THEN 'bottom 25%'
  END AS quartile_label
FROM ranked;

Так результат становится понятнее для аналитика, менеджера или BI-отчёта.

Как посчитать размер каждой корзины

Иногда важно проверить, как именно NTILE разложил строки.

Можно сгруппировать результат по корзине.

WITH user_spending AS (
  SELECT
    u.id,
    u.email,
    SUM(o.amount) AS spent
  FROM users u
  JOIN orders o ON o.user_id = u.id
  WHERE o.status = 'paid'
  GROUP BY u.id, u.email
),
ranked AS (
  SELECT
    *,
    NTILE(4) OVER (ORDER BY spent DESC, id) AS quartile
  FROM user_spending
)
SELECT
  quartile,
  COUNT(*) AS users_count,
  MIN(spent) AS min_spent,
  MAX(spent) AS max_spent
FROM ranked
GROUP BY quartile
ORDER BY quartile;

Результат может быть таким:

quartile | users_count | min_spent | max_spent
---------+-------------+-----------+----------
1        | 251         | 9000      | 50000
2        | 250         | 4000      | 8990
3        | 250         | 1500      | 3990
4        | 249         | 0         | 1490

Так вы видите:

  • размер каждой группы;
  • диапазон значений внутри группы;
  • насколько сильно различаются корзины.

Это полезно, потому что номер корзины сам по себе не показывает распределение.

Корзина 1 не всегда сильно лучше корзины 2

Есть важная аналитическая ловушка.

Допустим, граница между квартилями прошла так:

quartile 1: spent от 1000 до 1100
quartile 2: spent от 900 до 999

Разница между нижней границей первой корзины и верхней границей второй может быть совсем небольшой.

Но если смотреть только на номер корзины, кажется, будто это разные классы клиентов.

Поэтому для решений с деньгами, доступами, бонусами или ограничениями лучше показывать не только quartile, но и саму метрику.

Хороший отчёт:

user_id | spent | quartile
--------+-------+---------
1       | 1000  | 1
2       | 999   | 2

Плохой отчёт:

user_id | quartile
--------+---------
1       | 1
2       | 2

Без spent нельзя понять, насколько реально отличаются пользователи.

Главная мысль:

NTILE помогает сегментировать, но не заменяет анализ распределения.

NTILE в PostgreSQL

В PostgreSQL NTILE используется как оконная функция.

Пример:

SELECT
  id,
  amount,
  NTILE(4) OVER (ORDER BY amount DESC, id) AS amount_quartile
FROM orders;

Можно использовать PARTITION BY:

SELECT
  country,
  id,
  spent,
  NTILE(10) OVER (
    PARTITION BY country
    ORDER BY spent DESC, id
  ) AS country_decile
FROM user_spending;

Аргумент NTILE должен быть положительным числом.

То есть так можно:

NTILE(4)

А так нельзя:

NTILE(0)

Количество корзин должно быть больше нуля.

NTILE в MySQL

В MySQL оконная функция NTILE доступна в MySQL 8.0 и новее.

Синтаксис похожий:

SELECT
  id,
  amount,
  NTILE(4) OVER (ORDER BY amount DESC) AS quartile
FROM orders;

Также можно использовать PARTITION BY:

SELECT
  dept,
  name,
  salary,
  NTILE(4) OVER (
    PARTITION BY dept
    ORDER BY salary DESC
  ) AS pay_quartile
FROM employees;

Если проект работает на старой версии MySQL без оконных функций, такую логику приходится имитировать через переменные, подзапросы или выполнять сегментацию вне SQL. Но для новых проектов лучше использовать оконные функции.

NTILE в ClickHouse

В ClickHouse подход зависит от версии и доступных функций.

Если прямого NTILE в вашей версии нет или он не подходит по поведению, похожую разметку можно собрать через нумерацию строк и подсчёт общего количества строк.

Идея такая:

  1. отсортировать строки;
  2. присвоить каждой строке номер;
  3. посчитать общее количество строк;
  4. вычислить номер корзины формулой.

Примерно:

WITH ranked AS (
  SELECT
    id,
    amount,
    row_number() OVER (ORDER BY amount DESC, id) AS rn,
    count() OVER () AS total_rows
  FROM orders
)
SELECT
  id,
  amount,
  intDiv((rn - 1) * 4, total_rows) + 1 AS quartile
FROM ranked;

Идея похожа на NTILE(4): разделить строки на 4 группы по порядковому номеру.

Но при переносе между СУБД всегда проверяйте результат на тестовом наборе, особенно если важно точное распределение остатка по корзинам.

Практические шаблоны

Квартили заказов по сумме

SELECT
  id,
  amount,
  NTILE(4) OVER (ORDER BY amount DESC, id) AS amount_quartile
FROM orders;

Децили клиентов по сумме покупок

WITH user_spending AS (
  SELECT
    u.id,
    u.email,
    SUM(o.amount) AS spent
  FROM users u
  JOIN orders o ON o.user_id = u.id
  WHERE o.status = 'paid'
  GROUP BY u.id, u.email
)
SELECT
  id,
  email,
  spent,
  NTILE(10) OVER (ORDER BY spent DESC, id) AS spending_decile
FROM user_spending;

Квартили зарплат внутри отдела

SELECT
  id,
  name,
  dept,
  salary,
  NTILE(4) OVER (
    PARTITION BY dept
    ORDER BY salary DESC, id
  ) AS pay_quartile
FROM employees;

Децили активности внутри страны

WITH user_activity AS (
  SELECT
    u.id,
    u.country,
    COUNT(e.id) AS events_count
  FROM users u
  JOIN events e ON e.user_id = u.id
  GROUP BY u.id, u.country
)
SELECT
  id,
  country,
  events_count,
  NTILE(10) OVER (
    PARTITION BY country
    ORDER BY events_count DESC, id
  ) AS activity_decile
FROM user_activity;

Подписать квартиль словами

WITH ranked AS (
  SELECT
    id,
    email,
    spent,
    NTILE(4) OVER (ORDER BY spent DESC, id) AS quartile
  FROM user_spending
)
SELECT
  id,
  email,
  spent,
  quartile,
  CASE quartile
    WHEN 1 THEN 'top 25%'
    WHEN 2 THEN 'upper middle'
    WHEN 3 THEN 'lower middle'
    WHEN 4 THEN 'bottom 25%'
  END AS quartile_label
FROM ranked;

Проверить размер корзин

WITH ranked AS (
  SELECT
    id,
    spent,
    NTILE(4) OVER (ORDER BY spent DESC, id) AS quartile
  FROM user_spending
)
SELECT
  quartile,
  COUNT(*) AS rows_count,
  MIN(spent) AS min_spent,
  MAX(spent) AS max_spent
FROM ranked
GROUP BY quartile
ORDER BY quartile;

Фиксированные диапазоны через WIDTH_BUCKET

SELECT
  id,
  amount,
  WIDTH_BUCKET(amount, 0, 1000, 4) AS price_band
FROM orders;

Бизнес-сегменты через CASE

SELECT
  id,
  email,
  spent,
  CASE
    WHEN spent >= 10000 THEN 'vip'
    WHEN spent >= 1000 THEN 'middle'
    ELSE 'low'
  END AS segment
FROM user_spending;

Что важно запомнить

NTILE(n) делит отсортированные строки на n примерно равных по размеру групп.

Пример:

SELECT
  id,
  amount,
  NTILE(4) OVER (ORDER BY amount DESC, id) AS quartile
FROM orders;

Главные правила:

  • NTILE(4) делит строки на 4 группы;
  • NTILE(10) делит строки на 10 групп;
  • функция делит строки, а не диапазон значений;
  • результат полностью зависит от ORDER BY;
  • при одинаковых значениях добавляйте тай-брейкер;
  • если строки не делятся ровно, лишние строки попадают в корзины с меньшими номерами;
  • если корзин больше, чем строк, часть номеров не появится;
  • PARTITION BY считает корзины отдельно внутри каждой группы;
  • NTILE не заменяет бизнес-пороги;
  • для фиксированных диапазонов лучше использовать WIDTH_BUCKET или CASE;
  • для экспериментов NTILE не является случайной рандомизацией.

Короткий вывод

NTILE нужен, когда вы хотите разделить отсортированный список на равные по размеру группы.

Например:

NTILE(4) OVER (ORDER BY spent DESC)

делит клиентов на 4 квартиля по сумме покупок.

Корзина 1 при сортировке DESC — верхний сегмент.

Главная мысль:

NTILE делит не шкалу значений, а строки в отсортированной очереди.

Это удобно для аналитики:

  • топ-25% клиентов;
  • децили активности;
  • зарплатные квартили;
  • ранговые сегменты;
  • сравнение пользователей внутри страны или отдела.

Но номер корзины — это не абсолютная оценка качества. Он показывает место строки относительно других строк в выбранной сортировке.

Поэтому хороший отчёт с NTILE должен показывать не только номер корзины, но и саму метрику, правило сортировки и размер групп.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр