Чи повинен індекс охоплювати всі вибрані стовпці, щоб використовувати його для ЗАМОВЛЕННЯ?


15

Більше в SO, хтось нещодавно запитав: Чому НЕ ЗАМОВИТИ, використовуючи індекс?

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

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 вирішив використовувати індекс?

    • Якщо так, то де це задокументовано (якщо воно взагалі є)?

    • Якщо ні, що тут було?

Відповіді:


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';

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% таблиці, має відбутися шість речей:

  1. Приходьте до усвідомлення того, що ви повинні профілювати запити
  2. Знайдіть усі WHERE, GROUP BYі ЗАМОВИТИ ЗАКЛАДИ з цих запитів
  3. Складіть індекси в цьому порядку
    • WHERE стовпці пункту зі статичними значеннями
    • GROUP BY стовпчики
    • ORDER BY стовпчики
  4. Уникайте сканувань повних таблиць (запитів, у яких відсутні чітке WHEREзастереження)
  5. Уникайте популяцій поганих ключів (або принаймні кешуйте ці популяції поганих ключів)
  6. Виберіть найкращий двигун зберігання даних 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 пунктам, і тут не повинно бути проблематикою кардинальність). На жаль, я не думаю, що це відповідає на питання.
eggyal

Це цікаво, оскільки перша частина була і моїм першим інстинктом (він не мав гарної кардинальності, тому mysql вирішив використовувати повне сканування). Але чим більше я читав, це правило, схоже, не застосовувалося для замовлення шляхом оптимізації. Ви впевнені, що це замовлення за первинним ключем для кластерних індексів innodb? Цей пост вказує, що первинний ключ буде доданий до кінця, тож чи не все одно такий тип буде розміщений у явних стовпцях (-ях) індексу? Коротше кажучи, я все ще спотикався!
Дерек Дауні

1
filesortВибір був вирішений оптимізатором запитів з однієї простої причини: він відчуває нестачу в передбачення даних , які у вас є. Якщо ваш вибір використовувати підказки (заснований на випуску №2) приносить вам задовольняючи час роботи, то, безумовно, ідіть за цим. Відповідь, яку я надавав, була лише академічною вправою, щоб показати, наскільки темпераментним може бути оптимізатор запитів MySQL, а також запропонувати курси дій.
RolandoMySQLDBA

1
Я читав і перечитав цю та інші публікації, і можу погодитися лише з тим, що це стосується замовлення innodb в первинному ключі, оскільки ми вибираємо всі (а не індекс покриття). Я здивований, що цієї дивної специфіки для InnoDB не згадується на сторінці документа ORDER BY щодо оптимізації. У будь-якому разі, +1 Роландо
Дерек Дауні

1
@eggyal Це було написано цього тижня. Зверніть увагу на той самий план EXPLAIN, і повне сканування займе більше часу, якщо набір даних не вміститься в пам'яті.
Дерек Дауні

0

Адаптовано (з дозволу) з відповіді Дениса на інше запитання щодо ТА:

Оскільки всі записи (або майже всі) будуть отримані за запитом, вам зазвичай краще не мати індексу. Причиною цього є те, що насправді щось коштує прочитати індекс.

Якщо ви збираєтесь за цілою таблицею, послідовно читати таблицю і сортувати її рядки в пам'яті може бути вашим найдешевшим планом. Якщо вам потрібно лише кілька рядків, і більшість буде відповідати пункту де, перехід на найменший індекс зробить трюк.

Щоб зрозуміти, чому, сформулюйте диск, в який входить / виходить диск.

Припустимо, ви хочете всю таблицю без індексу. Для цього ви читаєте data_page1, data_page2, data_page3 і т.д., відвідуючи різні сторінки диска, що займаються порядком, до тих пір, поки не досягнете кінця таблиці. Потім ви сортуєте та повертаєтесь.

Якщо ви хочете, щоб перші 5 рядків були без індексу, ви послідовно читали всю таблицю, як і раніше, під час групового сортування верхніх 5 рядків. Справді, це читання та сортування для кількох рядів.

Припустимо, зараз вам потрібна вся таблиця з індексом. Для цього ви читаєте послідовно index_page1, index_page2 тощо. Потім це призводить до відвідування, скажімо, data_page3, потім data_page1, потім data_page3, потім data_page2 тощо, у абсолютно випадковому порядку (тому, за яким відсортовані рядки відображаються в даних). Залучений IO дозволяє дешевше просто читати весь безлад послідовно і сортувати мішок захоплення в пам'яті.

Якщо ви просто хочете, щоб перші 5 рядків індексованої таблиці, навпаки, використання індексу стає правильною стратегією. У гіршому випадку ви завантажуєте в пам'ять 5 сторінок даних і рухаєтесь далі.

Хороший планувальник запитів SQL, btw, прийме рішення про те, використовувати індекс чи ні, виходячи з того, наскільки фрагментарно ваші дані. Якщо отримання рядків у порядку означає масштабування вперед та назад по таблиці, хороший планувальник може вирішити, що не варто використовувати індекс. На відміну від цього, якщо таблиця кластеризована за тим самим індексом, рядки гарантовано є в порядку, збільшуючи ймовірність того, що вона звикне.

Але тоді, якщо ви приєднаєте один і той же запит до іншої таблиці, а інша таблиця має надзвичайно вибіркове місце, де застереження, яке може використовувати невеликий індекс, планувальник може вирішити, що насправді краще, наприклад, отримати всі ідентифікатори рядків, позначених як fooхеш приєднуйтесь до таблиць і купуйте їх по пам’яті.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.