Запрос MySQL медленный только при использовании поля ORDER BY DESC и LIMIT

Обзор

Я использую 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.

Эксперимент 1

Если я изменю запрос, чтобы использовать 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 |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

Эксперимент 2

Если я изменю запрос, чтобы придерживаться 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 |
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+

Эксперимент 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 и referrersiteid и упорядочиваю по __id, поэтому у меня есть индекс, определенный как (systemcreated, referrersiteid, __id), но MySQL все еще использует ПЕРВИЧНЫЙ ключ.

Какие-либо предложения?

Может быть, запуск ANALYZE TABLE referrals позволяет СУБД принять лучшее решение?

Ulrich Thomas Gabor 13.12.2020 01:14

Сколько у вас оперативной памяти?

Rick James 13.12.2020 01:39

ANALYZE TABLE ничего не изменило. На сервере 16 Гб.

Sam Crawford 13.12.2020 18:19
Освоение архитектуры микросервисов с Laravel: Лучшие практики, преимущества и советы для
Освоение архитектуры микросервисов с Laravel: Лучшие практики, преимущества и советы для
В последние годы архитектура микросервисов приобрела популярность как способ построения масштабируемых и гибких приложений. Laravel , популярный PHP...
Как построить CRUD-приложение в Laravel
Как построить CRUD-приложение в Laravel
Laravel - это популярный PHP-фреймворк, который позволяет быстро и легко создавать веб-приложения. Одной из наиболее распространенных задач в...
Освоение PHP и управление базами данных: Создание собственной СУБД - часть II
Освоение PHP и управление базами данных: Создание собственной СУБД - часть II
В предыдущем посте мы создали функциональность вставки и чтения для нашей динамической СУБД. В этом посте мы собираемся реализовать функции обновления...
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
Роли и разрешения пользователей без пакета Laravel 9
Роли и разрешения пользователей без пакета Laravel 9
Этот пост изначально был опубликован на techsolutionstuff.com .
Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
В предыдущей статье мы завершили установку базы данных, для тех, кто не знает.
1
3
980
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
  • "Средняя_длина_строки: 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 — десятки тысяч.

Sam Crawford 13.12.2020 18:18

@SamCrawford - Поскольку вы используете тест диапазона, кардинальность - это не показатель, а количество строк, удовлетворяющих LIKE '...%'. Сохраните оба индекса; Оптимизатор динамически определяет, что лучше для каждого запроса.

Rick James 13.12.2020 18:47

Понятно, спасибо. И, перечитав ваш первоначальный ответ, я теперь понимаю, что если бы у меня был исходный вторичный индекс (referrersiteid, systemcreated, __id), то все было бы в порядке. Я не знал, что индекс перестал использоваться после первого успешного совпадения диапазона. Так что еще раз спасибо за улучшение моих знаний!

Sam Crawford 13.12.2020 22:54

@SamCrawford - у меня есть еще советы здесь: mysql.rjweb.org/doc.php/index_cookbook_mysql

Rick James 14.12.2020 02:23

Другие вопросы по теме