Почему mysql использует неправильный индекс для заказа по запросу?

9

Вот моя таблица с ~ 10000000 строк данных

CREATE TABLE `votes` (
  `subject_name` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `subject_id` int(11) NOT NULL,
  `voter_id` int(11) NOT NULL,
  `rate` int(11) NOT NULL,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`subject_name`,`subject_id`,`voter_id`),
  KEY `IDX_518B7ACFEBB4B8AD` (`voter_id`),
  KEY `subject_timestamp` (`subject_name`,`subject_id`,`updated_at`),
  KEY `voter_timestamp` (`voter_id`,`updated_at`),
  CONSTRAINT `FK_518B7ACFEBB4B8AD` FOREIGN KEY (`voter_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Вот показатели кардинальности

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

Поэтому, когда я делаю этот запрос:

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

Я ожидал, что он использует индекс, voter_timestamp но MySQL предпочитает использовать это вместо:

explain select SQL_NO_CACHE * from votes  where subject_name = 'medium' and voter_id = 1001 and rate = 1 order by updated_at desc limit 20 offset 100;`

type:
    index_merge
possible_keys: 
    PRIMARY,IDX_518B7ACFEBB4B8AD,subject_timestamp,voter_timestamp
key:
    IDX_518B7ACFEBB4B8AD,PRIMARY
key_len:
    102,98
ref:
    NULL
rows:
    9255
filtered:
    10.00
Extra:
    Using intersect(IDX_518B7ACFEBB4B8AD,PRIMARY); Using where; Using filesort

И я получил время запроса 200-400 мс.

Если я заставлю это использовать правильный индекс как:

SELECT SQL_NO_CACHE * FROM votes USE INDEX (voter_timestamp) WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

Mysql может вернуть результаты в 1-2 мс

и вот объяснение:

type:
    ref
possible_keys:
    voter_timestamp
key:
    voter_timestamp
key_len:
    4
ref:
    const
rows:
    18714
filtered:
    1.00
Extra:
    Using where

Так почему же MySQL не выбрал voter_timestampиндекс для моего исходного запроса?

То , что я пытался это analyze table votes, optimize table votes, падение этого индекса и добавить его снова, но MySQL до сих пор использует неправильный индекс. не совсем понимаю в чем проблема.

Феникс
источник
1
@ ypercubeᵀᴹ Я не думаю, что необходимо индексировать все столбцы в условии where, так как вы видите, если я заставлю использовать индекс (voter_id, updated_at), он может его использовать и будет очень эффективным. Если я уберу subject_name = "medium"часть, она также может выбрать правильный индекс, нет необходимости индексироватьrate
Phoenix
Тем не менее, индекс с 4 столбцами будет более эффективным, чем 2 (voter_id, updated_at). Другой индекс будет (voter_id, subject_name, updated_at)или (subject_name, voter_id, updated_at)(без ставки).
ypercubeᵀᴹ
1
И да, вы - в какой-то момент - правы. Вам не нужен индекс из 4 столбцов. Это просто лучший индекс для этого запроса. 2 колонки (которые вы считаете «правильными») могут подходить для данных и распределения, которые у вас есть в настоящее время. С другим распределением, это может быть ужасно. Пример. Предположим, что 99% строк имели скорость> 1 и только 1% имели скорость = 1. Как вы думаете, использование двухколоночного индекса будет эффективным?
ypercubeᵀᴹ
Он должен был бы пройти большую часть индекса и выполнить тысячи поисков в таблице, только чтобы найти этот показатель> 1 и отклонить строки, пока не найдет 120, которые соответствуют критериям, которые не могут быть оценены по индексу ( subject_name='medium' and rate=1)
ypercubeᵀᴹ
ypercube, Phoenix - MySQL не получит LIMITили даже ORDER BYесли только индекс сначала не удовлетворит всю фильтрацию. То есть без полных четырех столбцов он соберет все соответствующие строки, отсортирует их все, а затем выберет LIMIT. С индексом в 4-столбца, запрос может избежать сортировки и остановки после прочтения только те LIMITстроки.
Рик Джеймс

Ответы:

5

MySQL использует относительно простую (более простую, чем другие СУБД) модель затрат для планирования запросов, в которых фильтрация вашего набора данных имеет достаточно высокий приоритет. В вашем первом запросе с индексом слияния предполагается, что сканирование ~ 9000 строк будет необходимо, в то время как второй запрос с подсказкой индекса потребует 18000. Моя ставка будет состоять в том, что это весит в вычислениях достаточно для перемещения шкалы к слиянию , Вы можете подтвердить это (или найти другие причины), включив optimizer_trace, выполнить свой запрос и оценить результаты.

set global optimizer_trace='enabled=on';

-- run your query 

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

select * from information_schema.`OPTIMIZER_TRACE`;

Одно замечание index_merge: в большинстве случаев вы обнаружите, что это довольно дорого. Хотя это очень полезно для сценариев типа OLAP, оно может не очень хорошо подходить для OLTP, поскольку операция может занять значительное время вашего запроса и, как вы можете видеть, иногда неоптимальный план выполнения на самом деле быстрее.

К счастью, MySQL предоставляет переключатели для оптимизатора, поэтому вы можете настроить его по своему желанию.

Для всех опций вы можете запустить:

show global variables like 'optimizer_switch';

Для его замены вам не нужно копировать и вставлять всю строку. Это работает, как dict.update()в Python.

 set global optimizer_switch='index_merge=off';

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

У вас есть четыре вторичных ключа, и некоторые из них излишни, например, (voter_id)индекс является подмножеством(voter_id, updated_at)

Кароли Надь
источник
«Пересечение слияния индекса» редко используется MySQL. Возможно, во всех случаях значительно лучше иметь индекс с большим количеством столбцов. «Объединение индексов» иногда полезно; превращение ORв UNIONчасто , как хорошо или лучше.
Рик Джеймс
5

Для этого запроса вам нужен этот индекс:

INDEX(voter_id, rate, subject_name, updated_at)

updated_atДолжен быть последним; остальные три могут быть в любом порядке. (3-колоночные индексы ypercube не очень полезны, так как они не заканчивают WHEREстолбцы перед попаданием в ORDER BYстолбец.)

Добавив этот индекс, вы, вероятно, сможете избавиться от всех остальных вторичных ключей:

KEY IDX_518B7ACFEBB4B8AD( voter_id), - The FK можно использовать указательный KEY subject_timestamp( subject_name, subject_id, updated_at), - в основном избыточный KEY voter_timestamp( voter_id, updated_at), - возможно, были ваши попытки

С индексом из 4 столбцов у вас есть шанс оптимизировать «нумерацию страниц» и избежать OFFSET. Смотрите этот блог.

В другой теме ... Когда я вижу , X_nameи X_id, я полагаю , «нормализация» происходит. Я ожидаю увидеть эти два столбца в таблице, практически ничего другого. Я не ожидал бы увидеть оба в другой таблице.

(voter_id, updated_at)не пройдет, voter_idтак как он не закончил с фильтрацией ( WHERE). Затем, поскольку другой индекс меньше, он выбирается. У меня есть 3 столбца для фильтрации, затем столбец для ORDER BY.

Рик Джеймс
источник