Автор: Чжоу Мэнкан
В одном из моих бизнес-сценариев мне нужно было запросить последние пять записей данных аудита.
SELECT `id`, `title`
FROM `th_content`
WHERE `audit_time` < 1541984478
AND `status` = 'ONLINE'
ORDER BY `audit_time` DESC, `id` DESC
LIMIT 5;
Данные мониторинга показали, что загрузка ЦП в то время превышала 100%, и я заметил, что многие похожие запросы имели статус create sort index
после выполнения show processlist
.
Посмотреть структуру таблицы
CREATE TABLE `th_content` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(500) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '内容标题',
`content` mediumtext CHARACTER SET utf8 NOT NULL COMMENT '正文内容',
`audit_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '审核时间',
`last_edit_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最近编辑时间',
`status` enum('CREATED','CHECKING','IGNORED','ONLINE','OFFLINE') CHARACTER SET utf8 NOT NULL DEFAULT 'CREATED' COMMENT '资讯状态',
PRIMARY KEY (`id`),
KEY `idx_at_let` (`audit_time`,`last_edit_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Индекс имеет составной индекс с audit_time
слева и не имеет индекса, связанного с status
.
Проанализируйте логику выполнения предыдущего оператора SQL:
- Найдите все идентификаторы первичных ключей, которые меньше времени аудита, из составного индекса. (Если до этой отметки времени был проведен аудит 1 миллиона записей данных, идентификаторы первичных ключей этих 1 миллиона записей данных будут извлечены из составного индекса.)
- Отсортируйте 1 миллион идентификаторов. Эта операция сортировки предназначена для оптимизации операций ввода-вывода на следующем этапе извлечения данных в таблице, поскольку близкие первичные ключи могут быть извлечены с помощью одной операции ввода-вывода на диске.
- Получите соответствующий 1 миллион записей данных в таблице и просканируйте полученные записи, чтобы отфильтровать строки со статусом «
ONLINE
». - Отсортируйте окончательные результаты запроса (если 500 000 строк имеют статус ONLINE, продолжайте сортировку 500 000 строк).
Хотя требуется получить только пять строк, в предыдущем примере, который я привел, запрос фактически сканирует 1 миллион строк и сортирует 500 000 строк в памяти базы данных из-за большого объема данных.
Следовательно, производительность запроса крайне низкая.
Я нарисовал следующую диаграмму, чтобы показать процесс запроса на шаге 1, где розовые части указывают строки данных, которые необходимо получить в таблице.
Я создал и ввел некоторые данные в соответствии с индексом и шаблоном хранения. Если вы обнаружите какие-либо ошибки, не стесняйтесь оставлять комментарии и дайте мне знать. Я надеюсь, что эта диаграмма поможет вам понять, как хранится составной индекс и как использовать индексы в запросах.
Идея улучшения 1
Не всегда просто использовать индексы в запросах диапазона. Если добавить составной индекс с audit_time
и status
, какие улучшения мы можем получить?
ALTER TABLE `th_content` ADD INDEX `idx_audit_status` (`audit_time`, `status`);
mysql> explain select `id`, `title` from `th_content` where `audit_time` < 1541984478 and `status` = 'ONLINE' order by `audit_time` desc, `id` desc limit 5;
+----+-------------+------------+-------+------------------------------------------+------------------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+------------------------------------------+------------------+---------+------+--------+-------------+
| 1 | SIMPLE | th_content | range | idx_at_ft_pt_let,idx_audit_status | idx_audit_status | 4 | NULL | 209754 | Using where |
+----+-------------+------------+-------+------------------------------------------+------------------+---------+------+--------+-------------+
Подробности: поскольку audit_time
является запросом диапазона, индекс во втором столбце не нужен, и будет использоваться только audit_time
. Следовательно, key_len
равно 4. В следующей идее улучшения 2, где включены те же два поля, key_len
равно 5.
Давайте также проанализируем процесс выполнения после добавления этого индекса:
- Найдите строку с наибольшим значением
audit_time
, меньшим, чем это значение времени аудита, из составного индекса. - Найдите другие строки с меньшими
audit_time
значениями, чем это значение времени аудита. Поскольку ‹audit_time - это запрос диапазона, а значения индекса во втором столбце разбросаны, нам нужно продолжить поиск совпадающих строк одну за другой и найти все строки индекса, соответствующие критерию (status = '_ 17_'), пока мы не получим пятый соответствующий ряд. - Получите и запросите определенные записи данных в таблице.
На предыдущей диаграмме розовым цветом обозначены строки, отвечающие требованиям первого столбца индекса. Продолжите запрос, и на этом листовом узле будут найдены три записи. Переместитесь влево и выполните запрос на предыдущем листовом узле. Когда будут найдены пять совпадающих строк, извлеките соответствующие записи данных в таблице.
Реализованные улучшения
Поскольку индекс содержит status
значений, не требуется извлекать соответствующие данные в процессе фильтрации строк со статусом «ONLINE». iops
значительно уменьшается, потому что в таблице необходимо получить только пять строк данных.
Недостаток этого индекса
Если пять строк, просканированных в idx_audit_status
, все имеют status
ONLINE
, нужно сканировать только пять строк.
Если только четыре строки из первых 1 миллиона строк, просканированных в idx_audit_status
, имеют status
ONLINE
, необходимо просканировать 1 миллион и 1 строку, чтобы получить необходимые пять строк. Число строк, которые нужно просканировать индексом, неизвестно.
Идея улучшения 2
ALTER TABLE `th_content` DROP INDEX `idx_audit_status`;
ALTER TABLE `th_content` ADD INDEX `idx_status_audit` (`status`, `audit_time`);
Это позволяет повысить эффективность как сортировки, так и извлечения данных в таблице.