DISTINCT to modyfikator agregujący dane w wyniku zapytania – zwraca tylko unikalne wartości, bez powtórzeń. Najlepiej zobaczyć to na przykładzie:

mysql> SELECT text1 FROM tab2;
+-------+
| text1 |
+-------+
| w     |
| e     |
| e     |
| e     |
| f     |
+-------+
5rows in set (0.00 sec)

mysql> SELECT DISTINCT text1 FROM tab2;
+-------+
| text1 |
+-------+
| w     |
| e     |
| f     |
+-------+
3 rows in set (0.00 sec)

Jak widać, w pierwszym zestawie wyników trzykrotnie występuje wartość ‚e’. Po zastosowaniu DISTINCT otrzymaliśmy zagregowane wyniki, powtórzenia zostały usunięte. Generalnie stosowanie agregacji wiąże się z koniecznością utworzenia tabeli tymczasowej.

mysql> EXPLAIN SELECT DISTINCT text1 FROM tab2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tab2
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
Extra: Using temporary
1 row in set (0.00 sec)

Administratorzy MySQL nie przepadają za tabelami tymczasowymi, bo stanowią one skazę na idealnym pięknie, jakim jest szybko i wydajnie działająca baza danych. Konkretnie mówiąc, potrafią one zmniejszyć tempo działania bazy z galopu, do czołgania się. Dzieje się tak szczególnie wtedy, gdy w zestawie wyników pojawi się jakaś kolumna typu BLOB czy TEXT i w efekcie tabela tworzona jest na dysku. Jak więc pozbyć się tabel tymczasowych z SELECT DISTINCT?

Mamy dwie opcje. Pierwszą z nich jest zastosowanie modyfikatora SQL_BIG_RESULT, który skutkuje tym, że MySQL preferuje sortowanie zamiast tworzenia tabeli tymczasowej z indeksem na kolumnę po której wykonywane jest grupowanie. Tak na marginesie i w ramach ciekawostki – tabela tymczasowa tworzona na potrzeby grupowania wyników tworzona jest z indeksem.  Jest to jeden z niewielu przypadków, w których MySQL tworzy indeksowane tabele tymczasowe. Drugą opcją jest nałożenie indeksu na kolumnę (lub kolumny), po których agregujemy. Efekt tych operacji wygląda na przykład tak:

mysql> EXPLAIN SELECT SQL_BIG_RESULT DISTINCT text1 FROM tab2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tab2
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
Extra:
1 row in set (0.00 sec)

mysql> ALTER TABLE tab2 ADD INDEX idx_text1 (text1);
Query OK, 5 rows affected (0.01 sec)
Rekordów: 5  Duplikatów: 0  Ostrzeżeń: 0

mysql> EXPLAIN SELECT DISTINCT text1 FROM tab2\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tab2
type: range
possible_keys: NULL
key: idx_text1
key_len: 53
ref: NULL
rows: 6
Extra: Using index for group-by
1 row in set (0.00 sec)

Jak widać, w obu przypadkach nie ma tworzonej tabeli tymczasowej, w drugim przypadku stosowany jest indeks. W polu Extra mamy opis – „Using index for group-by”. O co chodzi, skoro używamy DISTINCT? Popatrzmy:

mysql> SELECT id2, text1 FROM tab2 GROUP BY id2, text1;
+-----+-------+
| id2 | text1 |
+-----+-------+
|   2 | w     |
|   3 | e     |
|   4 | e     |
|   5 | f     |
+-----+-------+
4 rows in set (0.00 sec)

mysql> SELECT DISTINCT id2, text1 FROM tab2;
+-----+-------+
| id2 | text1 |
+-----+-------+
|   2 | w     |
|   3 | e     |
|   4 | e     |
|   5 | f     |
+-----+-------+
4 rows in set (0.00 sec)

Jak widać, wyniki zwrócone przez te dwa zapytania są identyczne. Dociekliwych zapraszam do lektury kodu źródłowego. W pliku sql_select.cc można znaleźć fragment kodu, który w pewnych sytuacjach po prostu przepisuje zapytanie usuwając DISTINCT a dodając GROUP BY. Wracamy jednak do tematu.
Co zrobić, jeśli chcemy wykorzystać indeks, ale w zapytaniu występują jakieś dodatkowe warunki? Przykładowo:

mysql> EXPLAIN SELECT DISTINCT text1 FROM tab2 WHERE id2=4\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tab2
type: ref
possible_keys: idx_id2_text1
key: idx_id2_text1
key_len: 4
ref: const
rows: 1
Extra: Using where; Using index
1 row in set (0.00 sec)

Rozwiązaniem jest dodanie indeksu, który obejmie kolumny pojawiające się w WHERE, a także kolumny, które agregujemy:

mysql> ALTER TABLE tab2 drop INDEX idx_id2_text1;
Query OK, 5 rows affected (0.02 sec)
Rekordów: 5  Duplikatów: 0  Ostrzeżeń: 0

mysql> ALTER TABLE tab2 ADD INDEX idx_id2_text1 (id2, text1);
Query OK, 5 rows affected (0.00 sec)
Rekordów: 5  Duplikatów: 0  Ostrzeżeń: 0

mysql> EXPLAIN SELECT DISTINCT text1 FROM tab2 WHERE id2=4\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tab2
type: ref
possible_keys: idx_id2_text1
key: idx_id2_text1
key_len: 4
ref: const
rows: 1
Extra: Using where; Using index
1 row in set (0.00 sec)

W przypadku stosowania SQL_BIG_RESULT nałożenie indeksu nie będzie konieczne, ale trzeba pamiętać, że sortowanie dużej ilości rekordów może się okazać wolniejsze, niż wykorzystanie indeksu do grupowania. Oczywiście, należałoby tą teorię potwierdzić w testach, ale to już zostawię zainteresowanym Czytelnikom. Wiemy przecież, że wszelkie tego typu modyfikacje zapytań trzeba wcześniej dokładnie przetestować, żeby nie okazało się, że wniosły więcej problemów niż jest z nich pożytku. W następnym poście zobaczymy jak to wygląda, gdy wykonujemy SELECT DISTINCT na JOINie dwóch tabel.