В оконном отчёте нужно отличить первую строку раздела от предыдущей строки, у которой само значение равно N...

В оконном отчёте нужно отличить первую строку раздела от предыдущей строки, у которой само значение равно NULL. Как построить такую проверку?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нельзя надёжно отличить эти случаи только по результату LAG(значение): в обоих случаях он может вернуть NULL. Нужно дополнительно проверять наличие предыдущей строки — например, вычислять через LAG гарантированно непустой идентификатор предыдущей записи.

Если идентификатор предыдущей строки равен NULL, предыдущей строки нет. Если идентификатор найден, а предыдущее значение равно NULL, строка существует, но её значение отсутствует.

Исторический контекст

Оконные функции появились как средство аналитической обработки без схлопывания строк в группы. В обычном GROUP BY после агрегации теряется индивидуальная строка, поэтому сравнивать запись с соседней или предыдущей строкой неудобно.

LAG решает задачу доступа к предыдущей строке внутри заданного раздела. Однако SQL использует NULL и для отсутствующего значения, и для результата, который невозможно получить из-за отсутствия строки, поэтому одного столбца результата недостаточно для различения этих ситуаций.

Постановка проблемы

Пусть в каждой группе событий нужно сравнить значение текущей записи со значением предыдущей. У первой записи группы предыдущей строки нет, а у последующей записи предыдущая строка может существовать, но содержать NULL.

Если трактовать любой NULL от LAG как отсутствие предыдущей строки, можно ошибочно пометить реальную запись как первую. Это приводит к неверным изменениям, пропущенным переходам статуса и неправильной классификации данных.

Подробное решение

Вычислите два оконных значения: предыдущее бизнес-значение и признак существования предыдущей строки. В качестве признака удобно использовать LAG по столбцу, который гарантированно не равен NULL, например по первичному ключу.

WITH events(entity, ts, id, amount) AS ( VALUES ('A', 1, 10, 100), ('A', 2, 11, NULL) ) SELECT entity, ts, amount, LAG(amount) OVER ( PARTITION BY entity ORDER BY ts, id ) AS previous_amount, LAG(id) OVER ( PARTITION BY entity ORDER BY ts, id ) AS previous_id FROM events ORDER BY entity, ts, id;

У первой строки previous_id равен NULL, потому что строки до неё в разделе нет. У второй строки previous_id содержит идентификатор первой записи, а previous_amount равен NULL: это уже существующая предыдущая строка с пропущенным значением.

Порядок в окне должен быть детерминированным. Если ts не уникален, добавьте уникальный ключ, как id в примере; иначе предыдущая строка может выбираться неоднозначно.

Третий аргумент LAG задаёт значение по умолчанию, когда строка с указанным смещением отсутствует. Но специальное значение может реально встретиться в данных, поэтому такой способ безопасен только при гарантии, что выбранный маркер не является допустимым значением. Проверка по непустому идентификатору обычно надёжнее.

Не следует пытаться отличить случаи выражением вида previous_amount IS NULL: оно проверяет только значение, но не существование строки. Для вычислений над значениями также важно отдельно определить бизнес-смысл NULL: это может быть неизвестное значение, неприменимость или специальное состояние.

Ситуация из практики

В отчёте по истории тарифов нужно показать изменение цены относительно предыдущей записи каждого клиента. У первой записи клиента изменение не рассчитывается, а у следующей записи предыдущая цена может быть NULL, если цена ещё не была задана.

Вариант с проверкой только LAG(price) IS NULL прост, но неверно объединяет две ситуации: начало истории и реально отсутствующую цену. Вариант с третьим аргументом LAG позволяет использовать маркер, но требует гарантии, что маркер не встречается среди настоящих цен.

Выбранное решение — получать через LAG идентификатор предыдущей записи и отдельно её цену. Оно не зависит от допустимого диапазона цен и явно разделяет структурный признак наличия строки и содержательное значение. В результате первая запись корректно помечается как не имеющая предшественника, а последующая запись с NULL — как имеющая предшественника с неизвестной ценой.

Что кандидаты часто упускают

  1. Вопрос: Что произойдёт, если в ORDER BY оконной функции несколько строк имеют одинаковый порядок?

    Ответ: Предыдущая строка может быть выбрана неоднозначно, если СУБД не получает полного детерминированного порядка. Нужно добавить уникальный tie-breaker, обычно первичный ключ или другой столбец, однозначно упорядочивающий записи.

  2. Вопрос: Можно ли использовать LAG с третьим аргументом как универсальный признак отсутствия предыдущей строки?

    Ответ: Только если значение по умолчанию гарантированно невозможно в исходных данных. Иначе реальное значение может совпасть с маркером, и результат потеряет различимость. Для надёжной проверки лучше использовать отдельный гарантированно непустой идентификатор строки.

  3. Вопрос: Что изменится, если разделение выполнить по клиенту, а сортировку — только по дате?

    Ответ: Предыдущая строка будет определяться отдельно внутри каждого клиента, но при одинаковых датах порядок может быть неопределённым. Если бизнес-логика различает события в одну дату, в оконный ORDER BY нужно включить дополнительный столбец, задающий точный порядок событий.