open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 04 · 03

Фреймы окна: ROWS против RANGE против GROUPS

Фрейм — это срез строк, который оконная функция реально агрегирует. По умолчанию это RANGE UNBOUNDED PRECEDING AND CURRENT ROW — он включает все строки-пиры с тем же значением ORDER BY, тихо переполняя накопительные итоги на ничьих. ROWS — нет.

SQL Senior ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Финансовый отчёт с накопительным итогом идеально сходится в staging и расходится на тысячи в продакшене. Разница: в продакшене есть две продажи в один sale_date. Накопительный итог прыгает на обе суммы на первой из связанных строк, затем стоит ровно на второй — каждый связанный день переполняет ранние строки. В запросе был фрейм по умолчанию. RANGE никто не печатал, но получил его, а RANGE считает строки одной даты пирами (peers) и втягивает их все разом. Это самый дорогой дефолт в оконных функциях. К концу этого урока ты будешь знать, что такое фрейм, почему RANGE тихо переполняет на ничьих и какое однострочное правило делает накопительные итоги корректными в продакшене.

Фрейм — это срез, который реально агрегируется

PARTITION BY решает, какие строки делят окно; ORDER BY упорядочивает их; фрейм решает, какое подмножество этой упорядоченной партиции функция агрегирует для каждой строки. Фрейм — это подвижное окно-внутри-окна. Для накопительного итога на строке 5 фрейм — «строки с 1 по 5»; на строке 6 он скользит до «строк с 1 по 6». У синтаксиса фрейма три части: режим (ROWS / RANGE / GROUPS) и две границы.

Границы, которыми будешь пользоваться постоянно:

  • UNBOUNDED PRECEDING — первая строка партиции.
  • CURRENT ROW — где сейчас идёт вычисление.
  • n PRECEDING / n FOLLOWING — n позиций назад или вперёд.
  • UNBOUNDED FOLLOWING — последняя строка партиции.

Так что ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — классический накопительный итог; ROWS BETWEEN 2 PRECEDING AND CURRENT ROW — трёхстрочное окно назад (скользящее среднее); ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING — вся партиция. Вместе режим и границы дают точный контроль над тем, какие строки питают вычисление — без их явного указания ты наследуешь дефолт, который может быть не тем, что ты хочешь.

RANGE против ROWS: разница в ничьих

ROWS считает физические строки: 1 PRECEDING значит буквально одну строку перед этой. RANGE работает на значениях выражения ORDER BY: CURRENT ROW в режиме RANGE не значит «эту одну строку» — он значит «все строки, чьё значение ORDER BY равно значению этой строки», то есть всех пиров. Если три продажи делят sale_date = '2026-01-10', эти три строки — пиры, и под RANGE они входят и выходят из фрейма вместе.

Для накопительного SUM, упорядоченного по sale_date, последствие точное и вредное: на связанной дате каждая из связанных строк получает одинаковый накопительный итог — итог включая всех пиров — вместо накопления по одной. Первая связанная строка уже показывает полный вклад дня; остальные его повторяют. Если твой отчёт ждёт, что каждая строка добавит свою сумму по порядку, связанные строки неверны.

-- Фрейм по умолчанию == RANGE; строки с тем же sale_date делят один накопительный итог (переполняют ранние строки)
SELECT sale_date, amount,
       SUM(amount) OVER (ORDER BY sale_date) AS running_default
FROM sales;

-- Фрейм ROWS: каждая физическая строка добавляет ровно свою сумму, по порядку
SELECT sale_date, amount,
       SUM(amount) OVER (ORDER BY sale_date
                         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_rows
FROM sales;

Фрейм по умолчанию и продакшен-правило

Когда ты добавляешь ORDER BY внутрь OVER и не задаёшь фрейм, Postgres использует RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Это дефолт стандарта SQL — и это RANGE, а не ROWS. На данных без ничьих в колонке ORDER BY оба идентичны, и именно поэтому баги проскальзывают: работает на чистом dev-наборе и ломается в день, когда в продакшене две строки с одинаковым timestamp или датой.

Продакшен-правило прямое: для любого накопительного агрегата пиши ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW явно. Это делает фрейм физическим (по одной строке за раз), это обычно ещё и быстрее (Postgres не нужно сканировать вперёд в поисках всех пиров) и убирает тихое поведение на ничьих. Используй RANGE только когда тебе действительно нужны сгруппированные пиры — например, фрейм на основе значения вроде RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW, который ROWS выразить не может. GROUPS — третий режим: его границы считают группы пиров, а не строки или значения — GROUPS 1 PRECEDING тянется назад на одну целую группу-ничью — редкий, но точный инструмент, когда нужны «строки предыдущей отдельной даты, сколько бы их ни было».

Граничные случаи

Тонкое следствие: RANGE n PRECEDING легален только когда есть ровно одна колонка ORDER BY и она числовая, дата или интервал — потому что Postgres должен вычислить value - n, чтобы определить границу. RANGE 100 PRECEDING по amount значит «все строки, чья сумма в пределах 100 ниже суммы текущей строки». Попробуй RANGE с многоколоночным ORDER BY и числовым смещением — получишь ошибку; у ROWS такого ограничения нет, потому что он считает позиции, а не значения. Так что ROWS — и более безопасный дефолт, и более широко применимый режим. Глава об execution-plans трека databases показывает, как каждый режим фрейма проявляется как узел WindowAgg и сколько он стоит.

Викторина

При ORDER BY sale_date и без явного фрейма две продажи делят sale_date = '2026-01-10'. Какой накопительный итог покажут эти две связанные строки под фреймом по умолчанию?

Викторина

Какой фрейм писать для корректного накопительного итога по одной строке за раз?

Закончи аналогию

Заполни пропуск: под дефолтным фреймом RANGE строки, связанные по значению ORDER BY, считаются _______ и входят во фрейм вместе, из-за чего накопительные итоги переполняют ранние связанные строки; переключение на ROWS считает физические позиции.

Вспомните перед уходом
  1. 01
    Что такое фрейм окна и каковы его две части?
  2. 02
    Почему накопительный итог может быть неверным под фреймом по умолчанию и в чём фикс?
  3. 03
    Когда RANGE действительно правильный выбор вместо ROWS?
Итог

Фрейм — это та часть механики окон, что отделяет тех, кто использует оконные функции, от тех, кто доверяет им в продакшене. После того как PARTITION BY ограничивает окно, а ORDER BY упорядочивает его, фрейм выбирает, какое подмножество этих упорядоченных строк каждая функция реально агрегирует, через режим (ROWS, RANGE, GROUPS) и две границы. По умолчанию, когда добавляешь внутренний ORDER BY, это RANGE UNBOUNDED PRECEDING AND CURRENT ROW — и поскольку RANGE работает на значениях, строки, связанные по колонке ORDER BY, — пиры, входящие во фрейм вместе, так что накопительный итог повторяет значение, включающее пиров, на всех связанных строках и переполняет ранние. Это проходит на dev-данных без ничьих и ломается в продакшене в день, когда две строки делят timestamp. Правило — писать ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW явно для накопительных агрегатов: это корректно, обычно быстрее и широко применимо — а тянуться к RANGE лишь для настоящих окон на основе значений вроде INTERVAL '7 days' PRECEDING. Теперь, когда пишешь любой накопительный агрегат, сразу добавляй клаузу ROWS-фрейма — до запуска запроса, до написания следующего окна — чтобы поведение на ничьих никогда не добралось до продакшена. С понятыми фреймами следующий урок переходит к ранжирующим функциям, где поведение на ничьих возвращается как разница между ROW_NUMBER, RANK и DENSE_RANK.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 8 завершено
Связанные уроки
опирается на

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.