В аналитическом запросе после добавления DISTINCT строки всё ещё не схлопываются. Как порядок вычисления ок...

В аналитическом запросе после добавления DISTINCT строки всё ещё не схлопываются. Как порядок вычисления оконной функции объясняет этот результат?

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

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

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

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

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

GROUP BY предназначен для уменьшения детализации: несколько исходных строк превращаются в одну строку группы. Оконные функции решают другую задачу — добавляют расчёт к каждой строке, не уничтожая исходную детализацию.

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

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

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

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

Неверное ожидание, что DISTINCT «сначала уберёт дубли, а потом посчитает окно», приводит к ошибочным выводам о размере окна и детализации результата.

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

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

Рассмотрим минимальный пример:

WITH sales(customer_id, order_id, amount) AS ( VALUES (1, 101, 10), (1, 102, 20), (2, 201, 15) ) SELECT DISTINCT customer_id, SUM(amount) OVER (PARTITION BY customer_id) AS customer_total FROM sales;

Для клиента 1 оконная сумма равна 30 в обеих исходных строках, поэтому пара (1, 30) повторяется и после DISTINCT остаётся один раз. Для клиента 2 остаётся пара (2, 15).

Если добавить order_id, строки клиента 1 уже не будут дубликатами: (1, 101, 30) и (1, 102, 30) различаются. Оконная функция при этом всё равно рассчитана по двум исходным строкам клиента.

Ключевое ограничение: DISTINCT не позволяет выбрать «одну произвольную строку на клиента» с гарантированно согласованными остальными столбцами. Для этого обычно применяют ранжирование с последующей фильтрацией, а для получения одной агрегированной строки — GROUP BY.

Также DISTINCT может быть дорогим: СУБД должна сравнить и устранить дубликаты, часто используя сортировку или хеширование. Чем больше выбранных столбцов и объём результата, тем выше потенциальные затраты памяти и времени.

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

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

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

Выбранное решение зависит от цели: для отчёта «одна строка на клиента» использовали GROUP BY, а для сохранения деталей и отображения общей выручки — оконную сумму без DISTINCT. Это устранило неоднозначность и позволило явно контролировать детализацию результата.

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

  1. Удаляет ли DISTINCT дубликаты только по столбцу, указанному в PARTITION BY?

Нет. DISTINCT сравнивает все столбцы итоговой строки, включённые в SELECT. PARTITION BY лишь определяет набор строк, по которому вычисляется окно, и не задаёт критерий устранения дубликатов.

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

  1. Изменит ли DISTINCT набор строк, по которому оконная функция выполняет расчёт?

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

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

  1. Можно ли заменить DISTINCT оконной функцией для выбора одной строки из группы?

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

Такой выбор требует явного правила: например, оставить запись с максимальной датой, а при совпадении дат использовать уникальный идентификатор как дополнительный критерий. Без полного порядка результат может быть неоднозначным, даже если синтаксис запроса допускает выполнение.