Raport pokazuje 240 zamówień, choć część z nich nie ma przypisanego klienta. Jeśli używasz w SQL funkcji COUNT, wynik zależy od tego, czy liczysz wiersze, czy niepuste wartości w konkretnej kolumnie, a po złączeniu tabel dochodzi jeszcze jeden haczyk.

Poprawny COUNT zaczyna się od ustalenia, co ma być policzone
COUNT(*)zlicza wiersze w wyniku zapytania.COUNT(kolumna)pomija wiersze, w których kolumna ma wartośćNULL.COUNT(DISTINCT kolumna)zlicza unikalne, niepuste wartości.LEFT JOINmoże zwrócić zero dla niepasującego rekordu po prawej stronie, jeśli liczysz jego kolumnę.COUNT(*) OVER (...)podaje licznik przy każdym wierszu, zamiast scalać wynik w jeden wiersz.
COUNT(*) liczy wiersze, a COUNT(kolumna) pomija NULL
W tabeli zamówień każde zamówienie powinno zwiększać licznik, nawet jeśli pole customer_id jest puste. Do takiego pytania służy COUNT(*); zapis z nazwą kolumny odpowiada na inne pytanie.
SELECT
COUNT(*) AS liczba_zamowien,
COUNT(customer_id) AS zamowienia_z_klientem
FROM orders;Jeśli tabela zawiera 240 wierszy, a w 12 z nich customer_id ma wartość NULL, pierwsza agregacja zwróci 240, a druga 228. Dokumentacja PostgreSQL opisuje tę samą zasadę: COUNT(*) zlicza wiersze wejściowe, zaś COUNT(expression) te, w których wyrażenie nie jest puste.
Nie podstawiaj nazwy kolumny tylko dlatego, że wygląda na identyfikator rekordu. Jeśli identyfikator może być pusty, wynik będzie niższy od liczby wierszy. Przy kolumnie z ograniczeniem NOT NULL wyniki mogą się zrównać, ale intencja zapytania nadal jest czytelniejsza przy COUNT(*).
LEFT JOIN potrafi zaniżyć liczbę rekordów albo zawyżyć liczbę zamówień
Przy złączeniu tabel wynik nie zawsze odpowiada liczbie rekordów z tabeli głównej. LEFT JOIN zachowuje zamówienie bez pasującego klienta, ale kolumny klienta w tym wierszu mają NULL, więc COUNT(customers.id) go nie uwzględni.
SELECT
COUNT(*) AS wszystkie_zamowienia,
COUNT(customers.id) AS zamowienia_z_dopasowanym_klientem
FROM orders
LEFT JOIN customers
ON customers.id = orders.customer_id;Jest też pułapka w drugą stronę. Jeśli jedno zamówienie ma kilka pozycji, złączenie z tabelą pozycji powieli wiersz zamówienia; wtedy COUNT(*) policzy pozycje, a nie zamówienia.
| Sytuacja | Co może oznaczać wynik | Co zrobić |
|---|---|---|
COUNT(p.id) po LEFT JOIN daje mniej niż COUNT(*) |
Brak dopasowanego rekordu lub wartość NULL w liczonej kolumnie. |
Wybierz COUNT(*), jeśli liczysz rekordy z tabeli głównej. |
| Wynik rośnie po dołączeniu tabeli szczegółów | Jeden rekord główny łączy się z wieloma rekordami szczegółowymi. | Policz unikalny identyfikator główny: COUNT(DISTINCT orders.id). |
| Potrzebujesz liczby powiązanych pozycji dla każdego zamówienia | Liczone mają być wiersze tabeli szczegółów. | Grupuj po zamówieniu i zliczaj identyfikator pozycji. |
Przykład: zamówienie o identyfikatorze 501 ma trzy pozycje. Po złączeniu pojawią się trzy wiersze z tym samym ID, zatem zwykły licznik zwróci 3 dla tego zamówienia, a COUNT(DISTINCT orders.id) zachowa wynik 1 dla całego zestawu. Uważam, że przy raportach obejmujących kilka tabel najpierw trzeba nazwać liczony obiekt, a dopiero potem wybierać wariant funkcji.
GROUP BY daje osobny licznik dla każdej grupy
Gdy potrzebujesz liczby zamówień na klienta, GROUP BY dzieli wiersze na grupy, a agregacja liczy każdą z nich osobno. To zmienia kształt odpowiedzi: zamiast jednego wyniku dostajesz wiersz dla każdej wartości grupującej.
SELECT customer_id, COUNT(*) AS liczba_zamowien
FROM orders
GROUP BY customer_id
ORDER BY liczba_zamowien DESC;Klienci bez zamówień nie pojawią się w takim wyniku, bo zapytanie zaczyna od tabeli orders. Jeśli raport ma pokazać również klientów z zerową liczbą zamówień, zacznij od tabeli klientów i użyj LEFT JOIN; wtedy zliczaj identyfikator zamówienia, nie *.
SELECT c.id, COUNT(o.id) AS liczba_zamowien
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
GROUP BY c.id;Dla klienta bez zamówień złączenie tworzy jeden wiersz z pustymi polami tabeli orders. Dlatego COUNT(o.id) zwraca zero, a COUNT(*) zwróciłby jeden. Ta drobna różnica często decyduje o tym, czy zestawienie pokazuje prawdziwe zera.
COUNT(DISTINCT) usuwa duplikaty, ale nie naprawia każdego złączenia
Raport z tabeli połączonej z pozycjami potrafi policzyć klienta wielokrotnie. COUNT(DISTINCT customer_id) zlicza unikalne, niepuste identyfikatory, więc bywa dobrym wyborem przy pytaniu „ilu różnych klientów złożyło zamówienie?”.
SELECT COUNT(DISTINCT customer_id) AS aktywni_klienci
FROM orders
WHERE status = 'paid';Jednak DISTINCT nie jest plastrem na każdą niepoprawną agregację. Jeśli chcesz zsumować wartość zamówień po złączeniu z kilkoma tabelami szczegółowymi, deduplikowanie sumy może ukryć problem zamiast go rozwiązać. W takiej sytuacji policz lub zagreguj tabele szczegółowe osobno, a potem połącz gotowe wyniki.
Jeśli potrzebujesz tylko licznika przy każdym wierszu, bez zwijania szczegółów, użyj funkcji okna. Na przykład COUNT(*) OVER (PARTITION BY customer_id) dopisze liczbę zamówień klienta do każdego wiersza, zachowując szczegółowe rekordy. Obsługa funkcji agregujących jako okiennych zależy od systemu bazodanowego i składni konkretnego silnika, więc sprawdź dokumentację używanej bazy.
COUNT(1) nie jest magicznym sposobem na szybsze liczenie
W sieci wraca rada, by zamienić COUNT(*) na COUNT(1), bo gwiazdka rzekomo każe bazie odczytać wszystkie kolumny. To mylący skrót myślowy: COUNT(*) oznacza liczenie wierszy, a nie zwracanie ich wszystkich. Nie zakładaj, że inny zapis przyspieszy zapytanie; plan wykonania i zachowanie optymalizatora zależą od silnika oraz warunków zapytania.
Dokładne policzenie całej tabeli może wymagać pracy proporcjonalnej do jej rozmiaru. Dokumentacja MySQL 8.4 wyjaśnia, że w InnoDB wynik zależy od wierszy widocznych dla bieżącej transakcji, a silnik odczytuje dostępny indeks wtórny albo indeks klastrowany. Dla dużej tabeli regularne odpytywanie o dokładny licznik może więc kosztować więcej, niż sugeruje krótki zapis SQL.
Jeśli działasz na SQL Serverze i licznik może przekroczyć zakres typu int, sprawdź typ wyniku. Według dokumentacji Microsoftu dla COUNT_BIG, COUNT zwraca int, a COUNT_BIG zwraca bigint. W aplikacji raportującej miliardy zdarzeń wybór COUNT_BIG(*) usuwa ryzyko przepełnienia po stronie tej funkcji.
Najpierw ustal, co ma oznaczać liczba
Przed uruchomieniem zapytania dokończ zdanie: „chcę policzyć liczbę…”. Jeśli odpowiedzią są wiersze, zacznij od COUNT(*); jeśli niepuste pola, użyj kolumny; jeśli różne osoby lub obiekty, rozważ DISTINCT. Przy złączeniach sprawdź, czy jeden rekord źródłowy nie rozbija się na kilka wynikowych.
To proste doprecyzowanie oszczędza poprawiania raportu po fakcie. Licznik w SQL jest krótki, ale jego znaczenie ustala wybór wiersza, kolumny, grupy i relacji między tabelami.
