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

В чём разница между WHERE и HAVING?

Запрос выполняется НЕ в том порядке, в котором написан:

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

🟢 Junior Level

WHERE и HAVING — оба используются для фильтрации данных, но на разных этапах:

  • WHERE — фильтрует строки до группировки
  • HAVING — фильтрует группы после группировки

Простая аналогия: Представьте, что вы сортируете монеты:

  1. WHERE — отбираете только монеты определённого номинала
  2. Группируете по годам
  3. HAVING — оставляете только группы, где монет больше 5

Пример:

-- WHERE: фильтруем заказы по статусу
-- HAVING: оставляем только пользователей с > 5 заказами
SELECT user_id, COUNT(*) as orders_count
FROM orders
WHERE status = 'COMPLETED'  -- Фильтр ДО группировки
GROUP BY user_id
HAVING COUNT(*) > 5;        -- Фильтр ПОСЛЕ группировки

Правило:

  • Нет GROUP BY → используйте WHERE
  • Есть GROUP BY и нужно отфильтровать по агрегату (COUNT, SUM) → HAVING

🟡 Middle Level

Порядок выполнения запроса (Logical Query Processing)

Запрос выполняется НЕ в том порядке, в котором написан:

SELECT user_id, COUNT(*) as cnt     -- 5. SELECT
FROM orders                         -- 1. FROM
WHERE status = 'COMPLETED'          -- 2. WHERE (индексы работают!)
GROUP BY user_id                    -- 3. GROUP BY
HAVING COUNT(*) > 5                 -- 4. HAVING
ORDER BY cnt DESC;                  -- 6. ORDER BY

Ключевое: WHERE выполняется ДО GROUP BY, а HAVINGПОСЛЕ.

Почему это важно для производительности

-- ❌ ПЛОХО: фильтрация в HAVING вместо WHERE
SELECT city, COUNT(*) 
FROM users
GROUP BY city
HAVING city = 'Moscow';  -- Группируем ВСЕ города, потом фильтруем!

-- ✅ ХОРОШО: фильтрация в WHERE
SELECT city, COUNT(*) 
FROM users
WHERE city = 'Moscow'  -- Фильтруем ДО группировки → меньше данных
GROUP BY city;

Разница: Первый запрос группирует миллионы строк, второй — только московских пользователей.

Сравнение

Характеристика WHERE HAVING
Что фильтрует Строки Группы
Когда ДО GROUP BY ПОСЛЕ GROUP BY
Агрегаты ❌ Нельзя ✅ Можно
Индексы ✅ Работают ❌ Не работают
Производительность Быстро Медленнее (после агрегации)

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

  1. Агрегат в WHERE
    -- ❌ ОШИБКА: агрегаты нельзя в WHERE
    SELECT user_id FROM orders WHERE COUNT(*) > 5;
       
    -- ✅ Правильно: через HAVING
    SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) > 5;
    
  2. Фильтрация неагрегатного поля в 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;
    
  3. Алиас из SELECT в HAVING
    -- ⚠️ Работает в 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;
    

HAVING без GROUP BY

-- Вся таблица = одна группа
SELECT 'Many orders' 
FROM orders
HAVING COUNT(*) > 1000000;

-- Вернёт 1 строку, если заказов > 1 млн, иначе 0 строк

🔴 Senior Level

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

  1. HAVING для неагрегатных фильтровHAVING city = 'Moscow' группирует ВСЕ города потом фильтрует. Используйте WHERE
  2. WHERE с агрегатамиWHERE COUNT(*) > 5 нельзя. Используйте HAVING
  3. HAVING для фильтрации по оконным функциям — оконные функции считаются ПОСЛЕ HAVING. Используйте подзапрос/CTE

Filter Push-down оптимизация

PostgreSQL 12+ пытается автоматически переносить условия из HAVING в WHERE:

До PG 12 Filter Push-down отсутствовал — условие в HAVING всегда оставалось в HAVING. Это означало, что HAVING city = 'Moscow' группирует все города, а затем фильтрует. На PG 12+ оптимизатор сам перенесёт city = 'Moscow' в WHERE, но полагаться на это нельзя.

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

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

Но нельзя полагаться на это!

Когда Push-down НЕ работает:

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

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

work_mem — параметр PostgreSQL, ограничивающий объём памяти для одной операции (сортировка, хэш-агрегация). По умолчанию 4MB. Если операция не влезает, данные сбрасываются на диск (spill to disk) во временные файлы (temp_files), что замедляет выполнение в 10-100 раз.

Влияние на work_mem и temp_files

-- Проблема: 100 млн строк без фильтра → GROUP BY
SELECT status, COUNT(*) FROM orders GROUP BY status;
-- work_mem = 4MB → Hash Aggregate не влезает → spill на диск → temp_files!

-- Решение: фильтр в WHERE
SELECT status, COUNT(*) FROM orders
WHERE created_at > '2024-01-01'  -- Сокращаем вход в 10 раз
GROUP BY status;
-- Теперь влезает в work_mem → в 100 раз быстрее
-- Операции в памяти (RAM) происходят в наносекундах, операции с диском —
-- в миллисекундах. Разница в 6 порядков. Даже с учётом того, что spill
-- не на каждую строку, итоговое замедление составляет 10-100 раз.

Мониторинг spill:

EXPLAIN (ANALYZE, BUFFERS) 
SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Ищите: "HashAggregate" + "Disk: X kB" → плохо!

WHERE vs HAVING vs Window Functions

Фильтрация по оконной функции невозможна в 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;  -- Ошибка: rn не существует на этапе HAVING

-- ✅ Решение: подзапрос или 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;

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

-- Правильное разделение ответственности
SELECT 
    department,
    COUNT(*) as emp_count,
    AVG(salary) as avg_salary
FROM employees
WHERE status = 'ACTIVE'              -- 1. Отсекаем уволенных (индекс!)
  AND hire_date > '2020-01-01'       -- 2. Сокращаем вход
GROUP BY department
HAVING COUNT(*) > 10                 -- 3. Бизнес-логика: только крупные отделы
   AND AVG(salary) > 100000;         -- 4. Фильтр по агрегату

Edge Cases

  1. NULL в HAVING
    SELECT status, COUNT(*)
    FROM orders
    GROUP BY status
    HAVING COUNT(*) > 10;
       
    -- NULL — это отдельная группа!
    -- Если есть строки с status = NULL, они сгруппируются вместе
    
  2. HAVING с несколькими агрегатами
    SELECT product_id
    FROM orders
    GROUP BY product_id
    HAVING COUNT(*) > 100               -- Минимум 100 заказов
       AND AVG(amount) > 1000            -- Средний чек > 1000
       AND MAX(amount) - MIN(amount) < 5000;  -- Разброс цен < 5000
    
  3. FILTER в HAVING
    SELECT project_id
    FROM tasks
    GROUP BY project_id
    HAVING COUNT(*) > 5
       AND COUNT(*) FILTER (WHERE status = 'blocked') > 2;
    -- Проекты с > 5 задачами, из них > 2 заблокированы
    

Performance Impact

Сценарий WHERE HAVING
Индексы ✅ Index Scan ❌ Нет
Объём данных Сокращает ДО GROUP BY Фильтрует ПОСЛЕ GROUP BY
work_mem Меньше → быстрее Больше → медленнее
temp_files Меньше риск Больше риск

Production Experience

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

  • Аналитика: отчёт по продажам (100 млн строк)
  • Проблема: Запрос 2 минуты, spill на диск 5 ГБ
  • EXPLAIN: HashAggregate Disk: 5GB
  • Причина: ORM генерировал HAVING status = 'ACTIVE' вместо WHERE
  • Решение: Перенесли status = 'ACTIVE' в WHERE
  • Результат: Запрос 3 секунды (ускорение в 40 раз), без spill

Monitoring

-- 1. Проверка на spill в планах
EXPLAIN (ANALYZE, BUFFERS)
SELECT city, COUNT(*) FROM users GROUP BY city;
-- Ищите: "Disk: X kB" → увеличьте work_mem или добавьте WHERE

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

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

Best Practices

  1. WHERE для фильтрации строк (до GROUP BY)
  2. HAVING только для фильтрации по агрегатам
  3. Никогда не используйте HAVING для неагрегатных условий
  4. Проверяйте план на наличие spill (Disk usage)
  5. Фильтруйте как можно раньше — экономите CPU и RAM
  6. FILTER внутри HAVING для сложных условий
  7. Подзапрос/CTE для фильтрации по оконным функциям

Резюме для Senior

  • WHERE фильтрует input, HAVING фильтрует output группировки
  • Filter Push-down (PG 12+) не всегда работает — не полагайтесь
  • work_mem давление: HAVING не спасает от переполнения при GROUP BY
  • Оконные функции считаются ПОСЛЕ HAVING → нужен подзапрос
  • temp_files — индикатор проблемы: EXPLAIN (ANALYZE, BUFFERS)
  • Золотое правило: Всё, что можно → в WHERE

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

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

  • WHERE фильтрует строки ДО GROUP BY, HAVING — группы ПОСЛЕ GROUP BY
  • WHERE использует индексы, HAVING — нет
  • Агрегаты (COUNT, SUM) нельзя в WHERE, можно в HAVING
  • Filter Push-down (PG 12+): оптимизатор переносит неагрегатные условия из HAVING в WHERE
  • FILTER внутри HAVING: COUNT(*) FILTER (WHERE status = 'ACTIVE') > 5
  • work_mem давление: HAVING не спасает — все группы уже созданы
  • Оконные функции считаются ПОСЛЕ HAVING → подзапрос/CTE
  • Алиасы из SELECT в HAVING: работает в PG, НЕ работает в SQL Server/Oracle

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

  • «Почему HAVING медленнее WHERE?» → Группирует ВСЕ данные, потом фильтрует
  • «Когда Filter Push-down не работает?» → CTE MATERIALIZED, Views, подзапросы
  • «Можно ли HAVING без GROUP BY?» → Да, вся таблица = одна группа
  • «Что лучше: FILTER или CASE WHEN в агрегате?» → FILTER (чистый SQL)

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

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

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

  • [[Что делает GROUP BY]] → стратегии агрегации
  • [[Когда использовать HAVING]] → сценарии использования
  • [[Что такое оконные функции]] → почему нельзя в HAVING