Чи дійсно необхідно, щоб усі вибрані стовпці були проіндексовані, щоб MySQL вирішив використовувати індекс?
Це завантажене питання, оскільки є фактори, які визначають, чи варто використовувати індекс.
ФАКТОР №1
Для будь-якого даного показника, яка ключова сукупність? Іншими словами, яка кардинальність (чітке число) всіх кортежів, записаних в індексі?
ФАКТОР №2
Який двигун зберігання ви використовуєте? Чи доступні всі необхідні стовпці з індексу?
ЩО ДАЛІ ???
Візьмемо простий приклад: таблиця, яка містить два значення (чоловіче та жіноче)
Дозвольте створити таку таблицю з тестом на використання індексу
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>
ТЕСТ МІЙСАМ
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>
Аналіз для InnoDB
Коли дані завантажувались як InnoDB, зауважте, що всі чотири EXPLAIN
плани використовували gender
індекс. Третій та четвертий EXPLAIN
плани використовували gender
індекс, навіть якщо запитувані дані були id
. Чому? Тому що id
є в PRIMARY KEY
і всі вторинні індекси мають опорні покажчики назад до PRIMARY KEY
(через gen_clust_index ).
Аналіз для MyISAM
Коли дані завантажувались як MyISAM, зауважте, що перші три EXPLAIN
плани використовували gender
індекс. У четвертому EXPLAIN
плані оптимізатор запитів вирішив взагалі не використовувати індекс. Натомість він вибрав повне сканування таблиці. Чому?
Незалежно від СУБД, оптимізатори запитів працюють на дуже простому принципі: якщо індекс перевіряється як кандидат, який буде використаний для здійснення пошуку, а Оптимізатор запитів обчислює, що він повинен шукати більше 5% від загальної кількості рядки в таблиці:
- повне сканування індексу проводиться, якщо всі необхідні стовпці для пошуку знаходяться у вибраному індексі
- повне сканування таблиці в іншому випадку
ВИСНОВОК
Якщо у вас немає належних показників покриття або якщо ключова сукупність для будь-якого кортежу становить більше 5% таблиці, має відбутися шість речей:
- Приходьте до усвідомлення того, що ви повинні профілювати запити
- Знайдіть усі
WHERE
, GROUP BY
і ЗАМОВИТИ ЗАКЛАДИ з цих запитів
- Складіть індекси в цьому порядку
WHERE
стовпці пункту зі статичними значеннями
GROUP BY
стовпчики
ORDER BY
стовпчики
- Уникайте сканувань повних таблиць (запитів, у яких відсутні чітке
WHERE
застереження)
- Уникайте популяцій поганих ключів (або принаймні кешуйте ці популяції поганих ключів)
- Виберіть найкращий двигун зберігання даних MySQL ( InnoDB або MyISAM ) для таблиць
Я писав про це правило 5%:
ОНОВЛЕННЯ 2012-11-14 13:05 EDT
Я озирнувся на ваше запитання та на оригінальний пост SO . Тоді я подумав про своє, про яке Analysis for InnoDB
я згадував раніше. Він збігається з person
таблицею. Чому?
Як для таблиць, так mf
і дляperson
- Двигун зберігання даних - InnoDB
- Первинний ключ є
id
- Доступ до таблиці здійснюється за вторинним індексом
- Якби стіл був MyISAM, ми побачили б зовсім інший
EXPLAIN
план
А тепер подивіться на запит із питання SO : select * from person order by age\G
. Оскільки цього WHERE
пункту немає , ви явно вимагали сканувати повну таблицю . Порядок сортування таблиці за замовчуванням був би id
(PRIMARY KEY) через його auto_increment, а gen_clust_index (aka Clustered Index) впорядковується внутрішнім rowid . Коли ви замовляєте індекс, майте на увазі, що вторинні індекси InnoDB мають рядковий додаток до кожного запису індексу. Це створює внутрішню потребу в повному доступі до рядків кожного разу.
Налаштування ORDER BY
на таблиці InnoDB може бути досить непростим завданням, якщо ви проігноруєте ці факти про те, як організовані індекси InnoDB.
Повертаючись до цього запиту SO, оскільки ви явно вимагали сканування повної таблиці , IMHO оптимізатор запитів MySQL зробив правильно (або, принаймні, обрав шлях найменшого опору). Що стосується InnoDB та запиту SO, то набагато простіше здійснити повне сканування таблиці, а потім деякі filesort
, ніж робити повне сканування індексу та пошук рядків через gen_clust_index для кожного другого запису індексу.
Я не є прихильником використання індексних підказок, оскільки він ігнорує план ПОЯСНЕННЯ. Незважаючи на це, якщо ви дійсно знаєте свої дані краще, ніж InnoDB, вам доведеться вдаватися до індексів підказки, особливо з запитами, які не мають WHERE
застереження.
ОНОВЛЕННЯ 2012-11-14 14:21 EDT
Відповідно до книги Understanding MySQL Internals
Page 202 Параграф 7 говорить наступне:
Дані зберігаються у спеціальній структурі, що називається кластерним індексом , що являє собою B-дерево з первинним ключем, що виконує роль ключового значення, та фактичним записом (а не вказівником) у частині даних. Таким чином, кожна таблиця InnoDB повинна мати первинний ключ. Якщо такий не надається, додається спеціальний стовпець ідентифікатора рядка, який зазвичай не видно користувачеві, щоб діяти в якості первинного ключа. Вторинний ключ зберігатиме значення первинного ключа, який ідентифікує запис. Код B-дерева можна знайти в Innobase / btr / btr0btr.c .
Ось чому я заявляв раніше: набагато простіше здійснити повне сканування таблиці, а потім деякі файли, ніж робити повне сканування індексу та пошук рядків через gen_clust_index для кожного другого запису індексу . InnoDB збирається робити подвійний пошук кожного разу . Це звучить якось жорстоко, але це лише факти. Знову ж таки, врахуйте відсутність WHERE
пункту. Це саме по собі є підказкою для оптимізатора запитів MySQL зробити повне сканування таблиці.
FOR ORDER BY
(що є конкретним випадком у цьому питанні). У запитанні було зазначено, що в цьому випадку двигун зберігання даних бувInnoDB
(і оригінальне запитання SO показує, що рядки 10k досить рівномірно розподілені по 8 пунктам, і тут не повинно бути проблематикою кардинальність). На жаль, я не думаю, що це відповідає на питання.