Как получить последнюю запись для каждого пользователя

«Последняя запись» — не то же самое, что MAX(дата). Если у двух заказов одинаковое время, нужно явно договориться, какой из них считать последним.

SQL Studio / Разборы SQL / Как получить последнюю запись для каждого пользователя

Решение: чаще всего удобно использовать ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC, order_id DESC), а затем выбрать rn = 1. Второе поле сортировки убирает случайность при совпадающих датах.

Почему MAX(created_at) недостаточно

Представьте, что у клиента №1 три заказа. Два из них созданы в один момент. Запрос с MAX(created_at) найдёт дату, но не определит единственную строку заказа. Если присоединить найденную дату к исходной таблице, могут вернуться две записи.

Минимальный пример заказов
customer_idorder_idcreated_at
11001.01, 10:00
11103.01, 12:00
11203.01, 12:00
22002.01, 09:00

1. ROW_NUMBER: ровно одна строка на клиента

Запрос сам создаёт тестовые строки через VALUES; его можно выполнить в SQL-песочнице без подготовки таблиц.

WITH sample_orders(customer_id, order_id, created_at) AS (
  VALUES
    (1, 10, '2026-01-01 10:00:00'),
    (1, 11, '2026-01-03 12:00:00'),
    (1, 12, '2026-01-03 12:00:00'),
    (2, 20, '2026-01-02 09:00:00')
),
ranked AS (
  SELECT *,
         ROW_NUMBER() OVER (
           PARTITION BY customer_id
           ORDER BY created_at DESC, order_id DESC
         ) AS rn
  FROM sample_orders
)
SELECT customer_id, order_id, created_at
FROM ranked
WHERE rn = 1
ORDER BY customer_id;

Получим (1, 12) и (2, 20). У клиента №1 даты заказов 11 и 12 совпадают, поэтому выбирается больший order_id. Это заранее выбранное правило, а не свойство каждой базы данных.

2. То же самое на таблице orders

В SQL Studio есть orders(customer_id, order_id, created_at, ...). Для учебной базы подойдёт такой запрос:

WITH ranked AS (
  SELECT customer_id, order_id, created_at,
         ROW_NUMBER() OVER (
           PARTITION BY customer_id
           ORDER BY created_at DESC, order_id DESC
         ) AS rn
  FROM orders
)
SELECT customer_id, order_id, created_at
FROM ranked
WHERE rn = 1
ORDER BY customer_id;

Здесь PARTITION BY начинает нумерацию заново для каждого клиента, сортировка идёт от новых заказов к старым, а внешний WHERE rn = 1 оставляет верхнюю строку. Для запроса нужна SQLite с поддержкой оконных функций или MySQL 8.

3. Вариант через NOT EXISTS

Если оконные функции недоступны, можно выбрать заказы, для которых не существует более позднего заказа того же клиента. Проверку равных дат по order_id обязательно сохраняем:

SELECT o.customer_id, o.order_id, o.created_at
FROM orders AS o
WHERE NOT EXISTS (
  SELECT 1
  FROM orders AS later
  WHERE later.customer_id = o.customer_id
    AND (
      later.created_at > o.created_at
      OR (later.created_at = o.created_at
          AND later.order_id > o.order_id)
    )
)
ORDER BY o.customer_id;

Этот вариант даёт тот же смысловой результат при уникальном order_id. Для больших таблиц производительность зависит от индексов и оптимизатора; сравнивайте план запроса на своих данных.

4. Как показать и тех клиентов, у которых нет заказов

Предыдущие запросы возвращают только тех клиентов, которые есть в orders. Если нужен полный список из customers, начинайте с него и используйте LEFT JOIN:

WITH ranked AS (
  SELECT customer_id, order_id, created_at,
         ROW_NUMBER() OVER (
           PARTITION BY customer_id
           ORDER BY created_at DESC, order_id DESC
         ) AS rn
  FROM orders
)
SELECT c.customer_id, r.order_id, r.created_at
FROM customers AS c
LEFT JOIN ranked AS r
  ON r.customer_id = c.customer_id AND r.rn = 1
ORDER BY c.customer_id;

При отсутствии заказов поля order_id и created_at будут NULL. В учебной базе, например, у Данила нет заказов, но такой запрос не потеряет его из результата.

Попробуйте в тренажёре

Сначала потренируйте сортировку по датам, затем — оконную нумерацию заказов внутри клиента. Это близкие по смыслу задачи, но не точная копия приведённого запроса.

Связанные темы: оконные функции SQL · что считать дубликатом.

Нужно не одну, а две или пять строк на группу? TOP-N на ROW_NUMBER и RANK.


Для последовательной практики на MySQL 8 — курс «SQL для аналитика» на Stepik с итоговой работой и возможностью получить сертификат.