SQL-рецепт: сегментация данных

Это заметка из серии «SQL-рецепты», в которой я описываю решения распространенных задач анализа данных в SQL.

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

Решение — использовать функцию ntile() поверх SQL-окна, упорядоченного по целевым столбцам.

Пример

Разобьем сотрудников из таблицы employees на три группы в зависимости от размера зарплаты:

  • высокооплачиваемые,
  • средние,
  • низкооплачиваемые.
select
  ntile(3) over w as tile,
  name, salary
from employees
window w as (order by salary desc)
order by salary desc, id;

ntile(n) разбивает все записи на n групп и возвращает номер группы для каждой записи. Если общее количество записей (10 в нашем случае) не делится на размер группы (3), то первые группы будут крупнее последних.

Альтернативы

ntile() всегда старается разбить данные так, чтобы группы были одинакового размера. Поэтому записи с одинаковым значением з/п вполне могут попасть в разные (соседние) группы:

select
  ntile(2) over w as tile,
  name, salary
from employees
window w as (order by salary desc, id)
order by salary desc, tile;

Чтобы избежать этого, придется использовать более сложную формулу:

1 + ((rank() over w) - 1) * N / count(*) over () as tile

Для n = 2:

select
  1 + ((rank() over w) - 1) * 2 / count(*) over () as tile,
  name, salary
from employees
window w as (order by salary desc)
order by salary desc, id;

Совместимость

Все основные СУБД поддерживают оконные функцию ntile(). Некоторые из них, такие как MS SQL и Oracle, не поддерживают инструкцию window. В этих случаях можно перенести определение окна в основную часть запроса:

select
  ntile(3) over (
    order by salary desc
  ) as tile,
  name, salary
from employees
order by salary desc, id;

Можно переписать запрос вообще без оконных функций:

select
  ceil(
    (select count(*) from employees as e2 where e2.salary > e1.salary) * 3 /
    (select count(*) from employees)
  ) + 1 as tile,
  name, salary
from employees as e1
order by salary desc, id;

Хотите узнать больше об оконных функциях? Записывайтесь на курс Оконные функции SQL

Подписывайтесь на канал, чтобы не пропустить новые заметки 🚀