Aggregazzjoni: inkejlu n-negozju

COUNT, SUM, AVG: ir-ringieli jsiru numri

25 min
What you'll learn
  • tikteb mistoqsijiet bi COUNT, SUM, AVG, MIN u MAX li jiġbru sett ta' ringieli f'sommarju wieħed
  • tgħaqqad WHERE mal-aggregati biex tgħodd it-total ta' dawk ir-ringieli biss li jinteressawk — pereżempju, l-ordnijiet imħallsa
  • tagħraf id-differenza bejn COUNT(*), COUNT(col) u COUNT(DISTINCT col) u tispjega għaliex fuq l-istess dejta jagħtu tliet numri differenti
  • tispjega għaliex SELECT status, COUNT(*) mingħajr GROUP BY jaqa' bi żball

Naqbżu 'l quddiem ftit. F'dan il-kapitlu xi eżempji jgħaqqdu żewġ tabelli b'JOIN — dik hija t-tema tal-Kapitlu 4, fejn se nispjegawha sewwa. Għalissa, aqra biss linja bħal orders o JOIN order_items oi ON oi.order_id = o.id bħala «għaqqadna r-ringieli ta' żewġ tabelli b'ċavetta». Hawnhekk dak li jgħodd huma l-aggregati nfushom, mhux il-JOIN.

Kapitlu 3 — «It-Taħbita tal-Ħanut»

Il-permess «Analista» jistrieħ fuq il-pass tiegħek bħala faxxa lewn l-ambra sħuna. Il-blokk encrypted ta' K. qatt ma nfetaħ, imma t-titlu tiegħu tafu bl-amment: «Ir-ringieli mhux maħduma huma ħoss żejjed. It-tifsira titfaċċa meta tkun taf tgħoddhom flimkien.» QUERY jitfi l-holopanels żejda u jħalli waħda — vojta, b'tikka waħda titħabbat fin-nofs.

QUERY: Kont qed taqra l-arkivju ringiela b'ringiela. Biżżejjed. Illum nisimgħu l-qalb tiegħu.

Funzjonijiet aggregati: ħafna ringieli → numru wieħed

SELECT ordinarju juri r-ringieli kif inhuma: ordni wara ordni, prodott wara prodott. Funzjoni aggregata tikkompressa sett ta' ringieli f'ċifra li tista' tlissinha b'leħen għoli: kemm-il ordni, kemm dħul, x'inhu l-valur medju tal-ordni. Għal arkivista, dik hija t-taħbita: numru wieħed li jġorr il-fluss kollu.

Il-ħames żwiemel tax-xogħol:

FunzjoniX'tgħodd
COUNT(*)l-għadd tar-ringieli, pereżempju l-ordnijiet
SUM(x)is-somma tal-valuri ta' kolonna
AVG(x)il-valur medju
MIN(x)il-minimu
MAX(x)il-massimu

Jekk fis-SELECT ma jkunx hemm GROUP BY, l-aggregat jiġbor it-tabella sħiħa (jew dak li jibqa' wara WHERE) f'ringiela waħda.

SELECT COUNT(*) AS ordnijiet, SUM(total_amount) AS dhul
FROM orders
WHERE status = 'paid';

Hawnhekk l-ewwel WHERE iżomm biss l-ordnijiet imħallsa, u mbagħad l-aggregati jgħoddu t-total fuq dak is-sett. Hekk tikseb mhux lista mhux maħduma imma sommarju qasir — l-ewwel taħbita tal-qalb ta' «Kotomarket».

Importanti: ma tistax tħallat fl-istess SELECT aggregat u kolonna ordinarja mingħajr GROUP BY. Il-mistoqsija SELECT status, COUNT(*) FROM orders taqa' bi żball — SQL mhux se jaqta' bl-amment liema status għandu juri ħdejn l-għadd ġenerali.

Kadett ħdejn holopanel: fluss ta' ringieli-kapsuli jiġi kkompressat f'tikka waħda titħabbat — ċifra waħda
Aggregat jikkompressa mijiet ta' ringieli f'taħbita waħda — numru li tista' tlissnu b'leħen għoli.

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

Например:

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
Funzjoni aggregata tikkompressa fluss ta' ringieli f'valur finali wieħed — ċifra waħda għas-sett kollu.

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'

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

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

Nisimgħu l-qalb tal-ħanut: ringiela waħda bis-sinjali vitali kollha tagħha — kemm-il ordni mħallsa, kemm xerrejja distinti hemm warajhom, kemm dħul u fejn huma l-limiti tal-ordni. ROUND(...,2) jħawwad il-medja sa żewġ ċifri deċimali.
Query result
всего_заказовпокупателейвыручкасредний_чекминимуммаксимум
86351381230.0016060.81890.0042110.00

COUNT(*) u COUNT(DISTINCT col) iwieġbu mistoqsijiet differenti. COUNT(*) jgħodd ir-ringieli, filwaqt li COUNT(DISTINCT user_id) jgħodd kemm xerrejja distinti hemm warajhom. Jekk hemm aktar ordnijiet milli xerrejja, l-arkivju mhux imħassar: xi ħadd reġa' lura u xtara mill-ġdid.

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

Mistoqsija tal-intervista: F'tabella hemm mitt ringiela, u fil-kolonna manager_id parti mill-valuri huma NULL. X'jirritornaw COUNT(*), COUNT(manager_id) u COUNT(DISTINCT manager_id), u għaliex in-numri huma differenti?

Tweġiba b'saħħitha: COUNT(*) jirritorna 100 — jgħodd ir-ringieli u ma jħarisx lejn in-NULL. COUNT(manager_id) jirritorna inqas: biss ir-ringieli fejn manager_id mimli, għax l-aggregati jinjoraw in-NULL. COUNT(DISTINCT manager_id) huwa inqas xorta jew daqshekk: l-għadd ta' valuri distinti mhux vojta. Dawn huma tliet mistoqsijiet differenti lid-dejta: «kemm-il ringiela», «f'kemm huwa mimli», «kemm distinti».

Mistoqsija tal-intervista: Għaliex SELECT status, COUNT(*) FROM orders mingħajr GROUP BY huwa żball?

Tweġiba b'saħħitha: L-aggregat jiġbor l-għażla f'ringiela waħda, filwaqt li status jibqa' b'ħafna valuri — f'ringieli differenti huwa differenti, u SQL ma jaqtax bl-amment liema valur juri. Jew tneħħi status, jew iżżid GROUP BY status, u l-għadd jingħadd separatament għal kull status.

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 некорректен?

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