#mysql #indexing #innodb
#mysql #индексирование #innodb
Вопрос:
Обзор
Я использую MySQL 5.7.30-33, и я сталкиваюсь с проблемой, которая, похоже, MySQL использует неправильный индекс при выполнении запроса. Я получаю 3-секундное время запроса, используя мой существующий запрос. Однако, просто изменив ORDER BY, удалив LIMIT или принудительно используя ИНДЕКС, я могу получить время запроса 0,01 секунды. К сожалению, мне нужно придерживаться моего исходного запроса (он встроен в приложение), поэтому было бы здорово, если бы это несоответствие можно было устранить в схеме / индексации.
Настройка / проблема
Моя структура таблицы выглядит следующим образом:
CREATE TABLE `referrals` (
`__id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`systemcreated` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`referrerid` mediumtext COLLATE utf8mb4_unicode_ci,
`referrersiteid` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
... lots more mediumtext fields ...
PRIMARY KEY (`__id`),
KEY `systemcreated` (`systemcreated`,`referrersiteid`,`__id`)
) ENGINE=InnoDB AUTO_INCREMENT=53368 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED
В таблице всего ~ 55 тыс. строк, но она очень широкая, так как некоторые поля содержат огромные двоичные объекты:
mysql> show table status like 'referrals'G;
*************************** 1. row ***************************
Name: referrals
Engine: InnoDB
Version: 10
Row_format: Compressed
Rows: 45641
Avg_row_length: 767640
Data_length: 35035897856
Max_data_length: 0
Index_length: 3653632
Data_free: 3670016
Auto_increment: 54008
Create_time: 2020-12-12 12:46:14
Update_time: 2020-12-12 17:50:28
Check_time: NULL
Collation: utf8mb4_unicode_ci
Checksum: NULL
Create_options: row_format=COMPRESSED
Comment:
1 row in set (0.00 sec)
Приложение моего клиента запрашивает таблицу, используя это, и, к сожалению, это нелегко изменить:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16;
Это приводит к времени запроса около 3 секунд.
ОБЪЯСНЕНИЕ выглядит следующим образом:
---- ------------- ------------- ------------ ------- --------------- --------- --------- ------ ------ ---------- -------------
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---- ------------- ------------- ------------ ------- --------------- --------- --------- ------ ------ ---------- -------------
| 1 | SIMPLE | referrals | NULL | index | systemcreated | PRIMARY | 4 | NULL | 32 | 5.56 | Using where |
---- ------------- ------------- ------------ ------- --------------- --------- --------- ------ ------ ---------- -------------
Обратите внимание, что для запроса используется ПЕРВИЧНЫЙ ключ, а не systemcreated
индекс.
Эксперимент 1
Если я изменю запрос на использование ASC, а не DESC:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id asc
limit 16;
then it takes 0.01 seconds, and the EXPLAIN looks to be the same:
---- ------------- ------------- ------------ ------- --------------- --------- --------- ------ ------ ---------- -------------
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---- ------------- ------------- ------------ ------- --------------- --------- --------- ------ ------ ---------- -------------
| 1 | SIMPLE | referrals | NULL | index | systemcreated | PRIMARY | 4 | NULL | 32 | 5.56 | Using where |
---- ------------- ------------- ------------ ------- --------------- --------- --------- ------ ------ ---------- -------------
Experimentation 2
If I change the query to stick with ORDER BY __id DESC, but remove the LIMIT:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc;
тогда это также занимает 0,01 секунды, с ОБЪЯСНЕНИЕМ, подобным этому:
---- ------------- ------------- ------------ ------- --------------- --------------- --------- ------ ------ ---------- ---------------------------------------
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---- ------------- ------------- ------------ ------- --------------- --------------- --------- ------ ------ ---------- ---------------------------------------
| 1 | SIMPLE | referrals | NULL | range | systemcreated | systemcreated | 406 | NULL | 2086 | 11.11 | Using index condition; Using filesort |
---- ------------- ------------- ------------ ------- --------------- --------------- --------- ------ ------ ---------- ---------------------------------------
Эксперимент 3
В качестве альтернативы, если я заставлю исходный запрос использовать systemcreated
индекс, это также даст время запроса 0,01 секунды. Вот ОБЪЯСНЕНИЕ:
mysql> explain SELECT *
FROM referrals USE INDEX (systemcreated)
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16;
---- ------------- -------------- ------------ ------- --------------- --------------- --------- ------ ------ ---------- ---------------------------------------
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---- ------------- -------------- ------------ ------- --------------- --------------- --------- ------ ------ ---------- ---------------------------------------
| 1 | SIMPLE | referrals | NULL | range | systemcreated | systemcreated | 406 | NULL | 2086 | 11.11 | Using index condition; Using filesort |
---- ------------- -------------- ------------ ------- --------------- --------------- --------- ------ ------ ---------- ---------------------------------------
Эксперимент 4
Наконец, если я использую исходный ORDER BY __id DESC LIMIT 16, но выбираю меньшее количество полей, тогда он также возвращается через 0,01 секунды! Вот объяснение:
mysql> explain SELECT field1, field2, field3, field4, field5
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16;
---- ------------- ------------- ------------ ------- --------------- --------- --------- ------ ------ ---------- -------------
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---- ------------- ------------- ------------ ------- --------------- --------- --------- ------ ------ ---------- -------------
| 1 | SIMPLE | referrals | NULL | index | systemcreated | PRIMARY | 4 | NULL | 32 | 5.56 | Using where |
---- ------------- ------------- ------------ ------- --------------- --------- --------- ------ ------ ---------- -------------
Краткие сведения
Таким образом, единственная комбинация, которая, похоже, работает плохо, это ORDER BY __id DESC LIMIT 16
.
Я думаю, что у меня правильно настроены индексы. Я запрашиваю через поля systemcreated
and и referrersiteid
упорядочиваю по __id , поэтому у меня есть индекс, определенный как (systemcreated, referrersiteid, __id), но MySQL, похоже, все еще использует ПЕРВИЧНЫЙ ключ.
Есть предложения?
Комментарии:
1. Может быть, запуск
ANALYZE TABLE referrals
позволяет СУБД принять лучшее решение?2. Сколько у вас оперативной памяти?
3. АНАЛИЗ ТАБЛИЦЫ не имел никакого значения. На сервере 16 ГБ.
Ответ №1:
- «Avg_row_length: 767640»; много
MEDIUMTEXT
. Строка ограничена примерно 8 КБ; переполнение переходит в блоки «вне записи». Чтение этих блоков требует дополнительных обращений к диску. SELECT *
будет доступно для всех этих толстых столбцов. Общее количество операций чтения составит около 50 (по 16 КБ каждое). Это требует времени.- (Пример 4)
SELECT a,b,c,d
выполнялся быстрее, потому что ему не нужно было извлекать все ~ 50 блоков в строке. - Ваш вторичный индекс, (
systemcreated
,referrersiteid
,__id
), — полезен только первый столбец. Это происходит из — заsystemcreated LIKE 'xxx%'
. Это «диапазон». Как только диапазон достигнут, остальная часть индекса становится неэффективной. За исключением… - «Индексные подсказки» (
USE INDEX(...)
) могут помочь сегодня, но могут ухудшить ситуацию завтра, когда изменится распределение данных. - Если вы не можете избавиться от подстановочных
LIKE
знаков, я рекомендую эти два индекса:INDEX(systemcreated) INDEX(referrersiteid)
- Реальное ускорение может произойти, если вывернуть запрос наизнанку. То есть сначала найдите 16 идентификаторов, а затем найдите все эти громоздкие столбцы:
SELECT r2... -- whatever you want FROM ( SELECT __id FROM referrals WHERE `systemcreated` LIKE 'XXXXXX%' AND `referrersiteid` LIKE 'XXXXXXXXXXXX%' order by __id desc limit 16 ) AS r1 JOIN referrals r2 USING(__id) ORDER BY __id DESC -- yes, this needs repeating
И сохраните имеющийся у вас вторичный индекс из 3 столбцов. Даже если он должен сканировать намного больше, чем 16 строк, чтобы найти нужные 16, он намного менее громоздкий. Это означает, что подзапрос («производная таблица») будет умеренно быстрым. Тогда во внешнем запросе все равно будет 16 запросов — возможно, 16 * 50 блоков для чтения. Общее количество прочитанных блоков все равно будет намного меньше.
Между ASC
и on редко бывает заметная разница DESC
ORDER BY
.
Почему оптимизатор выбирает PK вместо, казалось бы, лучшего вторичного индекса? PK может быть лучшим, особенно если 16 строк находятся в «конце» (DESC) таблицы. Но это был бы ужасный выбор, если бы ему пришлось сканировать всю таблицу, не найдя 16 строк.
Между тем, проверка подстановочных знаков делает вторичный индекс полезным лишь частично. Оптимизатор принимает решение на основе неадекватной статистики. Иногда это похоже на подбрасывание монеты.
Если вы используете мою переформулировку наизнанку, я рекомендую следующие два составных индекса — оптимизатор может сделать полуинтеллектуальный, полу-правильный выбор между ними для производной таблицы:
INDEX(systemcreated, referrersiteid, __id),
INDEX(referrersiteid, systemcreated, __id)
Он по-прежнему будет указывать «сортировка файлов», но не волнуйтесь; он сортирует только 16 строк.
И, помните, это снижает производительность. SELECT *
(Хотя, возможно, вы не можете это исправить.)
Комментарии:
1. Большое спасибо! Ваше предложение о добавлении отдельных индексов в
systemcreated
andreferrersiteid
сделало свое дело. Фактически,INDEX(referrersiteid)
alone выполнил свою работу. Я подозреваю, что моя проблема, возможно, имела какое-то отношение к мощности данных — systemcreated имеет только несколько уникальных значений, в то время как referrersiteid насчитывает десятки тысяч.2. @SamCrawford — Поскольку вы используете тест диапазона, мощность — это не показатель, а количество строк, которые удовлетворяют
LIKE '...%'
. Сохраните оба индекса; оптимизатор будет динамически определять, какой лучше для каждого запроса.3. Понял, спасибо. И, перечитав ваш первоначальный ответ, теперь я понимаю, что если бы у меня был исходный вторичный индекс as (referrersiteid, systemcreated, __id), тогда все было бы хорошо. Я не знал, что индекс перестал использоваться после первого успешного совпадения диапазона. Так что еще раз спасибо за улучшение моих знаний!
4. @SamCrawford — У меня есть еще несколько советов здесь: mysql.rjweb.org/doc.php/index_cookbook_mysql