SQLaggregatesCOUNTSUM

COUNT, SUM, AVG, MIN, MAX в SQL: как считать строки, суммы, средние и крайние значения

Агрегатные функции — это инструмент «посчитать что-то по группе строк». COUNT — сколько строк, SUM — сумма, AVG — среднее, MIN/MAX — минимум и максимум. Простыми словами: разница COUNT(*) и COUNT(column), как NULL влияет на агрегаты, разные сценарии и частые ошибки.

12 мин чтенияСправочникSQL · aggregates · COUNT · SUM · tutorial

В SQL есть пять функций, без которых почти невозможно представить отчёты и аналитику:

  • COUNT
  • SUM
  • AVG
  • MIN
  • MAX

Их называют агрегатными функциями.

Слово «агрегатные» звучит чуть сухо, но идея простая: такие функции берут много строк и превращают их в один итог.

Обычный запрос показывает данные построчно:

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;

Результат:

amount
1200
800
2400
500

А если применить SUM, SQL сложит значения и вернёт один итог:

SELECT SUM(amount) AS total_amount
FROM orders;

Результат:

total_amount
4900

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

COUNT — посчитать строки

COUNT отвечает на вопрос «сколько?».

Самый частый вариант — COUNT(*):

SELECT COUNT(*) AS users_count
FROM users;

Он считает количество строк в таблице.

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

id name email
1 Anna anna@example.com
2 Bob NULL
3 Vera vera@example.com
4 Bob bob@example.com

Запрос:

SELECT COUNT(*) AS all_rows
FROM users;

Вернёт:

all_rows
4

Потому что в таблице четыре строки.

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.

В нашей таблице:

id name email
1 Anna anna@example.com
2 Bob NULL
3 Vera vera@example.com
4 Bob bob@example.com

Результат будет:

users_with_email
3

Пользователей четыре, но 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, но имя считается один раз.

Результат:

unique_names
3

Потому что уникальные имена такие: 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;

Результат:

total_amount
4900

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;

Вернёт:

total_amount
400

NULL не стал нулём. Он просто не участвовал в расчёте.

Но есть важный нюанс: если подходящих строк нет или все значения равны NULL, результатом будет NULL, а не 0.

Например:

SELECT SUM(amount) AS total_amount
FROM orders
WHERE customer_id = 999;

Если у клиента с таким id нет заказов, результат будет:

total_amount
NULL

Для отчётов это часто неудобно. Обычно хочется увидеть ноль. Тогда используют 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;

Результат:

avg_rating
4.25

Среднее считается так:

Сумма значений делится на количество заполненных значений.

В примере:

(5 + 4 + 5 + 3) / 4 = 4.25

AVG тоже пропускает NULL

Если часть значений равна NULL, они не участвуют в расчёте.

Например:

id rating
1 5
2 4
3 NULL

Запрос:

SELECT AVG(rating) AS avg_rating
FROM reviews;

Посчитает не так:

(5 + 4 + 0) / 3

А так:

(5 + 4) / 2

Результат:

avg_rating
4.5

Это логично: 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;

Результат:

avg_score
4.50

В разных базах данных детали типов могут отличаться. Например, в MySQL результат AVG по целым числам обычно будет дробным, но при больших числах и финансовых расчётах стоит внимательно следить за точностью. Если нужна точная арифметика, лучше использовать подходящий числовой тип, например NUMERIC или DECIMAL.

MIN — найти самое маленькое значение

MIN возвращает минимальное значение в колонке.

Например, самый дешёвый товар:

SELECT MIN(price) AS cheapest_price
FROM products;

Результат:

cheapest_price
990

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 работают не как «самое короткое» или «самое длинное», а по порядку сортировки текста.

Например, есть значения:

version
1
2
10

Если они хранятся как текст, порядок может оказаться неожиданным:

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';

Такой запрос вернёт одну строку:

paid_orders
128

Здесь вся подходящая выборка считается одной большой группой.

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;

Его удобно читать так:

  1. Возьми таблицу orders.
  2. Разбей заказы на группы по customer_id.
  3. Для каждого клиента посчитай количество заказов через COUNT(*).
  4. Для каждого клиента посчитай сумму заказов через SUM(amount).
  5. Покажи одну строку на каждого клиента.

То есть результат будет не «одна строка на заказ», а «одна строка на клиента».

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

Новички часто пишут так:

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-тренажёре с мгновенной проверкой и подсказками.

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