Фильтр по столбцу группировки задан после агрегирования: при каком условии оптимизатор может безопасно перенести его до группировки?
Оптимизатор может перенести условие из HAVING до группировки, если оно зависит только от столбцов группировки, не использует результат агрегатной функции и такое преобразование сохраняет семантику запроса. Это называется проталкиванием предиката. Перенос уменьшает число строк до агрегации и может открыть возможность индексного поиска.
Реляционные оптимизаторы строят план не только по тексту запроса, но и преобразуют его логически эквивалентными способами. Один из фундаментальных принципов — выполнять фильтрацию как можно раньше, чтобы уменьшить промежуточные наборы данных.
Без такого преобразования сначала пришлось бы прочитать и сгруппировать больше строк, построить хеш-таблицу или выполнить сортировку, а затем отбросить ненужные группы. Ранний фильтр снижает стоимость чтения, агрегации и последующих операций.
Условие после GROUP BY может выглядеть как фильтр обычного столбца, но фактически оно задано на уровне сформированных групп. Если оптимизатор ошибочно применит его к исходным строкам, результат может измениться.
Безопасный перенос невозможен для условия, зависящего от COUNT, SUM, AVG и других агрегатов. Например, проверка суммы группы требует сначала собрать группу. Дополнительную осторожность требуют внешние соединения, недетерминированные выражения и условия, чувствительные к обработке NULL.
Если предикат использует только ключ группировки, то отбор строк по этому ключу до GROUP BY не меняет состав подходящих групп. Он лишь удаляет строки, которые всё равно не могли попасть в результат.
Например, логически эквивалентны следующие варианты:
Во втором варианте СУБД может использовать индекс по customer_id, выполнить узкий диапазонный поиск и агрегировать только выбранные строки. В первом варианте оптимизатор может сам получить такой же план благодаря проталкиванию предиката.
Перенос не гарантирует индексный поиск. Оптимизатор сравнивает стоимость доступных вариантов: учитывает селективность фильтра, статистику, стоимость случайного чтения, необходимость обращения к таблице и стоимость самой агрегации. При низкой селективности полное сканирование всё ещё может оказаться дешевле.
Условие вроде HAVING COUNT(*) > 10 нельзя перенести как эквивалентный фильтр на исходные строки: количество строк определяется только после группировки. Иногда оптимизатор может применить отдельные безопасные преобразования, но это уже не означает перенос всего предиката.
При внешнем соединении раннее применение фильтра к сохранённой стороне может изменить наличие строк с NULL и тем самым результат. Поэтому оптимизатор проверяет не только форму выражения, но и свойства соединений, типов сравнения и используемых функций.
Отчёт группировал заказы по customer_id, но после группировки оставлял одного клиента. План сначала читал большую таблицу заказов, выполнял агрегацию, а затем отбрасывал почти все группы. Это создавало лишнюю нагрузку на чтение и память.
Рассматривались три варианта: принудительно переписать запрос с WHERE, добавить индекс только ради отчёта или оставить текст запроса и проверить, умеет ли оптимизатор проталкивать предикат. Переписывание обычно проще, но не всегда возможно для генерируемого SQL; отдельный индекс ускоряет фильтр, но увеличивает стоимость вставок и обновлений.
Выбранным решением стало явное размещение фильтра до группировки и проверка плана после обновления статистики. Такой вариант сделал намерение запроса очевидным, позволил рассмотреть индексный доступ и не требовал хинта, который мог бы ухудшить план при изменении объёма данных.
Нет. В простом запросе над одной таблицей — обычно да, если выражение детерминировано и не зависит от агрегатов. Но внешние соединения, преобразования типов, особая семантика NULL и недетерминированные функции могут нарушить эквивалентность. Нужно анализировать не только столбец, но и контекст, в котором он вычисляется.
Нет. Проталкивание предиката и выбор индексного доступа — разные решения. После уменьшения логического набора оптимизатор всё равно оценивает стоимость вариантов. Если предикат возвращает значительную долю таблицы, а строки приходится получать случайными чтениями, последовательное сканирование может быть дешевле.
В общем случае нельзя. Условие SUM(amount) > 1000 относится к совокупному значению группы, а не к отдельной строке. Можно добавить независимый предварительный предикат по исходным данным, например ограничить период, но он будет дополнительным условием, а не эквивалентной заменой проверки агрегата.