July 3

🧠 CTE — это красиво. Но GROUP BY + HAVING — быстрее.

Если ты хочешь разделить пользователей по активности, не лезь сразу в подзапросы. В 90% случаев можно решить задачу одной агрегацией — и это будет быстрее, проще и понятнее.

— HAVING работает после GROUP BY — идеальное место для фильтрации по числу действий
— CASE в SELECT — гибкий способ сразу проставить сегмент
— Один проход по таблице — вместо каскада CTE

В этом посте:
— Как без вложенностей отобрать активных и «одноразовых»
— Почему HAVING count(*) = 1 — это не хак, а инженерная норма
— Как не перегружать планировщик избыточной вложенностью

Представь: тебе нужно выделить пользователей, которые сделали ровно одно действие. Самая простая мысль — писать вложенный запрос из двух этапов. Но давай честно — это похоже на то, как будто ты отправляешь курьера за хлебом дважды: одна улица туда, вторая обратно 😉

▪️ Что на самом деле происходит
• Ты сначала проходишь по всей таблице событий, собираешь статистику, создаёшь временный результат.
• Потом снова перебираешь его, отфильтровываешь, оставляя только нужных юзеров.
• Это может быть приятно в голове, но базы данных всё-таки не в восторге от повторных походов по миллионам строк.

▪️Почему GROUP BY + HAVING — это шаг вперед
1. Один проход — меньше боли
В стандартном запросе с HAVING ты группируешь и фильтруешь в одном цикле. База читает события, считает, и сразу отбирает нужных пользователей — всё за один проход.

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

3. Читается как по книге
Ты говоришь: “собери сгруппированных пользователей и оставь тех, у кого ровно одна запись”. Нет лишней болтовни и временных таблиц, только суть.

4. Стабильность при росте данных
На первых 10 тысяч строчек вложенность может и прокатить. Но когда их становится 100+ миллионов — всё рушится. HAVING сохраняет производительность гораздо дольше, почти как запас прочности у вашего железа.

5. Гибкость сегментации
Следующий шаг — сегментировать по разным группировкам: «редкие», «активные», «топовые». Вместо копипаста и CTE ты просто добавляешь кейсы с HAVING и условиями — и всё работает прямо в одном запросе.

▪️ Как движок обрабатывает HAVING ?
• Сначала он читает сырьё (таблицу событий).
• Потом выполняет агрегацию через группировку.
• И сразу применяет фильтр по сгруппированным данным.
• Уже после этого результат летит наружу — без лишних столбцов или временных таблиц.

А вот вложенность:
• Группа и агрегация → временная табличка.
• Чтение этой таблички и фильтрация → результат.
• Два прохода, две временные таблицы, двойной IO.

▪️ Пример из работы
Было: запущена витрина, где для каждого события считается количество действий, затем фильтрация по условию. Всё работало нормально… пока нагрузка выросла и обработка с 5 минут превратилась в 25.

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

▪️ В каких случаях я точно использую HAVING
• нужно фильтровать уже сгруппированные данные
• обрабатываю реальные миллионы строк, а не тестовые 100
• когда важно, чтобы запрос оставался предсказуемым, читабельным и оптимизируемым со стороны СУБД

🚨 Ошибки и ловушки
"Но у меня много отдельных условий!" — решение: не переписывай с CTE + UNION. Просто добавь CASE…HAVING… в одном запросе.
"Мне важно видеть промежуточную выборку?" — используй временные или материализованные представления, но фильтрацию делай через HAVING.
"Я не знаю, работает ли индекс" — просто запускай EXPLAIN. Видишь, что применяется IndexScan → отлично! Если нет — приоритет WHERE над HAVING и рефакторинг.

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

💥 Как тебе эта тема?
🔥 — узнал(а) что-то новое
🤝 — тема хорошо знакома


👉 Senior Data Analyst | #sql