COUNT, SUM, AVG: οι γραμμές γίνονται αριθμοί
What you'll learn
- να γράφεις ερωτήματα με
COUNT,SUM,AVG,MINκαιMAXπου συμπυκνώνουν ένα σύνολο γραμμών σε μία τελική σύνοψη - να συνδυάζεις το
WHEREμε συναρτήσεις συσσώρευσης, ώστε να υπολογίζεις το σύνολο μόνο για τις γραμμές που σε ενδιαφέρουν — για παράδειγμα, για τις πληρωμένες παραγγελίες - να ξεχωρίζεις τα
COUNT(*),COUNT(col)καιCOUNT(DISTINCT col)και να εξηγείς γιατί στα ίδια δεδομένα δίνουν τρεις διαφορετικούς αριθμούς - να εξηγείς γιατί το
SELECT status, COUNT(*)χωρίςGROUP BYαποτυγχάνει με σφάλμα
Κοιτάμε λίγο μπροστά. Σε αυτό το κεφάλαιο μερικά παραδείγματα συγκολλούν δύο πίνακες μέσω JOIN — αυτό είναι το θέμα του Κεφαλαίου 4, όπου θα το αναλύσουμε διεξοδικά. Προς το παρόν, διάβασε μια γραμμή όπως orders o JOIN order_items oi ON oi.order_id = o.id απλώς ως «κολλήσαμε τις γραμμές δύο πινάκων με βάση ένα κλειδί». Τώρα σημασία έχουν οι ίδιες οι συναρτήσεις συσσώρευσης, όχι το JOIN.
Κεφάλαιο 3 — «Ο σφυγμός του καταστήματος»
Η διαπίστευση «Αναλυτή» κάθεται πάνω στο πάσο σου σαν μια ζεστή κεχριμπαρένια λωρίδα. Το κρυπτογραφημένο μπλοκ του K. δεν άνοιξε ποτέ τελικά, μα τον τίτλο του τον ξέρεις απέξω: «Οι ωμές γραμμές είναι θόρυβος. Το νόημα εμφανίζεται μόλις μάθεις να τις αθροίζεις». Ο QUERY σβήνει τα περιττά ολοπάνελ και αφήνει ένα — άδειο, με μια μοναχική κουκκίδα να πάλλεται στο κέντρο του.
QUERY: Διάβαζες το αρχείο γραμμή προς γραμμή. Φτάνει. Σήμερα θα ακούσουμε την καρδιά του.
Συναρτήσεις συσσώρευσης: πολλές γραμμές → ένας αριθμός
Ένα απλό SELECT δείχνει τις γραμμές όπως είναι: παραγγελία προς παραγγελία, προϊόν προς προϊόν. Μια συνάρτηση συσσώρευσης συμπιέζει ένα σύνολο γραμμών σε ένα μέγεθος που μπορείς πλέον να το πεις δυνατά: πόσες παραγγελίες, πόσα έσοδα, ποια η μέση αξία παραγγελίας. Για έναν αρχειοθέτη αυτός ακριβώς είναι ο σφυγμός: ένας αριθμός μέσα στον οποίο ακούγεται όλη η ροή.
Τα πέντε άλογα κούρσας:
| Συνάρτηση | Τι υπολογίζει |
|---|---|
COUNT(*) | το πλήθος των γραμμών, π.χ. παραγγελιών |
SUM(x) | το άθροισμα των τιμών μιας στήλης |
AVG(x) | τη μέση τιμή |
MIN(x) | το ελάχιστο |
MAX(x) | το μέγιστο |
Αν στο SELECT δεν υπάρχει GROUP BY, η συνάρτηση συσσώρευσης συμπτύσσει ολόκληρο τον πίνακα (ή ό,τι απομένει μετά το WHERE) σε μία γραμμή.
SELECT COUNT(*) AS заказов, SUM(total_amount) AS выручка
FROM orders
WHERE status = 'paid';
Εδώ πρώτα το WHERE αφήνει μόνο τις πληρωμένες παραγγελίες, και έπειτα οι συναρτήσεις συσσώρευσης υπολογίζουν το σύνολο πάνω σε αυτό το σύνολο. Έτσι παίρνεις όχι μια ωμή λίστα, αλλά μια σύντομη σύνοψη — τον πρώτο χτύπο του σφυγμού του Kotomarket.
Σημαντικό: δεν μπορείς να ανακατεύεις στο ίδιο SELECT μια συνάρτηση συσσώρευσης και μια απλή στήλη χωρίς GROUP BY. Το ερώτημα SELECT status, COUNT(*) FROM orders θα αποτύχει με σφάλμα — η SQL δεν θα μαντέψει ποιο status να δείξει δίπλα στο συνολικό πλήθος.

До этого момента ты в основном доставал строки.
Например:
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) στρογγυλοποιεί τον μέσο όρο σε δύο δεκαδικά ψηφία.| всего_заказов | покупателей | выручка | средний_чек | минимум | максимум |
|---|---|---|---|---|---|
| 86 | 35 | 1381230.00 | 16060.81 | 890.00 | 42110.00 |
Τα COUNT(*) και COUNT(DISTINCT col) απαντούν σε διαφορετικά ερωτήματα. Το COUNT(*) μετράει γραμμές, ενώ το COUNT(DISTINCT user_id) μετράει πόσοι διαφορετικοί αγοραστές βρίσκονται πίσω τους. Αν οι παραγγελίες είναι περισσότερες από τους αγοραστές, δεν είναι βλάβη του αρχείου: κάποιος ξαναγύρισε και αγόρασε ξανά.
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
Ερώτηση συνέντευξης: Σε έναν πίνακα υπάρχουν εκατό γραμμές, και στη στήλη manager_id ένα μέρος των τιμών είναι NULL. Τι θα επιστρέψουν τα COUNT(*), COUNT(manager_id) και COUNT(DISTINCT manager_id) και γιατί οι αριθμοί διαφέρουν;
Δυνατή απάντηση: Το COUNT(*) θα επιστρέψει 100 — μετράει γραμμές και δεν κοιτάει τα NULL. Το COUNT(manager_id) θα επιστρέψει λιγότερα: μόνο τις γραμμές με συμπληρωμένο manager_id, επειδή οι συναρτήσεις συσσώρευσης αγνοούν το NULL. Το COUNT(DISTINCT manager_id) — ακόμη λιγότερα ή τόσα: το πλήθος των διαφορετικών μη κενών τιμών. Αυτά είναι τρία διαφορετικά ερωτήματα προς τα δεδομένα: «πόσες γραμμές», «σε πόσες είναι συμπληρωμένο», «πόσες διαφορετικές».
Ερώτηση συνέντευξης: Γιατί το SELECT status, COUNT(*) FROM orders χωρίς GROUP BY είναι σφάλμα;
Δυνατή απάντηση: Η συνάρτηση συσσώρευσης συμπτύσσει τη συλλογή σε μία γραμμή, ενώ το status παραμένει πολλαπλών τιμών — διαφέρει από γραμμή σε γραμμή, και η SQL δεν μαντεύει ποια τιμή να δείξει. Είτε αφαίρεσε το status, είτε πρόσθεσε GROUP BY status, και ο μετρητής θα υπολογιστεί ξεχωριστά για κάθε κατάσταση.
SELECT AVG(price) FROM products;, если GROUP BY не указан?COUNT(*)?COUNT(user_id) отличается от COUNT(DISTINCT user_id)?SELECT status, COUNT(*) FROM orders; без GROUP BY некорректен?КВЕРИ: Сырые строки показывают события. Агрегаты показывают пульс: сколько, на какую сумму, в каких границах и с каким средним ритмом.