Вопрос 13 · Раздел 1

Когда использовать HAVING?

Правильное разделение фильтров — признак хорошего SQL:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

HAVING используется, когда нужно отфильтровать результаты после группировки по агрегатным функциям (COUNT, SUM, AVG и т.д.).

Простое правило:

  • Фильтр по обычным полям → WHERE
  • Фильтр по результатам агрегации → HAVING

Пример:

-- Найти пользователей с более чем 5 заказами
SELECT user_id, COUNT(*) as orders_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;  -- Фильтр ПОСЛЕ группировки

-- Найти отделы со средней зарплатой > 100,000
SELECT department, AVG(salary) as avg_sal
FROM employees
GROUP BY department
HAVING AVG(salary) > 100000;

Частые сценарии:

  • Поиск дубликатов (HAVING COUNT(*) > 1)
  • Поиск групп, превышающих порог
  • Фильтрация по средним/суммарным значениям

🟡 Middle Level

Архитектурный паттерн: WHERE + HAVING

Правильное разделение фильтров — признак хорошего SQL:

SELECT customer_id, AVG(amount) as avg_amount
FROM orders
WHERE status = 'delivered'           -- 1. Фильтр ДО группировки (индекс!)
  AND created_at > '2024-01-01'      -- 2. Сокращаем входные данные
GROUP BY customer_id
HAVING COUNT(*) > 10                 -- 3. Фильтр ПОСЛЕ группировки
   AND AVG(amount) > 5000;           -- 4. Бизнес-логика

Почему так важно:

  • WHERE сокращает количество строк ДО GROUP BY → меньше работы
  • HAVING фильтрует уже сгруппированные данные

Основные сценарии

1. Агрегированные пороги:

-- Продавцы с выручкой > 1 млн
SELECT seller_id, SUM(price) as revenue
FROM sales
GROUP BY seller_id
HAVING SUM(price) > 1000000;

2. Поиск дубликатов:

-- Дубликаты email
SELECT email, COUNT(*) as cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

3. Проверка свойств группы:

-- Проекты с проблемами (больше 2 заблокированных задач)
SELECT project_id
FROM tasks
GROUP BY project_id
HAVING COUNT(*) > 5
   AND COUNT(*) FILTER (WHERE status = 'blocked') > 2;

4. HAVING без GROUP BY:

-- Проверка условия по всей таблице
SELECT 'Warning: High avg salary'
FROM employees
HAVING AVG(salary) > 200000;
-- Вернёт 1 строку если условие истинно, иначе 0 строк

Типичные ошибки

  1. Фильтрация неагрегатного поля в HAVING
    -- ❌ ПЛОХО: city не агрегат
    SELECT city, COUNT(*) FROM users
    GROUP BY city
    HAVING city = 'Moscow';
       
    -- ✅ ХОРОШО: в WHERE
    SELECT city, COUNT(*) FROM users
    WHERE city = 'Moscow'
    GROUP BY city;
    
  2. Избыточность в ORM
    -- ORM часто генерирует:
    SELECT city, COUNT(*) FROM users GROUP BY city HAVING city = 'Moscow';
    -- Это антипаттерн! Город должен быть в WHERE
    -- Позволяет использовать индекс и сократить данные для агрегации
    

Сравнение WHERE и HAVING

Сценарий WHERE HAVING
status = 'ACTIVE' ❌ (если нет GROUP BY)
COUNT(*) > 10 ❌ Нельзя
SUM(amount) > 1000 ❌ Нельзя
city = 'Moscow' ⚠️ Можно, но плохо

🔴 Senior Level

Когда НЕ использовать HAVING

  1. Для неагрегатных фильтровHAVING city = 'Moscow' группирует ВСЕ города, потом фильтрует. Перенесите в WHERE
  2. Для фильтрации по оконным функциям — оконные функции считаются ПОСЛЕ HAVING. Используйте подзапрос/CTE
  3. Когда групп слишком много — миллионы групп → spill на диск. Пре-фильтруйте в WHERE или используйте Materialized Views

Проблема huge number of groups

Если после WHERE остаётся 100 млн строк и 10 млн групп:

-- Проблема: база должна выделить память для ВСЕХ 10 млн групп
SELECT user_id, SUM(amount)
FROM orders
WHERE created_at > '2023-01-01'  -- 100 млн строк
GROUP BY user_id                  -- 10 млн групп
HAVING SUM(amount) > 10000;       -- Фильтр после

Что происходит:

  1. HashAggregate пытается разместить 10 млн групп в work_mem (work_mem — память для одной операции, по умолчанию 4MB)
  2. Если не влезает → spill на диск (temp_files — временные файлы на диске)
  3. Производительность падает в 10-100 раз (операции с диском в миллионы раз медленнее RAM)

Решения:

-- 1. Предварительная фильтрация через CTE
WITH filtered_orders AS (
    SELECT user_id, amount
    FROM orders
    WHERE created_at > '2023-01-01'
      AND amount > 100  -- Дополнительный фильтр
)
SELECT user_id, SUM(amount)
FROM filtered_orders
GROUP BY user_id
HAVING SUM(amount) > 10000;

-- 2. Материализованное представление для частых запросов
CREATE MATERIALIZED VIEW mv_user_stats AS
SELECT user_id, SUM(amount) as total, COUNT(*) as cnt
FROM orders
GROUP BY user_id;

-- Потом быстрый запрос
SELECT user_id FROM mv_user_stats WHERE total > 10000;

FILTER в HAVING для декларативности

-- Элегантная проверка сложных условий
SELECT project_id
FROM tasks
GROUP BY project_id
HAVING 
    COUNT(*) > 10                                      -- Минимум 10 задач
    AND COUNT(*) FILTER (WHERE status = 'done') >= 8   -- 80% выполнено
    AND COUNT(*) FILTER (WHERE status = 'blocked') = 0 -- Нет блокировок
    AND MAX(due_date) > CURRENT_DATE;                  -- Дедлайн не прошёл

Redundancy и оптимизатор

PostgreSQL пытается делать Filter Push-down — переносить условия из HAVING в WHERE:

-- Запрос
SELECT city, COUNT(*) FROM users GROUP BY city HAVING city = 'Moscow';

-- Оптимизатор превращает в:
SELECT city, COUNT(*) FROM users WHERE city = 'Moscow' GROUP BY city;

НО: Это работает не всегда!

  • Сложные выражения с подзапросами
  • Внутри Views
  • CTE с MATERIALIZED

Золотое правило: Всегда пишите неагрегатные фильтры в WHERE явно.

Стандарты SQL и переносимость

-- ⚠️ Работает в PostgreSQL, НЕ работает в SQL Server/Oracle
SELECT city, COUNT(*) as cnt FROM users
GROUP BY city
HAVING cnt > 10;  -- cnt — алиас из SELECT

-- ✅ Переносимо во все СУБД
SELECT city, COUNT(*) FROM users
GROUP BY city
HAVING COUNT(*) > 10;

Edge Cases

  1. HAVING с подзапросом
    -- Пользователи, у которых сумма заказов больше средней
    SELECT user_id, SUM(amount) as total
    FROM orders
    GROUP BY user_id
    HAVING SUM(amount) > (SELECT AVG(total) FROM (
        SELECT SUM(amount) as total FROM orders GROUP BY user_id
    ) sub);
    
  2. HAVING с оконными функциями — НЕЛЬЗЯ
    -- ❌ ОШИБКА: оконные функции считаются ПОСЛЕ HAVING
    SELECT user_id, amount,
           ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY created_at DESC) as rn
    FROM orders
    GROUP BY user_id, amount, created_at
    HAVING rn = 1;  -- Ошибка!
       
    -- ✅ Решение: CTE
    WITH ranked AS (
        SELECT user_id, amount,
               ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY created_at DESC) as rn
        FROM orders
    )
    SELECT user_id, amount FROM ranked WHERE rn = 1;
    
  3. HAVING с несколькими GROUP BY
    -- ROLLUP + HAVING
    SELECT department, city, COUNT(*)
    FROM employees
    GROUP BY ROLLUP(department, city)
    HAVING COUNT(*) > 5;
    -- Фильтрует ВСЕ уровни (детальные и итоговые строки)
    

Performance Impact

-- Мониторинг давления на work_mem
EXPLAIN (ANALYZE, BUFFERS)
SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > 100;

-- Ищите:
-- HashAggregate (memory usage: X kB)  → если X близко к work_mem → spill риск
-- HashAggregate (disk: Y kB)          → spill произошёл! Увеличьте work_mem

Production Experience

Реальный сценарий:

  • E-commerce: отчёт по продажам (500 млн строк)
  • ORM генерировал HAVING status = 'COMPLETED' вместо WHERE
  • HashAggregate: 500 млн строк → spill 30 ГБ на диск
  • Время: 15 минут
  • Решение: WHERE status = 'COMPLETED' до GROUP BY
  • Результат: 10 секунд (ускорение в 90 раз)
  • Почему: ORM генерировал HAVING status = 'COMPLETED' — база сначала группировала все 500 млн строк (включая отменённые, возвращённые и т.д.), создавая миллионы групп, и только потом фильтровала. Перенос в WHERE сократил входные данные для GROUP BY в 10 раз + позволил использовать индекс по status.

Monitoring

-- 1. Поиск запросов с HAVING в pg_stat_statements
SELECT query, mean_exec_time, calls, shared_blks_read
FROM pg_stat_statements
WHERE query LIKE '%HAVING%'
ORDER BY mean_exec_time DESC
LIMIT 10;

-- 2. Проверка на spill
EXPLAIN (ANALYZE, BUFFERS) 
SELECT ... GROUP BY ... HAVING ...;
-- "Disk: X kB" → проблема!

-- 3. Проверка Filter Push-down
EXPLAIN (VERBOSE)
SELECT city, COUNT(*) FROM users GROUP BY city HAVING city = 'Moscow';
-- В плане увидите, перенесён ли фильтр

Best Practices

  1. WHERE для неагрегатных фильтров, HAVING для агрегатных
  2. FILTER (WHERE …) внутри HAVING для сложных условий
  3. Избегайте алиасов из SELECT в HAVING (непереносимо)
  4. Мониторьте work_mem при больших GROUP BY
  5. CTE/Materialized Views для предварительной фильтрации
  6. Проверяйте план на spill (Disk usage)
  7. Помните: HAVING не спасает от переполнения памяти во время GROUP BY

Резюме для Senior

  • HAVING — последний рубеж фильтрации после GROUP BY
  • Не экономит ресурсы на этапе агрегации — все группы уже созданы
  • Filter Push-down (PG 12+) не всегда работает — пишите WHERE явно
  • work_mem давление: HAVING фильтрует ВЫХОД, не ВХОД
  • Оконные функции недоступны в HAVING → CTE/подзапрос
  • FILTER для декларативности сложных условий
  • Переносимость: не используйте алиасы из SELECT в HAVING

🎯 Шпаргалка для интервью

Обязательно знать:

  • HAVING фильтрует РЕЗУЛЬТАТ GROUP BY, не входные данные
  • WHERE сокращает данные ДО GROUP BY → меньше work_mem, быстрее
  • Filter Push-down (PG 12+): автоматический перенос из HAVING в WHERE, но НЕ всегда
  • Huge number of groups: 10 млн групп → spill на disk → CTE/Materialized View
  • FILTER в HAVING: COUNT(*) FILTER (WHERE status = 'blocked') > 2
  • Оконные функции считаются ПОСЛЕ HAVING → подзапрос/CTE
  • Алиасы из SELECT в HAVING: НЕ переносимо (SQL Server/Oracle не поддерживают)
  • HAVING с подзапросом: возможно, но медленно

Частые уточняющие вопросы:

  • «Почему ORM генерирует HAVING вместо WHERE?» → ORM антипаттерн, не оптимизирует
  • «Как избежать spill при 10 млн групп?» → Фильтр в WHERE, work_mem ↑, Materialized View
  • «Можно ли FILTER в HAVING?» → Да, элегантно для сложных условий
  • «Когда HAVING с подзапросом оправдан?» → Сравнение группы со средней по всем

Красные флаги (НЕ говорить):

  • ❌ «HAVING сокращает данные для GROUP BY» (нет, фильтрует ВЫХОД)
  • ❌ «Можно положиться на Filter Push-down» (не всегда работает!)
  • ❌ «Оконные функции можно в HAVING» (ПОСЛЕ HAVING считаются!)

Связанные темы:

  • [[В чём разница между WHERE и HAVING]] → порядок выполнения
  • [[Что делает GROUP BY]] → huge number of groups
  • [[Что такое оконные функции]] → почему нельзя в HAVING