Извлечение уникальных записей в SQL стало критично важной задачей с массовым переходом организаций на хранение мультиизмеряемых данных. Иногда требуется вывести неповторяющиеся строки по комбинации нескольких столбцов, иногда — лишь по одному ключевому.
История вопроса:
Первые версии SQL предлагали только DISTINCT для фильтрации дубликатов. Затем появились структурные приёмы, в том числе GROUP BY для агрегаций по уникальным наборам значений и оконные функции типа ROW_NUMBER() для более гибких сценариев работы с дубликатами, например: выборка по "последней" или "первой" записи.
Проблема:
DISTINCT работает только на уровне набора полей в SELECT, тогда как GROUP BY требует агрегаций. Оконные функции позволяют продвинутую логику, но их использование часто вызывает ошибки, если не продумывать порядок отбора строк. Часто разработчики путают эти подходы, ошибки приводят к неверным результатам.
Решение:
Пример кода:
Получить одну последнюю запись о заказах по каждому клиенту:
WITH OrdersRank AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY OrderDate DESC) as rn FROM Orders ) SELECT * FROM OrdersRank WHERE rn = 1;
Ключевые особенности:
Можно ли использовать DISTINCT вместе с агрегатными функциями без GROUP BY?
Нет, агрегатные функции требуют группировки, иначе будет ошибка синтаксиса.
SELECT COUNT(DISTINCT CustomerID) -- корректно SELECT SUM(Amount), DISTINCT CustomerID -- ошибка!
Что произойдет, если в GROUP BY не указывать все неагрегируемые поля из SELECT?
Это вызовет ошибку в большинстве СУБД: все поля в SELECT, кроме агрегатных, должны быть перечислены в GROUP BY.
Можно ли "убрать" дубликаты с помощью оконных функций без подзапроса?
Нет: использование ROW_NUMBER() внутри одного SELECT не фильтрует автоматически «повторы», необходим внешний запрос для выбора нужных строк.
Выбрали DISTINCT по всем столбцам для таблицы в 20 млн строк: запрос работал часами, Итог — тайм-аут или падение производительности БД.
Плюсы:
Минусы:
Использовали оконные функции: получили только нужную последнюю запись на клиента за миллисекунды; предыдущие и повторяющиеся не грузились.
Плюсы:
Минусы: