Czy rzeczywiście konieczne jest zaindeksowanie wszystkich wybranych kolumn, aby MySQL mógł użyć indeksu?
To jest załadowane pytanie, ponieważ istnieją czynniki, które określają, czy warto korzystać z indeksu.
CZYNNIK 1
Jaka jest kluczowa populacja dla danego indeksu? Innymi słowy, jaka jest liczność (odrębna liczba) wszystkich krotek zarejestrowanych w indeksie?
CZYNNIK 2
Jakiego silnika pamięci używasz? Czy wszystkie potrzebne kolumny są dostępne z indeksu?
CO DALEJ ???
Weźmy prosty przykład: tabela, która zawiera dwie wartości (mężczyzna i kobieta)
Utwórzmy taką tabelę z testem użycia indeksu
USE test
DROP TABLE IF EXISTS mf;
CREATE TABLE mf
(
id int not null auto_increment,
gender char(1),
primary key (id),
key (gender)
) ENGINE=InnODB;
INSERT INTO mf (gender) VALUES
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
ANALYZE TABLE mf;
EXPLAIN SELECT gender FROM mf WHERE gender='F';
EXPLAIN SELECT gender FROM mf WHERE gender='M';
EXPLAIN SELECT id FROM mf WHERE gender='F';
EXPLAIN SELECT id FROM mf WHERE gender='M';
TEST InnoDB
mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE mf
-> (
-> id int not null auto_increment,
-> gender char(1),
-> primary key (id),
-> key (gender)
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.07 sec)
mysql> INSERT INTO mf (gender) VALUES
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.06 sec)
Records: 40 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status | OK |
+---------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql>
TEST MyISAM
mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE mf
-> (
-> id int not null auto_increment,
-> gender char(1),
-> primary key (id),
-> key (gender)
-> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.05 sec)
mysql> INSERT INTO mf (gender) VALUES
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.00 sec)
Records: 40 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status | OK |
+---------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 36 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | mf | ALL | gender | NULL | NULL | NULL | 40 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
mysql>
Analiza dla InnoDB
Gdy dane zostały załadowane jako InnoDB, należy pamiętać, że wszystkie cztery EXPLAIN
plany korzystały z gender
indeksu. Trzeci i czwarty EXPLAIN
plan wykorzystywały gender
indeks, mimo że żądane dane były id
. Dlaczego? Ponieważ id
znajduje się w nim, PRIMARY KEY
a wszystkie indeksy wtórne mają wskaźniki odniesienia z powrotem do PRIMARY KEY
(za pośrednictwem ind_klust_genu ).
Analiza dla MyISAM
Gdy dane zostały załadowane jako MyISAM, pamiętaj, że pierwsze trzy EXPLAIN
plany korzystały z gender
indeksu. W czwartym EXPLAIN
planie Optymalizator zapytań postanowił w ogóle nie używać indeksu. Zamiast tego wybrał pełny skan tabeli. Dlaczego?
Niezależnie od DBMS, Optymalizatory Zapytań działają na bardzo prostej zasadzie: jeśli indeks jest sprawdzany jako kandydat do użycia w celu przeprowadzenia wyszukiwania, a Optymalizator Zapytań oblicza, że musi on wyszukać więcej niż 5% całkowitej liczby wiersze w tabeli:
- pełne skanowanie indeksu jest wykonywane, jeśli wszystkie potrzebne kolumny do pobrania znajdują się w wybranym indeksie
- w przeciwnym razie pełny skan tabeli
WNIOSEK
Jeśli nie masz odpowiednich indeksów pokrycia lub jeśli kluczowa populacja dla dowolnej krotki wynosi więcej niż 5% tabeli, musi się zdarzyć sześć rzeczy:
- Przekonaj się, że musisz profilować zapytania
- Znajdź wszystko
WHERE
, GROUP BY
i porządek BY` klauzule z tych zapytań
- Sformułuj indeksy w tej kolejności
WHERE
kolumny klauzul o wartościach statycznych
GROUP BY
kolumny
ORDER BY
kolumny
- Unikaj pełnego skanowania tabeli (zapytania pozbawione sensownej
WHERE
klauzuli)
- Unikaj populacji złych kluczy (lub przynajmniej buforuj te populacje złych kluczy)
- Wybierz najlepszy silnik pamięci MySQL ( InnoDB lub MyISAM ) dla tabel
W przeszłości pisałem o tej ogólnej zasadzie 5%:
AKTUALIZACJA 14.11.2012 13:05 EDT
Spojrzałem na twoje pytanie i na oryginalny post SO . Potem pomyślałem o mojej Analysis for InnoDB
wcześniej wspomnianej. To pokrywa się ze person
stołem. Dlaczego?
Dla obu tabel mf
iperson
- Silnik pamięci to InnoDB
- Klucz podstawowy to
id
- Dostęp do tabeli odbywa się poprzez indeks wtórny
- Gdyby tabelą był MyISAM, zobaczylibyśmy zupełnie inny
EXPLAIN
plan
Teraz spojrzeć na zapytania z pytaniem SO: select * from person order by age\G
. Ponieważ nie ma WHERE
klauzuli, wyraźnie zażądałeś pełnego skanowania tabeli . Domyślna kolejność sortowania tabeli byłaby według id
(KLUCZ PODSTAWOWY) ze względu na jej auto_increment, a gen_clust_index (aka Clustered Index) jest uporządkowany według wewnętrznego rowid . Po zamówieniu według indeksu należy pamiętać, że indeksy wtórne InnoDB mają identyfikator rowid dołączony do każdej pozycji indeksu. To powoduje wewnętrzną potrzebę pełnego dostępu do wiersza za każdym razem.
Konfigurowanie ORDER BY
tabeli InnoDB może być dość zniechęcającym zadaniem, jeśli zignorujesz te fakty dotyczące organizacji indeksów InnoDB.
Wracając do tego zapytania SO, ponieważ wyraźnie zażądałeś pełnego skanowania tabeli , IMHO Optymalizator zapytań MySQL zrobił właściwą czynność (lub przynajmniej wybrał ścieżkę najmniejszego oporu). Jeśli chodzi o InnoDB i kwerendę SO, o wiele łatwiej jest wykonać pełne skanowanie tabeli, a następnie niektóre, filesort
zamiast wykonywać pełne skanowanie indeksu i wyszukiwanie wierszy za pośrednictwem gen_clust_index dla każdej pozycji indeksu wtórnego.
Nie jestem zwolennikiem korzystania ze Wskazówek dotyczących indeksu, ponieważ ignoruje on plan WYJAŚNIENIA. Niezależnie od tego, jeśli naprawdę znasz swoje dane lepiej niż InnoDB, będziesz musiał skorzystać ze wskazówek indeksu, szczególnie w przypadku zapytań, które nie zawierają żadnych WHERE
klauzul.
AKTUALIZACJA 14.11.2012 14:21 EDT
Zgodnie z książką Understanding MySQL Internals
Page 202 Ustęp 7 mówi, co następuje:
Dane są przechowywane w specjalnej strukturze zwanej indeksem klastrowym , który jest drzewem B z kluczem podstawowym działającym jako wartość klucza oraz rzeczywistym rekordem (a nie wskaźnikiem) w części danych. Dlatego każda tabela InnoDB musi mieć klucz podstawowy. Jeśli nie zostanie dostarczony, zostanie dodana specjalna kolumna identyfikatora wiersza, która zwykle nie jest widoczna dla użytkownika, aby działała jako klucz podstawowy. Klucz pomocniczy przechowuje wartość klucza podstawowego, który identyfikuje rekord. Kod B-drzewa można znaleźć w innobase / btr / btr0btr.c .
Właśnie dlatego powiedziałem wcześniej: o wiele łatwiej jest wykonać pełne skanowanie tabeli, a następnie niektóre sortowanie plików, niż pełne skanowanie indeksu i wyszukiwanie wierszy za pomocą gen_clust_index dla każdej pozycji indeksu wtórnego . InnoDB za każdym razem przeprowadzi podwójne wyszukiwanie indeksu . Brzmi to trochę brutalnie, ale to tylko fakty. Ponownie weź pod uwagę brak WHERE
klauzuli. To samo w sobie jest wskazówką dla MySQL Query Optimizer, aby wykonać pełne skanowanie tabeli.
FOR ORDER BY
(co jest szczególnym przypadkiem w tym pytaniu). Pytanie zawierało stwierdzenie, że w tym przypadku był to silnik pamięci masowejInnoDB
(a oryginalne pytanie SO pokazuje, że wiersze 10 000 są dość równomiernie rozmieszczone na 8 elementach, tutaj również nie powinna być problemem liczność). Niestety nie sądzę, że to odpowiada na pytanie.filesort
Selekcji podejmowane przez optymalizator kwerendy z jednego prostego powodu: Brakuje foreknowledge danych, które masz. Jeśli wybór użycia wskazówek dotyczących indeksu (na podstawie problemu nr 2) zapewnia satysfakcjonujący czas działania, to idź za tym. Odpowiedź, którą podałem, była tylko ćwiczeniem akademickim, które miało pokazać, jak temperamentny może być Optymalizator zapytań MySQL, a także sugerować kierunki działania.Dostosowane (za zgodą) z odpowiedzi Denisa na inne pytanie dotyczące SO:
Ponieważ wszystkie rekordy (lub prawie wszystkie) zostaną pobrane przez zapytanie, zwykle lepiej jest bez żadnego indeksu. Powodem tego jest fakt, że odczyt indeksu kosztuje coś.
Kiedy idziesz do całego stołu, sekwencyjne czytanie tabeli i sortowanie jej wierszy w pamięci może być najtańszym planem. Jeśli potrzebujesz tylko kilku wierszy, a większość dopasuje klauzulę where, wybranie najmniejszego indeksu załatwi sprawę.
Aby zrozumieć, dlaczego, wyobraź sobie dysk I / O, którego to dotyczy.
Załóżmy, że chcesz mieć całą tabelę bez indeksu. Aby to zrobić, czytaj dane strona_1, strona_ danych2, strona_ danych3 itd., Odwiedzając kolejno różne strony dysku, aż dojdziesz do końca tabeli. Następnie sortuj i wróć.
Jeśli chcesz uzyskać 5 pierwszych wierszy bez indeksu, sekwencyjnie odczytujesz całą tabelę, jak poprzednio, podczas sortowania sterty pierwszych 5 wierszy. Trzeba przyznać, że to dużo czytania i sortowania dla kilku wierszy.
Załóżmy teraz, że chcesz mieć całą tabelę z indeksem. Aby to zrobić, sekwencyjnie czytasz stronę indeksową, stronę indeksową 2 itd. To następnie prowadzi do odwiedzenia, powiedzmy, data_page3, następnie data_page1, następnie data_page3, następnie data_page2 itd., W całkowicie losowej kolejności (w kolejności, w której posortowane wiersze pojawiają się w danych). Zaangażowane we / wy sprawia, że taniej jest po prostu sekwencyjnie czytać cały bałagan i sortować torbę w pamięci.
Jeśli chcesz tylko 5 górnych wierszy zindeksowanej tabeli, w przeciwieństwie do tego użycie indeksu staje się właściwą strategią. W najgorszym przypadku ładujesz 5 stron danych do pamięci i przechodzisz dalej.
Dobry planista zapytań SQL, btw, podejmie decyzję, czy użyć indeksu, czy nie, na podstawie stopnia fragmentacji danych. Jeśli pobieranie wierszy w kolejności oznacza powiększanie w tę iz powrotem po stole, dobry planista może zdecydować, że nie warto używać indeksu. W przeciwieństwie do tego, jeśli tabela jest grupowana przy użyciu tego samego indeksu, gwarantuje się, że wiersze są w porządku, co zwiększa prawdopodobieństwo, że zostanie wykorzystana.
Ale jeśli połączysz to samo zapytanie z inną tabelą, a ta inna tabela ma bardzo selektywną klauzulę where, która może używać małego indeksu, planista może zdecydować, że w rzeczywistości lepiej, np. Pobrać wszystkie identyfikatory wierszy oznaczonych jako
foo
hash połącz tabele i posortuj je w pamięci.źródło