Learning Platform
Урок 09.04 · 22 мин
Средний
Frame clauseROWS BETWEENRANGE BETWEENUNBOUNDED PRECEDINGMoving average

До этого момента мы говорили об окне как о «всей партиции» или «всех строках от начала до текущей». Это были упрощения — реальное окно гораздо гибче. У окна есть frame (рамка) — подмножество строк партиции, которое именно сейчас используется для вычисления функции. Frame можно сделать «последние 7 строк», «3 строки до и 3 после», «строки этого часа», «всё кроме последней».

Это самая глубокая тема window functions и самая полезная для практики — moving averages, скользящие медианы, временные тренды все строятся через frame.

Из чего состоит frame

Полный синтаксис:

{ROWS | RANGE | GROUPS} BETWEEN <start> AND <end>

Каждая граница — одно из:

  • UNBOUNDED PRECEDING — от самого начала партиции;
  • UNBOUNDED FOLLOWING — до самого конца партиции;
  • CURRENT ROW — текущая строка;
  • N PRECEDING — N строк назад;
  • N FOLLOWING — N строк вперёд.

Тип frame (ROWS, RANGE, GROUPS) определяет, что именно считается «строкой». Об этом ниже.

ROWS BETWEEN 2 PRECEDING AND CURRENT ROW

Окно — текущая строка плюс две предыдущие. Скользит вместе с курсором: для строки 5 это будут строки 3, 4, 5.

row1200
row2150
row3 ←1002 PRECEDING для row5: считается
row4 ←1801 PRECEDING для row5: считается
row5 ⭐220CURRENT ROW: считается
row6160FOLLOWING: не входит в окно для row5
row7140

Для строки 5 в окно ROWS BETWEEN 2 PRECEDING AND CURRENT ROW попадают строки 3, 4, 5 — то есть мы получим, например, SUM = 100 + 180 + 220 = 500. Для строки 6 окно сдвинется на одну позицию — попадут строки 4, 5, 6. Это и есть скользящее окно.

Дефолтный frame — нюанс, который кусает

Если в OVER есть ORDER BY, но нет явного frame — действует дефолт:

RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

То есть «все строки от начала окна до текущей включительно». Это объясняет, почему SUM(x) OVER (ORDER BY date) даёт running total — не магия, просто дефолтный frame.

Если в OVER нет ORDER BY — frame по умолчанию: «вся партиция целиком» (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING). Это объясняет, почему SUM(x) OVER (PARTITION BY ...) даёт сумму по всей партиции, а не нарастающую.

Дефолтный frame зависит от наличия ORDER BY

Два разных дефолта — потому что без ORDER BY понятия «до» и «после» нет.

OVER (PARTITION BY ...)без ORDER BY
frame по умолчаниювся партиция
SUM = сумма всех строк партицииодинаковая для каждой строки
OVER (PARTITION BY ... ORDER BY ...)с ORDER BY
frame по умолчаниюRANGE UNBOUNDED PRECEDING — CURRENT ROW
SUM = running totalрастёт с каждой строкой

Многие новички путаются именно здесь: добавил ORDER BY в OVER — и привычное «общее SUM» внезапно стало нарастающим. Понимая дефолтный frame, это перестаёт быть сюрпризом.

ROWS vs RANGE — самое тонкое место

ROWS и RANGE различаются в том, что значит N PRECEDING/FOLLOWING.

  • ROWS считает физические строки: 1 PRECEDING — это ровно одна строка назад.
  • RANGE считает по значению ORDER BY: 1 PRECEDING это «строки со значением ORDER BY-ключа от (текущее - 1) до текущего».

Разница видна там, где в ORDER BY есть дубликаты.

ROWS считает физически, RANGE — по значению. Смотри на колонку day:

PostgreSQL

В этом запросе каждый день уникален — ROWS BETWEEN 1 PRECEDING AND CURRENT ROW и RANGE BETWEEN 1 PRECEDING AND CURRENT ROW совпадают, потому что между соседними днями всегда дистанция в 1 день. Но если у тебя несколько строк с одинаковой датой и ORDER BY dateRANGE для CURRENT ROW включит все строки с этой датой, а ROWS — только одну (физически текущую).

Правило простое: для скользящих агрегатов по позиции (последние 3 транзакции) — ROWS. Для скользящих по времени (последние 7 дней по факту) — RANGE с интервалом. Дефолтный frame — RANGE, что часто незаметно, но в задачах с дублями ORDER BY-ключа критически важно.

RANGE BETWEEN INTERVAL — moving average по реальному времени

В PostgreSQL RANGE BETWEEN N PRECEDING работает только с числами и интервалами. Это позволяет делать истинный moving average по времени, а не по числу строк.

Moving average выручки за последние 7 дней по календарю (а не по числу транзакций):

PostgreSQL

Заметь: окно меряется во временном промежутке. Если 5 дней без платежей, а потом сразу несколько — окно «забудет» те, что были раньше 7 дней назад. С ROWS BETWEEN 6 PRECEDING AND CURRENT ROW это бы не сработало — пришлось бы фильтровать по количеству транзакций, а не по реальному времени.

Running total — самый частый кейс

Все примеры выше — это частные случаи скользящего окна. Самый классический — running total: сумма от начала до текущей строки.

Running total по клиенту: каждый платёж видит свою сумму и накопленную сумму клиента:

PostgreSQL

Эквивалентный запрос с дефолтным frame:

То же самое — без явного frame. Работает за счёт дефолта RANGE UNBOUNDED PRECEDING AND CURRENT ROW:

PostgreSQL

Оба запроса дают одинаковый результат, потому что paid_at уникален в пределах одного клиента (нет двух платежей в одну и ту же миллисекунду). Если бы были одновременные платежи — RANGE-дефолт схватил бы их все в одну группу, а ROWS — нет. Это редко важно на практике, но полезно знать.

Moving average по N строкам

Скользящее среднее по последним 3 платежам клиента — частая задача для сглаживания шумных рядов:

Moving average по последним 3 платежам клиента:

PostgreSQL

Обрати внимание на нюанс края: для самого первого платежа клиента в окно попадает только 1 строка (он сам), для второго — 2 строки. Только начиная с третьей AVG считается по полному окну 3. Это края — частая проблема при moving statistics: первые N-1 значения «недоопределены». В отчётах это решают через NULL или дисклеймер «данные с начала периода».

FILTER inside window — точечное исключение

В PostgreSQL можно добавить FILTER (WHERE ...) к агрегатной функции внутри окна — суммировать только подходящие строки. Это уменьшает потребность в CASE WHEN.

Running total — но только по card-платежам:

PostgreSQL

Эта возможность недоступна для непустых ranking-функций (ROW_NUMBER, RANK), но прекрасно работает с SUM, COUNT, AVG. Часто заменяет коррелированный подзапрос на 5 строк.

GROUPS — третий редкий тип frame

GROUPS BETWEEN N PRECEDING AND ... — это «N групп строк с одинаковым ORDER BY-значением назад». Полезно, когда ORDER BY-ключ — это категория или временной шаг с дублями. Используется редко, упоминаю для полноты.

Проверка знанийKnowledge check
Запрос: SELECT x, SUM(x) OVER (ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) по строкам 10, 20, 30, 40, 50. Какие значения будут в колонке SUM?
ОтветAnswer
Окно — текущая строка плюс одна назад и одна вперёд (всего до 3 строк). Для x=10: окно [10, 20] — нет предыдущей, есть текущая и одна следующая. SUM = 30. Для x=20: окно [10, 20, 30]. SUM = 60. Для x=30: окно [20, 30, 40]. SUM = 90. Для x=40: окно [30, 40, 50]. SUM = 120. Для x=50: окно [40, 50] — нет следующей. SUM = 90. То есть: 30, 60, 90, 120, 90.
Оконные функции для временных рядов в ClickHouse

Чек-лист

  • Frame — подмножество строк партиции, по которому считается функция для текущей строки.
  • Полный синтаксис: {ROWS | RANGE | GROUPS} BETWEEN <start> AND <end>.
  • Дефолтный frame с ORDER BY: RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (running от начала до сейчас).
  • Дефолтный frame без ORDER BY: вся партиция целиком.
  • ROWS — по физическим строкам. RANGE — по значениям ORDER BY-ключа. Различия видны при дубликатах ключа.
  • RANGE BETWEEN INTERVAL '7 days' PRECEDING — moving average по времени, а не по количеству строк.
  • FILTER (WHERE ...) работает с агрегатами и внутри окна — даёт точечное исключение строк.
  • Края скользящего окна — частая проблема: первые N-1 значений работают на неполном окне.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Каков frame по умолчанию у window function, если в OVER есть ORDER BY, но frame явно не задан?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7