Должен ли индекс охватывать все выбранные столбцы, чтобы он использовался для ORDER BY?

15

Недавно на SO кто-то спросил, почему ORDER BY не использует индекс?

Ситуация включала простую таблицу InnoDB в MySQL, состоящую из трех столбцов и 10 тыс. Строк. Один из столбцов, целое число, был проиндексирован - и ОП попытался получить всю свою таблицу, отсортированную по этому столбцу:

SELECT * FROM person ORDER BY age

Он приложил EXPLAINвывод, показывающий, что этот запрос был разрешен с помощью filesort(а не индекса), и спросил, почему это будет.

Несмотря на подсказку, FORCE INDEX FOR ORDER BY (age) приводящую к использованию индекса , кто-то ответил (с поддержкой комментариев / комментариев от других), что индекс используется только для сортировки, когда все выбранные столбцы считываются из индекса (то есть, как обычно указано Using indexв Extraстолбце по EXPLAINвыходу). Позже было дано объяснение, что обход индекса и последующая выборка столбцов из таблицы приводит к случайному вводу-выводу, который MySQL считает более дорогим, чем a filesort.

Похоже, что это идет вразрез с главой руководства по ORDER BYоптимизации , которая не только создает сильное впечатление, что удовлетворение ORDER BYот индекса предпочтительнее, чем выполнение дополнительной сортировки (действительно, filesortэто комбинация быстрой сортировки и слияния и, следовательно, должна иметь нижнюю границу ; хотя проход по индексу по порядку и поиск в таблице должны быть - так что это имеет смысл), но он также пренебрегает упоминанием этой предполагаемой «оптимизации», в то же время заявляя:Ω(nlog n)O(n)

Следующие запросы используют индекс для разрешения ORDER BYдетали:

SELECT * FROM t1
  ORDER BY key_part1,key_part2,... ;

Насколько я понимаю, это именно тот случай в этой ситуации (но индекс не использовался без явного намека).

Мои вопросы:

  • Действительно ли необходимо индексировать все выбранные столбцы, чтобы MySQL мог использовать индекс?

    • Если да, то где это задокументировано (если вообще)?

    • Если нет, что здесь происходит?

eggyal
источник

Ответы:

14

Действительно ли необходимо индексировать все выбранные столбцы, чтобы 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';

ТЕСТ 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>

ТЕСТ 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>

Анализ для InnoDB

Когда данные были загружены как InnoDB, обратите внимание, что все четыре EXPLAINплана использовали genderиндекс. Третий и четвертый EXPLAINпланы использовали genderиндекс, хотя запрошенные данные были id. Почему? Потому idчто в PRIMARY KEYи все вторичные индексы имеют ссылки на указатели PRIMARY KEY(через gen_clust_index ).

Анализ для MyISAM

Когда данные были загружены как MyISAM, обратите внимание, что первые три EXPLAINплана использовали genderиндекс. В четвертом EXPLAINплане Оптимизатор запросов решил вообще не использовать индекс. Вместо этого он выбрал полное сканирование таблицы. Почему?

Независимо от СУБД Оптимизаторы запросов работают по очень простому практическому правилу: если индекс отбирается как кандидат для использования при выполнении поиска, а Оптимизатор запросов вычисляет, что он должен искать более 5% от общего числа строки в таблице:

  • полное сканирование индекса выполняется, если все необходимые столбцы для поиска находятся в выбранном индексе
  • полное сканирование таблицы в противном случае

ВЫВОД

Если у вас нет подходящих индексов покрытия или если ключевая совокупность для любого данного кортежа составляет более 5% таблицы, должно произойти шесть вещей:

  1. Осознайте, что вы должны профилировать запросы
  2. Найти все WHERE, GROUP BYи предложения ORDER BY` из этих запросов
  3. Сформулируйте индексы в этом порядке
    • WHERE столбцы условия со статическими значениями
    • GROUP BY столбцы
    • ORDER BY столбцы
  4. Избегайте полных сканирований таблиц (в запросах отсутствует разумное WHEREпредложение)
  5. Избегайте групп с плохими ключами (или, по крайней мере, кешируйте эти группы с плохими ключами)
  6. Выберите лучший механизм хранения MySQL ( InnoDB или MyISAM ) для таблиц

Я писал об этом 5% -ом практическом правиле в прошлом:

ОБНОВЛЕНИЕ 2012-11-14 13:05 ПО ВОСТОЧНОМУ ВРЕМЕНИ

Я оглянулся на ваш вопрос и на оригинальный пост 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 (он же Clustered Index) упорядочен по внутреннему rowid . Когда вы упорядочиваетесь по индексу, имейте в виду, что вторичные индексы InnoDB имеют идентификатор строки, присоединенный к каждой записи индекса. Это создает внутреннюю потребность в полном доступе к строке каждый раз.

Настройка ORDER BYтаблицы InnoDB может быть довольно сложной задачей, если вы игнорируете эти факты об организации индексов InnoDB.

Возвращаясь к этому SO-запросу, поскольку вы явно требовали полного сканирования таблицы , IMHO, MySQL Query Optimizer сделал правильную вещь (или, по крайней мере, выбрал путь наименьшего сопротивления). Когда дело доходит до InnoDB и запроса SO, гораздо проще выполнить полное сканирование таблицы, а затем и некоторое, filesortа не полное сканирование индекса и поиск строки с помощью gen_clust_index для каждой записи вторичного индекса.

Я не сторонник использования Index Hints, потому что он игнорирует план EXPLAIN. Несмотря на это, если вы действительно знаете свои данные лучше, чем InnoDB, вам придется прибегнуть к индексным подсказкам, особенно с запросами, которые не содержат WHEREоговорок.

ОБНОВЛЕНИЕ 2012-11-14 14:21 ПО ВОСТОЧНОМУ ВРЕМЕНИ

Согласно книге « Понимание внутренних особенностей MySQL»

введите описание изображения здесь

В параграфе 7 говорится следующее:

Данные хранятся в специальной структуре, называемой кластерным индексом , который представляет собой B-дерево с первичным ключом, действующим в качестве значения ключа, и фактической записью (а не указателем) в части данных. Таким образом, каждая таблица InnoDB должна иметь первичный ключ. Если он не указан, то в качестве первичного ключа добавляется специальный столбец идентификатора строки, который обычно не виден пользователю. Вторичный ключ будет хранить значение первичного ключа, который идентифицирует запись. Код B-дерева можно найти в innobase / btr / btr0btr.c .

Вот почему я говорил ранее: гораздо проще выполнить полное сканирование таблицы и затем некоторую сортировку файлов, чем выполнять полное сканирование индекса и поиск строки с помощью gen_clust_index для каждой записи вторичного индекса . InnoDB будет делать двойной поиск индекса каждый раз . Это звучит жестоко, но это только факты. Опять же, принять во внимание отсутствие WHEREпункта. Это само по себе является подсказкой оптимизатору запросов MySQL для полного сканирования таблицы.

RolandoMySQLDBA
источник
Роландо, спасибо за такой подробный и подробный ответ. Тем не менее, это не имеет отношения к выбору индексов FOR ORDER BY(что является конкретным случаем в этом вопросе). В вопросе указывалось, что в этом случае механизм хранения был InnoDB(и исходный вопрос SO показывает, что строки по 10 КБ довольно равномерно распределены по 8 элементам, здесь также не должно быть проблемы с количеством элементов). К сожалению, я не думаю, что это отвечает на вопрос.
eggyal
Это интересно, так как первая часть была моим первым инстинктом (у него не было хорошей мощности, поэтому mysql решил использовать полное сканирование). Но чем больше я читаю, тем не менее, это правило не относится к порядку при оптимизации. Вы уверены, что он упорядочен по первичному ключу для кластерных индексов innodb? Этот пост указывает на то, что первичный ключ добавляется в конец, поэтому не будет ли сортировка по-прежнему в явных столбцах индекса? Короче я все еще в тупике!
Дерек Дауни
1
filesortВыбор был решен оптимизатором запросов по одной простой причине: он испытывает недостаток в предвидение данных , которые у вас есть. Если ваш выбор использования индексных подсказок (основанный на проблеме № 2) приносит вам удовлетворительное время выполнения, тогда непременно сделайте это. Ответ, который я дал, был всего лишь академическим упражнением, чтобы показать, насколько темпераментным может быть MySQL Query Optimizer, а также предложить варианты действий.
RolandoMySQLDBA
1
Я прочитал и перечитал этот и другие посты, и я могу только согласиться, что это связано с упорядочением innodb по первичному ключу, поскольку мы выбираем все (а не индекс покрытия). Я удивлен, что на странице документа по оптимизации ORDER BY нет упоминания об этой специфической для InnoDB странности. Во всяком случае, +1 к Роландо
Дерек Дауни
1
@eggyal Это было написано на этой неделе. Обратите внимание на тот же план EXPLAIN, и полное сканирование занимает больше времени, если набор данных не помещается в памяти.
Дерек Дауни
0

Адаптировано (с разрешения) от ответа Дениса на другой вопрос о SO:

Поскольку все записи (или почти все) будут выбираться запросом, обычно лучше вообще не иметь индекса. Причина этого в том, что чтение индекса на самом деле стоит чего-то.

Поскольку вы работаете со всей таблицей, последовательное чтение таблицы и сортировка ее строк в памяти может быть вашим самым дешевым планом. Если вам нужно всего несколько строк, и большинство из них будет соответствовать предложению where, переход к наименьшему индексу поможет.

Чтобы понять почему, представьте, как работает дисковый ввод-вывод.

Предположим, вам нужна вся таблица без индекса. Для этого вы читаете data_page1, data_page2, data_page3 и т. Д., Посещая различные страницы диска, связанные с порядком, пока не дойдете до конца таблицы. Вы потом сортируете и возвращаете.

Если вы хотите, чтобы верхние 5 строк были без индекса, вы последовательно читали бы всю таблицу, как и раньше, при сортировке кучи верхних 5 строк. По общему признанию, это много чтения и сортировки для нескольких строк.

Предположим теперь, что вам нужна вся таблица с индексом. Для этого вы последовательно читаете index_page1, index_page2 и т. Д. Это затем приводит вас к посещению, скажем, data_page3, затем data_page1, затем data_page3, затем data_page2 и т. Д. В совершенно случайном порядке (то есть, по которому отсортированные строки появляются в данных). Внедрение ввода-вывода делает более дешевым последовательное считывание всего беспорядка и сортировку пакета памяти в памяти.

Если вы просто хотите, чтобы верхние 5 строк индексированной таблицы, напротив, использование индекса стало правильной стратегией. В худшем случае вы загружаете 5 страниц данных в память и идете дальше.

Кстати, хороший планировщик SQL-запросов примет решение о том, использовать индекс или нет, исходя из того, насколько фрагментированы ваши данные. Если выборка строк по порядку означает масштабирование назад и вперед по всей таблице, хороший планировщик может решить, что индекс не стоит использовать. Напротив, если таблица кластеризована с использованием того же индекса, строки гарантированно будут в порядке, что увеличивает вероятность ее использования.

Но затем, если вы объединяете тот же запрос с другой таблицей, и эта другая таблица имеет чрезвычайно избирательное предложение where, которое может использовать небольшой индекс, планировщик может решить, что на самом деле лучше, например, извлечь все идентификаторы строк, помеченных как fooхеш присоединяйся к таблицам, и куча сортируй их в памяти.

eggyal
источник