SQLPARTITION BYwindowtutorial

Что такое PARTITION BY в SQL?

PARTITION BY — это часть OVER, которая делит строки на группы для оконных функций. Простыми словами: то же что GROUP BY, но строки не схлопываются — каждая остаётся, и к ней дописывается агрегат по её группе. Разница с GROUP BY, типичные применения и поведение агрегатов в окне.

16 мин чтенияСправочникSQL · PARTITION BY · window · tutorial

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

Звучит похоже на GROUP BY, но это совсем другой инструмент.

Главная разница такая:

  • GROUP BY собирает много строк в одну итоговую строку;
  • PARTITION BY оставляет все строки на месте, но позволяет посчитать что-то внутри группы.

Представь таблицу заказов. Ты хочешь видеть каждый заказ отдельно, но рядом с ним — общую сумму заказов этого клиента. Обычный GROUP BY здесь не подойдёт: он схлопнет все заказы клиента в одну строку. А PARTITION BY как раз позволит сохранить каждый заказ и добавить к нему расчёт по клиенту.

Это одна из самых полезных идей в оконных функциях: строка остаётся строкой, но начинает «знать» что-то о своей группе.

Где используется PARTITION BY

PARTITION BY пишется внутри OVER.

Оконные функции выглядят примерно так:

function_name(...) OVER (
  PARTITION BY column_name
  ORDER BY another_column
)

Например:

SUM(amount) OVER (PARTITION BY customer_id)

Такой код означает:

Посчитай сумму amount отдельно для каждого customer_id, но не удаляй исходные строки.

PARTITION BY отвечает за то, на какие группы делить строки перед расчётом оконной функции.

Если сказать проще:

  • PARTITION BY customer_id — отдельное окно для каждого клиента;
  • PARTITION BY category_id — отдельное окно для каждой категории;
  • PARTITION BY department — отдельное окно для каждого отдела;
  • PARTITION BY country, city — отдельное окно для каждой пары страна плюс город.

Почему PARTITION BY так полезен

В реальных отчётах редко достаточно просто получить итоговую сумму.

Обычно хочется увидеть и детали, и общий контекст.

Например:

  • каждый заказ и общую сумму заказов клиента;
  • каждого сотрудника и среднюю зарплату по его отделу;
  • каждый товар и среднюю цену по его категории;
  • каждую покупку и её долю в покупках клиента;
  • каждое событие пользователя и номер этого события в его истории;
  • последние 3 заказа каждого клиента;
  • самый дорогой товар в каждой категории.

Через GROUP BY такие задачи часто превращаются в подзапросы и JOIN. А оконные функции с PARTITION BY позволяют написать короче и понятнее.

GROUP BY и PARTITION BY на одних данных

Допустим, есть таблица orders:

id customer_id amount
1 1 100
2 1 250
3 2 80
4 1 150
5 2 300

Сначала посмотрим на GROUP BY.

SELECT
  customer_id,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;

Результат:

customer_id total_amount
1 500
2 380

Что произошло?

Было 5 заказов, стало 2 строки. По одной строке на клиента. Это нормально: GROUP BY именно так и работает. Он собирает строки в группы и возвращает итог по каждой группе.

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

Теперь сделаем то же самое через оконную функцию:

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders;

Результат:

id customer_id amount customer_total
1 1 100 500
2 1 250 500
3 2 80 380
4 1 150 500
5 2 300 380

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

Вот в этом и сила PARTITION BY.

GROUP BY отвечает на вопрос:

Какой итог по группе?

А PARTITION BY помогает ответить на вопрос:

Что известно о группе для каждой конкретной строки?

Простая аналогия

Представь школьный журнал.

GROUP BY class покажет одну строку на каждый класс:

class_name avg_score
5A 4.4
5B 4.1

Это полезно, если нужен только итог по классу.

А PARTITION BY class позволит показать каждого ученика и рядом средний балл его класса:

student_name class_name score class_avg_score
Anna 5A 5 4.4
Boris 5A 4 4.4
Vera 5B 3 4.1
Denis 5B 5 4.1

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

Именно для таких задач PARTITION BY подходит идеально.

Синтаксис PARTITION BY

Общий вид:

window_function(...) OVER (
  PARTITION BY column1, column2
  ORDER BY column3
)

Внутри OVER обычно встречаются две важные части:

  • PARTITION BY — делит строки на группы;
  • ORDER BY — задаёт порядок строк внутри каждой группы.

PARTITION BY не всегда обязателен. ORDER BY тоже не всегда обязателен. Всё зависит от задачи и оконной функции.

Например, для суммы по клиенту порядок не нужен:

SUM(amount) OVER (PARTITION BY customer_id)

А для номера заказа в истории клиента порядок уже нужен:

ROW_NUMBER() OVER (
  PARTITION BY customer_id
  ORDER BY created_at
)

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

Пример: сумма заказов клиента в каждой строке

Возьмём таблицу orders:

id customer_id amount created_at
1 1 100 2024-03-01
2 1 250 2024-03-05
3 2 80 2024-03-07
4 1 150 2024-03-10
5 2 300 2024-03-12

Хотим показать каждый заказ и общую сумму заказов клиента:

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders;

Результат:

id customer_id amount customer_total
1 1 100 500
2 1 250 500
3 2 80 380
4 1 150 500
5 2 300 380

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

Например, заказ на 250 у клиента 1 — это часть общей суммы 500.

Доля строки внутри группы

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

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS customer_total,
  ROUND(
    100.0 * amount / SUM(amount) OVER (PARTITION BY customer_id),
    1
  ) AS percent_of_customer_total
FROM orders;

Результат:

id customer_id amount customer_total percent_of_customer_total
1 1 100 500 20.0
2 1 250 500 50.0
3 2 80 380 21.1
4 1 150 500 30.0
5 2 300 380 78.9

Вот это уже настоящая аналитика.

Мы не просто получили сумму. Мы поняли, какой вклад каждый заказ внёс в сумму своего клиента.

Для бизнеса это очень полезный паттерн:

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

Несколько оконных агрегатов сразу

С PARTITION BY можно добавить в каждую строку сразу несколько показателей по группе.

Например:

SELECT
  id,
  customer_id,
  amount,
  COUNT(*) OVER (PARTITION BY customer_id) AS customer_orders_count,
  SUM(amount) OVER (PARTITION BY customer_id) AS customer_total_amount,
  AVG(amount) OVER (PARTITION BY customer_id) AS customer_avg_amount,
  MAX(amount) OVER (PARTITION BY customer_id) AS customer_max_amount
FROM orders;

Так в каждой строке заказа будет видно:

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

Обычный GROUP BY тоже может посчитать эти значения, но он вернёт одну строку на клиента. А здесь мы сохраняем каждый заказ.

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

Сравнение строки со средней по группе

Допустим, есть таблица products:

id name category price
1 Keyboard Electronics 80
2 Mouse Electronics 40
3 Monitor Electronics 300
4 Chair Furniture 150
5 Desk Furniture 250

Хотим показать каждый товар и среднюю цену в его категории.

SELECT
  id,
  name,
  category,
  price,
  AVG(price) OVER (PARTITION BY category) AS category_avg_price
FROM products;

Результат:

id name category price category_avg_price
1 Keyboard Electronics 80 140.0000
2 Mouse Electronics 40 140.0000
3 Monitor Electronics 300 140.0000
4 Chair Furniture 150 200.0000
5 Desk Furniture 250 200.0000

Теперь можно сравнить цену товара со средней ценой в категории:

SELECT
  id,
  name,
  category,
  price,
  AVG(price) OVER (PARTITION BY category) AS category_avg_price,
  price - AVG(price) OVER (PARTITION BY category) AS diff_from_category_avg
FROM products;

Результат:

id name category price category_avg_price diff_from_category_avg
1 Keyboard Electronics 80 140.0000 -60.0000
2 Mouse Electronics 40 140.0000 -100.0000
3 Monitor Electronics 300 140.0000 160.0000
4 Chair Furniture 150 200.0000 -50.0000
5 Desk Furniture 250 200.0000 50.0000

Теперь сразу видно, какой товар дороже среднего, а какой дешевле.

Это очень частый аналитический приём: сравнить строку не со всей таблицей, а именно со своей группой.

ROW_NUMBER с PARTITION BY

PARTITION BY особенно часто используют с функцией ROW_NUMBER.

ROW_NUMBER нумерует строки. А PARTITION BY говорит: нумеруй не всю таблицу подряд, а отдельно внутри каждой группы.

Например, нужно пронумеровать заказы каждого клиента от первого к последнему:

SELECT
  id,
  customer_id,
  amount,
  created_at,
  ROW_NUMBER() OVER (
    PARTITION BY customer_id
    ORDER BY created_at
  ) AS order_number
FROM orders;

Результат:

id customer_id amount created_at order_number
1 1 100 2024-03-01 1
2 1 250 2024-03-05 2
4 1 150 2024-03-10 3
3 2 80 2024-03-07 1
5 2 300 2024-03-12 2

У клиента 1 своя нумерация: 1, 2, 3.
У клиента 2 своя нумерация: 1, 2.

Именно это даёт PARTITION BY customer_id.

Топ-N в каждой группе

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

Например:

Последние 3 заказа каждого клиента.

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

WITH ranked_orders AS (
  SELECT
    id,
    customer_id,
    amount,
    created_at,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id
      ORDER BY created_at DESC, id DESC
    ) AS row_num
  FROM orders
)
SELECT
  id,
  customer_id,
  amount,
  created_at
FROM ranked_orders
WHERE row_num <= 3;

Что здесь происходит:

  1. PARTITION BY customer_id делит заказы по клиентам.
  2. ORDER BY created_at DESC, id DESC ставит новые заказы выше старых внутри каждого клиента.
  3. ROW_NUMBER нумерует заказы каждого клиента.
  4. Внешний запрос оставляет только строки с номером 1, 2 или 3.

Так можно делать:

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

Почему оконную функцию нельзя сразу использовать в WHERE

Новички часто пытаются написать так:

SELECT
  id,
  customer_id,
  amount,
  ROW_NUMBER() OVER (
    PARTITION BY customer_id
    ORDER BY created_at DESC
  ) AS row_num
FROM orders
WHERE row_num = 1;

Такой запрос не сработает.

Причина в порядке выполнения SQL-запроса. WHERE фильтрует строки раньше, чем вычисляются оконные функции. В момент работы WHERE колонки row_num ещё не существует.

Поэтому нужен подзапрос или CTE:

WITH ranked_orders AS (
  SELECT
    id,
    customer_id,
    amount,
    created_at,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id
      ORDER BY created_at DESC
    ) AS row_num
  FROM orders
)
SELECT
  id,
  customer_id,
  amount,
  created_at
FROM ranked_orders
WHERE row_num = 1;

Так мы сначала создаём результат с row_num, а потом уже фильтруем его во внешнем запросе.

PARTITION BY по нескольким колонкам

В PARTITION BY можно указать не одну колонку, а несколько.

Например:

ROW_NUMBER() OVER (
  PARTITION BY customer_id, EXTRACT(YEAR FROM created_at)
  ORDER BY amount DESC
)

Здесь группа — это не просто клиент. Группа — это пара:

  • конкретный клиент;
  • конкретный год.

То есть нумерация начнётся заново для каждого клиента в каждом году.

Полный пример:

SELECT
  id,
  customer_id,
  amount,
  created_at,
  ROW_NUMBER() OVER (
    PARTITION BY customer_id, EXTRACT(YEAR FROM created_at)
    ORDER BY amount DESC, id DESC
  ) AS order_rank_in_year
FROM orders;

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

WITH ranked_orders AS (
  SELECT
    id,
    customer_id,
    amount,
    created_at,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id, EXTRACT(YEAR FROM created_at)
      ORDER BY amount DESC, id DESC
    ) AS order_rank_in_year
  FROM orders
)
SELECT
  id,
  customer_id,
  amount,
  created_at
FROM ranked_orders
WHERE order_rank_in_year = 1;

PARTITION BY a, b означает: отдельная группа для каждой уникальной пары значений a и b.

Что будет без PARTITION BY

Если не написать PARTITION BY, оконная функция будет работать по всему набору строк.

Например:

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER () AS grand_total
FROM orders;

Результат:

id customer_id amount grand_total
1 1 100 880
2 1 250 880
3 2 80 880
4 1 150 880
5 2 300 880

SUM(amount) OVER () посчитал сумму по всей таблице и показал её в каждой строке.

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

Например, доля каждого заказа в общей выручке:

SELECT
  id,
  customer_id,
  amount,
  ROUND(100.0 * amount / SUM(amount) OVER (), 1) AS percent_of_all_orders
FROM orders;

Без PARTITION BY окно — вся выборка.

С PARTITION BY окно — отдельная группа внутри выборки.

PARTITION BY и ORDER BY внутри OVER

Внутри OVER можно писать и PARTITION BY, и ORDER BY.

Они отвечают за разные вещи:

  • PARTITION BY делит строки на группы;
  • ORDER BY задаёт порядок строк внутри каждой группы.

Например:

ROW_NUMBER() OVER (
  PARTITION BY customer_id
  ORDER BY created_at DESC
)

Это означает:

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

Если убрать PARTITION BY, получится другая логика:

ROW_NUMBER() OVER (
  ORDER BY created_at DESC
)

Теперь будет одна общая нумерация по всем заказам. Это уже не «последний заказ каждого клиента», а «последние заказы во всей таблице».

Если убрать ORDER BY, у ROW_NUMBER не будет понятного порядка:

ROW_NUMBER() OVER (
  PARTITION BY customer_id
)

Такой запрос может выполниться, но смысл будет слабым: база пронумерует строки внутри клиента без гарантированного порядка. Для ROW_NUMBER почти всегда нужно явно указывать ORDER BY.

Важный нюанс: SUM с ORDER BY даёт нарастающий итог

Это место часто путают.

Смотри на такой запрос:

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

Многие ожидают, что SUM посчитает общую сумму клиента. Но из-за ORDER BY внутри OVER результат становится нарастающим итогом.

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

Пример:

id customer_id amount created_at running_total
1 1 100 2024-03-01 100
2 1 250 2024-03-05 350
4 1 150 2024-03-10 500
3 2 80 2024-03-07 80
5 2 300 2024-03-12 380

Это называется running total — нарастающий итог.

Если нужна сумма всей группы, пиши без ORDER BY:

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders;

А если нужен именно нарастающий итог, тогда ORDER BY нужен:

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

Запомни простое правило:

  • SUM(amount) OVER (PARTITION BY customer_id) — сумма по всему клиенту;
  • SUM(amount) OVER (PARTITION BY customer_id ORDER BY created_at) — сумма от начала истории клиента до текущей строки.

Нарастающий итог и одинаковые даты

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

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

Чтобы порядок был стабильным, добавляй уникальную колонку:

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

id помогает базе понять, какой заказ идёт раньше, если даты одинаковые.

Это хорошая привычка для оконных функций: если порядок важен, делай его детерминированным.

LAG и LEAD с PARTITION BY

PARTITION BY полезен не только с агрегатами и ROW_NUMBER, но и с функциями LAG и LEAD.

LAG позволяет посмотреть на предыдущую строку.
LEAD позволяет посмотреть на следующую строку.

Например, хотим для каждого заказа увидеть предыдущий заказ этого же клиента:

SELECT
  id,
  customer_id,
  amount,
  created_at,
  LAG(amount) OVER (
    PARTITION BY customer_id
    ORDER BY created_at, id
  ) AS previous_amount
FROM orders;

Что важно: благодаря PARTITION BY customer_id предыдущий заказ берётся только внутри того же клиента.

Без PARTITION BY функция могла бы взять предыдущий заказ другого клиента, и такой результат был бы почти бесполезен для анализа истории конкретного пользователя.

Можно посчитать разницу с предыдущим заказом:

SELECT
  id,
  customer_id,
  amount,
  created_at,
  amount - LAG(amount) OVER (
    PARTITION BY customer_id
    ORDER BY created_at, id
  ) AS diff_from_previous_amount
FROM orders;

Так можно анализировать, растёт ли чек клиента со временем.

NULL в PARTITION BY

Если колонка для PARTITION BY содержит NULL, такие строки попадают в одну общую партицию.

Например:

COUNT(*) OVER (PARTITION BY manager_id)

Все строки, где manager_id равен NULL, будут считаться одной группой.

Это не значит, что в SQL NULL = NULL в обычном сравнении. В условиях WHERE manager_id = NULL всё равно будет ошибка по смыслу, потому что NULL проверяют через IS NULL.

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

Если строки с пустым значением не нужны в расчёте, отфильтруй их заранее:

SELECT
  employee_id,
  manager_id,
  salary,
  AVG(salary) OVER (PARTITION BY manager_id) AS avg_salary_by_manager
FROM employees
WHERE manager_id IS NOT NULL;

PARTITION BY не фильтрует строки

Важно: PARTITION BY не удаляет строки из результата.

Он только говорит оконной функции, как разбить строки на группы для расчёта.

Например:

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders;

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

Если тебе нужна одна строка на клиента, используй GROUP BY:

SELECT
  customer_id,
  SUM(amount) AS customer_total
FROM orders
GROUP BY customer_id;

А если тебе нужна, например, одна последняя строка на каждого клиента, используй оконную функцию и внешний фильтр:

WITH ranked_orders AS (
  SELECT
    id,
    customer_id,
    amount,
    created_at,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id
      ORDER BY created_at DESC, id DESC
    ) AS row_num
  FROM orders
)
SELECT
  id,
  customer_id,
  amount,
  created_at
FROM ranked_orders
WHERE row_num = 1;

PARTITION BY сам по себе не уменьшает количество строк.

Когда нужен GROUP BY, а когда PARTITION BY

Выбирай GROUP BY, когда тебе нужен итоговый отчёт без деталей.

Например:

SELECT
  customer_id,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;

Результат: одна строка на клиента.

Выбирай PARTITION BY, когда тебе нужны детали и расчёт по группе рядом с каждой строкой.

Например:

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS total_amount
FROM orders;

Результат: каждый заказ плюс сумма по клиенту.

Хорошая шпаргалка:

Задача Что использовать
Одна строка на клиента с суммой заказов GROUP BY
Каждый заказ плюс сумма заказов клиента PARTITION BY
Топ-3 заказа каждого клиента PARTITION BY + ROW_NUMBER
Средняя зарплата по отделу в каждой строке сотрудника PARTITION BY
Просто отчёт по отделам без сотрудников GROUP BY

Переиспользование окна через WINDOW

Если в запросе несколько оконных функций с одним и тем же PARTITION BY и ORDER BY, можно вынести окно в отдельное имя через WINDOW.

Например:

SELECT
  id,
  customer_id,
  amount,
  created_at,
  ROW_NUMBER() OVER customer_window AS row_num,
  COUNT(*) OVER customer_window AS orders_count
FROM orders
WINDOW customer_window AS (
  PARTITION BY customer_id
  ORDER BY created_at, id
);

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

Но для новичка это не первый инструмент, который нужно запоминать. Сначала важно уверенно понять обычную форму:

SUM(amount) OVER (PARTITION BY customer_id)

А WINDOW пригодится позже, когда запросы станут длиннее.

Частые ошибки с PARTITION BY

Путать PARTITION BY и GROUP BY

GROUP BY уменьшает количество строк.

SELECT
  customer_id,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;

PARTITION BY сохраняет строки.

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS total_amount
FROM orders;

Если после запроса ты ожидал одну строку на клиента, а получил все заказы, значит, скорее всего, ты использовал оконную функцию там, где нужен был GROUP BY.

Забыть PARTITION BY

Например, нужно найти последний заказ каждого клиента.

Правильно:

ROW_NUMBER() OVER (
  PARTITION BY customer_id
  ORDER BY created_at DESC
)

Если забыть PARTITION BY, получится нумерация по всей таблице:

ROW_NUMBER() OVER (
  ORDER BY created_at DESC
)

Тогда row_num = 1 даст один самый последний заказ вообще, а не по одному последнему заказу на каждого клиента.

Добавить ORDER BY к SUM и случайно получить running total

Если нужна сумма по всей группе, пиши:

SUM(amount) OVER (PARTITION BY customer_id)

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

SUM(amount) OVER (
  PARTITION BY customer_id
  ORDER BY created_at
)

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

Пытаться использовать оконную функцию в WHERE

Так нельзя:

SELECT
  id,
  customer_id,
  ROW_NUMBER() OVER (
    PARTITION BY customer_id
    ORDER BY created_at DESC
  ) AS row_num
FROM orders
WHERE row_num = 1;

Нужно так:

WITH ranked_orders AS (
  SELECT
    id,
    customer_id,
    created_at,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id
      ORDER BY created_at DESC
    ) AS row_num
  FROM orders
)
SELECT
  id,
  customer_id,
  created_at
FROM ranked_orders
WHERE row_num = 1;

Сначала считаем оконную функцию, потом фильтруем.

Не задавать стабильный порядок

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

Менее надёжно:

ROW_NUMBER() OVER (
  PARTITION BY customer_id
  ORDER BY created_at DESC
)

Надёжнее:

ROW_NUMBER() OVER (
  PARTITION BY customer_id
  ORDER BY created_at DESC, id DESC
)

id делает порядок стабильным.

Думать, что PARTITION BY сортирует результат

PARTITION BY не сортирует итоговую выдачу. Он только делит строки на группы внутри оконной функции.

Например:

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders;

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

Если нужен порядок результата, добавь внешний ORDER BY:

SELECT
  id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders
ORDER BY customer_id, id;

ORDER BY внутри OVER управляет порядком для оконной функции.
ORDER BY в конце запроса управляет порядком итоговой таблицы.

Это разные вещи.

Главное

PARTITION BY — это способ сказать оконной функции: «считай не по всей таблице, а отдельно внутри каждой группы».

Главная идея:

SUM(amount) OVER (PARTITION BY customer_id)

означает:

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

Самое важное:

  • PARTITION BY используется внутри OVER;
  • он делит строки на группы для оконной функции;
  • он не схлопывает строки, в отличие от GROUP BY;
  • без PARTITION BY окно обычно работает по всей выборке;
  • с ROW_NUMBER он помогает делать топы внутри каждой группы;
  • с SUM, AVG, COUNT, MIN, MAX он добавляет групповые показатели к каждой строке;
  • SUM с ORDER BY внутри OVER даёт нарастающий итог;
  • оконные функции нельзя фильтровать напрямую в WHERE, для этого нужен CTE или подзапрос;
  • строки с NULL в колонке партиции попадают в одну группу;
  • PARTITION BY не сортирует итоговую выдачу, для этого нужен обычный ORDER BY в конце запроса.

Если GROUP BY — это «сделай краткий итог по группам», то PARTITION BY — это «оставь детали, но покажи рядом умный расчёт по группе». Именно поэтому оконные функции так любят в аналитике: они позволяют видеть и конкретную строку, и её контекст одновременно.

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

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

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