Сохранение последней строки для каждого ключа
Шаблон «самая свежая запись для каждого клиента» с разбиением по ключу и сортировкой по дате.
«Сохранение последней строки для каждого ключа» — бесплатный урок SQL Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения SQL Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс SQL Interview Prep содержит 4 уроков всего.
Вопрос о последней строке для каждого ключа
«Верните самый недавний заказ для каждого клиента». «Получите последний статус каждого устройства». Эта задача поиска последней строки для каждого ключа — одна из самых распространённых задач на собеседованиях по базам данных, поскольку постоянно встречается в реальной аналитической работе.
Это специализированный вариант поиска одной лучшей строки в каждой группе: разделите строки по ключу, отсортируйте по временной метке в порядке убывания и оставьте первую строку. В этом уроке подробно разбирается этот шаблон и его альтернативы.
Почему одного MAX недостаточно
Соблазнительный первый ответ — сгруппировать данные по клиенту и использовать MAX(order_date). Это даст самую позднюю дату, но не остальные данные этой строки заказа: идентификатор заказа, сумму или статус.
Если интервьюеру нужна полная последняя строка, для MAX с GROUP BY потребуется дополнительное соединение с исходной таблицей по ключу и максимальной дате. Такой запрос получается громоздким и может некорректно работать при совпадении дат. Оконные функции аккуратнее.
-- Gives the date, not the full row
SELECT customer_id, MAX(order_date) AS last_order
FROM orders
GROUP BY customer_id;Шаблон ROW_NUMBER
Разделите строки по ключу, отсортируйте по временной метке в порядке убывания — и последняя строка получит rn = 1. Оставьте только такие строки, и Вы получите полную самую новую запись для каждого ключа.
Это стандартный ответ для такой задачи. Он возвращает ровно одну строку на ключ даже при совпадении временных меток, что обычно и подразумевается под «последней строкой».
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date DESC
) AS rn
FROM orders
)
SELECT customer_id, order_id, order_date, amount
FROM ranked
WHERE rn = 1;Разрешение совпадений временных меток
У двух заказов одного клиента может быть одинаковое значение order_date (один и тот же день или идентичные временные метки). Без дополнительного критерия строка, которая получит rn = 1, определяется произвольно и может меняться от запуска к запуску.
Добавьте уникальный вторичный ключ, например order_id DESC, чтобы последняя строка определялась однозначно. Интервьюеры специально проверяют, заметили ли Вы этот крайний случай.
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date DESC, order_id DESC
) AS rnПоследняя строка или все совпадения
Определите, что означает «последний» при совпадении временных меток:
- Если нужна ровно одна строка на ключ → используйте
ROW_NUMBERс дополнительным критерием. - Если нужны все строки с максимальной временной меткой → используйте вместо этого
RANK() = 1, который вернёт каждую последнюю строку с совпавшей временной меткой.
Такой уточняющий вопрос показывает, что Вы понимаете смысл операции, а не только синтаксис.
WITH ranked AS (
SELECT *,
RANK() OVER (
PARTITION BY customer_id ORDER BY order_date DESC
) AS rnk
FROM orders
)
SELECT * FROM ranked WHERE rnk = 1;Альтернатива с коррелированным подзапросом
До того как оконные функции стали универсальными, задачу поиска последней строки для каждого ключа решали с помощью коррелированного подзапроса: оставляли строку только в том случае, если для того же ключа не существовало другой строки с более поздней датой.
Этот способ работает, но запускает внутренний запрос для каждой строки, поэтому на больших таблицах он медленнее, а при совпадениях дат становится неудобным. Упомяните его, чтобы показать широту знаний, но для производительности отдавайте предпочтение оконной функции.
SELECT o.*
FROM orders o
WHERE o.order_date = (
SELECT MAX(o2.order_date)
FROM orders o2
WHERE o2.customer_id = o.customer_id
);Сокращённая запись PostgreSQL с DISTINCT ON
В PostgreSQL есть лаконичный приём: DISTINCT ON (key) оставляет первую строку для каждого ключа согласно ORDER BY. Конструкция ORDER BY должна начинаться с тех же столбцов ключа, а затем содержать критерий разрешения совпадений или временную метку.
Этот приём элегантен и быстр в PostgreSQL, но непереносим. Упоминайте его как дополнительный приём для конкретного диалекта, сохраняя ROW_NUMBER в качестве переносимого варианта по умолчанию.
SELECT DISTINCT ON (customer_id)
customer_id, order_id, order_date, amount
FROM orders
ORDER BY customer_id, order_date DESC, order_id DESC;Последняя строка с условием
В реальных вопросах добавляются фильтры: «самый недавний завершённый заказ для каждого клиента». Применяйте фильтр до ранжирования, чтобы нумеровать только подходящие строки.
Поместите условие в WHERE внутреннего запроса (он выполняется до оконной функции), а затем выберите rn = 1 во внешнем запросе. Фильтрация после ранжирования вернула бы неправильную строку.
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC
) AS rn
FROM orders
WHERE status = 'completed'
)
SELECT * FROM ranked WHERE rn = 1;Пример: последний статус устройства
Таблица status_log содержит device_id, status и logged_at. Чтобы получить текущий статус каждого устройства, разделите строки по device_id, отсортируйте по logged_at DESC и оставьте rn = 1.
Это основа информационных панелей, показывающих «текущее состояние» множества объектов по журналу событий, в который записи только добавляются. Этот же рецепт используется для запросов о последней цене, последнем местоположении и последней версии.
WITH latest AS (
SELECT device_id, status, logged_at,
ROW_NUMBER() OVER (
PARTITION BY device_id ORDER BY logged_at DESC
) AS rn
FROM status_log
)
SELECT device_id, status, logged_at
FROM latest
WHERE rn = 1;О производительности
Тезисы, которые покажут уровень опытного специалиста:
- Индекс по
(customer_id, order_date DESC)позволяет СУБД эффективно считывать последнюю строку для каждого ключа. - Оконный подход просматривает таблицу один раз, а коррелированный подзапрос — нет.
DISTINCT ONв PostgreSQL может использовать тот же индекс и часто является самым быстрым вариантом для одной таблицы.- Для журналов событий, в которые записи добавляются особенно часто, рассмотрите материализованную таблицу «последних значений», обновляемую постепенно.
Распространённые ошибки
Обратите внимание на следующие ошибки:
- Использование
MAX(date)с возвратом только даты, а не полной строки. - Отсутствие дополнительного критерия, из-за чего при совпадении дат результаты становятся неоднозначными.
- Фильтрация по условию после ранжирования, из-за чего может быть выбрана строка, которую следовало исключить.
- Путаница между «одной последней строкой» (
ROW_NUMBER) и «всеми последними строками с одинаковым значением» (RANK).
Быстрая проверка
Выберите правильный запрос для получения последней строки каждого ключа.
Итоги: последняя строка для каждого ключа
Шаблон: PARTITION BY key, ORDER BY timestamp DESC (плюс уникальный дополнительный критерий), сохранить rn = 1.
MAX(date)возвращает дату, а не полную строку.- Всегда добавляйте дополнительный критерий для однозначного результата.
- Используйте
RANK() = 1, если нужны все строки с одинаковой последней временной меткой. - Условия фильтрации должны находиться во внутреннем запросе, до ранжирования.
- Конструкция
DISTINCT ONв PostgreSQL — это лаконичная и быстрая альтернатива для конкретного диалекта.
Часто задаваемые вопросы
Урок «Сохранение последней строки для каждого ключа» бесплатный?
Да — полный текст урока «Сохранение последней строки для каждого ключа» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс SQL Interview Prep, подпишись на CoddyKit PRO. Курс SQL Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Сохранение последней строки для каждого ключа»?
Шаблон «самая свежая запись для каждого клиента» с разбиением по ключу и сортировкой по дате. Ты практикуешь SQL Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать SQL Interview Prep?
Предыдущий опыт не требуется. SQL Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Сохранение последней строки для каждого ключа»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке SQL Interview Prep?
Да. Каждый урок SQL Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Первые N строк в каждой группе с ROW_NUMBER
- Обработка совпадений среди первых N строк
- Безопасное удаление дубликатов строк
- Сохранение последней строки для каждого ключа