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_id | order_id | created_at |
|---|---|---|
| 1 | 10 | 01.01, 10:00 |
| 1 | 11 | 03.01, 12:00 |
| 1 | 12 | 03.01, 12:00 |
| 2 | 20 | 02.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. В учебной базе, например, у Данила нет заказов, но такой запрос не потеряет его из результата.
Попробуйте в тренажёре
Сначала потренируйте сортировку по датам, затем — оконную нумерацию заказов внутри клиента. Это близкие по смыслу задачи, но не точная копия приведённого запроса.
Для последовательной практики на MySQL 8 — курс «SQL для аналитика» на Stepik с итоговой работой и возможностью получить сертификат.