Аналитика25 октября 2026 г.

Оконная сумма в SQL: почему при одинаковых датах важна рамка ROWS

#COURSE##INNER#

Оконная сумма в SQL: почему при одинаковых датах важна рамка ROWS

Когда в таблице продаж несколько строк имеют одну и ту же дату, накопительная сумма может вести себя не так, как ожидают начинающие аналитики. Запрос выглядит правильно, функция sum() over (...) считается без ошибок, но итог повторяется сразу для всех строк одной даты. Вопрос обычно звучит так: почему сумма «перепрыгивает» через отдельные продажи и как получить накопление именно по строкам, а не по группе одинаковых значений сортировки.

Короткий ответ такой: одной сортировки недостаточно. В оконной функции важно не только ORDER BY, но и рамка окна. Если положиться на поведение по умолчанию, SQL может считать сумму не по одной текущей строке, а по набору строк с тем же значением сортировки. Для продаж с повторяющимися датами это различие сразу заметно.

Одинаковые значения сортировки и задача накопления

Оконная функция считает значение для каждой строки, не склеивая строки в одну группу результата. Но внутри окна есть еще один уровень логики: какие строки попадают в расчет для текущей строки. Когда в OVER есть ORDER BY, появляется понятие рамки окна. Именно она отвечает на вопрос: «до какого места накопление уже дошло».

Проблема возникает там, где порядок задан неуникальным полем, например только датой продажи. Для SQL две продажи от 2026-05-01 равны по ключу сортировки. Если рамку явно не указать, для текущей строки в расчет могут попасть и другие строки с той же датой. Тогда обе продажи за этот день получат одинаковую накопительную сумму, хотя бизнес-задача могла требовать шаг за шагом видеть итог после каждой отдельной операции.

Поэтому в задаче накопления по строкам нужны сразу две вещи: однозначный порядок и явная рамка. Однозначный порядок удобно задавать связкой sale_date, sale_id, где sale_id различает строки одной даты. Явная рамка ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW говорит базе данных: бери все предыдущие строки в установленном порядке и текущую строку, но не подмешивай «соседей по равенству» только потому, что у них такая же дата.

Учебные строки и явная рамка ROWS

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

WITH sales(sale_id, sale_date, amount) AS (
    VALUES
        (101, DATE '2026-05-01', 100),
        (102, DATE '2026-05-01', 40),
        (103, DATE '2026-05-02', 70),
        (104, DATE '2026-05-02', 30),
        (105, DATE '2026-05-03', 60)
)
SELECT
    sale_id,
    sale_date,
    amount,
    sum(amount) OVER (
        ORDER BY sale_date
    ) AS running_default_by_date,
    sum(amount) OVER (
        ORDER BY sale_date, sale_id
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_by_row
FROM sales
ORDER BY sale_date, sale_id;

Здесь первый столбец с накоплением намеренно сортирует только по дате и использует рамку по умолчанию. Это удобно для демонстрации: мы сразу видим сумму по «ступеням даты». Второй столбец задает и однозначный порядок, и рамку ROWS, поэтому показывает накопление после каждой конкретной продажи.

sale_idsale_dateamountrunning_default_by_daterunning_by_row
1012026-05-01100140100
1022026-05-0140140140
1032026-05-0270240210
1042026-05-0230240240
1052026-05-0360300300

Если смотреть на левую накопительную сумму, обе строки от 2026-05-01 сразу получают 140. То же происходит с 2026-05-02: обе строки получают 240. Это не ошибка базы. Это другой смысл расчета: сумма на текущую дату целиком. Правый столбец показывает иной смысл: 100, потом 140, потом 210, потом 240, потом 300. Такой результат обычно и нужен, когда аналитик строит ленту операций, кассовый поток или баланс после каждого события.

Проверка накопленных сумм вручную

Проверка руками полезна не только для обучения, но и для поиска логических ошибок в реальном отчете. Сначала фиксируем порядок строк: 2026-05-01/101, 2026-05-01/102, 2026-05-02/103, 2026-05-02/104, 2026-05-03/105. Дальше считаем накопление по строкам: первая строка дает 100, вторая добавляет 40 и дает 140, третья добавляет 70 и дает 210, четвертая добавляет 30 и дает 240, пятая добавляет 60 и дает 300.

Теперь проверим поведение рамки по умолчанию при сортировке только по дате. Для первой строки дата равна 2026-05-01, а значит в расчет попадают обе строки этой даты: 100 + 40 = 140. Для второй строки дата та же, поэтому результат тоже 140. Для третьей строки текущая дата уже 2026-05-02, значит в окне лежат все строки от начала плюс обе строки с датой 2026-05-02: 100 + 40 + 70 + 30 = 240. Для четвертой строки сумма та же. Для пятой строки накапливается уже весь набор: 300.

Эта ручная проверка важна еще по одной причине: она показывает, что различие между двумя запросами не связано с арифметикой, пропусками дат или типом поля суммы. Разница возникает именно из-за смысла рамки. Если вы ожидаете последовательное накопление, а видите одинаковые суммы у строк одной даты, сначала проверяйте ORDER BY внутри окна, затем наличие явного ROWS.

Когда нужна другая рамка окна

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW полезна не всегда, а тогда, когда важна каждая строка в своей позиции: продажи, платежи, движения по складу, события пользователя, очередь обращений. Это рамка «накопление по шагам». Она отвечает на вопрос: какой итог получился именно после текущей записи в установленном порядке.

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

Есть и еще один практический вывод. Иногда разработчик добавляет в оконный ORDER BY уникальный id и видит, что сумма стала «правильной» даже без явного ROWS. В таком наборе данных это может сработать, потому что одинаковых значений сортировки больше не осталось. Но такой запрос хуже читается и хуже защищен от будущих изменений. Стоит убрать id, заменить его на менее уникальный признак или перестроить запрос через подзапрос, и поведение снова станет другим. Явная рамка полезна тем, что закрепляет намерение автора: считать по строкам, а не по группам равных значений.

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

Если вы хотите отработать именно такие случаи на курсе, полезно проверить три вещи: есть ли в программе отдельная тема про оконные функции, дают ли задания с повторяющимися значениями сортировки и есть ли проверка, где нужно объяснить различие между двумя корректными, но смыслово разными запросами. На официальной странице онлайн-курса по SQL и в сохраненном описании программы у Бруноям заявлен модуль про оконные функции и работу с датами; в программе отдельно перечислены OVER, PARTITION BY, ROW_NUMBER, RANK и LAG/LEAD. Для этой темы важнее не название модуля, а наличие практики с разбором результата.

Курсы

12398
0,0
0 отзывов
Онлайн-курс по SQL
  • В любой момент
  • 1 месяц
  • Онлайн
  • Диплом / Сертификат
23 900 ₽
Перейти на сайт