← Назад к вопросам

Как работает GROUP BY в SQL? Что такое HAVING и чем он отличается от WHERE?

1.6 Junior🔥 261 комментариев
#SQL и базы данных

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

GROUP BY, WHERE и HAVING в SQL

Группировка данных — это фундаментальный навык для любого аналитика. Понимание, как работает GROUP BY и отличие между WHERE и HAVING критично для написания правильных аналитических запросов.

Как работает GROUP BY

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

Простой пример:

SELECT 
  country,
  COUNT(*) as user_count,
  AVG(revenue) as avg_revenue
FROM users
GROUP BY country;

Этот запрос:

  1. Разделяет всех пользователей по странам
  2. Для каждой страны считает количество пользователей
  3. Для каждой страны считает среднюю выручку

Группировка по нескольким столбцам:

SELECT 
  DATE_TRUNC('day', created_at) as day,
  device_type,
  COUNT(*) as sessions,
  SUM(duration) as total_duration,
  AVG(duration) as avg_duration
FROM sessions
GROUP BY DATE_TRUNC('day', created_at), device_type
ORDER BY day DESC, device_type;

Это создаст комбинации день + тип устройства. Если в базе 2 дня и 3 типа устройств, результат будет до 6 строк.

WHERE vs HAVING: ключевое отличие

WHERE:

  • Фильтрует строки ДО группировки
  • Работает со строками, а не с агрегатами
  • Выполняется РАНЬШЕ GROUP BY
  • Можешь использовать любые столбцы из таблицы
  • Более эффективен (уменьшает данные перед группировкой)

HAVING:

  • Фильтрует группы ПОСЛЕ группировки
  • Работает с агрегатными функциями (COUNT, SUM, AVG)
  • Выполняется ПОСЛЕ GROUP BY
  • Можешь использовать только столбцы, по которым группируешь, или агрегаты
  • Менее эффективен (группирует, потом отсеивает)

Пример:

-- WHERE: найти активные сеансы ПЕРЕД группировкой
SELECT 
  user_id,
  COUNT(*) as session_count,
  AVG(duration) as avg_duration
FROM sessions
WHERE duration > 60 -- ПЕРЕД группировкой, для каждой строки
GROUP BY user_id
HAVING COUNT(*) > 5; -- ПОСЛЕ группировки, для каждой группы

Этот запрос:

  1. WHERE отсеивает сеансы, длительность которых > 60 секунд
  2. GROUP BY группирует по пользователям
  3. HAVING отсеивает пользователей, у которых менее 5 таких сеансов

Порядок выполнения SQL

Важно помнить, в каком порядке выполняются части запроса:

  1. FROM — выбирается таблица
  2. WHERE — фильтруются строки
  3. GROUP BY — строки группируются
  4. HAVING — фильтруются группы
  5. SELECT — выбираются столбцы
  6. ORDER BY — сортируется результат
  7. LIMIT — ограничивается количество строк

Практические примеры для Product Analytics

Пример 1: Когорты по удержанию

SELECT 
  DATE_TRUNC('week', signup_date) as cohort,
  COUNT(DISTINCT user_id) as cohort_size,
  COUNT(DISTINCT CASE WHEN last_activity >= NOW() - INTERVAL '7 days' THEN user_id END) as retained_7d
FROM users
WHERE signup_date >= '2024-01-01' -- WHERE: только новые пользователи
GROUP BY DATE_TRUNC('week', signup_date)
HAVING COUNT(DISTINCT user_id) >= 10 -- HAVING: когорты с минимум 10 пользователями
ORDER BY cohort DESC;

Пример 2: Анализ доходов по стране и платформе

SELECT 
  country,
  platform,
  COUNT(DISTINCT user_id) as user_count,
  COUNT(*) as transaction_count,
  SUM(amount) as total_revenue,
  AVG(amount) as avg_transaction,
  ROUND(SUM(amount) / COUNT(DISTINCT user_id), 2) as revenue_per_user
FROM transactions
WHERE created_at >= DATE_TRUNC('month', NOW() - INTERVAL '1 month')
  AND status = 'completed' -- WHERE: только завершенные транзакции
GROUP BY country, platform
HAVING COUNT(DISTINCT user_id) >= 100 -- HAVING: только страны/платформы с 100+ пользователями
ORDER BY total_revenue DESC;

Пример 3: Поиск аномалий

WITH daily_stats AS (
  SELECT 
    DATE_TRUNC('day', created_at) as day,
    event_type,
    COUNT(*) as event_count
  FROM events
  WHERE created_at >= NOW() - INTERVAL '30 days'
  GROUP BY DATE_TRUNC('day', created_at), event_type
)
SELECT 
  event_type,
  day,
  event_count,
  AVG(event_count) OVER (PARTITION BY event_type) as avg_count
FROM daily_stats
HAVING event_count > 2 * AVG(event_count) OVER (PARTITION BY event_type)
ORDER BY day DESC, event_type;

Частые ошибки

Ошибка 1: Использование столбца в SELECT, которого нет в GROUP BY

-- Неправильно (в PostgreSQL даст ошибку)
SELECT user_id, name, COUNT(*) as order_count
FROM orders
GROUP BY user_id; -- name не в GROUP BY!

-- Правильно
SELECT user_id, name, COUNT(*) as order_count
FROM orders
GROUP BY user_id, name;

Ошибка 2: Использование WHERE вместо HAVING для фильтрации агрегатов

-- Неправильно
SELECT country, COUNT(*) as user_count
FROM users
GROUP BY country
WHERE COUNT(*) > 1000; -- синтаксическая ошибка!

-- Правильно
SELECT country, COUNT(*) as user_count
FROM users
GROUP BY country
HAVING COUNT(*) > 1000;

Производительность

  • WHERE более эффективен, так как уменьшает количество данных перед группировкой
  • Всегда используй WHERE для фильтрации по исходным данным
  • HAVING используй только для фильтрации по агрегатам

Эти принципы критичны для написания эффективных аналитических запросов.