← Back to course

Funkcje agregujące: liczenie, sumowanie i znajdowanie przydatnych liczb

Funkcje agregujące: liczenie, sumowanie i znajdowanie przydatnych liczb

Witaj z powrotem.

W poprzedniej lekcji poznałeś JOIN.

Nauczyłeś się czytać dane z powiązanych tabel.

Bardzo ważna sprawa.

Teraz możemy zadawać PostgreSQL pytania typu:

Pokaż produkty z nazwami kategorii.
Pokaż zamówienia z nazwami klientów.
Pokaż posty z nazwami autorów.

Świetnie.

Ale czasem nie chcemy widzieć każdego wiersza.

Czasem chcemy dostać odpowiedź.

Na przykład:

Ile mamy produktów?
Jaka jest całkowita wartość wszystkich produktów?
Jaka jest średnia cena?
Który produkt jest najtańszy?
Który produkt jest najdroższy?
Ile produktów jest w każdej kategorii?

Tutaj pomagają funkcje agregujące.

Funkcje agregujące biorą wiele wierszy i zwracają jeden użyteczny wynik.

Bardzo grzecznie.

Bardzo produktywnie.

Jak zmęczony księgowy z supermocami.

Czego się nauczysz

W tej lekcji nauczysz się:

Pod koniec tej lekcji będziesz potrafił zadawać PostgreSQL praktyczne pytania biznesowe.

Nie tylko:

Pokaż mi wszystko.

Ale:

Podsumuj ten chaos i powiedz mi, co jest ważne.

To duży krok naprzód.

Baza danych przestaje być tylko pudełkiem na dane.

Zaczyna być asystentem, który umie liczyć.

A to jest przydatne.

Bo ludzie nie zawsze są dobrzy w liczeniu.

Szczególnie po czwartej kawie.

Przygotowanie bazy danych

Otwórz PostgreSQL:

sudo -iu postgres psql

Połącz się ze swoją bazą danych:

\c learning_postgresql

Jeśli baza nie istnieje, utwórz ją:

CREATE DATABASE learning_postgresql;

Potem połącz się z nią:

\c learning_postgresql

Teraz usuń stare tabele, jeśli istnieją:

DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS categories;

Usuwamy najpierw products, ponieważ ta tabela zależy od categories.

PostgreSQL pamięta relacje.

Czasem lepiej niż ludzie.

Tworzenie tabel

Utwórz tabelę categories:

CREATE TABLE categories (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) UNIQUE NOT NULL
);

Utwórz tabelę products:

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  price NUMERIC(10, 2) CHECK (price >= 0),
  quantity INTEGER CHECK (quantity >= 0),
  category_id INTEGER REFERENCES categories(id)
);

Ta tabela ma:

To już wygląda całkiem realistycznie.

Jeszcze nie pełny system sklepu.

Ale wystarczy, żeby PostgreSQL trochę popracował.

Zdrowo.

Edukacyjnie.

Bez narzekania na nadgodziny.

Dodawanie danych

Dodaj kategorie:

INSERT INTO categories (name)
VALUES
  ('Electronics'),
  ('Furniture'),
  ('Office'),
  ('Kitchen');

Dodaj produkty:

INSERT INTO products (name, price, quantity, category_id)
VALUES
  ('Laptop', 900.00, 5, 1),
  ('Mouse', 25.00, 30, 1),
  ('Keyboard', 70.00, 15, 1),
  ('Desk Chair', 150.00, 8, 2),
  ('Bookshelf', 120.00, 4, 2),
  ('Notebook', 5.00, 100, 3),
  ('Pen', 2.00, 200, 3),
  ('Whiteboard', 80.00, 3, 3);

Sprawdź dane:

SELECT * FROM categories;
SELECT * FROM products;

Zauważ jedną rzecz.

Kategoria Kitchen nie ma produktów.

To przyda się później.

Biedna Kitchen.

Czeka na łyżkę.

Czym są funkcje agregujące?

Funkcje agregujące obliczają jeden wynik na podstawie wielu wierszy.

Na przykład ta tabela ma wiele produktów.

Ale możemy zapytać:

Ile jest produktów?

PostgreSQL może zwrócić jedną liczbę.

To właśnie agregacja.

Najczęstsze funkcje agregujące to:

COUNT
SUM
AVG
MIN
MAX

Odpowiadają na pytania typu:

Ile?
Ile razem?
Jaka jest średnia?
Jaka jest najmniejsza wartość?
Jaka jest największa wartość?

To bardzo przydatne w dashboardach, raportach, panelach administracyjnych, API, fakturach, statystykach i narzędziach biznesowych.

Czyli wszędzie tam, gdzie ludzie chcą liczb.

A potem kłócą się o nie na spotkaniach.

Klasyka.

COUNT

COUNT liczy wiersze.

Wykonaj:

SELECT COUNT(*)
FROM products;

Wynik:

count
-----
8

COUNT(*) oznacza:

Policz wszystkie wiersze.

Nie interesuje go konkretna kolumna.

Po prostu liczy wiersze.

Prosto.

Mocno.

Bez dramatu.

COUNT z aliasem

Kolumna wyniku nazywa się count.

Może być.

Ale możemy zrobić to czytelniej:

SELECT COUNT(*) AS total_products
FROM products;

Wynik:

total_products
--------------
8

Dużo lepiej.

Aliasy sprawiają, że wyniki są łatwiejsze do zrozumienia.

Przyszły ty będzie wdzięczny.

Przyszły ty już jest zmęczony.

Pomóż mu.

COUNT dla kolumny

Możesz też policzyć konkretną kolumnę:

SELECT COUNT(category_id) AS products_with_category
FROM products;

To liczy wiersze, w których category_id nie jest NULL.

W naszych danych każdy produkt ma kategorię.

Więc wynik to:

products_with_category
----------------------
8

Ale jest tutaj ważna różnica.

COUNT(*) liczy wszystkie wiersze.

COUNT(column) liczy tylko te wiersze, w których dana kolumna nie jest NULL.

To ma ogromne znaczenie.

Bo NULL to nie zero.

NULL oznacza brak wartości albo wartość nieznaną.

PostgreSQL traktuje to ostrożnie.

I bardzo dobrze.

NULL nie jest liczbą.

To mały duch bazy danych.

Przykład z NULL

Dodaj produkt bez kategorii:

INSERT INTO products (name, price, quantity, category_id)
VALUES ('Mystery Box', 50.00, 2, NULL);

Teraz wykonaj:

SELECT COUNT(*) AS total_products
FROM products;

Otrzymasz:

total_products
--------------
9

Teraz wykonaj:

SELECT COUNT(category_id) AS products_with_category
FROM products;

Otrzymasz:

products_with_category
----------------------
8

Dlaczego?

Bo Mystery Box ma category_id = NULL.

Więc COUNT(category_id) go nie liczy.

To ważne.

Bardzo ważne.

Ważne jak baza danych.

Nie jak “kup mleko”.

Bardziej jak “dlaczego mój raport jest zły?” ważne.

SUM

SUM sumuje wartości.

Na przykład możemy obliczyć całkowitą liczbę sztuk w magazynie:

SELECT SUM(quantity) AS total_items
FROM products;

To sumuje wszystkie ilości.

Przykładowy wynik:

total_items
-----------
367

Możesz też obliczyć całkowitą wartość magazynu.

Dla każdego produktu potrzebujemy:

price * quantity

Potem sumujemy wynik:

SELECT SUM(price * quantity) AS total_stock_value
FROM products;

Przykładowy wynik:

total_stock_value
-----------------
8945.00

To jest użyteczne.

Teraz PostgreSQL nie tylko przechowuje dane.

On oblicza przydatne informacje.

Bardzo profesjonalnie.

Bardzo “chyba ktoś powinien wystawić fakturę”.

AVG

AVG oblicza średnią wartość.

Przykład:

SELECT AVG(price) AS average_price
FROM products;

PostgreSQL może zwrócić dużo cyfr po przecinku.

Żeby wynik był ładniejszy, użyj ROUND:

SELECT ROUND(AVG(price), 2) AS average_price
FROM products;

Przykładowy wynik:

average_price
-------------
155.78

Dokładna liczba zależy od twoich danych.

AVG jest przydatne przy pytaniach typu:

Jaka jest średnia cena produktu?
Jaka jest średnia wartość zamówienia?
Jaka jest średnia pensja?
Jaki jest średni poziom chaosu w tym projekcie?

PostgreSQL odpowie na pierwsze trzy.

Do czwartego potrzeba wsparcia emocjonalnego.

MIN

MIN znajduje najmniejszą wartość.

Przykład:

SELECT MIN(price) AS cheapest_price
FROM products;

Wynik:

cheapest_price
--------------
2.00

To mówi nam, jaka jest najniższa cena.

Ale nie mówi, który produkt ma tę cenę.

Do tego możemy użyć ORDER BY i LIMIT:

SELECT name, price
FROM products
ORDER BY price ASC
LIMIT 1;

Wynik:

name | price
-----|------
Pen  | 2.00

MIN daje minimalną wartość.

ORDER BY z LIMIT może pokazać cały wiersz.

Oba narzędzia są użyteczne.

Różne narzędzia.

Ta sama skrzynka.

MAX

MAX znajduje największą wartość.

Przykład:

SELECT MAX(price) AS highest_price
FROM products;

Wynik:

highest_price
-------------
900.00

Aby znaleźć najdroższy produkt:

SELECT name, price
FROM products
ORDER BY price DESC
LIMIT 1;

Wynik:

name   | price
-------|------
Laptop | 900.00

Bardzo dobrze.

PostgreSQL znalazł drogą bestię.

Prawdopodobnie z podświetleniem RGB.

Kilka agregacji w jednym zapytaniu

Możesz użyć kilku funkcji agregujących w jednym zapytaniu.

Przykład:

SELECT
  COUNT(*) AS total_products,
  SUM(quantity) AS total_items,
  ROUND(AVG(price), 2) AS average_price,
  MIN(price) AS cheapest_price,
  MAX(price) AS highest_price
FROM products;

To daje jeden wiersz podsumowania.

Przykład:

total_products | total_items | average_price | cheapest_price | highest_price
---------------|-------------|---------------|----------------|--------------
9              | 367         | 155.78        | 2.00           | 900.00

Idealne do dashboardów.

Jedno zapytanie.

Wiele przydatnych liczb.

PostgreSQL robi pracę biurową bez proszenia o krzesło.

Szacunek.

GROUP BY

Do tej pory funkcje agregujące dawały jeden wynik dla całej tabeli.

Ale często chcemy wyniki dla grup.

Na przykład:

Ile produktów jest w każdej kategorii?

Do tego używamy GROUP BY.

Przykład:

SELECT
  category_id,
  COUNT(*) AS products_count
FROM products
GROUP BY category_id;

Przykładowy wynik:

category_id | products_count
------------|---------------
1           | 3
2           | 2
3           | 3
            | 1

Pusta kategoria oznacza NULL.

To nasz Mystery Box.

Nie ma kategorii.

Bardzo tajemniczy.

Bardzo box.

GROUP BY z JOIN

category_id jest przydatne dla PostgreSQL.

Ale ludzie chcą nazw kategorii.

Więc używamy JOIN.

SELECT
  c.name AS category_name,
  COUNT(p.id) AS products_count
FROM categories AS c
JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name;

Wynik:

category_name | products_count
--------------|---------------
Electronics   | 3
Furniture     | 2
Office        | 3

Dużo ładniej.

Ale zauważ jedną rzecz.

Kitchen zniknęła.

Dlaczego?

Bo JOIN oznacza tutaj INNER JOIN.

Pojawiają się tylko pasujące wiersze.

Kitchen nie ma produktów.

Więc się nie pojawia.

Biedna Kitchen znowu.

GROUP BY z LEFT JOIN

Jeżeli chcemy pokazać wszystkie kategorie, nawet te bez produktów, używamy LEFT JOIN.

SELECT
  c.name AS category_name,
  COUNT(p.id) AS products_count
FROM categories AS c
LEFT JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name
ORDER BY c.name;

Wynik:

category_name | products_count
--------------|---------------
Electronics   | 3
Furniture     | 2
Kitchen       | 0
Office        | 3

Teraz Kitchen się pojawia.

Ważny szczegół:

Użyliśmy:

COUNT(p.id)

A nie:

COUNT(*)

Dlaczego?

Bo przy LEFT JOIN, COUNT(*) policzyłoby także wiersz kategorii, nawet jeśli nie ma produktu.

Dla kategorii bez produktów COUNT(*) mogłoby dać 1.

To byłoby błędne.

COUNT(p.id) liczy tylko prawdziwe produkty.

Ten szczegół ratuje raporty.

A czasem też pracę.

SUM z GROUP BY

Możemy obliczyć wartość magazynu dla każdej kategorii.

SELECT
  c.name AS category_name,
  SUM(p.price * p.quantity) AS stock_value
FROM categories AS c
LEFT JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name
ORDER BY stock_value DESC;

Przykładowy wynik:

category_name | stock_value
--------------|------------
Electronics   | 6750.00
Furniture     | 1680.00
Office        | 1140.00
Kitchen       |

Kitchen nie ma produktów.

Więc suma to NULL.

Jeśli chcemy pokazać 0 zamiast NULL, możemy użyć COALESCE.

SELECT
  c.name AS category_name,
  COALESCE(SUM(p.price * p.quantity), 0) AS stock_value
FROM categories AS c
LEFT JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name
ORDER BY stock_value DESC;

Teraz Kitchen pokazuje 0.

COALESCE zwraca pierwszą wartość, która nie jest NULL.

Czyli to:

COALESCE(SUM(p.price * p.quantity), 0)

oznacza:

Jeśli suma istnieje, pokaż ją.
Jeśli suma jest NULL, pokaż 0.

Bardzo użyteczne.

Bardzo praktyczne.

Bardzo “proszę nie zepsuć mojego dashboardu”.

AVG z GROUP BY

Możemy obliczyć średnią cenę produktu w każdej kategorii:

SELECT
  c.name AS category_name,
  ROUND(AVG(p.price), 2) AS average_price
FROM categories AS c
LEFT JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name
ORDER BY average_price DESC;

Przykładowy wynik:

category_name | average_price
--------------|---------------
Electronics   | 331.67
Furniture     | 135.00
Office        | 29.00
Kitchen       |

Znowu Kitchen nie ma produktów.

Więc średnia cena to NULL.

I to jest poprawne.

Nie można obliczyć średniej z braku wartości.

PostgreSQL odmawia wymyślania liczb.

Dobry database.

HAVING

WHERE filtruje wiersze przed grupowaniem.

HAVING filtruje grupy po grupowaniu.

To ważne.

Załóżmy, że chcemy zobaczyć kategorie z więcej niż dwoma produktami.

Źle:

SELECT
  category_id,
  COUNT(*) AS products_count
FROM products
WHERE COUNT(*) > 2
GROUP BY category_id;

To nie zadziała.

Dlaczego?

Bo WHERE nie może używać wyników agregacji.

Użyj HAVING:

SELECT
  category_id,
  COUNT(*) AS products_count
FROM products
GROUP BY category_id
HAVING COUNT(*) > 2;

Wynik:

category_id | products_count
------------|---------------
1           | 3
3           | 3

HAVING filtruje grupy.

Prosta idea.

Bardzo częste źródło błędów.

SQL lubi porządek.

Ludzie lubią zgadywać.

SQL wygrywa.

HAVING z JOIN

Teraz pokażmy nazwy kategorii.

SELECT
  c.name AS category_name,
  COUNT(p.id) AS products_count
FROM categories AS c
JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name
HAVING COUNT(p.id) > 2
ORDER BY products_count DESC;

Wynik:

category_name | products_count
--------------|---------------
Electronics   | 3
Office        | 3

To znaczy:

Pokaż tylko kategorie, które mają więcej niż dwa produkty.

To jest przydatne w raportach.

Na przykład:

Pokaż aktywne kategorie.
Pokaż klientów z więcej niż pięcioma zamówieniami.
Pokaż autorów z więcej niż dziesięcioma postami.
Pokaż produkty sprzedane więcej niż sto razy.

HAVING służy do filtrowania wyników agregacji.

Nie zapominaj o nim.

Czeka spokojnie.

Jak poważny bibliotekarz SQL.

WHERE i HAVING razem

Możesz użyć WHERE i HAVING w jednym zapytaniu.

Przykład:

SELECT
  c.name AS category_name,
  COUNT(p.id) AS products_count
FROM categories AS c
JOIN products AS p
ON c.id = p.category_id
WHERE p.price >= 10
GROUP BY c.name
HAVING COUNT(p.id) >= 2
ORDER BY products_count DESC;

To oznacza:

Najpierw zostaw tylko produkty z ceną co najmniej 10.
Potem pogrupuj je według kategorii.
Potem pokaż tylko kategorie z co najmniej dwoma produktami.

Kolejność ma znaczenie.

WHERE działa przed grupowaniem.

HAVING działa po grupowaniu.

Zapamiętaj to, a SQL będzie bolał mniej.

Nie nigdy.

Mniej.

Typowe błędy

Używanie WHERE zamiast HAVING

Źle:

SELECT category_id, COUNT(*)
FROM products
WHERE COUNT(*) > 2
GROUP BY category_id;

Dobrze:

SELECT category_id, COUNT(*)
FROM products
GROUP BY category_id
HAVING COUNT(*) > 2;

Używaj WHERE dla zwykłych wierszy.

Używaj HAVING dla grup.

Zapomnienie o GROUP BY

Źle:

SELECT category_id, COUNT(*)
FROM products;

PostgreSQL będzie narzekał, bo category_id nie jest ani agregowane, ani grupowane.

Dobrze:

SELECT category_id, COUNT(*)
FROM products
GROUP BY category_id;

Jeśli wybierasz zwykłą kolumnę razem z funkcją agregującą, zwykle potrzebujesz GROUP BY.

PostgreSQL jest surowy.

Ale sprawiedliwy.

Przeważnie.

Liczenie złej rzeczy przy LEFT JOIN

Źle dla liczenia produktów w kategorii:

SELECT
  c.name,
  COUNT(*) AS products_count
FROM categories AS c
LEFT JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name;

To może policzyć kategorię bez produktów jako 1.

Lepiej:

SELECT
  c.name,
  COUNT(p.id) AS products_count
FROM categories AS c
LEFT JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name;

Licz kolumnę z tej tabeli, którą naprawdę chcesz policzyć.

Mały szczegół.

Duża różnica.

Klasyczna pułapka baz danych.

Praktyka

Policz wszystkie produkty:

SELECT COUNT(*) AS total_products
FROM products;

Oblicz całkowitą liczbę sztuk:

SELECT SUM(quantity) AS total_items
FROM products;

Oblicz całkowitą wartość magazynu:

SELECT SUM(price * quantity) AS total_stock_value
FROM products;

Znajdź średnią cenę:

SELECT ROUND(AVG(price), 2) AS average_price
FROM products;

Znajdź najniższą i najwyższą cenę:

SELECT
  MIN(price) AS cheapest_price,
  MAX(price) AS highest_price
FROM products;

Policz produkty według kategorii:

SELECT
  c.name AS category_name,
  COUNT(p.id) AS products_count
FROM categories AS c
LEFT JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name
ORDER BY c.name;

Oblicz wartość magazynu według kategorii:

SELECT
  c.name AS category_name,
  COALESCE(SUM(p.price * p.quantity), 0) AS stock_value
FROM categories AS c
LEFT JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name
ORDER BY stock_value DESC;

Pokaż tylko kategorie z więcej niż dwoma produktami:

SELECT
  c.name AS category_name,
  COUNT(p.id) AS products_count
FROM categories AS c
JOIN products AS p
ON c.id = p.category_id
GROUP BY c.name
HAVING COUNT(p.id) > 2;

Uruchamiaj zapytania.

Zmieniaj liczby.

Dodawaj produkty.

Usuwaj produkty.

Patrz, jak zmieniają się wyniki.

To jest ta zabawna część.

No dobrze.

Zabawna jak na bazę danych.

Inny gatunek zabawy.

Mini wyzwanie

Utwórz dwie tabele:

customers
orders

Zasady:

Utwórz tabele:

DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);
CREATE TABLE orders (
  id SERIAL PRIMARY KEY,
  total NUMERIC(10, 2) CHECK (total >= 0),
  customer_id INTEGER REFERENCES customers(id)
);

Dodaj dane:

INSERT INTO customers (name)
VALUES
  ('Anna'),
  ('Marco'),
  ('Sofia'),
  ('Luca');
INSERT INTO orders (total, customer_id)
VALUES
  (49.99, 1),
  (120.00, 1),
  (35.50, 2),
  (200.00, 2),
  (15.00, 2),
  (80.00, 3);

Teraz napisz zapytania, które:

Podpowiedź dla klientów bez zamówień:

SELECT
  c.name AS customer_name,
  COUNT(o.id) AS orders_count
FROM customers AS c
LEFT JOIN orders AS o
ON c.id = o.customer_id
GROUP BY c.name
HAVING COUNT(o.id) = 0;

To jest SQL z prawdziwego świata.

Klienci.

Zamówienia.

Sumy.

Raporty.

Dokładnie takie rzeczy, do których bazy danych zostały stworzone.

Jak arkusze kalkulacyjne.

Ale z większą dyscypliną.

I bez tajemniczych scalonych komórek.

Podsumowanie

Dzisiaj nauczyłeś się:

To bardzo ważna lekcja.

Teraz potrafisz analizować dane.

Nie tylko je czytać.

Możesz tworzyć raporty.

Możesz tworzyć dashboardy.

Możesz odpowiadać na pytania biznesowe.

Możesz sprawić, że PostgreSQL będzie liczył rzeczy, podczas gdy ty pijesz kawę.

To jest cywilizacja.

Następna lekcja

W następnej lekcji poznamy indeksy i podstawy wydajności.

Zobaczymy, dlaczego niektóre zapytania stają się wolne.

Nauczymy się, jak indeksy pomagają PostgreSQL szybciej znajdować dane.

Bo kiedy baza danych ma dziesięć wierszy, wszystko jest szybkie.

Kiedy ma dziesięć milionów wierszy, PostgreSQL zaczyna zadawać poważne pytania.

I ty też powinieneś.