Agregavimas: skaičiuojame verslo rodiklius

COUNT, SUM, AVG: eilutės virsta skaičiais

25 min
What you'll learn
  • rašyti užklausas su COUNT, SUM, AVG, MIN ir MAX, kurios suspaudžia eilučių rinkinį į vieną galutinę suvestinę
  • derinti WHERE su agregatais, kad skaičiuotum bendrą rezultatą tik pagal reikiamas eilutes — pavyzdžiui, pagal apmokėtus užsakymus
  • atskirti COUNT(*), COUNT(col) ir COUNT(DISTINCT col) bei paaiškinti, kodėl su tais pačiais duomenimis jie grąžina tris skirtingus skaičius
  • paaiškinti, kodėl SELECT status, COUNT(*) be GROUP BY nulūžta su klaida

Žvilgsnis į priekį. Šiame skyriuje kai kurie pavyzdžiai sujungia dvi lenteles per JOIN — tai 4 skyriaus tema, kurią ten išnagrinėsime išsamiai. Kol kas eilutę, tokią kaip orders o JOIN order_items oi ON oi.order_id = o.id, skaityk paprasčiausiai kaip „suklijavome dviejų lentelių eilutes pagal raktą“. Dabar svarbūs patys agregatai, o ne JOIN.

3 skyrius — „Parduotuvės pulsas“

„Analitiko“ prieiga užgula tavo leidimą šiltu gintaro ruoželiu. Užšifruotas K. blokas taip ir neatsivėrė, bet jo antraštę moki atmintinai: „Žalios eilutės — triukšmas. Prasmė atsiranda tada, kai moki jas sudėti.“ QUERY užgesina nereikalingas holopaneles ir palieka vieną — tuščią, su vieniša pulsuojančia taškeliu viduryje.

QUERY: Tu skaitei archyvą eilutė po eilutės. Užteks. Šiandien klausysimės jo širdies.

Agregavimo funkcijos: daug eilučių → vienas skaičius

Įprastas SELECT rodo eilutes tokias, kokios jos yra: užsakymas po užsakymo, prekė po prekės. Agregavimo funkcija suspaudžia eilučių rinkinį į rodiklį, kurį jau galima ištarti balsu: kiek užsakymų, kokios pajamos, koks vidutinis čekis. Archyvarui tai ir yra pulsas: vienas skaičius, kuriame girdėti visas srautas.

Penki darbiniai arkliukai:

FunkcijaKą skaičiuoja
COUNT(*)eilučių skaičių, pavyzdžiui užsakymų
SUM(x)stulpelio reikšmių sumą
AVG(x)vidutinę reikšmę
MIN(x)minimumą
MAX(x)maksimumą

Jei SELECT nėra GROUP BY, agregatas suspaudžia visą lentelę (arba tai, kas liko po WHERE) į vieną eilutę.

SELECT COUNT(*) AS užsakymų, SUM(total_amount) AS pajamos
FROM orders
WHERE status = 'paid';

Čia iš pradžių WHERE palieka tik apmokėtus užsakymus, o paskui agregatai apskaičiuoja bendrą rezultatą pagal šį rinkinį. Taip gauni ne žalią sąrašą, o trumpą suvestinę — pirmąjį „Kotomarket“ pulso dūžį.

Svarbu: viename SELECT negalima maišyti agregato ir įprasto stulpelio be GROUP BY. Užklausa SELECT status, COUNT(*) FROM orders nulūš su klaida — SQL nespėlios, kurį status parodyti šalia bendro skaičiaus.

Kursantas prie holopanelės: eilučių-kapsulių srautas suspaudžiamas į vieną pulsuojantį taškelį-rodiklį
Agregatas suspaudžia šimtus eilučių į vieną pulso dūžį — skaičių, kurį galima ištarti balsu.

До этого момента ты в основном доставал строки.

Например:

SELECT id, user_id, status, total_amount
FROM orders
WHERE status = 'paid';

Такой запрос показывает оплаченные заказы один за другим:

iduser_idstatustotal_amount
1017paid1200
1029paid3500
1037paid800
10412paid2100

Это полезно, если нужно посмотреть сами строки.

Но часто бизнесовый вопрос звучит иначе:

Сколько было оплаченных заказов?
Какая общая выручка?
Какой средний чек?
Какой был минимальный и максимальный чек?
Сколько разных покупателей сделали заказ?

На такие вопросы не нужен длинный список строк. Нужна сводка.

Для этого в 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_countrevenueavg_order_amountmin_order_amountmax_order_amount
4760019008003500
9905990129034902790SUM14 550one number
Agregavimo funkcija suspaudžia eilučių srautą į vieną galutinę reikšmę — vieną rodiklį visam rinkiniui.

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'

Сначала формируется набор строк. Потом агрегат превращает его в число.

КВЕРИ: Прежде чем слушать пульс, выбери, чьё сердце слушаешь. Все заказы, оплаченные заказы и отменённые заказы звучат по-разному.

Klausomės parduotuvės širdies: viena eilutė su visais gyvybiniais rodikliais — kiek apmokėtų užsakymų, kiek skirtingų pirkėjų už jų stovi, kokios pajamos ir kur čekio ribos. ROUND(...,2) suapvalina vidurkį iki centų.
Query result
всего_заказовпокупателейвыручкасредний_чекминимуммаксимум
86351381230.0016060.81890.0042110.00

COUNT(*) ir COUNT(DISTINCT col) atsako į skirtingus klausimus. COUNT(*) skaičiuoja eilutes, o COUNT(DISTINCT user_id) — kiek skirtingų pirkėjų už jų stovi. Jei užsakymų daugiau nei pirkėjų, tai nėra archyvo gedimas: kažkas grįžo ir pirko dar kartą.

COUNT(*), COUNT(col), COUNT(DISTINCT col)

У COUNT есть несколько форм, и они отвечают на разные вопросы.

Разберём на маленьком примере.

Пусть есть таблица visits:

idpromo_code
1SALE
2SALE
3
4VIP
5NULL

Запрос:

SELECT
    COUNT(*) AS rows_count,
    COUNT(promo_code) AS with_promo_code,
    COUNT(DISTINCT promo_code) AS different_promo_codes
FROM visits;

вернёт:

rows_countwith_promo_codedifferent_promo_codes
532

Почему так?

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_countrevenueavg_order_amountmin_order_amountmax_order_amount
0NULLNULLNULL

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 — это обычный столбец. В таблице у разных заказов разные статусы:

idstatus
1paid
2pending
3paid
4cancelled

Если весь набор строк схлопнулся в одну строку результата, какой 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;

Тогда результат будет не одна общая строка, а отдельная строка на каждый статус:

statusorders_count
paid80
pending25
cancelled18
refunded5

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

Pokalbio klausimas: Lentelėje šimtas eilučių, stulpelyje manager_id dalis reikšmių — NULL. Ką grąžins COUNT(*), COUNT(manager_id) ir COUNT(DISTINCT manager_id) ir kodėl skaičiai skiriasi?

Stiprus atsakymas: COUNT(*) grąžins 100 — jis skaičiuoja eilutes ir į NULL nežiūri. COUNT(manager_id) grąžins mažiau: tik eilutes su užpildytu manager_id, nes agregatai ignoruoja NULL. COUNT(DISTINCT manager_id) — dar mažiau arba tiek pat: skirtingų netuščių reikšmių skaičių. Tai trys skirtingi klausimai duomenims: „kiek eilučių“, „kiek užpildyta“, „kiek skirtingų“.

Pokalbio klausimas: Kodėl SELECT status, COUNT(*) FROM orders be GROUP BY yra klaida?

Stiprus atsakymas: Agregatas suspaudžia atranką į vieną eilutę, o status lieka daugiareikšmis — skirtingose eilutėse jis skiriasi, ir SQL nespėlios, kurią reikšmę parodyti. Arba pašalink status, arba pridėk GROUP BY status, ir skaitiklis bus apskaičiuotas atskirai kiekvienam statusui.

Check yourself
Что даст запрос SELECT AVG(price) FROM products;, если GROUP BY не указан?
Check yourself
Что считает COUNT(*)?
Check yourself
Чем COUNT(user_id) отличается от COUNT(DISTINCT user_id)?
Check yourself
Почему запрос SELECT status, COUNT(*) FROM orders; без GROUP BY некорректен?

КВЕРИ: Сырые строки показывают события. Агрегаты показывают пульс: сколько, на какую сумму, в каких границах и с каким средним ритмом.