PARTITION BY SQL: шпаргалка

Зачем знать PARTITION BY

PARTITION BY — ключевая часть оконных функций в SQL. Она делит данные на группы (окна) для вычисления агрегата внутри каждого окна, сохраняя при этом все строки исходной таблицы.

Синтаксис

функция() OVER (PARTITION BY столбец ORDER BY столбец)
  • PARTITION BY — делит строки на группы (окна).
  • ORDER BY — сортирует строки внутри окна.
  • Оба опциональны, но обычно используются вместе.

Базовый пример

SELECT user_id, order_id, amount,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_num
FROM orders;

Результат: каждому заказу пользователя присвоен номер от 1 до N (для него) — в порядке created_at.

PARTITION BY vs GROUP BY

Критерий GROUP BY PARTITION BY
Сворачивает строки? ✅ Да ❌ Нет
Количество результатов = числу групп = числу строк исходной таблицы
Использование с агрегатами с оконными функциями
Синтаксис в WHERE/SELECT в OVER()

Пример разницы:

-- GROUP BY — одна строка на пользователя
SELECT user_id, SUM(amount) FROM orders GROUP BY user_id;

-- PARTITION BY — все строки + агрегат в каждой
SELECT user_id, order_id, amount,
    SUM(amount) OVER (PARTITION BY user_id) AS user_total
FROM orders;

Второй вариант удобнее, когда нужен контекст по группе в каждой строке.

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

Частые паттерны

1. Нарастающий итог по группе

SELECT user_id, order_id, amount,
    SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS running_total
FROM orders;

Для каждого пользователя — нарастающая сумма по дате.

2. Доля в общей сумме группы

SELECT user_id, order_id, amount,
    ROUND(amount * 100.0 / SUM(amount) OVER (PARTITION BY user_id), 2) AS pct
FROM orders;

Процент каждого заказа от общей суммы пользователя.

3. Разница с предыдущим в группе

SELECT user_id, order_id, amount,
    amount - LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS diff_from_prev
FROM orders;

Разница между текущим и предыдущим заказом того же пользователя.

4. Топ-N в группе

WITH ranked AS (
    SELECT category, product_id, revenue,
        ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC) AS rn
    FROM product_stats
)
SELECT * FROM ranked WHERE rn <= 3;

Топ-3 товара в каждой категории.

5. Среднее по группе без агрегации

SELECT user_id, order_id, amount,
    amount - AVG(amount) OVER (PARTITION BY user_id) AS deviation_from_avg
FROM orders;

Сколько этот заказ отличается от среднего данного пользователя.

Несколько столбцов в PARTITION BY

SELECT *,
    ROW_NUMBER() OVER (PARTITION BY user_id, category ORDER BY created_at) AS rn
FROM orders JOIN products USING (product_id);

Окна создаются по комбинации user_id + category. Например, у одного пользователя может быть 2 окна: «электроника» и «одежда».

PARTITION BY без ORDER BY

SELECT order_id, amount,
    SUM(amount) OVER (PARTITION BY user_id) AS user_total
FROM orders;

Без ORDER BY агрегат считается на всём окне, не нарастающий. Это именно то, что нужно для «общая сумма по группе в каждой строке».

ORDER BY без PARTITION BY

SELECT created_at, amount,
    SUM(amount) OVER (ORDER BY created_at) AS running_total
FROM orders;

Одно окно = вся таблица. Нарастающий итог по хронологии.

PARTITION BY в агрегатах

Не только в оконных функциях. Это стандартный приём в OVER():

SELECT city, user_id, orders_count,
    MAX(orders_count) OVER (PARTITION BY city) AS city_max,
    AVG(orders_count) OVER (PARTITION BY city) AS city_avg,
    COUNT(*) OVER (PARTITION BY city) AS city_size
FROM user_stats;

Все три агрегата на одном окне (city), без GROUP BY — строки сохраняются.

Пройти 30–50 задач по теме за вечер можно в Telegram-тренажёре. Это то, что отличает «знаю» от «уверенно отвечу на собесе».

Комбинирование с ROWS BETWEEN

SELECT user_id, day, amount,
    AVG(amount) OVER (
        PARTITION BY user_id ORDER BY day
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS ma_7d
FROM daily_orders;

Скользящее среднее за 7 дней для каждого пользователя отдельно.

10 задач на PARTITION BY

1. Номер заказа пользователя

SELECT user_id, order_id,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS n
FROM orders;

2. Первый заказ каждого пользователя

WITH t AS (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS rn
    FROM orders
)
SELECT * FROM t WHERE rn = 1;

3. Топ-3 в категории

WITH t AS (
    SELECT category, product_id, revenue,
        ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC) AS rn
    FROM sales
)
SELECT * FROM t WHERE rn <= 3;

4. Доля заказа в общей сумме группы

SELECT user_id, amount,
    amount / SUM(amount) OVER (PARTITION BY user_id) AS share
FROM orders;

5. Max значение в группе

SELECT user_id, amount,
    MAX(amount) OVER (PARTITION BY user_id) AS max_in_group
FROM orders;

6. Ранг пользователя по выручке в городе

SELECT city, user_id, revenue,
    RANK() OVER (PARTITION BY city ORDER BY revenue DESC) AS city_rank
FROM user_stats;

7. Предыдущий заказ того же пользователя

SELECT user_id, order_id, amount,
    LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_amount
FROM orders;

8. Среднее в группе без агрегации

SELECT user_id, amount,
    AVG(amount) OVER (PARTITION BY user_id) AS user_avg
FROM orders;

9. Нарастающий итог с обнулением каждый месяц

SELECT *,
    SUM(amount) OVER (
        PARTITION BY DATE_TRUNC('month', created_at)
        ORDER BY created_at
    ) AS monthly_cumulative
FROM orders;

10. Процент изменения от предыдущей строки

SELECT *,
    (amount - LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at)) * 100.0 /
    NULLIF(LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at), 0) AS pct_change
FROM orders;

Как тренироваться

PARTITION BY учится вместе с оконными функциями. 90% задач на оконки содержат PARTITION BY.

Совет: на собесе, говоря про оконные функции, чётко проговаривайте «PARTITION BY делит данные на окна, ORDER BY задаёт порядок внутри окна, ROWS BETWEEN — рамку». Это три ключевых элемента, и их смешение — типичная ошибка.

Читайте также

FAQ

PARTITION BY медленнее GROUP BY?

На бумаге нет — сложность та же. Но оконные запросы обычно возвращают больше строк (не сворачивают), поэтому общий объём результата больше. Если нужна просто агрегация — GROUP BY.

Можно ли несколько PARTITION BY в одном SELECT?

Да, каждая оконная функция может иметь свой OVER:

SELECT *,
    SUM(x) OVER (PARTITION BY a) AS total_by_a,
    AVG(x) OVER (PARTITION BY b) AS avg_by_b
FROM t;

Чем PARTITION BY отличается от sub-partition в таблицах?

Это разные вещи. В оконных функциях PARTITION BY — логическое разбиение для расчёта. В DDL (CREATE TABLE ... PARTITION BY) — физическое разбиение таблицы.

Можно ли PARTITION BY без ORDER BY?

Да. Тогда агрегат считается на всём окне. Полезно для «общая сумма группы в каждой строке».