Я использую MySQL 5.7.30-33 и столкнулся с проблемой, похожей на то, что MySQL использует неправильный индекс при выполнении запроса. Я получаю 3-секундное время запроса, используя мой существующий запрос. Однако, просто изменив ORDER BY, удалив LIMIT или форсировав USE INDEX, я могу получить время запроса 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 тысяч строк, но она очень широкая, так как некоторые поля содержат огромные BLOB-объекты:
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
.
Если я изменю запрос, чтобы использовать ASC, а не DESC:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id asc
limit 16;
затем это занимает 0,01 секунды, и EXPLAIN выглядит так же:
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 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:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc;
то это также занимает 0,01 секунды с EXPLAIN следующим образом:
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| 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 |
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
В качестве альтернативы, если я заставлю исходный запрос использовать индекс 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 |
+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
Наконец, если я использую исходный 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
и referrersiteid
и упорядочиваю по __id, поэтому у меня есть индекс, определенный как (systemcreated, referrersiteid, __id), но MySQL все еще использует ПЕРВИЧНЫЙ ключ.
Какие-либо предложения?
Сколько у вас оперативной памяти?
ANALYZE TABLE ничего не изменило. На сервере 16 Гб.
"Средняя_длина_строки: 767640"; много MEDIUMTEXT
. Строка ограничена примерно 8 КБ; переполнение переходит в блоки «не для записи». Чтение этих блоков требует дополнительных обращений к диску.
SELECT *
потянется за всеми этими толстыми столбиками. Всего будет около 50 чтений (по 16 КБ каждое). Это требует времени.
(Exp 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
И сохраните вторичный индекс с тремя столбцами, который у вас есть. Даже если он должен сканировать намного больше, чем 16 строк, чтобы найти нужные 16, он намного менее громоздкий. Это означает, что подзапрос ("производная таблица") будет выполняться умеренно быстро. Тогда внешний запрос по-прежнему будет иметь 16 поисков - возможно, 16 * 50 блоков для чтения. Общее количество прочитанных блоков все равно будет намного меньше.
Редко бывает заметная разница между ASC
и DESC
на ORDER BY
.
Почему оптимизатор выбирает ПК вместо, казалось бы, лучшего вторичного индекса? PK может быть лучшим, особенно если 16 строк находятся в «конце» (DESC) таблицы. Но это был бы ужасный выбор, если бы ему пришлось сканировать всю таблицу, не найдя 16 строк.
Между тем, проверка по подстановочным знакам делает вторичный индекс полезным лишь частично. Оптимизатор принимает решение на основе неадекватной статистики. Иногда это похоже на подбрасывание монеты.
Если вы используете мою переформулировку наизнанку, то я рекомендую следующие два составных индекса. Оптимизатор может сделать полуинтеллектуальный, полуправильный выбор между ними для производной таблицы:
INDEX(systemcreated, referrersiteid, __id),
INDEX(referrersiteid, systemcreated, __id)
Он будет продолжать говорить «сортировка по файлам», но не волнуйтесь; это только сортировка 16 строк.
И помните, SELECT *
вредит производительности. (Хотя, возможно, вы не можете это исправить.)
Большое спасибо! Ваше предложение добавить отдельные индексы для systemcreated
и referrersiteid
помогло. На самом деле, INDEX(referrersiteid)
в одиночку сделал эту работу. Я подозреваю, что моя проблема могла быть как-то связана с кардинальностью данных — systemcreated имеет только несколько уникальных значений, а referrersiteid — десятки тысяч.
@SamCrawford - Поскольку вы используете тест диапазона, кардинальность - это не показатель, а количество строк, удовлетворяющих LIKE '...%'
. Сохраните оба индекса; Оптимизатор динамически определяет, что лучше для каждого запроса.
Понятно, спасибо. И, перечитав ваш первоначальный ответ, я теперь понимаю, что если бы у меня был исходный вторичный индекс (referrersiteid, systemcreated, __id), то все было бы в порядке. Я не знал, что индекс перестал использоваться после первого успешного совпадения диапазона. Так что еще раз спасибо за улучшение моих знаний!
@SamCrawford - у меня есть еще советы здесь: mysql.rjweb.org/doc.php/index_cookbook_mysql
Может быть, запуск
ANALYZE TABLE referrals
позволяет СУБД принять лучшее решение?