0Pricing
SQL Interview Prep · Урок

Сохранение последней строки для каждого ключа

Шаблон «самая свежая запись для каждого клиента» с разбиением по ключу и сортировкой по дате.

«Сохранение последней строки для каждого ключа» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Первые N строк в каждой группе с ROW_NUMBER
  2. Обработка совпадений среди первых N строк
  3. Безопасное удаление дубликатов строк
  4. Сохранение последней строки для каждого ключа
← Назад к SQL Interview Prep