До этого урока каждый запрос возвращал строки из таблицы — одну или несколько. Агрегатные функции работают иначе: они обрабатывают набор строк и возвращают одно значение. Это незаменимо когда нужно посчитать итоги, средние значения, найти максимум или минимум.
COUNT(*) считает все строки в результате запроса:
-- Сколько всего товаров в магазине?
SELECT COUNT(*) FROM products;┌──────────┐
│ COUNT(*) │
├──────────┤
│ 22 │
└──────────┘
COUNT(столбец) считает строки где значение столбца не равно NULL:
-- Сколько пользователей зарегистрировано?
SELECT COUNT(id) FROM users;Разница между COUNT(*) и COUNT(столбец) проявляется только когда в столбце есть NULL-значения. COUNT(*) считает все строки включая те где есть NULL, COUNT(столбец) — только те где значение есть. В нашей базе данных все поля заполнены, поэтому результат одинаков. Для надёжности в большинстве случаев используют COUNT(*).
Псевдонимы с агрегатными функциями — обязательная практика, иначе заголовок столбца выглядит как COUNT(*):
SELECT COUNT(*) AS total_users FROM users;┌─────────────┐
│ total_users │
├─────────────┤
│ 15 │
└─────────────┘
-- Общая стоимость всех товаров на складе
SELECT SUM(price * stock) AS total_stock_value FROM products;┌───────────────────┐
│ total_stock_value │
├───────────────────┤
│ 2270200.00 │
└───────────────────┘
-- Сколько всего единиц товаров на складе
SELECT SUM(stock) AS total_items FROM products;-- Средняя цена товара в магазине
SELECT AVG(price) AS avg_price FROM products;┌──────────────────┐
│ avg_price │
├──────────────────┤
│ 19972.7272727273 │
└──────────────────┘
AVG возвращает точное среднее — без округления. Для округления используется функция ROUND(значение, знаки_после_запятой):
SELECT ROUND(AVG(price), 2) AS avg_price FROM products;┌───────────┐
│ avg_price │
├───────────┤
│ 19972.73 │
└───────────┘
-- Самый дешёвый и самый дорогой товар
SELECT
MIN(price) AS min_price,
MAX(price) AS max_price
FROM products;┌───────────┬───────────┐
│ min_price │ max_price │
├───────────┼───────────┤
│ 800.00 │ 75000.00 │
└───────────┴───────────┘
-- Самый ранний и самый поздний заказ
SELECT
MIN(created_at) AS first_order,
MAX(created_at) AS last_order
FROM orders;┌─────────────┬────────────┐
│ first_order │ last_order │
├─────────────┼────────────┤
│ 2024-02-01 │ 2024-06-14 │
└─────────────┴────────────┘
Агрегатные функции игнорируют
NULL-значения —SUM,AVG,MIN,MAXпропускают строки где значение отсутствует.COUNT(*)— единственное исключение, он считает все строки включая те где естьNULL.
Агрегатные функции выше считали итоги по всей таблице. Но чаще нужны итоги по группам — отдельно для каждой категории, для каждого города, для каждого статуса.
GROUP BY разбивает строки таблицы на группы по значению указанного столбца, и агрегатная функция применяется к каждой группе отдельно:
-- Количество товаров в каждой категории
SELECT
category_id,
COUNT(*) AS product_count
FROM products
GROUP BY category_id;┌─────────────┬───────────────┐
│ category_id │ product_count │
├─────────────┼───────────────┤
│ 1 │ 6 │
│ 2 │ 5 │
│ 3 │ 3 │
│ 4 │ 4 │
│ 5 │ 4 │
└─────────────┴───────────────┘
Вместо одной строки с итогом по всей таблице — пять строк, по одной для каждой группы.
Можно представить это так: GROUP BY category_id как будто "складывает" все строки с одинаковым category_id в одну кучку, а затем агрегатная функция обрабатывает каждую кучку:
Кучка category_id = 1: Ноутбук Lenovo, Ноутбук ASUS, Смартфон Samsung... → COUNT = 6
Кучка category_id = 2: Мышь Logitech, Клавиатура Keychron, Монитор LG... → COUNT = 5
Кучка category_id = 3: Стол IKEA, Кресло Hara, Подставка... → COUNT = 3
...
В SELECT можно указывать только те столбцы которые:
- перечислены в
GROUP BY, или - обёрнуты в агрегатную функцию
Это правило — не прихоть синтаксиса, а логика: если строки сгруппированы, у каждой группы одно значение category_id, но много разных name. Какое имя выводить? СУБД не знает — поэтому запрещает:
-- Ошибка: name не входит в GROUP BY и не агрегирован
SELECT category_id, name, COUNT(*) FROM products
GROUP BY category_id;
-- Правильно: только category_id и агрегат
SELECT category_id, COUNT(*) AS product_count FROM products
GROUP BY category_id;В одном запросе можно использовать сразу несколько агрегатных функций:
-- Статистика по каждой категории
SELECT
category_id,
COUNT(*) AS product_count,
ROUND(AVG(price), 2) AS avg_price,
MIN(price) AS min_price,
MAX(price) AS max_price,
SUM(stock) AS total_stock
FROM products
GROUP BY category_id
ORDER BY avg_price DESC;┌─────────────┬───────────────┬───────────┬───────────┬───────────┬─────────────┐
│ category_id │ product_count │ avg_price │ min_price │ max_price │ total_stock │
├─────────────┼───────────────┼───────────┼───────────┼───────────┼─────────────┤
│ 3 │ 3 │ 16833.33 │ 3500.00 │ 32000.00 │ 39 │
│ 1 │ 6 │ 39166.67 │ 22000.00 │ 75000.00 │ 106 │
│ 2 │ 5 │ 11220.00 │ 2200.00 │ 32000.00 │ 153 │
│ 4 │ 4 │ 1375.00 │ 1100.00 │ 1800.00 │ 330 │
│ 5 │ 4 │ 1650.00 │ 800.00 │ 2500.00 │ 475 │
└─────────────┴───────────────┴───────────┴───────────┴───────────┴─────────────┘
Можно группировать по нескольким столбцам одновременно. Группа тогда — уникальная комбинация значений всех указанных столбцов:
-- Количество заказов каждого статуса для каждого пользователя
SELECT
user_id,
status,
COUNT(*) AS order_count
FROM orders
GROUP BY user_id, status
ORDER BY user_id;┌─────────┬───────────┬─────────────┐
│ user_id │ status │ order_count │
├─────────┼───────────┼─────────────┤
│ 1 │ delivered │ 2 │
│ 2 │ delivered │ 1 │
│ 3 │ cancelled │ 1 │
│ 3 │ delivered │ 1 │
│ 5 │ delivered │ 2 │
│ ... │ ... │ ... │
└─────────┴───────────┴─────────────┘
WHERE фильтрует строки до группировки. Но что если нужно отфильтровать уже сгруппированные результаты? Для этого существует HAVING.
-- Категории в которых больше 4 товаров
SELECT
category_id,
COUNT(*) AS product_count
FROM products
GROUP BY category_id
HAVING product_count > 4;┌─────────────┬───────────────┐
│ category_id │ product_count │
├─────────────┼───────────────┤
│ 1 │ 6 │
│ 2 │ 5 │
└─────────────┴───────────────┘
-- Пользователи у которых больше одного заказа
SELECT
user_id,
COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING order_count > 1;┌─────────┬─────────────┐
│ user_id │ order_count │
├─────────┼─────────────┤
│ 1 │ 2 │
│ 3 │ 2 │
│ 5 │ 2 │
└─────────┴─────────────┘
Это один из самых частых вопросов — когда что использовать. Ответ простой:
| WHERE | HAVING | |
|---|---|---|
| Когда применяется | До группировки | После группировки |
| К чему применяется | К отдельным строкам | К группам (результатам агрегации) |
| Может использовать агрегаты | Нет | Да |
WHERE и HAVING можно использовать в одном запросе — они работают на разных этапах:
-- Среди товаров дороже 1000 рублей (WHERE)
-- найти категории где средняя цена выше 10 000 (HAVING)
SELECT
category_id,
ROUND(AVG(price), 2) AS avg_price,
COUNT(*) AS product_count
FROM products
WHERE price > 1000
GROUP BY category_id
HAVING avg_price > 10000
ORDER BY avg_price DESC;┌─────────────┬───────────┬───────────────┐
│ category_id │ avg_price │ product_count │
├─────────────┼───────────┼───────────────┤
│ 1 │ 39166.67 │ 6 │
│ 3 │ 23500.00 │ 2 │
│ 2 │ 17220.00 │ 4 │
└─────────────┴───────────┴───────────────┘
Как выполняется этот запрос шаг за шагом:
FROM products— берём все строки таблицыWHERE price > 1000— оставляем только строки где цена больше 1000GROUP BY category_id— группируем оставшиеся строки по категорииHAVING avg_price > 10000— оставляем только группы где среднее больше 10 000ORDER BY avg_price DESC— сортируем результат
Попытка написать
WHERE AVG(price) > 10000вызовет ошибку — агрегатные функции вWHEREнедопустимы. Именно для этого существуетHAVING.
После этого урока структура запроса выглядит так:
SELECT [DISTINCT] столбцы / агрегаты
FROM таблица
WHERE условие_по_строкам
GROUP BY столбец
HAVING условие_по_группам
ORDER BY столбец [ASC|DESC]
LIMIT n OFFSET m;Логический порядок выполнения SQL-запроса с группировкой и HAVING:
1. FROM — выбираются таблицы и выполняются соединения (JOIN)
2. WHERE — фильтруются отдельные строки
3. GROUP BY — строки объединяются в группы
4. HAVING — фильтруются группы по условиям (агрегатные функции)
5. SELECT — формируются итоговые столбцы, создаются псевдонимы и выполняются агрегаты
6. DISTINCT — удаляются дубликаты строк
7. ORDER BY — сортировка результата
8. LIMIT/OFFSET — ограничение количества строк
Таким образом, хотя синтаксис запроса начинается с SELECT, СУБД сначала отбирает строки, затем фильтрует их, потом группирует, применяет HAVING, и только потом формирует итоговые столбцы. Это объясняет, почему агрегаты нельзя использовать в WHERE, но можно в HAVING и в SELECT.
- Чем отличается
COUNT(*)отCOUNT(столбец)? - Почему следующий запрос вызовет ошибку?
- Что произойдёт если написать
WHERE COUNT(*) > 2вместоHAVING COUNT(*) > 2? - На каком этапе выполняется
WHERE, а на какомHAVING? - Что вернёт
AVG(price)если в одной из строкpriceравенNULL? - Можно ли использовать
ORDER BYвместе сGROUP BY? Если да — к чему применяется сортировка? - Чем
GROUP BY user_idотличается отGROUP BY user_id, status? - Как получить только среднее значение без лишних знаков после запятой?
- Можно ли в
HAVINGиспользовать условие без агрегатной функции? Например:HAVING category_id = 1? - Каков полный порядок выполнения клаузул в запросе с
GROUP BY?
Посчитайте общее количество заказов в таблице orders. Выведите результат с псевдонимом total_orders.
Найдите минимальную, максимальную и среднюю (округлить до 2 знаков) цену среди всех товаров.
Посчитайте количество заказов с каждым статусом. Выведите status и order_count. Отсортируйте по количеству по убыванию.
Для каждого пользователя посчитайте количество его заказов. Выведите user_id и order_count. Отсортируйте по user_id.
Найдите суммарное количество товаров на складе (SUM(stock)) и общую стоимость складского остатка (SUM(price * stock)) для каждой категории. Округлите стоимость до 2 знаков. Отсортируйте по суммарной стоимости по убыванию.
Найдите пользователей у которых ровно один заказ. Выведите user_id и количество заказов.
Найдите категории где средняя цена товара ниже 5000 рублей. Выведите category_id и среднюю цену округлённую до 2 знаков.
Среди товаров дешевле 30 000 рублей найдите категории где суммарный складской остаток превышает 100 единиц. Выведите category_id и суммарный остаток.
Найдите количество пользователей из каждого города. Выведите только те города где пользователей больше одного. Отсортируйте по количеству по убыванию.
Для каждого пользователя у которого есть хотя бы два заказа — найдите количество заказов с каждым статусом. Выведите user_id, status, order_count. Отсортируйте по user_id, затем по status.
Эта задача должна быть разделена на два шага: сначала найти пользователей с двумя и более заказами, затем для них посчитать разбивку по статусам. Чистое решение требует подзапроса (Урок 7).