Baza wiedzy / SQL
SELECT DISTINCT w SQL: unikalne wiersze, NULL i GROUP BY
Pokazuję, co dokładnie SQL uznaje za powtórzenie i dlaczego dodanie identyfikatora zmienia wynik. Każdy przykład ma małe dane i sprawdzalny rezultat.

Gdy potrzebuję listy różnych wartości, zaczynam od określenia, co ma oznaczać jeden wiersz wyniku. Unikalne imię, para imię i miasto oraz pełny rekord klienta to trzy różne zestawy. SELECT DISTINCT porównuje wszystkie wybrane wyrażenia razem. Nie wybiera automatycznie jednego klienta na imię ani nie naprawia identyfikacji osób.
Poniższe przykłady wykonano w SQLite 3.53.4 przez Node 24. Są samodzielnym ćwiczeniem na danych syntetycznych. Podstawową zasadę porównywania całych wierszy opisuje również dokumentacja PostgreSQL. Szczegóły typów i kolacji sprawdzaj dla używanego silnika; przykład SQLite nie jest testem każdego dialektu.
Przygotuj dane
W nowej, pustej bazie uruchom poniższy blok. Powtórzenia imion i miast są celowe. Nie zakładam, że dwie osoby z tym samym imieniem to jeden klient.
-- example: fixture
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT
);
INSERT INTO customers VALUES
(1, 'Ada', 'Warsaw'),
(2, 'Ben', 'Krakow'),
(3, 'Ada', 'Warsaw'),
(4, 'Ada', 'Krakow'),
(5, 'Celina', NULL),
(6, 'Celina', NULL),
(7, 'ada', 'Warsaw');
Tabela ma siedem rekordów. Kolumna id jest unikalna, a pozostałe wartości mogą się powtarzać. Używam angielskich nazw miast i prostych identyfikatorów, żeby ten sam kod działał w obu wersjach językowych materiału.
DISTINCT na jednej i wielu kolumnach
Najpierw wybieram różne imiona. Jawny ORDER BY odpowiada za kolejność; DISTINCT sam nie daje gwarancji sortowania.
-- example: names
SELECT DISTINCT name
FROM customers
ORDER BY name;
| name |
|---|
| Ada |
| Ben |
| Celina |
| ada |
Domyślna kolacja BINARY w tej tabeli rozróżnia Ada i ada. Teraz dodaję miasto:
-- example: pairs
SELECT DISTINCT name, city
FROM customers
ORDER BY name, city;
| name | city |
|---|---|
| Ada | Krakow |
| Ada | Warsaw |
| Ben | Krakow |
| Celina | NULL |
| ada | Warsaw |
Dwa źródłowe wiersze Ada/Warsaw dają jedną kombinację. Dwa wiersze Celina/NULL również dają jedną. Ada/Krakow zostaje osobno, bo różni się miastem. Wynik ma pięć wierszy. Jeśli dopiszesz do listy SELECT identyfikator, wszystkie siedem rekordów pozostanie różne. DISTINCT nie oznacza więc usunięcia powtórzeń z każdej kolumny niezależnie.
NULL i COUNT(DISTINCT) odpowiadają na różne pytania
W wyniku DISTINCT braki w porównywanych kombinacjach są traktowane jako równoważne na potrzeby eliminacji powtórzeń. Nie zmienia to zachowania zwykłego porównania NULL = NULL w warunku. Zasady DISTINCT opisuje SQLite SELECT.
-- example: cities
SELECT DISTINCT city
FROM customers
ORDER BY city;
| city |
|---|
| NULL |
| Krakow |
| Warsaw |
To trzy pozycje, z których jedna reprezentuje brak miasta. Funkcja agregująca działa inaczej:
-- example: city-counts
SELECT COUNT(DISTINCT city) AS known_cities,
(SELECT COUNT(*) FROM
(SELECT DISTINCT city FROM customers)
) AS city_options
FROM customers;
| known_cities | city_options |
|---|---|
| 2 | 3 |
COUNT(DISTINCT city) pomija NULL, więc liczy dwa znane miasta. Podzapytanie liczy wszystkie wiersze wyniku DISTINCT, włącznie z brakiem. To rozróżnienie wynika z reguł COUNT w SQLite. W raporcie nazwa miary powinna mówić, czy brak jest osobną kategorią, czy pozostaje poza licznikiem.

DISTINCT można łączyć z GROUP BY
Nie ma ogólnego zakazu używania obu konstrukcji. Mogę policzyć różne imiona wewnątrz każdego miasta:
-- example: grouped
SELECT city, COUNT(DISTINCT name) AS unique_names
FROM customers
GROUP BY city
ORDER BY city;
| city | unique_names |
|---|---|
| NULL | 1 |
| Krakow | 2 |
| Warsaw | 2 |
W Warszawie wynik obejmuje dwa zapisy imienia: Ada i ada. To liczba różnych wartości, a nie liczba jednoznacznie rozpoznanych osób. Takie ograniczenie interpretacji jest istotne nawet wtedy, gdy zapytanie działa poprawnie.
SELECT DISTINCT może też działać na wyniku grupowania:
-- example: group-sizes
SELECT DISTINCT COUNT(*) AS group_size
FROM customers
GROUP BY city
ORDER BY group_size;
| group_size |
|---|
| 2 |
| 3 |
Grupy miast mają rozmiary dwa, dwa i trzy. Ponieważ wybieram tylko rozmiar, DISTINCT usuwa powtórzoną dwójkę. Gdybym wybrał również miasto, wiersze grup byłyby już różne. Zanim połączysz obie konstrukcje, ustal, czy dodatkowe usuwanie powtórzeń ma znaczenie dla pytania.
Wielkość liter i reguły porównywania
Wynik zależy od kolacji, czyli reguł porównywania tekstu. W SQLite mogę jawnie porównać dwa ustawienia:
-- example: collations
SELECT COUNT(DISTINCT name COLLATE BINARY) AS binary_names,
COUNT(DISTINCT name COLLATE NOCASE) AS nocase_names
FROM customers;
| binary_names | nocase_names |
|---|---|
| 4 | 3 |
NOCASE łączy w tym przykładzie Ada i ada. Wbudowane NOCASE w SQLite obejmuje zmianę wielkości liter ASCII; nie należy z tego wyciągać wniosku o pełnym porównywaniu wszystkich liter Unicode. Szczegóły opisuje dokumentacja typów i kolacji. Nie usuwaj także spacji ani znaków diakrytycznych bez reguły uzgodnionej dla danych.
Nie ukrywaj DISTINCT-em błędnego JOIN
Jeżeli po połączeniu tabel liczba wierszy wzrosła, najpierw sprawdziłbym relację. Jeden klient może mieć kilka zamówień, więc powtórzenie jego nazwy bywa prawidłowe. Jeśli potrzebuję listy klientów mających zamówienie, DISTINCT może odpowiadać na to pytanie. Jeśli chcę policzyć wartość zamówień, ukrycie części wierszy może natomiast usunąć prawidłowe obserwacje.
Nie przyjmuję również zasady, że DISTINCT zawsze jest wolniejszy od GROUP BY. Sprawdzam znaczenie zapytania, plan wykonania i pomiar na właściwych danych. Indeksy, rozmiar wyniku i możliwości optymalizatora mają znaczenie. Samo zastąpienie jednego słowa kluczowego innym nie jest dowodem poprawy wydajności.
Ćwiczenie: określ, co liczysz
Na tych samych danych przygotuj listę znanych miast, czyli bez NULL. Powinny zostać Krakow i Warsaw. Następnie wybierz różne kombinacje id, name, city: otrzymasz siedem wierszy. Sprawdź też liczbę rekordów w tabeli po wszystkich zapytaniach. Nadal wynosi siedem, ponieważ SELECT nie usuwa źródłowych danych.
Jeśli potrzebujesz ograniczyć rozmiar uporządkowanej listy, przejdź do LIMIT i paginacji. Jeśli analizujesz, dlaczego połączenie tworzy wiele par, zobacz SELF JOIN. W każdym przypadku zacznij od zdania opisującego jeden oczekiwany wiersz wyniku.
Najważniejsze w pigułce
Czy DISTINCT usuwa rekordy z tabeli?
Nie. SELECT DISTINCT ogranicza powtórzenia w wyniku zapytania. Dane źródłowe pozostają bez zmian, a porównanie obejmuje wszystkie wybrane kolumny.
Czy DISTINCT można łączyć z GROUP BY?
Tak. COUNT(DISTINCT kolumna) liczy różne niepuste wartości wewnątrz grupy. SELECT DISTINCT po grupowaniu może dodatkowo usunąć identyczne wiersze wyniku, choć często jest zbędny.
Czy DISTINCT uwzględnia NULL?
SELECT DISTINCT może zwrócić jeden wiersz z NULL dla danej kombinacji kolumn. COUNT(DISTINCT kolumna) pomija NULL. To różne pytania i mogą dawać różne liczby.
Przy pracy nad artykułem użyto w zautomatyzowany sposób sztucznej inteligencji. Jeśli też chcesz tak umieć, sprawdź ten projekt.
