В SQL есть пять функций, без которых почти невозможно представить отчёты и аналитику:
Их называют агрегатными функциями.
Слово «агрегатные» звучит чуть сухо, но идея простая: такие функции берут много строк и превращают их в один итог.
Обычный запрос показывает данные построчно:
SELECT name, email
FROM users;
А агрегатный запрос отвечает на вопрос по всей выборке:
SELECT COUNT(*)
FROM users;
То есть не «покажи всех пользователей», а «скажи, сколько их всего».
Зачем нужны агрегатные функции
Почти любой отчёт начинается с таких вопросов:
Сколько пользователей зарегистрировалось?
SELECT COUNT(*)
FROM users;
Сколько денег принесли заказы?
SELECT SUM(amount)
FROM orders;
Какая средняя оценка у товара?
SELECT AVG(rating)
FROM reviews;
Какой заказ был самым дорогим?
SELECT MAX(amount)
FROM orders;
Какая самая ранняя дата заказа?
SELECT MIN(created_at)
FROM orders;
Без агрегатных функций пришлось бы забирать строки в приложение и считать всё вручную: циклом, переменными, условиями. SQL создан как раз для того, чтобы такие вещи делать прямо в базе.
Главное правило: агрегат превращает строки в итог
Представь таблицу заказов:
| id |
customer_id |
amount |
| 1 |
10 |
1200 |
| 2 |
10 |
800 |
| 3 |
15 |
2400 |
| 4 |
21 |
500 |
Если сделать обычный SELECT, мы увидим строки:
SELECT amount
FROM orders;
Результат:
А если применить SUM, SQL сложит значения и вернёт один итог:
SELECT SUM(amount) AS total_amount
FROM orders;
Результат:
Вот в этом и смысл агрегатов: они не просто показывают данные, а считают результат по набору строк.
COUNT — посчитать строки
COUNT отвечает на вопрос «сколько?».
Самый частый вариант — COUNT(*):
SELECT COUNT(*) AS users_count
FROM users;
Он считает количество строк в таблице.
Например, есть таблица users:
Запрос:
SELECT COUNT(*) AS all_rows
FROM users;
Вернёт:
Потому что в таблице четыре строки.
COUNT(*), COUNT(column) и COUNT(DISTINCT column)
У COUNT есть несколько важных форм.
COUNT(*) считает все строки
SELECT COUNT(*) AS all_rows
FROM users;
COUNT(*) не смотрит, заполнены ли отдельные колонки. Ему важно только количество строк.
Если строка есть — она считается.
COUNT(column) считает только заполненные значения
SELECT COUNT(email) AS users_with_email
FROM users;
Такой запрос посчитает только строки, где email не равен NULL.
В нашей таблице:
Результат будет:
Пользователей четыре, но email указан только у трёх.
Это важная разница:
SELECT
COUNT(*) AS all_rows,
COUNT(email) AS users_with_email
FROM users;
Результат:
| all_rows |
users_with_email |
| 4 |
3 |
COUNT(*) отвечает на вопрос «сколько строк?».
COUNT(email) отвечает на вопрос «в скольких строках заполнен email?».
COUNT(DISTINCT column) считает уникальные значения
Если нужно посчитать не все значения, а только разные, используют DISTINCT.
SELECT COUNT(DISTINCT name) AS unique_names
FROM users;
В таблице два пользователя с именем Bob, но имя считается один раз.
Результат:
Потому что уникальные имена такие: Anna, Bob, Vera.
Чаще всего COUNT(DISTINCT ...) используют, когда нужно посчитать уникальных пользователей, клиентов, товаров или сессии:
SELECT COUNT(DISTINCT customer_id) AS unique_customers
FROM orders;
SUM — посчитать сумму
SUM складывает значения в колонке.
Например, есть таблица заказов:
| id |
customer_id |
amount |
| 1 |
10 |
1200 |
| 2 |
10 |
800 |
| 3 |
15 |
2400 |
| 4 |
21 |
500 |
Запрос:
SELECT SUM(amount) AS total_amount
FROM orders;
Результат:
SUM используют для денег, количества товаров, баллов, просмотров, часов, километров — всего, что можно сложить.
Например, сумма оплаченных заказов:
SELECT SUM(amount) AS paid_amount
FROM orders
WHERE status = 'paid';
Здесь сначала сработает WHERE, останутся только оплаченные заказы, и уже по ним будет посчитана сумма.
Важная особенность SUM: NULL не превращается в ноль
SUM пропускает значения NULL.
Например:
| id |
amount |
| 1 |
100 |
| 2 |
NULL |
| 3 |
300 |
Запрос:
SELECT SUM(amount) AS total_amount
FROM orders;
Вернёт:
NULL не стал нулём. Он просто не участвовал в расчёте.
Но есть важный нюанс: если подходящих строк нет или все значения равны NULL, результатом будет NULL, а не 0.
Например:
SELECT SUM(amount) AS total_amount
FROM orders
WHERE customer_id = 999;
Если у клиента с таким id нет заказов, результат будет:
Для отчётов это часто неудобно. Обычно хочется увидеть ноль. Тогда используют COALESCE:
SELECT COALESCE(SUM(amount), 0) AS total_amount
FROM orders
WHERE customer_id = 999;
COALESCE говорит: если результат SUM(amount) оказался NULL, покажи вместо него 0.
AVG — посчитать среднее
AVG считает среднее значение.
Например, есть отзывы:
| id |
product_id |
rating |
| 1 |
100 |
5 |
| 2 |
100 |
4 |
| 3 |
100 |
5 |
| 4 |
100 |
3 |
Запрос:
SELECT AVG(rating) AS avg_rating
FROM reviews
WHERE product_id = 100;
Результат:
Среднее считается так:
Сумма значений делится на количество заполненных значений.
В примере:
(5 + 4 + 5 + 3) / 4 = 4.25
AVG тоже пропускает NULL
Если часть значений равна NULL, они не участвуют в расчёте.
Например:
Запрос:
SELECT AVG(rating) AS avg_rating
FROM reviews;
Посчитает не так:
(5 + 4 + 0) / 3
А так:
(5 + 4) / 2
Результат:
Это логично: NULL означает не «ноль», а «значение неизвестно» или «значения нет».
Если оценка неизвестна, её нельзя честно использовать в среднем.
Округление результата AVG
Среднее часто получается дробным. В PostgreSQL для целочисленной колонки AVG возвращает точное значение типа NUMERIC.
Например:
SELECT AVG(score) AS avg_score
FROM tests;
Результат может быть таким:
| avg_score |
| 4.5000000000000000 |
Для красивого вывода можно округлить:
SELECT ROUND(AVG(score), 2) AS avg_score
FROM tests;
Результат:
В разных базах данных детали типов могут отличаться. Например, в MySQL результат AVG по целым числам обычно будет дробным, но при больших числах и финансовых расчётах стоит внимательно следить за точностью. Если нужна точная арифметика, лучше использовать подходящий числовой тип, например NUMERIC или DECIMAL.
MIN — найти самое маленькое значение
MIN возвращает минимальное значение в колонке.
Например, самый дешёвый товар:
SELECT MIN(price) AS cheapest_price
FROM products;
Результат:
MIN работает не только с числами.
Можно найти самую раннюю дату заказа:
SELECT MIN(created_at) AS first_order_date
FROM orders;
Или первое имя по алфавитному порядку:
SELECT MIN(name) AS first_name
FROM users;
MAX — найти самое большое значение
MAX возвращает максимальное значение.
Например, самый дорогой товар:
SELECT MAX(price) AS most_expensive_price
FROM products;
Самая поздняя дата заказа:
SELECT MAX(created_at) AS last_order_date
FROM orders;
Последнее имя по алфавитному порядку:
SELECT MAX(name) AS last_name
FROM users;
MIN и MAX тоже пропускают NULL.
Если все значения в колонке равны NULL, результат будет NULL.
MIN и MAX по тексту: осторожно с порядком
По строкам MIN и MAX работают не как «самое короткое» или «самое длинное», а по порядку сортировки текста.
Например, есть значения:
Если они хранятся как текст, порядок может оказаться неожиданным:
SELECT
MIN(version) AS min_version,
MAX(version) AS max_version
FROM app_versions;
Для текстовых значений '10' может идти раньше '2', потому что сравнение идёт посимвольно: сначала сравнивается первый символ.
Поэтому версии, номера и другие числовые значения лучше хранить в числовых колонках, если вы планируете сравнивать их как числа.
Агрегаты без GROUP BY: итог по всей выборке
Все примеры выше считали один итог по всей таблице или по строкам после WHERE.
Например:
SELECT COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid';
Такой запрос вернёт одну строку:
Здесь вся подходящая выборка считается одной большой группой.
SQL как будто говорит: «Я взял все оплаченные заказы и посчитал по ним один общий итог».
Агрегаты с GROUP BY: итог по каждой группе
Но чаще в отчётах нужно получить не один общий итог, а итоги по группам.
Например:
- сколько товаров в каждой категории;
- сколько заказов сделал каждый клиент;
- какая средняя оценка у каждого товара;
- сколько денег пришло в каждый день.
Для этого нужен GROUP BY.
Допустим, есть товары:
| id |
category |
price |
| 1 |
books |
700 |
| 2 |
books |
1200 |
| 3 |
games |
3000 |
| 4 |
games |
2500 |
| 5 |
devices |
9000 |
Запрос:
SELECT
category,
COUNT(*) AS items_count,
SUM(price) AS total_price,
AVG(price) AS avg_price,
MIN(price) AS min_price,
MAX(price) AS max_price
FROM products
GROUP BY category;
Результат:
| category |
items_count |
total_price |
avg_price |
min_price |
max_price |
| books |
2 |
1900 |
950 |
700 |
1200 |
| games |
2 |
5500 |
2750 |
2500 |
3000 |
| devices |
1 |
9000 |
9000 |
9000 |
9000 |
Теперь каждая категория получила свою строку с итогами.
GROUP BY category означает: «разбей строки на группы по категории и посчитай агрегаты отдельно для каждой группы».
Как читать запрос с агрегатами и GROUP BY
Возьмём запрос:
SELECT
customer_id,
COUNT(*) AS orders_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id;
Его удобно читать так:
- Возьми таблицу
orders.
- Разбей заказы на группы по
customer_id.
- Для каждого клиента посчитай количество заказов через
COUNT(*).
- Для каждого клиента посчитай сумму заказов через
SUM(amount).
- Покажи одну строку на каждого клиента.
То есть результат будет не «одна строка на заказ», а «одна строка на клиента».
Почему нельзя просто написать колонку рядом с агрегатом
Новички часто пишут так:
SELECT
customer_id,
COUNT(*) AS orders_count
FROM orders;
Такой запрос некорректен.
Проблема в том, что COUNT(*) хочет вернуть один итог по всей таблице, а customer_id — обычная колонка, где много разных значений.
SQL не может угадать, какой именно customer_id показать рядом с общим количеством заказов.
Правильно так:
SELECT
customer_id,
COUNT(*) AS orders_count
FROM orders
GROUP BY customer_id;
Теперь всё честно: для каждого customer_id будет свой счётчик.
Общее правило такое:
Если в SELECT есть агрегатные функции и обычные колонки, то обычные колонки должны быть в GROUP BY.
Агрегаты с DISTINCT
DISTINCT можно использовать не только с COUNT.
Например:
SELECT
COUNT(DISTINCT customer_id) AS unique_customers,
SUM(DISTINCT amount) AS sum_unique_amounts
FROM orders;
Но на практике чаще всего используют именно:
SELECT COUNT(DISTINCT customer_id) AS unique_customers
FROM orders;
Это понятный и полезный сценарий: посчитать уникальных клиентов.
А вот SUM(DISTINCT amount) нужен редко. Он складывает только уникальные суммы заказов, а не все заказы.
Например, если есть три заказа на суммы 100, 100 и 200, то обычный SUM(amount) вернёт 400, а SUM(DISTINCT amount) вернёт 300.
Чаще бизнесу нужна сумма всех заказов, поэтому с SUM(DISTINCT ...) стоит быть осторожнее.
Как агрегаты работают с NULL
NULL — одна из главных причин неожиданных результатов в агрегатах.
Короткая таблица поведения:
| Функция |
Что делает с NULL |
COUNT(*) |
Считает все строки, даже если в колонках есть NULL |
COUNT(column) |
Считает только строки, где колонка не равна NULL |
COUNT(DISTINCT column) |
Считает уникальные значения, пропуская NULL |
SUM(column) |
Пропускает NULL; если считать нечего, возвращает NULL |
AVG(column) |
Пропускает NULL; если считать нечего, возвращает NULL |
MIN(column) |
Пропускает NULL; если считать нечего, возвращает NULL |
MAX(column) |
Пропускает NULL; если считать нечего, возвращает NULL |
Самая частая практическая защита — COALESCE.
Например, для суммы:
SELECT COALESCE(SUM(amount), 0) AS total_amount
FROM orders
WHERE status = 'paid';
Для среднего иногда тоже можно подставить ноль, но делать это нужно осторожно:
SELECT COALESCE(AVG(rating), 0) AS avg_rating
FROM reviews
WHERE product_id = 100;
Сумма ноль обычно означает «денег нет».
А средняя оценка ноль может выглядеть так, будто товар действительно получил оценку 0. Поэтому в интерфейсах иногда лучше показывать не 0, а текст вроде «пока нет оценок».
Частые ошибки новичков
Путать COUNT(*) и COUNT(column)
SELECT
COUNT(*) AS all_users,
COUNT(email) AS users_with_email
FROM users;
Если результат такой:
| all_users |
users_with_email |
| 100 |
47 |
Это не ошибка SQL.
Это значит: всего пользователей 100, но email заполнен только у 47.
Ждать от SUM ноль на пустой выборке
Запрос:
SELECT SUM(amount) AS total_amount
FROM orders
WHERE status = 'cancelled_by_meteor';
Если таких заказов нет, результатом будет NULL.
Для отчёта часто лучше так:
SELECT COALESCE(SUM(amount), 0) AS total_amount
FROM orders
WHERE status = 'cancelled_by_meteor';
Писать обычную колонку рядом с агрегатом без GROUP BY
Неправильно:
SELECT
name,
COUNT(*) AS users_count
FROM users;
Правильно:
SELECT
name,
COUNT(*) AS users_count
FROM users
GROUP BY name;
Если хочешь получить количество пользователей по каждому имени, нужен GROUP BY name.
Если хочешь получить только общее количество, убери name:
SELECT COUNT(*) AS users_count
FROM users;
Забывать, что AVG пропускает NULL
Если у товара две оценки: 5 и NULL, средняя будет 5, а не 2.5.
Потому что NULL не считается нулём.
SELECT AVG(rating) AS avg_rating
FROM reviews;
SQL считает среднее только по известным значениям.
Использовать MIN и MAX по тексту там, где нужны числа
Если числа лежат в текстовой колонке, результат может удивить.
SELECT MAX(version) AS latest_version
FROM app_versions;
Для строк значения сравниваются как текст. Поэтому для числовых сравнений лучше хранить данные в числовом формате или заранее приводить типы, если это безопасно.
Думать, что точный COUNT(*) на огромной таблице всегда мгновенный
В PostgreSQL точный COUNT(*) по большой таблице может быть дорогой операцией. База должна честно посчитать строки, а на таблицах в десятки или сотни миллионов записей это может занять заметное время.
Если нужна точная цифра — придётся считать точно.
Если нужна примерная оценка для админки или внутренней статистики, в PostgreSQL иногда используют статистику планировщика, например данные из pg_class. Но для пользовательских отчётов и финансовых расчётов такие приблизительные значения не подходят.
Пример: небольшой отчёт по заказам
Допустим, у нас есть таблица orders:
| id |
customer_id |
status |
amount |
| 1 |
10 |
paid |
1200 |
| 2 |
10 |
paid |
800 |
| 3 |
15 |
cancelled |
2400 |
| 4 |
21 |
paid |
500 |
| 5 |
21 |
paid |
NULL |
Хотим получить общую статистику по оплаченным заказам:
SELECT
COUNT(*) AS paid_orders,
COUNT(amount) AS paid_orders_with_amount,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount,
MIN(amount) AS min_amount,
MAX(amount) AS max_amount
FROM orders
WHERE status = 'paid';
Результат:
| paid_orders |
paid_orders_with_amount |
total_amount |
avg_amount |
min_amount |
max_amount |
| 4 |
3 |
2500 |
833.3333333333333333 |
500 |
1200 |
Что здесь произошло:
COUNT(*) посчитал все оплаченные заказы — их 4.
COUNT(amount) посчитал только оплаченные заказы с заполненной суммой — их 3.
SUM(amount) сложил известные суммы: 1200 + 800 + 500.
AVG(amount) посчитал среднее только по известным суммам.
MIN(amount) нашёл минимальную известную сумму.
MAX(amount) нашёл максимальную известную сумму.
Заказ с NULL в amount попал в COUNT(*), но не участвовал в COUNT(amount), SUM, AVG, MIN и MAX.
Пример: отчёт по каждому клиенту
Теперь посчитаем статистику отдельно по каждому клиенту:
SELECT
customer_id,
COUNT(*) AS orders_count,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
ORDER BY customer_id;
Результат:
| customer_id |
orders_count |
total_amount |
avg_amount |
| 10 |
2 |
2000 |
1000 |
| 21 |
2 |
500 |
500 |
Обрати внимание на клиента 21.
У него два оплаченных заказа, поэтому COUNT(*) вернул 2.
Но сумма есть только в одном заказе, поэтому SUM(amount) вернул 500, а AVG(amount) тоже посчитал среднее только по заполненным значениям.
Главное из статьи
COUNT, SUM, AVG, MIN и MAX — базовые агрегатные функции SQL. Они берут набор строк и возвращают итоговое значение.
COUNT(*) считает все строки.
COUNT(column) считает только строки, где колонка не равна NULL.
COUNT(DISTINCT column) считает уникальные заполненные значения.
SUM складывает значения.
AVG считает среднее.
MIN находит минимальное значение.
MAX находит максимальное значение.
SUM, AVG, MIN и MAX пропускают NULL. Если считать нечего, они возвращают NULL, а не 0.
Чтобы заменить NULL на ноль, используют COALESCE.
SELECT COALESCE(SUM(amount), 0) AS total_amount
FROM orders;
Без GROUP BY агрегат считает один общий итог по всей выборке.
С GROUP BY агрегат считает отдельный итог для каждой группы.
Главное — всегда понимать, какой вопрос ты задаёшь базе: «сколько всего?», «сколько заполнено?», «какая сумма?», «какое среднее?», «где минимум?» или «где максимум?». Тогда агрегатные функции перестают быть набором команд и становятся понятным языком для отчётов.
В SQL есть пять функций, без которых почти невозможно представить отчёты и аналитику:
COUNTSUMAVGMINMAXИх называют агрегатными функциями.
Слово «агрегатные» звучит чуть сухо, но идея простая: такие функции берут много строк и превращают их в один итог.
Обычный запрос показывает данные построчно:
SELECT name, email FROM users;А агрегатный запрос отвечает на вопрос по всей выборке:
SELECT COUNT(*) FROM users;То есть не «покажи всех пользователей», а «скажи, сколько их всего».
Зачем нужны агрегатные функции
Почти любой отчёт начинается с таких вопросов:
Сколько пользователей зарегистрировалось?
SELECT COUNT(*) FROM users;Сколько денег принесли заказы?
SELECT SUM(amount) FROM orders;Какая средняя оценка у товара?
SELECT AVG(rating) FROM reviews;Какой заказ был самым дорогим?
SELECT MAX(amount) FROM orders;Какая самая ранняя дата заказа?
SELECT MIN(created_at) FROM orders;Без агрегатных функций пришлось бы забирать строки в приложение и считать всё вручную: циклом, переменными, условиями. SQL создан как раз для того, чтобы такие вещи делать прямо в базе.
Главное правило: агрегат превращает строки в итог
Представь таблицу заказов:
Если сделать обычный
SELECT, мы увидим строки:SELECT amount FROM orders;Результат:
А если применить
SUM, SQL сложит значения и вернёт один итог:SELECT SUM(amount) AS total_amount FROM orders;Результат:
Вот в этом и смысл агрегатов: они не просто показывают данные, а считают результат по набору строк.
COUNT— посчитать строкиCOUNTотвечает на вопрос «сколько?».Самый частый вариант —
COUNT(*):SELECT COUNT(*) AS users_count FROM users;Он считает количество строк в таблице.
Например, есть таблица
users:Запрос:
SELECT COUNT(*) AS all_rows FROM users;Вернёт:
Потому что в таблице четыре строки.
COUNT(*),COUNT(column)иCOUNT(DISTINCT column)У
COUNTесть несколько важных форм.COUNT(*)считает все строкиSELECT COUNT(*) AS all_rows FROM users;COUNT(*)не смотрит, заполнены ли отдельные колонки. Ему важно только количество строк.Если строка есть — она считается.
COUNT(column)считает только заполненные значенияSELECT COUNT(email) AS users_with_email FROM users;Такой запрос посчитает только строки, где
emailне равенNULL.В нашей таблице:
Результат будет:
Пользователей четыре, но email указан только у трёх.
Это важная разница:
SELECT COUNT(*) AS all_rows, COUNT(email) AS users_with_email FROM users;Результат:
COUNT(*)отвечает на вопрос «сколько строк?».COUNT(email)отвечает на вопрос «в скольких строках заполнен email?».COUNT(DISTINCT column)считает уникальные значенияЕсли нужно посчитать не все значения, а только разные, используют
DISTINCT.SELECT COUNT(DISTINCT name) AS unique_names FROM users;В таблице два пользователя с именем Bob, но имя считается один раз.
Результат:
Потому что уникальные имена такие: Anna, Bob, Vera.
Чаще всего
COUNT(DISTINCT ...)используют, когда нужно посчитать уникальных пользователей, клиентов, товаров или сессии:SELECT COUNT(DISTINCT customer_id) AS unique_customers FROM orders;SUM— посчитать суммуSUMскладывает значения в колонке.Например, есть таблица заказов:
Запрос:
SELECT SUM(amount) AS total_amount FROM orders;Результат:
SUMиспользуют для денег, количества товаров, баллов, просмотров, часов, километров — всего, что можно сложить.Например, сумма оплаченных заказов:
SELECT SUM(amount) AS paid_amount FROM orders WHERE status = 'paid';Здесь сначала сработает
WHERE, останутся только оплаченные заказы, и уже по ним будет посчитана сумма.Важная особенность
SUM:NULLне превращается в нольSUMпропускает значенияNULL.Например:
Запрос:
SELECT SUM(amount) AS total_amount FROM orders;Вернёт:
NULLне стал нулём. Он просто не участвовал в расчёте.Но есть важный нюанс: если подходящих строк нет или все значения равны
NULL, результатом будетNULL, а не0.Например:
SELECT SUM(amount) AS total_amount FROM orders WHERE customer_id = 999;Если у клиента с таким id нет заказов, результат будет:
Для отчётов это часто неудобно. Обычно хочется увидеть ноль. Тогда используют
COALESCE:SELECT COALESCE(SUM(amount), 0) AS total_amount FROM orders WHERE customer_id = 999;COALESCEговорит: если результатSUM(amount)оказалсяNULL, покажи вместо него0.AVG— посчитать среднееAVGсчитает среднее значение.Например, есть отзывы:
Запрос:
SELECT AVG(rating) AS avg_rating FROM reviews WHERE product_id = 100;Результат:
Среднее считается так:
Сумма значений делится на количество заполненных значений.
В примере:
AVGтоже пропускаетNULLЕсли часть значений равна
NULL, они не участвуют в расчёте.Например:
Запрос:
SELECT AVG(rating) AS avg_rating FROM reviews;Посчитает не так:
А так:
Результат:
Это логично:
NULLозначает не «ноль», а «значение неизвестно» или «значения нет».Если оценка неизвестна, её нельзя честно использовать в среднем.
Округление результата
AVGСреднее часто получается дробным. В PostgreSQL для целочисленной колонки
AVGвозвращает точное значение типаNUMERIC.Например:
SELECT AVG(score) AS avg_score FROM tests;Результат может быть таким:
Для красивого вывода можно округлить:
SELECT ROUND(AVG(score), 2) AS avg_score FROM tests;Результат:
В разных базах данных детали типов могут отличаться. Например, в MySQL результат
AVGпо целым числам обычно будет дробным, но при больших числах и финансовых расчётах стоит внимательно следить за точностью. Если нужна точная арифметика, лучше использовать подходящий числовой тип, напримерNUMERICилиDECIMAL.MIN— найти самое маленькое значениеMINвозвращает минимальное значение в колонке.Например, самый дешёвый товар:
SELECT MIN(price) AS cheapest_price FROM products;Результат:
MINработает не только с числами.Можно найти самую раннюю дату заказа:
SELECT MIN(created_at) AS first_order_date FROM orders;Или первое имя по алфавитному порядку:
SELECT MIN(name) AS first_name FROM users;MAX— найти самое большое значениеMAXвозвращает максимальное значение.Например, самый дорогой товар:
SELECT MAX(price) AS most_expensive_price FROM products;Самая поздняя дата заказа:
SELECT MAX(created_at) AS last_order_date FROM orders;Последнее имя по алфавитному порядку:
SELECT MAX(name) AS last_name FROM users;MINиMAXтоже пропускаютNULL.Если все значения в колонке равны
NULL, результат будетNULL.MINиMAXпо тексту: осторожно с порядкомПо строкам
MINиMAXработают не как «самое короткое» или «самое длинное», а по порядку сортировки текста.Например, есть значения:
Если они хранятся как текст, порядок может оказаться неожиданным:
SELECT MIN(version) AS min_version, MAX(version) AS max_version FROM app_versions;Для текстовых значений
'10'может идти раньше'2', потому что сравнение идёт посимвольно: сначала сравнивается первый символ.Поэтому версии, номера и другие числовые значения лучше хранить в числовых колонках, если вы планируете сравнивать их как числа.
Агрегаты без
GROUP BY: итог по всей выборкеВсе примеры выше считали один итог по всей таблице или по строкам после
WHERE.Например:
SELECT COUNT(*) AS paid_orders FROM orders WHERE status = 'paid';Такой запрос вернёт одну строку:
Здесь вся подходящая выборка считается одной большой группой.
SQL как будто говорит: «Я взял все оплаченные заказы и посчитал по ним один общий итог».
Агрегаты с
GROUP BY: итог по каждой группеНо чаще в отчётах нужно получить не один общий итог, а итоги по группам.
Например:
Для этого нужен
GROUP BY.Допустим, есть товары:
Запрос:
SELECT category, COUNT(*) AS items_count, SUM(price) AS total_price, AVG(price) AS avg_price, MIN(price) AS min_price, MAX(price) AS max_price FROM products GROUP BY category;Результат:
Теперь каждая категория получила свою строку с итогами.
GROUP BY categoryозначает: «разбей строки на группы по категории и посчитай агрегаты отдельно для каждой группы».Как читать запрос с агрегатами и
GROUP BYВозьмём запрос:
SELECT customer_id, COUNT(*) AS orders_count, SUM(amount) AS total_amount FROM orders GROUP BY customer_id;Его удобно читать так:
orders.customer_id.COUNT(*).SUM(amount).То есть результат будет не «одна строка на заказ», а «одна строка на клиента».
Почему нельзя просто написать колонку рядом с агрегатом
Новички часто пишут так:
SELECT customer_id, COUNT(*) AS orders_count FROM orders;Такой запрос некорректен.
Проблема в том, что
COUNT(*)хочет вернуть один итог по всей таблице, аcustomer_id— обычная колонка, где много разных значений.SQL не может угадать, какой именно
customer_idпоказать рядом с общим количеством заказов.Правильно так:
SELECT customer_id, COUNT(*) AS orders_count FROM orders GROUP BY customer_id;Теперь всё честно: для каждого
customer_idбудет свой счётчик.Общее правило такое:
Если в
SELECTесть агрегатные функции и обычные колонки, то обычные колонки должны быть вGROUP BY.Агрегаты с
DISTINCTDISTINCTможно использовать не только сCOUNT.Например:
SELECT COUNT(DISTINCT customer_id) AS unique_customers, SUM(DISTINCT amount) AS sum_unique_amounts FROM orders;Но на практике чаще всего используют именно:
SELECT COUNT(DISTINCT customer_id) AS unique_customers FROM orders;Это понятный и полезный сценарий: посчитать уникальных клиентов.
А вот
SUM(DISTINCT amount)нужен редко. Он складывает только уникальные суммы заказов, а не все заказы.Например, если есть три заказа на суммы 100, 100 и 200, то обычный
SUM(amount)вернёт 400, аSUM(DISTINCT amount)вернёт 300.Чаще бизнесу нужна сумма всех заказов, поэтому с
SUM(DISTINCT ...)стоит быть осторожнее.Как агрегаты работают с
NULLNULL— одна из главных причин неожиданных результатов в агрегатах.Короткая таблица поведения:
NULLCOUNT(*)NULLCOUNT(column)NULLCOUNT(DISTINCT column)NULLSUM(column)NULL; если считать нечего, возвращаетNULLAVG(column)NULL; если считать нечего, возвращаетNULLMIN(column)NULL; если считать нечего, возвращаетNULLMAX(column)NULL; если считать нечего, возвращаетNULLСамая частая практическая защита —
COALESCE.Например, для суммы:
SELECT COALESCE(SUM(amount), 0) AS total_amount FROM orders WHERE status = 'paid';Для среднего иногда тоже можно подставить ноль, но делать это нужно осторожно:
SELECT COALESCE(AVG(rating), 0) AS avg_rating FROM reviews WHERE product_id = 100;Сумма ноль обычно означает «денег нет».
А средняя оценка ноль может выглядеть так, будто товар действительно получил оценку 0. Поэтому в интерфейсах иногда лучше показывать не 0, а текст вроде «пока нет оценок».
Частые ошибки новичков
Путать
COUNT(*)иCOUNT(column)SELECT COUNT(*) AS all_users, COUNT(email) AS users_with_email FROM users;Если результат такой:
Это не ошибка SQL.
Это значит: всего пользователей 100, но email заполнен только у 47.
Ждать от
SUMноль на пустой выборкеЗапрос:
SELECT SUM(amount) AS total_amount FROM orders WHERE status = 'cancelled_by_meteor';Если таких заказов нет, результатом будет
NULL.Для отчёта часто лучше так:
SELECT COALESCE(SUM(amount), 0) AS total_amount FROM orders WHERE status = 'cancelled_by_meteor';Писать обычную колонку рядом с агрегатом без
GROUP BYНеправильно:
SELECT name, COUNT(*) AS users_count FROM users;Правильно:
SELECT name, COUNT(*) AS users_count FROM users GROUP BY name;Если хочешь получить количество пользователей по каждому имени, нужен
GROUP BY name.Если хочешь получить только общее количество, убери
name:SELECT COUNT(*) AS users_count FROM users;Забывать, что
AVGпропускаетNULLЕсли у товара две оценки: 5 и
NULL, средняя будет 5, а не 2.5.Потому что
NULLне считается нулём.SELECT AVG(rating) AS avg_rating FROM reviews;SQL считает среднее только по известным значениям.
Использовать
MINиMAXпо тексту там, где нужны числаЕсли числа лежат в текстовой колонке, результат может удивить.
SELECT MAX(version) AS latest_version FROM app_versions;Для строк значения сравниваются как текст. Поэтому для числовых сравнений лучше хранить данные в числовом формате или заранее приводить типы, если это безопасно.
Думать, что точный
COUNT(*)на огромной таблице всегда мгновенныйВ PostgreSQL точный
COUNT(*)по большой таблице может быть дорогой операцией. База должна честно посчитать строки, а на таблицах в десятки или сотни миллионов записей это может занять заметное время.Если нужна точная цифра — придётся считать точно.
Если нужна примерная оценка для админки или внутренней статистики, в PostgreSQL иногда используют статистику планировщика, например данные из
pg_class. Но для пользовательских отчётов и финансовых расчётов такие приблизительные значения не подходят.Пример: небольшой отчёт по заказам
Допустим, у нас есть таблица
orders:Хотим получить общую статистику по оплаченным заказам:
SELECT COUNT(*) AS paid_orders, COUNT(amount) AS paid_orders_with_amount, SUM(amount) AS total_amount, AVG(amount) AS avg_amount, MIN(amount) AS min_amount, MAX(amount) AS max_amount FROM orders WHERE status = 'paid';Результат:
Что здесь произошло:
COUNT(*)посчитал все оплаченные заказы — их 4.COUNT(amount)посчитал только оплаченные заказы с заполненной суммой — их 3.SUM(amount)сложил известные суммы: 1200 + 800 + 500.AVG(amount)посчитал среднее только по известным суммам.MIN(amount)нашёл минимальную известную сумму.MAX(amount)нашёл максимальную известную сумму.Заказ с
NULLвamountпопал вCOUNT(*), но не участвовал вCOUNT(amount),SUM,AVG,MINиMAX.Пример: отчёт по каждому клиенту
Теперь посчитаем статистику отдельно по каждому клиенту:
SELECT customer_id, COUNT(*) AS orders_count, SUM(amount) AS total_amount, AVG(amount) AS avg_amount FROM orders WHERE status = 'paid' GROUP BY customer_id ORDER BY customer_id;Результат:
Обрати внимание на клиента 21.
У него два оплаченных заказа, поэтому
COUNT(*)вернул 2.Но сумма есть только в одном заказе, поэтому
SUM(amount)вернул 500, аAVG(amount)тоже посчитал среднее только по заполненным значениям.Главное из статьи
COUNT,SUM,AVG,MINиMAX— базовые агрегатные функции SQL. Они берут набор строк и возвращают итоговое значение.COUNT(*)считает все строки.COUNT(column)считает только строки, где колонка не равнаNULL.COUNT(DISTINCT column)считает уникальные заполненные значения.SUMскладывает значения.AVGсчитает среднее.MINнаходит минимальное значение.MAXнаходит максимальное значение.SUM,AVG,MINиMAXпропускаютNULL. Если считать нечего, они возвращаютNULL, а не0.Чтобы заменить
NULLна ноль, используютCOALESCE.SELECT COALESCE(SUM(amount), 0) AS total_amount FROM orders;Без
GROUP BYагрегат считает один общий итог по всей выборке.С
GROUP BYагрегат считает отдельный итог для каждой группы.Главное — всегда понимать, какой вопрос ты задаёшь базе: «сколько всего?», «сколько заполнено?», «какая сумма?», «какое среднее?», «где минимум?» или «где максимум?». Тогда агрегатные функции перестают быть набором команд и становятся понятным языком для отчётов.