PostgreSQL DISTINCT ON: последняя запись для каждой группы

PostgreSQL DISTINCT ON: последняя запись для каждой группы
Содержание статьи

Чтобы получить последнюю запись каждого клиента вместе с её полями, в PostgreSQL можно использовать DISTINCT ON. Ключевой момент: порядок должен однозначно определять победителя внутри группы. На Mentoring.digital разберём пример с одинаковыми датами и пустым временем события.

Почему MAX по дате недостаточно

Агрегат MAX(occurred_at) находит значение времени, но сам по себе не возвращает остальные поля той же строки. Если затем соединить результат с исходной таблицей только по клиенту и дате, две записи с одинаковым временем могут обе попасть в ответ.

Сначала определите правило задачи. В примере ниже «последняя» означает наибольшее непустое время события; при равенстве выбирается больший уникальный идентификатор. Это договорённость примера, а не универсальная бизнес-логика: если идентификатор не отражает нужный приоритет, выберите другой критерий.

Полный запрос на небольших данных

WITH events(customer_id, occurred_at, id, status) AS (
    VALUES
      (1, TIMESTAMP '2026-01-02 10:00', 10, 'new'),
      (1, TIMESTAMP '2026-01-02 10:00', 11, 'paid'),
      (1, NULL::timestamp, 12, 'unknown'),
      (2, TIMESTAMP '2026-01-01 09:00', 20, 'new'),
      (2, TIMESTAMP '2026-01-03 09:00', 21, 'paid')
)
SELECT DISTINCT ON (customer_id)
       customer_id, occurred_at, id, status
FROM events
ORDER BY customer_id,
         occurred_at DESC NULLS LAST,
         id DESC;

По заданному порядку для клиента 1 выбирается запись 11, для клиента 2 запись 21. Это разбор учебных данных по правилам запроса, а не измерение на рабочей базе. При переносе примера используйте свою таблицу и проверьте ожидания отдельно.

Первое выражение ORDER BY совпадает с DISTINCT ON (customer_id). Далее идут критерии выбора внутри группы. PostgreSQL сохраняет первую строку каждой группы, поэтому без подходящей сортировки «первая» не означает «последняя по времени».

Как обработать NULL и одинаковое время

При сортировке по убыванию PostgreSQL по умолчанию ставит NULL раньше непустых значений. Явное NULLS LAST не даёт записи с неизвестным временем обойти известное более позднее событие. Если в группе время отсутствует у всех строк, победителя в этом примере всё равно определит id DESC.

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

Если итоговый список нужен в другом порядке

Для отбора требуется порядок по ключу группы. Для показа пользователю может понадобиться общий порядок по времени. Разделите эти действия вложенным запросом:

SELECT *
FROM (
    SELECT DISTINCT ON (customer_id)
           customer_id, occurred_at, id, status
    FROM events
    ORDER BY customer_id,
             occurred_at DESC NULLS LAST, id DESC
) AS latest
ORDER BY occurred_at DESC NULLS LAST, id DESC;

Во втором примере events обозначает вашу таблицу либо CTE из первого запроса. Внешняя сортировка меняет представление уже выбранных записей и не заменяет правило выбора внутри группы.

DISTINCT ON относится к возможностям PostgreSQL. Если запрос должен работать в нескольких СУБД, рассмотрите оконную нумерацию строк и сверяйте синтаксис каждой системы. Скорость конкретного варианта оценивают по плану и данным, а не по краткости SQL. Обсудить запрос и критерий последней записи можно с ментором по PostgreSQL.

Похожие статьи по данной теме