


Чтобы получить последнюю запись каждого клиента вместе с её полями, в PostgreSQL можно использовать DISTINCT ON. Ключевой момент: порядок должен однозначно определять победителя внутри группы. На Mentoring.digital разберём пример с одинаковыми датами и пустым временем события.
Агрегат 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 сохраняет первую строку каждой группы, поэтому без подходящей сортировки «первая» не означает «последняя по времени».
При сортировке по убыванию 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.