SQLHAVINGaggregationtutorial

Что такое HAVING в SQL?

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

13 мин чтенияСправочникSQL · HAVING · aggregation · tutorial

HAVING — это фильтр для групп после GROUP BY.

Если сказать совсем просто:

  • WHERE фильтрует обычные строки до группировки;
  • HAVING фильтрует уже готовые группы после группировки.

Представь таблицу заказов. В ней каждая строка — один заказ. Ты хочешь найти клиентов, у которых больше 10 заказов.

На уровне одной строки такого числа нет. В одной строке есть только один конкретный заказ. Чтобы узнать, что у клиента больше 10 заказов, базе сначала нужно собрать все заказы клиента в группу, посчитать COUNT(*), и только потом отфильтровать результат.

Вот для таких задач и нужен HAVING.

Зачем нужен HAVING

GROUP BY собирает строки в группы.

Агрегатные функции считают итоги внутри этих групп:

  • COUNT(*) — сколько строк в группе;
  • SUM(amount) — какая сумма по группе;
  • AVG(rating) — какое среднее значение;
  • MIN(created_at) — самое раннее значение;
  • MAX(created_at) — самое позднее значение.

А HAVING отбирает только те группы, которые подходят под условие.

Например:

  • показать только страны, где больше 100 пользователей;
  • показать только клиентов, у которых больше 10 заказов;
  • показать только товары со средней оценкой выше 4;
  • показать только категории, где суммарная выручка больше 50000;
  • показать только авторов, у которых опубликовано минимум 5 статей.

То есть HAVING — это не фильтр отдельных строк. Это фильтр итогов.

Простая идея на примере

Допустим, есть таблица orders:

id customer_id amount
1 1 100
2 1 50
3 1 200
4 2 80
5 3 30
6 3 70

Каждая строка — отдельный заказ.

Сначала посчитаем статистику по каждому клиенту:

SELECT
  customer_id,
  COUNT(*) AS orders_count,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;

Результат:

customer_id orders_count total_amount
1 3 350
2 1 80
3 2 100

Теперь задача: оставить только клиентов, у которых сумма заказов больше 150.

Это условие нельзя нормально проверить до группировки, потому что сумма появляется только после SUM(amount).

Поэтому добавляем HAVING:

SELECT
  customer_id,
  COUNT(*) AS orders_count,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 150;

Результат:

customer_id orders_count total_amount
1 3 350

Клиент 1 остался, потому что его сумма заказов 350.

Клиенты 2 и 3 отсеялись, потому что у них суммы 80 и 100.

Базовый синтаксис HAVING

Обычно запрос с HAVING выглядит так:

SELECT
  group_column,
  aggregate_function(column) AS result
FROM table_name
WHERE row_condition
GROUP BY group_column
HAVING group_condition
ORDER BY result;

Например:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

Этот запрос читается так:

Сгруппируй пользователей по стране и покажи только те страны, где пользователей больше 100.

Результат может быть таким:

country users_count
RU 240
BR 180
DE 126

Страны, где пользователей 100 или меньше, в результат не попадут.

Где стоит HAVING в запросе

HAVING пишется после GROUP BY.

Правильный порядок:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
WHERE status = 'active'
GROUP BY country
HAVING COUNT(*) > 100
ORDER BY users_count DESC;

Логически запрос выполняется примерно так:

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

Именно поэтому HAVING может работать с COUNT(*), SUM(amount), AVG(rating) и другими агрегатами: к моменту HAVING группы уже собраны, а агрегаты уже можно посчитать.

WHERE и HAVING: главное отличие

Это самая важная часть темы.

WHERE фильтрует строки до группировки.

HAVING фильтрует группы после группировки.

Посмотрим на два похожих запроса.

Первый запрос:

SELECT
  customer_id,
  COUNT(*) AS orders_count
FROM orders
WHERE amount > 50
GROUP BY customer_id;

Что делает WHERE amount > 50?

Он сначала убирает все заказы на сумму 50 или меньше. И только потом оставшиеся заказы группируются по клиентам.

То есть это вопрос:

Сколько у каждого клиента заказов дороже 50?

Второй запрос:

SELECT
  customer_id,
  COUNT(*) AS orders_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 5;

Что делает HAVING COUNT(*) > 5?

Он сначала группирует заказы по клиентам, считает количество заказов у каждого клиента, а потом оставляет только тех клиентов, у кого заказов больше 5.

То есть это вопрос:

Какие клиенты сделали больше 5 заказов?

WHERE и HAVING вместе

WHERE и HAVING часто используют в одном запросе. Это нормально.

Например, нужно найти клиентов, у которых минимум 3 дорогих заказа.

Дорогой заказ — это заказ дороже 50.

SELECT
  customer_id,
  COUNT(*) AS big_orders_count
FROM orders
WHERE amount > 50
GROUP BY customer_id
HAVING COUNT(*) >= 3
ORDER BY big_orders_count DESC;

Разберём по шагам:

  1. WHERE amount > 50 оставляет только дорогие заказы.
  2. GROUP BY customer_id группирует эти заказы по клиентам.
  3. COUNT(*) считает дорогие заказы каждого клиента.
  4. HAVING COUNT(*) >= 3 оставляет только клиентов, у которых таких заказов минимум 3.
  5. ORDER BY big_orders_count DESC сортирует результат от большего количества к меньшему.

Именно так обычно пишутся нормальные аналитические запросы: сначала убираем лишние строки, потом считаем итоги, потом фильтруем итоги.

Почему нельзя написать агрегат в WHERE

Новички часто пытаются написать так:

SELECT
  customer_id,
  COUNT(*) AS orders_count
FROM orders
WHERE COUNT(*) > 5
GROUP BY customer_id;

Такой запрос неправильный.

Проблема в том, что WHERE выполняется до GROUP BY. На этом этапе ещё нет групп. А если групп ещё нет, то и COUNT(*) ещё не посчитан.

Поэтому условие на агрегат нужно писать в HAVING:

SELECT
  customer_id,
  COUNT(*) AS orders_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 5;

Запомнить можно так:

Если условие про обычную строку — используй WHERE.
Если условие про результат подсчёта по группе — используй HAVING.

Что можно писать в HAVING

В HAVING обычно пишут условия на агрегатные функции:

HAVING COUNT(*) > 10
HAVING SUM(amount) >= 50000
HAVING AVG(rating) >= 4.5
HAVING MIN(price) > 0
HAVING MAX(created_at) >= DATE '2026-01-01'

Можно объединять условия через AND и OR:

SELECT
  product_id,
  COUNT(*) AS reviews_count,
  AVG(rating) AS avg_rating
FROM reviews
GROUP BY product_id
HAVING COUNT(*) >= 10
   AND AVG(rating) >= 4.0;

Такой запрос покажет только товары, у которых есть минимум 10 отзывов и средняя оценка не ниже 4.

Можно использовать диапазоны:

SELECT
  customer_id,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
HAVING SUM(amount) BETWEEN 1000 AND 10000;

Такой запрос покажет клиентов, у которых сумма заказов от 1000 до 10000 включительно.

Можно использовать IN:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING country IN ('RU', 'US', 'DE');

Но здесь есть важный нюанс: если условие не использует агрегат, его чаще лучше перенести в WHERE.

Условия без агрегатов лучше писать в WHERE

Технически в HAVING можно писать условия не только на агрегаты, но и на колонки из GROUP BY.

Например:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING country = 'RU';

Такой запрос может работать, потому что country есть в группировке.

Но лучше написать так:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
WHERE country = 'RU'
GROUP BY country;

Почему второй вариант лучше?

Потому что WHERE отфильтрует строки раньше. Базе не придётся группировать пользователей из всех стран, чтобы потом оставить только одну страну.

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

Если условие можно написать в WHERE, пиши его в WHERE.
HAVING оставляй для условий на агрегаты.

Пример: страны с большим количеством пользователей

Есть таблица users:

id name country status
1 Anna RU active
2 Bob US active
3 Vera RU inactive
4 Gleb RU active
5 Dan US active
6 Eva DE active

Задача: показать страны, где активных пользователей минимум 2.

SELECT
  country,
  COUNT(*) AS active_users_count
FROM users
WHERE status = 'active'
GROUP BY country
HAVING COUNT(*) >= 2
ORDER BY active_users_count DESC;

Результат:

country active_users_count
RU 2
US 2

Что произошло:

  • WHERE status = 'active' убрал неактивных пользователей;
  • GROUP BY country собрал оставшихся пользователей по странам;
  • HAVING COUNT(*) >= 2 оставил только страны, где активных пользователей минимум 2.

Германия не попала в результат, потому что там только один активный пользователь.

Пример: клиенты с большой суммой заказов

Есть таблица orders:

id customer_id amount
1 101 500
2 101 700
3 102 300
4 103 100
5 103 150
6 104 2000

Нужно найти клиентов, которые заказали больше чем на 1000.

SELECT
  customer_id,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 1000
ORDER BY total_amount DESC;

Результат:

customer_id total_amount
104 2000
101 1200

Здесь HAVING проверяет не один заказ, а сумму всех заказов клиента.

Пример: товары с хорошей средней оценкой

Есть таблица reviews:

id product_id rating
1 10 5
2 10 4
3 10 5
4 20 3
5 20 4
6 30 5

Задача: показать товары со средней оценкой от 4.5.

SELECT
  product_id,
  COUNT(*) AS reviews_count,
  AVG(rating) AS avg_rating
FROM reviews
GROUP BY product_id
HAVING AVG(rating) >= 4.5
ORDER BY avg_rating DESC;

Результат:

product_id reviews_count avg_rating
30 1 5.0000
10 3 4.6667

Но у товара 30 всего один отзыв. Иногда это плохо для аналитики: один восторженный отзыв ещё не значит, что товар действительно стабильно хороший.

Добавим второе условие: отзывов должно быть минимум 3.

SELECT
  product_id,
  COUNT(*) AS reviews_count,
  AVG(rating) AS avg_rating
FROM reviews
GROUP BY product_id
HAVING COUNT(*) >= 3
   AND AVG(rating) >= 4.5
ORDER BY avg_rating DESC;

Результат:

product_id reviews_count avg_rating
10 3 4.6667

Теперь результат честнее: мы оставили только товары, у которых и оценка высокая, и отзывов достаточно.

HAVING и алиасы из SELECT

Частый вопрос: можно ли в HAVING использовать алиас из SELECT?

Например, хочется написать так:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING users_count > 100;

В некоторых базах данных такой запрос может сработать, например в MySQL. Но в PostgreSQL такой вариант обычно не работает: HAVING не видит алиас users_count из SELECT.

Более надёжный и переносимый вариант — повторить выражение:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

Да, выражение повторяется. Но зато запрос понятный и лучше переносится между разными СУБД.

В ORDER BY алиас использовать обычно можно:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100
ORDER BY users_count DESC;

Здесь ORDER BY users_count DESC уже работает с итоговым результатом, где алиас известен.

HAVING без GROUP BY

Иногда HAVING можно встретить без GROUP BY.

Например:

SELECT
  SUM(amount) AS total_amount
FROM orders
HAVING SUM(amount) > 1000;

Что здесь происходит?

Если GROUP BY нет, вся таблица считается одной большой группой. База считает общую сумму заказов. Если сумма больше 1000, строка вернётся. Если нет — результат будет пустым.

Пример результата, если условие выполнилось:

total_amount
3250

Если условие не выполнилось, строк не будет.

Такой приём существует, но в обычных учебных и рабочих задачах HAVING чаще используют вместе с GROUP BY.

COUNT(*) и COUNT(column) в HAVING

Важно помнить разницу между COUNT(*) и COUNT(column).

COUNT(*) считает все строки.

COUNT(column) считает только строки, где значение в колонке не NULL.

Допустим, есть таблица users:

id country email
1 RU a@example.com
2 RU NULL
3 RU c@example.com
4 US NULL
5 US b@example.com
6 US NULL

Запрос:

SELECT
  country,
  COUNT(*) AS users_count,
  COUNT(email) AS emails_count
FROM users
GROUP BY country;

Результат:

country users_count emails_count
RU 3 2
US 3 1

Теперь посмотрим на HAVING.

Если написать так:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) >= 3;

В результат попадут страны, где минимум 3 пользователя.

А если написать так:

SELECT
  country,
  COUNT(email) AS emails_count
FROM users
GROUP BY country
HAVING COUNT(email) >= 3;

В результат не попадёт ни одна страна, потому что ни в RU, ни в US нет трёх заполненных email.

Поэтому всегда уточняй для себя:

  • тебе нужно посчитать строки?
  • или только заполненные значения в конкретной колонке?

HAVING и NULL

Агрегатные функции по-разному относятся к NULL.

COUNT(*) считает строки даже с NULL.

COUNT(column) игнорирует NULL.

SUM(column) и AVG(column) тоже игнорируют NULL.

Например, есть таблица orders:

id customer_id amount
1 1 100
2 1 NULL
3 1 200
4 2 NULL

Запрос:

SELECT
  customer_id,
  COUNT(*) AS rows_count,
  COUNT(amount) AS paid_orders_count,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;

Результат:

customer_id rows_count paid_orders_count total_amount
1 3 2 300
2 1 0 NULL

У клиента 2 сумма получилась NULL, потому что у него нет ни одного ненулевого значения amount.

Если написать:

SELECT
  customer_id,
  SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 100;

клиент 2 не попадёт в результат: сравнение NULL > 100 не даёт TRUE.

Если хочешь считать пустую сумму как ноль, можно использовать COALESCE:

SELECT
  customer_id,
  COALESCE(SUM(amount), 0) AS total_amount
FROM orders
GROUP BY customer_id
HAVING COALESCE(SUM(amount), 0) > 100;

HAVING с несколькими агрегатами

В реальных отчётах часто одного условия мало.

Например, нужно найти клиентов, которые:

  • сделали минимум 3 заказа;
  • потратили суммарно больше 1000;
  • имеют средний чек не меньше 300.
SELECT
  customer_id,
  COUNT(*) AS orders_count,
  SUM(amount) AS total_amount,
  AVG(amount) AS avg_amount
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 3
   AND SUM(amount) > 1000
   AND AVG(amount) >= 300
ORDER BY total_amount DESC;

Такой запрос уже похож на бизнес-логику:

Покажи нам ценных клиентов, которые покупают не один раз, тратят достаточно много и имеют хороший средний чек.

HAVING здесь работает как сито: через него проходят только группы, которые соответствуют всем условиям.

HAVING и фильтрация по датам

Допустим, нужно найти клиентов, которые сделали минимум 2 заказа в 2026 году.

Здесь важно правильно разделить работу между WHERE и HAVING.

SELECT
  customer_id,
  COUNT(*) AS orders_count
FROM orders
WHERE created_at >= DATE '2026-01-01'
  AND created_at < DATE '2027-01-01'
GROUP BY customer_id
HAVING COUNT(*) >= 2;

Что здесь делает WHERE?

Он оставляет только заказы 2026 года.

Что делает HAVING?

Он оставляет только клиентов, у которых после этого осталось минимум 2 заказа.

Это хороший вариант: фильтр по дате применяется до группировки, а фильтр по количеству заказов — после.

Плохой вариант — пытаться всё засунуть в HAVING:

SELECT
  customer_id,
  COUNT(*) AS orders_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 2;

Этот запрос считает все заказы за всё время, а не только за 2026 год. Если задача была именно про 2026 год, результат будет неправильным.

HAVING и DISTINCT

Иногда перед HAVING внутри агрегата используют DISTINCT.

Например, нужно найти пользователей, которые покупали товары минимум из 3 разных категорий.

Допустим, есть таблица orders:

id customer_id category
1 1 books
2 1 books
3 1 games
4 1 courses
5 2 books
6 2 books

Запрос:

SELECT
  customer_id,
  COUNT(DISTINCT category) AS categories_count
FROM orders
GROUP BY customer_id
HAVING COUNT(DISTINCT category) >= 3;

Результат:

customer_id categories_count
1 3

Клиент 1 покупал в трёх разных категориях: books, games, courses.

Клиент 2 покупал только в одной категории, поэтому не прошёл условие.

Как читать запрос с HAVING

Возьмём запрос:

SELECT
  category,
  COUNT(*) AS products_count,
  AVG(price) AS avg_price
FROM products
WHERE is_active = true
GROUP BY category
HAVING COUNT(*) >= 5
   AND AVG(price) > 1000
ORDER BY avg_price DESC;

Читать его лучше не сверху вниз, а по смыслу:

  1. Берём товары из products.
  2. Оставляем только активные товары.
  3. Группируем их по категории.
  4. Для каждой категории считаем количество товаров и среднюю цену.
  5. Оставляем только категории, где минимум 5 товаров.
  6. Среди них оставляем только категории со средней ценой выше 1000.
  7. Сортируем по средней цене от большей к меньшей.

Такой способ чтения помогает не путаться в длинных запросах.

Частые ошибки новичков

Ошибка 1. Использовать WHERE для агрегата

Плохой вариант:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
WHERE COUNT(*) > 100
GROUP BY country;

Так нельзя: WHERE не знает результата COUNT(*).

Правильно:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

Ошибка 2. Писать HAVING до GROUP BY

Плохой вариант:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
HAVING COUNT(*) > 100
GROUP BY country;

Правильный порядок:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

HAVING всегда пишется после GROUP BY.

Ошибка 3. Использовать HAVING вместо WHERE

Плохой вариант:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING country = 'RU';

Лучше:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
WHERE country = 'RU'
GROUP BY country;

Если условие можно применить к обычным строкам до группировки, используй WHERE.

Ошибка 4. Путать COUNT(*) и COUNT(column)

SELECT
  country,
  COUNT(email) AS emails_count
FROM users
GROUP BY country
HAVING COUNT(email) > 100;

Этот запрос считает только пользователей с заполненным email.

Если нужно посчитать всех пользователей, надо писать так:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

Ошибка 5. Использовать алиас в HAVING и ждать, что везде сработает

Ненадёжный вариант:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING users_count > 100;

Более переносимый вариант:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

В HAVING лучше повторять агрегатное выражение явно.

Ошибка 6. Забыть, что HAVING фильтрует уже после WHERE

Посмотри на запрос:

SELECT
  customer_id,
  COUNT(*) AS orders_count
FROM orders
WHERE amount > 100
GROUP BY customer_id
HAVING COUNT(*) >= 2;

Он не ищет клиентов, у которых всего минимум 2 заказа.

Он ищет клиентов, у которых минимум 2 заказа дороже 100.

Потому что WHERE amount > 100 сначала убрал все заказы на 100 и меньше.

Это не ошибка синтаксиса, но частая логическая ловушка.

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

Используй HAVING, когда условие звучит так:

  • «где количество больше...»;
  • «где сумма больше...»;
  • «где среднее значение меньше...»;
  • «где минимум не ниже...»;
  • «где максимум не старше...»;
  • «у кого заказов больше 5»;
  • «у каких товаров средняя оценка выше 4»;
  • «в каких странах больше 100 пользователей».

Почти всегда рядом будут слова «по каждой группе» и агрегатные функции.

Например:

SELECT
  author_id,
  COUNT(*) AS posts_count
FROM posts
GROUP BY author_id
HAVING COUNT(*) >= 5;

Это значит:

Покажи авторов, у которых минимум 5 постов.

Когда HAVING не нужен

HAVING не нужен, если ты фильтруешь обычные строки.

Например, нужны только активные пользователи:

SELECT *
FROM users
WHERE status = 'active';

Нужны заказы дороже 100:

SELECT *
FROM orders
WHERE amount > 100;

Нужны пользователи из конкретной страны:

SELECT *
FROM users
WHERE country = 'RU';

Во всех этих случаях нет групп и агрегатов. Значит, HAVING здесь лишний.

Главное из статьи

HAVING — это фильтр для групп после GROUP BY.

Главная разница:

Оператор Что фильтрует Когда работает
WHERE Отдельные строки До группировки
HAVING Группы После группировки

Базовый пример:

SELECT
  country,
  COUNT(*) AS users_count
FROM users
GROUP BY country
HAVING COUNT(*) > 100;

Главные правила:

  • WHERE используется до GROUP BY.
  • HAVING используется после GROUP BY.
  • В WHERE нельзя писать условия на COUNT(*), SUM(amount), AVG(rating) и другие агрегаты.
  • В HAVING можно фильтровать по агрегатам.
  • Если условие не требует агрегата, лучше писать его в WHERE.
  • WHERE и HAVING можно использовать вместе.
  • COUNT(*) считает все строки.
  • COUNT(column) считает только строки, где column не NULL.
  • Для переносимости не полагайся на алиасы из SELECT внутри HAVING; лучше повторяй выражение явно.

Если сказать совсем коротко: WHERE решает, какие строки попадут в расчёт, а HAVING решает, какие группы останутся после расчёта.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр