SQLPARTITION BYwindowtutorial

¿Qué es PARTITION BY en SQL? Grupos dentro de una ventana para principiantes

PARTITION BY es la parte de OVER que divide las filas en grupos para las funciones de ventana. En pocas palabras: es como GROUP BY, pero las filas no se colapsan: cada fila se conserva, con el agregado de su grupo añadido. La diferencia con GROUP BY, los patrones típicos y el comportamiento de los agregados dentro de las ventanas.

16 min de lecturaReferenceSQL · PARTITION BY · window · tutorial
Este artículo está actualmente en ruso — la traducción está en curso.

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 — это «оставь детали, но покажи рядом умный расчёт по группе». Именно поэтому оконные функции так любят в аналитике: они позволяют видеть и конкретную строку, и её контекст одновременно.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador