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;
Логически запрос выполняется примерно так:
FROM — берём таблицу.
WHERE — фильтруем обычные строки.
GROUP BY — собираем строки в группы.
HAVING — фильтруем группы.
SELECT — выбираем, что показать.
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;
Разберём по шагам:
WHERE amount > 50 оставляет только дорогие заказы.
GROUP BY customer_id группирует эти заказы по клиентам.
COUNT(*) считает дорогие заказы каждого клиента.
HAVING COUNT(*) >= 3 оставляет только клиентов, у которых таких заказов минимум 3.
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, строка вернётся. Если нет — результат будет пустым.
Пример результата, если условие выполнилось:
Если условие не выполнилось, строк не будет.
Такой приём существует, но в обычных учебных и рабочих задачах HAVING чаще используют вместе с GROUP BY.
COUNT(*) и COUNT(column) в HAVING
Важно помнить разницу между COUNT(*) и COUNT(column).
COUNT(*) считает все строки.
COUNT(column) считает только строки, где значение в колонке не NULL.
Допустим, есть таблица users:
Запрос:
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;
Читать его лучше не сверху вниз, а по смыслу:
- Берём товары из
products.
- Оставляем только активные товары.
- Группируем их по категории.
- Для каждой категории считаем количество товаров и среднюю цену.
- Оставляем только категории, где минимум 5 товаров.
- Среди них оставляем только категории со средней ценой выше 1000.
- Сортируем по средней цене от большей к меньшей.
Такой способ чтения помогает не путаться в длинных запросах.
Частые ошибки новичков
Ошибка 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 решает, какие группы останутся после расчёта.
HAVING— это фильтр для групп послеGROUP BY.Если сказать совсем просто:
WHEREфильтрует обычные строки до группировки;HAVINGфильтрует уже готовые группы после группировки.Представь таблицу заказов. В ней каждая строка — один заказ. Ты хочешь найти клиентов, у которых больше 10 заказов.
На уровне одной строки такого числа нет. В одной строке есть только один конкретный заказ. Чтобы узнать, что у клиента больше 10 заказов, базе сначала нужно собрать все заказы клиента в группу, посчитать
COUNT(*), и только потом отфильтровать результат.Вот для таких задач и нужен
HAVING.Зачем нужен HAVING
GROUP BYсобирает строки в группы.Агрегатные функции считают итоги внутри этих групп:
COUNT(*)— сколько строк в группе;SUM(amount)— какая сумма по группе;AVG(rating)— какое среднее значение;MIN(created_at)— самое раннее значение;MAX(created_at)— самое позднее значение.А
HAVINGотбирает только те группы, которые подходят под условие.Например:
То есть
HAVING— это не фильтр отдельных строк. Это фильтр итогов.Простая идея на примере
Допустим, есть таблица
orders:Каждая строка — отдельный заказ.
Сначала посчитаем статистику по каждому клиенту:
SELECT customer_id, COUNT(*) AS orders_count, SUM(amount) AS total_amount FROM orders GROUP BY customer_id;Результат:
Теперь задача: оставить только клиентов, у которых сумма заказов больше 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;Результат:
Клиент
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 или меньше, в результат не попадут.
Где стоит 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;Логически запрос выполняется примерно так:
FROM— берём таблицу.WHERE— фильтруем обычные строки.GROUP BY— собираем строки в группы.HAVING— фильтруем группы.SELECT— выбираем, что показать.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или меньше. И только потом оставшиеся заказы группируются по клиентам.То есть это вопрос:
Второй запрос:
SELECT customer_id, COUNT(*) AS orders_count FROM orders GROUP BY customer_id HAVING COUNT(*) > 5;Что делает
HAVING COUNT(*) > 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;Разберём по шагам:
WHERE amount > 50оставляет только дорогие заказы.GROUP BY customer_idгруппирует эти заказы по клиентам.COUNT(*)считает дорогие заказы каждого клиента.HAVING COUNT(*) >= 3оставляет только клиентов, у которых таких заказов минимум 3.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;Запомнить можно так:
Что можно писать в HAVING
В
HAVINGобычно пишут условия на агрегатные функции:HAVING COUNT(*) > 10HAVING SUM(amount) >= 50000HAVING AVG(rating) >= 4.5HAVING MIN(price) > 0HAVING 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отфильтрует строки раньше. Базе не придётся группировать пользователей из всех стран, чтобы потом оставить только одну страну.Простое правило:
Пример: страны с большим количеством пользователей
Есть таблица
users:Задача: показать страны, где активных пользователей минимум 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;Результат:
Что произошло:
WHERE status = 'active'убрал неактивных пользователей;GROUP BY countryсобрал оставшихся пользователей по странам;HAVING COUNT(*) >= 2оставил только страны, где активных пользователей минимум 2.Германия не попала в результат, потому что там только один активный пользователь.
Пример: клиенты с большой суммой заказов
Есть таблица
orders:Нужно найти клиентов, которые заказали больше чем на 1000.
SELECT customer_id, SUM(amount) AS total_amount FROM orders GROUP BY customer_id HAVING SUM(amount) > 1000 ORDER BY total_amount DESC;Результат:
Здесь
HAVINGпроверяет не один заказ, а сумму всех заказов клиента.Пример: товары с хорошей средней оценкой
Есть таблица
reviews:Задача: показать товары со средней оценкой от 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;Результат:
Но у товара
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;Результат:
Теперь результат честнее: мы оставили только товары, у которых и оценка высокая, и отзывов достаточно.
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, строка вернётся. Если нет — результат будет пустым.Пример результата, если условие выполнилось:
Если условие не выполнилось, строк не будет.
Такой приём существует, но в обычных учебных и рабочих задачах
HAVINGчаще используют вместе сGROUP BY.COUNT(*) и COUNT(column) в HAVING
Важно помнить разницу между
COUNT(*)иCOUNT(column).COUNT(*)считает все строки.COUNT(column)считает только строки, где значение в колонке неNULL.Допустим, есть таблица
users:Запрос:
SELECT country, COUNT(*) AS users_count, COUNT(email) AS emails_count FROM users GROUP BY country;Результат:
Теперь посмотрим на
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:Запрос:
SELECT customer_id, COUNT(*) AS rows_count, COUNT(amount) AS paid_orders_count, SUM(amount) AS total_amount FROM orders GROUP BY customer_id;Результат:
У клиента
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 с несколькими агрегатами
В реальных отчётах часто одного условия мало.
Например, нужно найти клиентов, которые:
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:Запрос:
SELECT customer_id, COUNT(DISTINCT category) AS categories_count FROM orders GROUP BY customer_id HAVING COUNT(DISTINCT category) >= 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;Читать его лучше не сверху вниз, а по смыслу:
products.Такой способ чтения помогает не путаться в длинных запросах.
Частые ошибки новичков
Ошибка 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, когда условие звучит так:Почти всегда рядом будут слова «по каждой группе» и агрегатные функции.
Например:
SELECT author_id, COUNT(*) AS posts_count FROM posts GROUP BY author_id HAVING COUNT(*) >= 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.Главная разница:
WHEREHAVINGБазовый пример:
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решает, какие группы останутся после расчёта.