Почему оконные функции могут давать неверный результат из-за RANGE вместо ROWS!Одна из самых неприятных особенностей window functions — frame по умолчанию. Запрос выглядит корректно, проходит проверку глазами, но накопительная сумма может считаться совсем не так, как ожидает разработчик.
Особенно часто это встречается там, где важен порядок операций: расчёт балансов, транзакции, аналитика событий и любые накопительные показатели.
Есть таблица:
payments(id, created_at, amount)
Допустим, нужно получить обычный running total — сумму всех предыдущих платежей до текущей строки.
Запрос выглядит очевидно:
SELECT
id,
created_at,
amount,
SUM(amount) OVER (
ORDER BY created_at
) AS running_total
FROM payments;
Логика кажется простой: первая строка берёт свой amount, вторая прибавляет своё значение к предыдущей сумме, третья делает то же самое. То есть ожидается: 100 — 300 — 600. Но у оконных функций есть скрытая настройка — frame.
Если после
ORDER BY не указать его явно, SQL использует значение по умолчанию:
RANGE BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW
И именно здесь появляется разница между ожиданием и реальным результатом.
RANGE работает не по физическому положению строк, а по значениям сортировки. То есть база смотрит на поле из
ORDER BY и объединяет строки с одинаковым значением в одну логическую группу.
Например:
id=1, created_at='2026-01-01 10:00:00', amount=100
id=2, created_at='2026-01-01 10:00:00', amount=200
id=3, created_at='2026-01-01 11:00:00', amount=300
Здесь первые две записи имеют одинаковое время. Для человека это две разные операции. Но для
RANGE они являются одной группой, потому что значение сортировки совпадает.
При таком запросе:
SUM(amount) OVER (
ORDER BY created_at
)
результат будет:
300
300
600
Почему? Потому что при расчёте первой строки
RANGE уже включает обе записи с одинаковым
created_at. Получается: 100 + 200 = 300. Поэтому первая строка сразу показывает итог двух операций. Это не ошибка SQL, это стандартное поведение
RANGE.
Но чаще всего для накопительных сумм ожидается другое поведение: каждая строка должна учитываться отдельно. Для этого используется
ROWS, который работает именно с физическими строками окна.
Он не смотрит, одинаковые ли значения сортировки у соседних строк. Для него важен порядок строк после сортировки. Например:
SELECT
id,
created_at,
amount,
SUM(amount) OVER (
ORDER BY created_at, id
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW
) AS running_total
FROM payments;
Теперь база получает явную инструкцию: сначала отсортировать записи, затем считать накопление строка за строкой.
Результат становится ожидаемым:
100
300
600
Ещё один важный момент — сама сортировка. Даже если используется
ROWS, одного
created_at может быть недостаточно.
Если несколько событий произошли в одну секунду, SQL не обязан выбирать между ними определённый порядок. Например:
id=1, created_at='10:00:00', amount=100
id=2, created_at='10:00:00', amount=200
Какая операция должна идти первой? Без дополнительного поля ответа нет. Поэтому обычно добавляют уникальный идентификатор:
ORDER BY created_at, id
Теперь порядок строк становится однозначным, а результат расчёта стабильным.
RANGE удобен, когда нужно работать с группами одинаковых значений сортировки.
ROWS нужен, когда важен порядок конкретных строк. Для обычного running total почти всегда стоит явно указывать
ROWS.
🔥 Вывод: если
cumulative sum показывает одинаковые значения на нескольких строках подряд — проверьте
ORDER BY и frame окна. Чаще всего причина в том, что ожидали
ROWS, а получили поведение
RANGE.
➡️ SQL Ready | #практика