COUNT, SUM, AVG: rivit muuttuvat luvuiksi
What you'll learn
- kirjoittamaan kyselyitä, joissa
COUNT,SUM,AVG,MINjaMAXtiivistävät rivijoukon yhdeksi koonniksi - yhdistämään
WHERE-ehdon ja aggregaatit niin, että laskenta koskee vain haluamiasi rivejä — esimerkiksi maksettuja tilauksia - erottamaan
COUNT(*),COUNT(col)jaCOUNT(DISTINCT col)toisistaan ja selittämään, miksi ne tuottavat samasta datasta kolme eri lukua - selittämään, miksi
SELECT status, COUNT(*)ilmanGROUP BY-lausetta kaatuu virheeseen
Hieman edellä. Osa tämän luvun esimerkeistä liimaa kaksi taulua yhteen JOIN-lauseella — se on luvun 4 aihe, jossa käymme sen kunnolla läpi. Lue toistaiseksi rivi kuten orders o JOIN order_items oi ON oi.order_id = o.id yksinkertaisesti niin, että ”kahden taulun rivit liimattiin yhteen avaimen perusteella”. Nyt tärkeintä ovat aggregaatit itse, eivät JOIN-lauseet.
Luku 3 — ”Kaupan pulssi”
”Analyytikko”-käyttöoikeus laskeutuu kulkulupaasi lämpimänä meripihkanvärisenä raitana. K:n salattu lohko ei koskaan auennut, mutta sen otsikon muistat ulkoa: ”Raa'at rivit ovat hälyä. Merkitys syntyy, kun osaat laskea ne yhteen.” QUERY himmentää ylimääräiset holopaneelit ja jättää yhden — tyhjän, jonka keskellä sykkii yksinäinen piste.
QUERY: Olet lukenut arkistoa rivi riviltä. Riittää. Tänään kuuntelemme sen sydäntä.
Aggregaattifunktiot: monta riviä → yksi luku
Tavallinen SELECT näyttää rivit sellaisinaan: tilaus tilauksen, tuote tuotteen jälkeen. Aggregaattifunktio puristaa rivijoukon tunnusluvuksi, jonka voi jo sanoa ääneen: montako tilausta, paljonko liikevaihtoa, mikä on keskiostos. Arkistonhoitajalle se on juuri pulssi: yksi luku, jossa kuuluu koko virta.
Viisi työjuhtaa:
| Funktio | Mitä se laskee |
|---|---|
COUNT(*) | rivien määrä, esimerkiksi tilausten |
SUM(x) | sarakkeen arvojen summa |
AVG(x) | keskiarvo |
MIN(x) | minimi |
MAX(x) | maksimi |
Jos SELECT-lauseessa ei ole GROUP BY-lausetta, aggregaatti luhistaa koko taulun (tai sen, mitä WHERE-ehdon jälkeen jää) yhdeksi riviksi.
SELECT COUNT(*) AS tilauksia, SUM(total_amount) AS liikevaihto
FROM orders
WHERE status = 'paid';
Tässä WHERE jättää ensin jäljelle vain maksetut tilaukset, ja sitten aggregaatit laskevat koonnin tästä joukosta. Näin et saa raakaa listaa vaan lyhyen koonnin — Kotomarketin ensimmäisen sydämenlyönnin.
Tärkeää: samassa SELECT-lauseessa ei voi sekoittaa aggregaattia ja tavallista saraketta ilman GROUP BY-lausetta. Kysely SELECT status, COUNT(*) FROM orders kaatuu virheeseen — SQL ei ala arvailla, minkä status-arvon se näyttäisi kokonaisluvun vieressä.

До этого момента ты в основном доставал строки.
Например:
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) pyöristää keskiarvon kahteen desimaaliin.| всего_заказов | покупателей | выручка | средний_чек | минимум | максимум |
|---|---|---|---|---|---|
| 86 | 35 | 1381230.00 | 16060.81 | 890.00 | 42110.00 |
COUNT(*) ja COUNT(DISTINCT col) vastaavat eri kysymyksiin. COUNT(*) laskee rivit, kun taas COUNT(DISTINCT user_id) laskee, montako eri ostajaa niiden takana on. Jos tilauksia on enemmän kuin ostajia, arkisto ei ole rikki: joku palasi ja osti uudelleen.
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
Haastattelukysymys: Taulussa on sata riviä, ja osa manager_id-sarakkeen arvoista on NULL. Mitä palauttavat COUNT(*), COUNT(manager_id) ja COUNT(DISTINCT manager_id), ja miksi luvut ovat erilaiset?
Vahva vastaus: COUNT(*) palauttaa 100 — se laskee rivit eikä katso NULL-arvoja. COUNT(manager_id) palauttaa vähemmän: vain rivit, joilla manager_id on täytetty, koska aggregaatit jättävät NULL-arvot huomiotta. COUNT(DISTINCT manager_id) on vielä pienempi tai yhtä suuri: eri ei-tyhjien arvojen määrä. Nämä ovat kolme eri kysymystä datalle: ”montako riviä”, ”monellako on täytetty”, ”montako eri arvoa”.
Haastattelukysymys: Miksi SELECT status, COUNT(*) FROM orders ilman GROUP BY-lausetta on virhe?
Vahva vastaus: Aggregaatti luhistaa tuloksen yhdeksi riviksi, mutta status jää moniarvoiseksi — eri riveillä se on erilainen, eikä SQL arvaa, minkä arvon näyttäisi. Joko poista status tai lisää GROUP BY status, jolloin laskuri lasketaan erikseen jokaiselle statukselle.
SELECT AVG(price) FROM products;, если GROUP BY не указан?COUNT(*)?COUNT(user_id) отличается от COUNT(DISTINCT user_id)?SELECT status, COUNT(*) FROM orders; без GROUP BY некорректен?КВЕРИ: Сырые строки показывают события. Агрегаты показывают пульс: сколько, на какую сумму, в каких границах и с каким средним ритмом.