COUNT, SUM, AVG: a sorokból számok lesznek
What you'll learn
COUNT,SUM,AVG,MINésMAXfüggvényekkel írsz lekérdezéseket, amelyek egy sorhalmazt egyetlen összegző értékké tömörítenek- a
WHERE-t aggregátumokkal kombinálod, hogy csak a számodra fontos sorokra számolj összesítést — például a fizetett rendelésekre - megkülönbözteted a
COUNT(*), aCOUNT(col)és aCOUNT(DISTINCT col)függvényt, és el tudod magyarázni, miért adnak ugyanazon az adaton három különböző számot - elmagyarázod, miért hasal el hibával a
SELECT status, COUNT(*)GROUP BYnélkül
Egy kis előreszaladás. Ebben a fejezetben néhány példa JOIN-nal fűz össze két táblát — ez a 4. fejezet témája, ott vesszük majd át részletesen. Egyelőre egy olyan sort, mint az orders o JOIN order_items oi ON oi.order_id = o.id, olvass egyszerűen úgy, hogy „összeragasztottuk két tábla sorait egy kulcs mentén”. Most maguk az aggregátumok a fontosak, nem a JOIN.
3. fejezet — „A bolt pulzusa”
Az „Elemző” besorolás meleg, borostyánsárga csíkként ül ki a belépőkártyádra. K. titkosított blokkja máig nem nyílt meg, de a fejlécét kívülről tudod: „A nyers sorok zaj. A jelentés akkor születik meg, amikor össze tudod adni őket.” A QUERY elhalványítja a fölösleges holopaneleket, és csak egyet hagy nyitva — üresen, közepén egyetlen lüktető ponttal.
QUERY: Soronként olvastad az archívumot. Elég. Ma a szívverését hallgatjuk meg.
Aggregáló függvények: sok sorból → egy szám
Egy sima SELECT úgy mutatja a sorokat, ahogy vannak: rendelés rendelés után, termék termék után. Egy aggregáló függvény egy sorhalmazt olyan értékké tömörít, amelyet már ki is lehet mondani: hány rendelés, mekkora bevétel, mennyi az átlagos kosárérték. Egy archiváriusnak épp ez a pulzus: egyetlen szám, amelyben benne dobog az egész áradat.
Az öt igásló:
| Függvény | Mit számol |
|---|---|
COUNT(*) | a sorok számát, például a rendelésekét |
SUM(x) | egy oszlop értékeinek összegét |
AVG(x) | az átlagértéket |
MIN(x) | a minimumot |
MAX(x) | a maximumot |
Ha a SELECT-ben nincs GROUP BY, az aggregátum az egész táblát (vagy azt, ami a WHERE után megmarad) egyetlen sorba roppantja össze.
SELECT COUNT(*) AS заказов, SUM(total_amount) AS выручка
FROM orders
WHERE status = 'paid';
Itt először a WHERE csak a fizetett rendeléseket hagyja meg, majd az aggregátumok ezen a halmazon számolják ki az összesítést. Így nem nyers listát kapsz, hanem egy rövid összefoglalót — a Kotomarket első szívdobbanását.
Fontos: ugyanabban a SELECT-ben nem keverhetsz aggregátumot és sima oszlopot GROUP BY nélkül. A SELECT status, COUNT(*) FROM orders lekérdezés hibával hasal el — az SQL nem fogja kitalálni, melyik status-t mutassa az összesített szám mellett.

До этого момента ты в основном доставал строки.
Например:
SELECT id, user_id, status, total_amount
FROM orders
WHERE status = 'paid';
Такой запрос показывает оплаченные заказы один за другим:
| id | user_id | status | total_amount |
|---|---|---|---|
| 101 | 7 | paid | 1200 |
| 102 | 9 | paid | 3500 |
| 103 | 7 | paid | 800 |
| 104 | 12 | paid | 2100 |
Это полезно, если нужно посмотреть сами строки.
Но часто бизнесовый вопрос звучит иначе:
Сколько было оплаченных заказов?
Какая общая выручка?
Какой средний чек?
Какой был минимальный и максимальный чек?
Сколько разных покупателей сделали заказ?
На такие вопросы не нужен длинный список строк. Нужна сводка.
Для этого в SQL есть .
Агрегатная функция берёт набор строк и сворачивает его в одно итоговое значение.
Например:
SELECT COUNT(*) AS orders_count
FROM orders;
Этот запрос не показывает каждый заказ отдельно. Он возвращает одно число: сколько строк есть в таблице orders.
Агрегатные функции: много строк → одно значение
Агрегаты превращают поток строк в показатель, который можно произнести вслух.
Не:
заказ 101
заказ 102
заказ 103
заказ 104
А:
всего 4 заказа
Не:
1200
3500
800
2100
А:
общая сумма 7600
средний чек 1900
минимальный чек 800
максимальный чек 3500
Пять основных агрегатных функций:
| Функция | Что считает | Пример вопроса |
|---|---|---|
COUNT(*) | количество строк | Сколько заказов? |
SUM(x) | сумму значений | Какая выручка? |
AVG(x) | среднее значение | Какой средний чек? |
MIN(x) | минимальное значение | Какой самый маленький чек? |
MAX(x) | максимальное значение | Какой самый большой чек? |
Например:
SELECT
COUNT(*) AS orders_count,
SUM(total_amount) AS revenue,
AVG(total_amount) AS avg_order_amount,
MIN(total_amount) AS min_order_amount,
MAX(total_amount) AS max_order_amount
FROM orders;
Если в запросе нет GROUP BY, считают итог по всему набору строк, который дошёл до них.
То есть результатом будет одна строка:
| orders_count | revenue | avg_order_amount | min_order_amount | max_order_amount |
|---|---|---|---|---|
| 4 | 7600 | 1900 | 800 | 3500 |
COUNT: сколько строк
COUNT отвечает на вопрос «сколько?».
Самая частая форма:
COUNT(*)
Она считает строки.
Например:
SELECT COUNT(*) AS orders_count
FROM orders;
Результат:
| orders_count |
|---|
| 128 |
Звёздочка в COUNT(*) не означает «выбери все столбцы». Здесь она означает:
считай сами строки.
COUNT(*) не смотрит, какие значения лежат внутри столбцов. Ему важно только, существует строка или нет.
Поэтому COUNT(*) подходит, когда вопрос звучит так:
Сколько всего заказов?
Сколько всего товаров?
Сколько всего пользователей?
Сколько строк осталось после фильтра?
Например, сколько оплаченных заказов:
SELECT COUNT(*) AS paid_orders_count
FROM orders
WHERE status = 'paid';
Здесь сначала WHERE оставляет только оплаченные заказы, а потом COUNT(*) считает оставшиеся строки.
SUM, AVG, MIN и MAX
SUM считает сумму.
SELECT SUM(total_amount) AS revenue
FROM orders
WHERE status = 'paid';
Так можно получить выручку по оплаченным заказам.
AVG считает среднее значение.
SELECT AVG(total_amount) AS avg_order_amount
FROM orders
WHERE status = 'paid';
Так можно получить средний чек по оплаченным заказам.
MIN ищет минимальное значение.
SELECT MIN(total_amount) AS min_order_amount
FROM orders
WHERE status = 'paid';
MAX ищет максимальное значение.
SELECT MAX(total_amount) AS max_order_amount
FROM orders
WHERE status = 'paid';
Все эти функции можно использовать вместе:
SELECT
COUNT(*) AS paid_orders_count,
SUM(total_amount) AS revenue,
AVG(total_amount) AS avg_order_amount,
MIN(total_amount) AS min_order_amount,
MAX(total_amount) AS max_order_amount
FROM orders
WHERE status = 'paid';
Так один запрос возвращает сводку по оплаченным заказам.
WHERE сначала отбирает строки, потом агрегат считает
Важно понимать порядок мысли.
В запросе:
SELECT
COUNT(*) AS paid_orders_count,
SUM(total_amount) AS revenue
FROM orders
WHERE status = 'paid';
сначала работает фильтр:
WHERE status = 'paid'
Он оставляет только оплаченные заказы.
И только потом считают итог по оставшимся строкам:
COUNT(*)
SUM(total_amount)
То есть запрос не считает все заказы, а потом как-то помечает оплаченные. Он сначала убирает лишние строки, а затем считает итог.
Это удобно для любых уточнений.
Выручка только по оплаченным заказам:
WHERE status = 'paid'
Количество отменённых заказов:
WHERE status = 'cancelled'
Средний чек за определённый период:
WHERE created_at >= '2024-01-01'
AND created_at < '2024-02-01'
Сначала формируется набор строк. Потом агрегат превращает его в число.
КВЕРИ: Прежде чем слушать пульс, выбери, чьё сердце слушаешь. Все заказы, оплаченные заказы и отменённые заказы звучат по-разному.
ROUND(...,2) két tizedesjegyre kerekíti az átlagot.| всего_заказов | покупателей | выручка | средний_чек | минимум | максимум |
|---|---|---|---|---|---|
| 86 | 35 | 1381230.00 | 16060.81 | 890.00 | 42110.00 |
A COUNT(*) és a COUNT(DISTINCT col) más-más kérdésre válaszol. A COUNT(*) a sorokat számolja, a COUNT(DISTINCT user_id) pedig azt, hány különböző vásárló áll mögöttük. Ha több a rendelés, mint a vásárló, az nem az archívum hibája: valaki visszatért, és újra vásárolt.
COUNT(*), COUNT(col), COUNT(DISTINCT col)
У COUNT есть несколько форм, и они отвечают на разные вопросы.
Разберём на маленьком примере.
Пусть есть таблица visits:
| id | promo_code |
|---|---|
| 1 | SALE |
| 2 | SALE |
| 3 | |
| 4 | VIP |
| 5 | NULL |
Запрос:
SELECT
COUNT(*) AS rows_count,
COUNT(promo_code) AS with_promo_code,
COUNT(DISTINCT promo_code) AS different_promo_codes
FROM visits;
вернёт:
| rows_count | with_promo_code | different_promo_codes |
|---|---|---|
| 5 | 3 | 2 |
Почему так?
COUNT(*) считает все строки.
В таблице 5 строк, значит результат — 5.
COUNT(promo_code) считает строки, где promo_code заполнен.
Значения NULL он не считает. Заполненные промокоды есть в трёх строках:
SALE
SALE
VIP
Значит результат — 3.
COUNT(DISTINCT promo_code) считает разные заполненные значения.
Из заполненных значений:
SALE
SALE
VIP
разных только два:
SALE
VIP
Значит результат — 2.
Итого:
COUNT(*) → сколько строк
COUNT(promo_code) → у скольких строк промокод заполнен
COUNT(DISTINCT promo_code) → сколько разных заполненных промокодов
Это три разных вопроса к данным, поэтому ответы могут быть разными.
Агрегаты и NULL
Общее правило:
обычно игнорируют NULL.
Например, есть значения:
| total_amount |
|---|
| 1000 |
| 2000 |
SUM(total_amount) сложит только заполненные значения:
1000 + 2000 = 3000
AVG(total_amount) тоже возьмёт только заполненные значения:
(1000 + 2000) / 2 = 1500
Не так:
(1000 + 2000 + NULL) / 3
MIN и MAX тоже ищут минимум и максимум среди заполненных значений.
Единственное важное исключение — COUNT(*).
COUNT(*) считает строки, а не значение конкретного столбца, поэтому NULL внутри столбцов его не волнует.
Сравни:
SELECT
COUNT(*) AS rows_count,
COUNT(total_amount) AS filled_amounts,
SUM(total_amount) AS total_sum,
AVG(total_amount) AS avg_amount
FROM orders;
COUNT(*) ответит на вопрос:
сколько строк
COUNT(total_amount) ответит на вопрос:
у скольких строк заполнен total_amount
Это не одно и то же.
Если после WHERE не осталось строк
Иногда фильтр может не найти ни одной строки.
Например:
SELECT
COUNT(*) AS orders_count,
SUM(total_amount) AS revenue,
AVG(total_amount) AS avg_order_amount,
MIN(total_amount) AS min_order_amount,
MAX(total_amount) AS max_order_amount
FROM orders
WHERE status = 'status_that_does_not_exist';
Если таких заказов нет, результат всё равно будет одной строкой, потому что без GROUP BY возвращает одну сводку по выбранному набору.
Но значения будут разными:
| orders_count | revenue | avg_order_amount | min_order_amount | max_order_amount |
|---|---|---|---|---|
| 0 | NULL | NULL | NULL |
COUNT(*) вернёт 0, потому что строк нет.
А SUM, AVG, MIN и MAX вернут NULL, потому что им нечего суммировать, усреднять или сравнивать.
Если в отчёте вместо NULL нужна нулевая выручка, можно использовать COALESCE:
SELECT
COUNT(*) AS orders_count,
COALESCE(SUM(total_amount), 0) AS revenue
FROM orders
WHERE status = 'status_that_does_not_exist';
Это не меняет данные в таблице. Просто в результате запроса вместо пустого итога показывается 0.
COUNT(*) и COUNT(1)
Иногда в чужих запросах можно встретить:
COUNT(1)
Например:
SELECT COUNT(1)
FROM orders;
В PostgreSQL для обычного подсчёта строк это по смыслу то же самое, что:
SELECT COUNT(*)
FROM orders;
Почему?
Потому что 1 — это выражение, которое не бывает NULL. В каждой строке оно существует, значит COUNT(1) считает каждую строку.
Но для читаемости лучше писать:
COUNT(*)
Так сразу видно, что ты считаешь строки.
Миф о том, что COUNT(1) обязательно быстрее COUNT(*), для PostgreSQL не является полезным правилом. На этом уровне запомни проще:
нужны строки — пиши
COUNT(*).
Почему обычный столбец рядом с агрегатом даёт ошибку
Посмотри на запрос:
SELECT status, COUNT(*)
FROM orders;
Кажется, что он должен показать статус и количество заказов.
Но без GROUP BY такой запрос падает с ошибкой.
Почему?
COUNT(*) без GROUP BY сворачивает все выбранные строки в одну итоговую строку.
Например:
| count |
|---|
| 128 |
А status — это обычный столбец. В таблице у разных заказов разные статусы:
| id | status |
|---|---|
| 1 | paid |
| 2 | pending |
| 3 | paid |
| 4 | cancelled |
Если весь набор строк схлопнулся в одну строку результата, какой status SQL должен показать рядом с общим количеством?
paid?
pending?
cancelled?
SQL не угадывает. Он требует, чтобы ты явно описал логику.
Если нужен общий счётчик всех заказов — убери status:
SELECT COUNT(*) AS orders_count
FROM orders;
Если нужно количество заказов по каждому статусу — добавь GROUP BY:
SELECT status, COUNT(*) AS orders_count
FROM orders
GROUP BY status;
Тогда результат будет не одна общая строка, а отдельная строка на каждый статус:
| status | orders_count |
|---|---|
| paid | 80 |
| pending | 25 |
| cancelled | 18 |
| refunded | 5 |
GROUP BY подробно разберём дальше. Пока важно понять саму причину ошибки:
схлопывает строки, а обычный столбец остаётся многозначным. SQL не выбирает одно значение за тебя.
Главное правило без GROUP BY
Если в запросе нет GROUP BY, агрегатные функции считают итог по всему набору строк, который остался после WHERE.
SELECT COUNT(*), SUM(total_amount)
FROM orders
WHERE status = 'paid';
Такой запрос вернёт одну строку.
Обычный столбец рядом с агрегатом без группировки писать нельзя:
SELECT status, COUNT(*)
FROM orders;
Такой запрос некорректен, потому что status может быть разным у разных строк.
Interview question
Állásinterjús kérdés: Egy táblában száz sor van, a manager_id oszlopban néhány érték NULL. Mit ad vissza a COUNT(*), a COUNT(manager_id) és a COUNT(DISTINCT manager_id), és miért különböznek a számok?
Erős válasz: A COUNT(*) 100-at ad vissza — a sorokat számolja, és a NULL-ra rá sem néz. A COUNT(manager_id) kevesebbet: csak azokat a sorokat, ahol a manager_id ki van töltve, mert az aggregátumok figyelmen kívül hagyják a NULL-t. A COUNT(DISTINCT manager_id) még kevesebbet vagy ugyanannyit: a különböző, nem üres értékek számát. Ez három különböző kérdés az adatokhoz: „hány sor”, „hánynál van kitöltve”, „hány különböző”.
Állásinterjús kérdés: Miért hiba a SELECT status, COUNT(*) FROM orders GROUP BY nélkül?
Erős válasz: Az aggregátum egyetlen sorba roppantja össze a kiválasztott halmazt, a status viszont többértékű marad — soronként más, és az SQL nem találgatja ki, melyik értéket mutassa. Vagy vedd ki a status-t, vagy add hozzá a GROUP BY status-t, és a számláló statuszonként külön számolódik.
SELECT AVG(price) FROM products;, если GROUP BY не указан?COUNT(*)?COUNT(user_id) отличается от COUNT(DISTINCT user_id)?SELECT status, COUNT(*) FROM orders; без GROUP BY некорректен?КВЕРИ: Сырые строки показывают события. Агрегаты показывают пульс: сколько, на какую сумму, в каких границах и с каким средним ритмом.