Исторический контекст
Стандарт ANSI SQL:2011 ввел клаузулы исключения фрейма в синтаксис оконных функций, решая проблему, при которой фреймы окон обязательно включали текущую строку. До этого улучшения разработчикам приходилось прибегать к громоздким самосоединениям или алгебраическим манипуляциям (вычитая текущее значение из общего) для вычисления агрегатов, исключающих фокусную строку. Стандарт определяет четыре варианта исключения: EXCLUDE NO OTHERS, EXCLUDE CURRENT ROW, EXCLUDE GROUP и EXCLUDE TIES, предоставляя детерминированную семантику для операций над множествами в рамках упорядоченных разделов.
Проблема
При анализе конкурентных метрик—таких как вычисление средней цены продажи аналогичных продуктов при исключении самого продукта из этой средней—запрос должен определить окно, которое охватывает все связанные строки, кроме текущей. Традиционные оконные функции, такие как AVG() OVER (PARTITION BY category), включают текущую строку, искажающую результат. Реализация этого через подзапросы или соединения вызывает ненужную сложность и снижение производительности, особенно при работе с крупными разделенными наборами данных, где декартовы произведения или коррелированные подзапросы были бы слишком дорогими.
Решение
Используйте клаузулу исключения фрейма в спецификации окна: AVG(price) OVER (PARTITION BY category ORDER BY price ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE CURRENT ROW). Этот синтаксис указывает движку SQL сначала установить полный фрейм раздела, а затем логически удалить текущую строку перед вычислением агрегата. Для сценариев, требующих исключения всех связей (например, всех продуктов, находящихся по одной и той же цене), EXCLUDE GROUP удаляет как текущую строку, так и ее упорядоченных аналогов, тогда как EXCLUDE TIES сохраняет текущую строку, но удаляет дублирующие значения упорядочивания.
Команда аналитики электронной коммерции должна создать отчет о "Рыночной позиции". Для каждой листинга продавца электронного гаджета они должны отобразить цену этого продавца наряду со средней ценой всех других продавцов, предлагающих ту же модель гаджета.
Сначала был протестирован подход с самосоединением, где таблица листингов соединялась сама с собой по model_id, исключая совпадающие первичные ключи. Плюсы: Он поддерживается во всех диалектах SQL и концептуально прост. Минусы: Исполнение обладает сложностью O(n²) в худшем случае, вызывая экспоненциальное замедление на миллионах строк; кроме того, оптимизатор запросов часто сталкивается с трудностями с неравным предикатом соединения, создавая неэффективные планы исполнения с хэш-выходами или вложенными циклом соединений.
Также была оценена алгебраическая обходная стратегия, вычисляющая глобальную сумму и количество по модели, затем выводящая среднее значение других через (SUM(price) - current_price) / (COUNT(*) - 1). Плюсы: Она избегает соединений и требует только одного сканирования оконной функции. Минусы: Она катастрофически терпит неудачу, когда COUNT(*) = 1 (деление на ноль) или когда цены NULL, требуя многословных охран CASE; кроме того, она не может быть применена к неалгебраическим агрегатам, таким как MEDIAN или MODE.
Команда в конечном итоге выбрала спецификацию фрейма EXCLUDE CURRENT ROW. Аргументация: Она декларативна, устраняет необходимость в проверке NULL-значений с помощью выражений CASE, естественным образом возвращая NULL для пустых фреймов, и выполняется за O(n) времени с использованием одного отсортированного прохода с минимальными затратами памяти. В результате запрос снизил время генерации отчета с двенадцати минут до менее чем десяти секунд.
Результат: Производственный отчет теперь точно вычисляет конкурентные показатели для 50 миллионов листингов ежедневно, элегантно обрабатывая редкие предметы с единственными продавцами, отображая NULL (интерпретируемый как "Нет конкуренции"), а не ошибки или нулевые значения.
Как ведет себя EXCLUDE CURRENT ROW при использовании оконных фреймов на основе RANGE по сравнению с фреймами на основе ROWS, особенно относительно групп аналогов?
Когда оконный фрейм использует ROWS, EXCLUDE CURRENT ROW удаляет точно одну физическую строку—текущую строку—из агрегации. Однако, когда используется RANGE (например, RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING), концепция "текущей строки" концептуально представляет все строки, разделяющие то же значение упорядочивания, что и текущая строка в указанном диапазоне. В этом контексте EXCLUDE CURRENT ROW удаляет только конкретный экземпляр строки, оставляя других аналогов (связей) в фрейме. Напротив, EXCLUDE GROUP удаляет текущую строку и всех аналогов независимо от единицы фрейма, в то время как EXCLUDE TIES удаляет всех аналогов, кроме текущей строки. Кандидаты часто путают эти понятия, предполагая, что EXCLUDE CURRENT ROW с RANGE ведет себя как EXCLUDE GROUP, что приводит к неправильным результатам агрегации, когда существуют дублирующие ключи упорядочивания.
Почему запрос, использующий EXCLUDE CURRENT ROW на одномаршрутном разделе, может вернуть NULL, и как это отличается от методов ручного вычитания?
Стандарт ANSI SQL определяет, что агрегат над пустым множеством возвращает NULL. Когда EXCLUDE CURRENT ROW применяется к разделу, содержащему только одну строку, фрейм становится пустым, что приводит к тому, что AVG, SUM или COUNT автоматически возвращают NULL. Напротив, ручные методы, такие как (SUM(col) - col) / (COUNT(*) - 1), сталкиваются с проблемами деления на ноль или распространения NULL в арифметике, требуя явных операторов CASE для безопасного управления единичными разделами. Кандидаты часто упускают это автоматическое поведение обработки NULL, ожидая нуля или текущего значения, и не понимают, что EXCLUDE обеспечивает более надежную защиту от нулей для пограничных условий.
Можно ли комбинировать EXCLUDE с произвольными размерами фрейма, такими как скользящие окна (например, ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING), и каковы последствия для производительности?
Да, клаузулы EXCLUDE действительны с любым размером фрейма, включая BETWEEN скользящие окна. Например, AVG(val) OVER (ORDER BY time ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING EXCLUDE CURRENT ROW) вычисляет среднее значение двух предшествующих и двух последующих значений, фактически создавая 4-точечное скользящее среднее, сосредоточенное на—но исключая—текущую точку. По вопросам производительности современные оптимизаторы реализуют это через алгоритм потоковой передачи с кольцевым буфером или деком, поддерживая O(n) сложность на каждом разделе. Кандидаты часто предполагают, что EXCLUDE требует полной материализации раздела или работает только с UNBOUNDED фреймами, не замечая, что он гармонично интегрируется с ограниченными скользящими окнами для расчетов, таких как центрированные скользящие корреляции или устойчивое сглаживание выбросов, где фокусная точка не должна влиять на статистику.