В таблице несколько строк одного клиента; почему DISTINCT не выбирает среди них самую свежую?

В таблице несколько строк одного клиента; почему DISTINCT не выбирает среди них самую свежую?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

DISTINCT удаляет дубликаты только среди полностью совпадающих наборов выбранных значений. Он не понимает, какая строка является «самой свежей», и не выбирает одну строку по правилу сравнения дат.

Если у одного клиента отличаются дата или другие выбранные столбцы, строки не считаются дубликатами. Для выбора последней записи нужно явно задать критерий: обычно применяют оконную нумерацию строк либо находят максимальную дату и связывают её с исходными строками.

Исторический контекст

В реляционной модели результат часто рассматривается как множество, где одинаковые элементы не повторяются. Однако SQL по умолчанию работает с мультимножествами: одинаковые строки могут присутствовать в результате, потому что сохранение их не требует дополнительного удаления и важно для корректного поведения многих операций и агрегатов.

Ключевое слово DISTINCT было предназначено именно для явного устранения повторяющихся проекций результата. Оно решает задачу дедупликации значений, но не задачу выбора представителя из группы строк.

Постановка проблемы

Предположим, у клиента есть несколько заказов. Нужно получить ровно один заказ — последний по времени. Простое добавление DISTINCT не гарантирует результат: заказы различаются идентификатором, датой или суммой, поэтому строки остаются уникальными.

Попытка выбрать дату отдельно тоже может быть недостаточной. Максимальная дата не определяет однозначно конкретную строку, если несколько заказов имеют одинаковое время; кроме того, остальные столбцы нельзя безопасно взять произвольно.

Подробное решение

Сначала нужно определить, что именно означает «последняя строка»: обычно это максимальная дата, а при совпадении дат — дополнительный детерминирующий ключ, например идентификатор заказа. Затем этот критерий явно включают в запрос.

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

Здесь строки разбиваются на группы по client_id, внутри каждой группы сортируются по дате и идентификатору, а фильтр оставляет строку с номером один. В отличие от DISTINCT, этот подход выражает правило выбора строки.

Если нужны только клиент и последняя дата, достаточно агрегирования с MAX(created_at). Но если требуются остальные поля заказа, агрегат сам по себе их не определяет; понадобится соединение с исходной таблицей или оконный механизм.

Следует учитывать ничьи. Если сортировать только по дате, выбор при одинаковых датах может быть недетерминированным или зависеть от плана выполнения. Добавление уникального ключа делает правило выбора явным, но при этом выбирается одна конкретная строка, а не все строки с одинаковой максимальной датой.

Ситуация из практики

В отчёте требовался последний статус клиента. Вариант с DISTINCT оставлял несколько строк, потому что статусы и даты различались. Вариант с группировкой по клиенту и максимальной датой находил дату, но при совпадении дат мог возвращать несколько статусов или требовал дополнительной логики.

Была выбрана оконная нумерация с сортировкой по дате и уникальному идентификатору. Она явно задала приоритет строк, сохранила все нужные столбцы и дала ровно одну запись на клиента. Цена решения — необходимость сортировки внутри групп и проверки индексов, особенно на больших объёмах данных.

Что кандидаты часто упускают

1. Удалит ли DISTINCT повторяющиеся значения, если в SELECT включён уникальный идентификатор?

Нет. DISTINCT сравнивает весь набор выражений из списка выбора. Если идентификатор различается, строки считаются разными, даже когда остальные столбцы совпадают. Поэтому добавление технического ключа часто полностью устраняет эффект дедупликации.

2. Гарантирует ли DISTINCT порядок строк или выбор первой строки?

Нет. DISTINCT не выбирает «первую» строку и сам по себе не задаёт порядок результата. Без явного ORDER BY порядок строк не следует считать гарантированным, а даже наличие сортировки не превращает DISTINCT в механизм выбора одной строки из группы.

3. Чем отличается выбор одной строки от поиска всех строк с максимальной датой?

Оконная функция с ROW_NUMBER выбирает одну строку согласно заданному правилу, включая правило разрешения ничьих. Если нужно вернуть всех клиентовых заказов, имеющих максимальную дату, применяют другой критерий, например ранжирование с одинаковым рангом для совпадающих дат или соединение с результатом MAX. Выбор механизма зависит от бизнес-смысла: «ровно одна запись» и «все записи-лидеры» — разные требования.