Новое в журнале. Маленькие pull request: почему их быстрее ревьюят и как резать задачу
Разборы и проверки

Разбор медленного запроса в MySQL 8: EXPLAIN, индексы и EXPLAIN ANALYZE

Запрос, который тормозит на проде, почти всегда можно объяснить через план выполнения. Разбираем реальный EXPLAIN по полям: type, rows, key, и когда нужен EXPLAIN ANALYZE.

14 июля 20269 мин чтенияПродвинутым
Разбор медленного запроса в MySQL 8: EXPLAIN, индексы и EXPLAIN ANALYZE
12 полейв стандартном выводе EXPLAIN
1 запросEXPLAIN ANALYZE реально выполняет его

В отличие от баз данных, где администратор отдельно настраивает сервер, большинство веб-разработчиков встречаются с производительностью запросов уже на уровне кода приложения: ORM собрал запрос, запрос ушёл в базу, где-то в середине цепочки пользователь увидел крутящийся индикатор загрузки на несколько секунд. Полезно уметь дойти от этого симптома до конкретной причины самостоятельно, не дожидаясь отдельного специалиста по базам данных.

Запрос, который раньше выполнялся за миллисекунды, через полгода роста таблицы начинает выполняться секундами. Первая реакция многих разработчиков, добавить индекс наугад на столбец, который используется в условии WHERE, иногда помогает, иногда нет, а иногда делает только хуже за счёт лишней нагрузки на вставку. Правильный путь начинается не с догадки, а с плана выполнения: команда EXPLAIN показывает, как оптимизатор MySQL собирается выполнить запрос, и по этому плану видно, где он теряет время.

Как получить план выполнения

Чтобы увидеть план, достаточно поставить слово EXPLAIN перед запросом. MySQL не выполняет сам запрос, а строит и показывает план, который оптимизатор счёл бы лучшим. Для запросов на выборку данных этого обычно достаточно для первого анализа.

EXPLAIN
SELECT o.id, o.total, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'pending'
  AND o.created_at > '2026-06-01';

Вывод EXPLAIN это таблица из одной строки на каждую таблицу, участвующую в запросе. Для запроса выше их будет две: одна строка для orders, другая для customers. Разбирать нужно каждую строку отдельно, потому что медленным может быть доступ к любой из таблиц в соединении.

Главная мысльEXPLAIN не говорит, медленный у вас запрос или быстрый, он говорит, как оптимизатор планирует его выполнить, а выводы делает тот, кто умеет это читать.

Главные поля: type, key и rows

Среди полей вывода EXPLAIN три дают больше всего информации для быстрого диагноза: type показывает способ доступа к таблице, key показывает, какой индекс реально использован, а rows показывает оценку оптимизатора, сколько строк придётся просмотреть.

Значение typeЧто значитНасколько это хорошо
const / eq_refПоиск по первичному ключу или уникальному индексу, одна строкаотлично, лучший возможный случай
refПоиск по неуникальному индексухорошо, если rows невелико
rangeДиапазон по индексу (BETWEEN, >, <)нормально, зависит от размера диапазона
indexПолное чтение индекса без поиска по таблицеприемлемо, но часто можно лучше
ALLПолное сканирование таблицы, индекс не используетсяплохо на больших таблицах

Если type равен ALL для таблицы с миллионами строк, а в условии WHERE есть столбец без индекса, причина почти наверняка найдена: оптимизатору ничего не остаётся, кроме как прочитать таблицу целиком и проверить условие у каждой строки. Поле key при этом будет пустым или NULL, что подтверждает: подходящего индекса нет.

Поле rows это не точное число, а статистическая оценка оптимизатора, основанная на собранной статистике по таблице. Если эта оценка сильно расходится с реальностью, например оптимизатор ожидает сто строк, а на деле их миллион, это сигнал устаревшей статистики, и команда ANALYZE TABLE может заметно улучшить качество последующих планов.

Не добавляйте индекс только потому, что столбец встречается в WHERE. Индекс полезен, если он реально снижает число просматриваемых строк и оправдывает цену на каждую вставку и обновление. Индекс на столбец с низкой избирательностью, например булево поле со значениями true и false пополам, почти не ускорит выборку.

Составные индексы и порядок столбцов

Для запроса из первого примера с условиями по status и created_at одного индекса на каждый столбец отдельно недостаточно: MySQL может использовать только один индекс на таблицу за раз в обычном плане, и тогда второе условие придётся проверять построчно после первого отбора. Составной индекс по обоим столбцам сразу решает эту задачу, но порядок столбцов в нём важен.

-- столбец с равенством (status) идёт первым,
-- столбец с диапазоном (created_at) вторым
CREATE INDEX idx_orders_status_created
  ON orders (status, created_at);

Правило порядка простое: столбцы, по которым в запросе стоит точное равенство, идут в индексе раньше столбцов с диапазоном или сортировкой. Если поставить created_at первым, индекс всё равно будет использован, но менее эффективно: диапазон по первому столбцу индекса ограничивает возможность использовать второй столбец для дальнейшей фильтрации в рамках того же индекса.

Вы в начале разбораДальше: разбор компромиссов, чек-лист и ответы на частые вопросы.

EXPLAIN ANALYZE: когда план нужно проверить на практике

Обычный EXPLAIN показывает только теоретический план, построенный по статистике, но не гарантирует, что в реальности всё будет именно так: статистика может быть неточной, а реальное время выполнения зависит ещё и от того, какие данные уже лежат в буферном пуле. EXPLAIN ANALYZE, доступный начиная с MySQL 8.0.18, реально выполняет запрос и показывает не только план, но и фактическое время на каждом его шаге.

EXPLAIN ANALYZE
SELECT o.id, o.total, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'pending'
  AND o.created_at > '2026-06-01';

Вывод EXPLAIN ANALYZE устроен как дерево операций с фактическим временем и числом строк на каждом узле, от самых внутренних операций до верхней. Это особенно полезно для запросов с несколькими соединениями, где обычный EXPLAIN не показывает, на каком именно шаге теряется время: соединение может быть само по себе быстрым, а затраты уходить на сортировку результата или временную таблицу.

Важная оговорка: EXPLAIN ANALYZE реально выполняет запрос, включая изменяющие таблицу операции в более сложных случаях с подзапросами. Для запросов на чтение это не проблема, но для отладки запроса, который меняет данные, или очень тяжёлого запроса на проде, лучше сначала проверить на копии базы или в непиковые часы.

Типичная последовательность разбора на практике

  1. Снять EXPLAIN на обычном запросе, найти строки с type ALL или index на больших таблицах
  2. Проверить, какой индекс ожидался и почему оптимизатор его не выбрал: возможно, индекса просто нет, либо он не покрывает нужные столбцы
  3. Если индекс есть, но план всё равно плохой, обновить статистику через ANALYZE TABLE и повторить EXPLAIN
  4. Для подтверждения реального эффекта изменения снять EXPLAIN ANALYZE до и после добавления индекса на одинаковых условиях

Частые ошибки при разборе плана выполнения

Первая ошибка, смотреть только на итоговое время выполнения запроса и не заглядывать в план вообще, если время кажется приемлемым. Запрос может выполняться быстро на тестовой базе с тысячей строк и стать проблемой, когда таблица вырастет до миллиона, причём план с полным сканированием уже сейчас показывает эту уязвимость заранее, если на него посмотреть.

Вторая ошибка, добавлять индекс и сразу считать задачу решённой без повторной проверки EXPLAIN. Иногда новый индекс не используется вовсе, потому что оптимизатор предпочёл другой план по своим соображениям, или потому что запрос написан так, что индекс физически не может быть применён, например из-за функции, применённой к индексируемому столбцу прямо в условии WHERE.

-- индекс по created_at не будет использован в таком виде,
-- потому что функция YEAR() применяется к самому столбцу
SELECT * FROM orders WHERE YEAR(created_at) = 2026;

-- индекс сработает, если переписать условие как диапазон
SELECT * FROM orders
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';

Третья ошибка, оптимизировать запрос в отрыве от того, как он используется на практике: пагинация с большим смещением, например страница с номером пятьсот, заставляет MySQL просмотреть и отбросить все строки до нужного смещения, даже если по индексу находит начальную точку быстро. В таких случаях решение часто не в индексе, а в смене подхода к пагинации, например через курсор по последнему увиденному идентификатору вместо числового смещения.

Чтение EXPLAIN не требует запоминания всех двенадцати полей вывода наизусть. Для большинства медленных запросов достаточно последовательно проверить type, key и rows по каждой таблице в соединении, понять, какой индекс реально нужен и в каком порядке столбцов его строить, а затем подтвердить улучшение через EXPLAIN ANALYZE на реальных данных, а не только по теоретическому плану.

Частые вопросы

Почему MySQL иногда не использует индекс, даже если он существует?

Оптимизатор может посчитать, что при низкой избирательности индекса или малом размере таблицы полное сканирование окажется быстрее, чем обращение к индексу и затем к самой таблице по каждой найденной строке.

Можно ли запускать EXPLAIN ANALYZE прямо на продовой базе?

Для обычных запросов на чтение это безопасно, но запрос реально выполняется, поэтому для тяжёлых или редких запросов лучше сначала проверить на копии базы либо выполнять в период низкой нагрузки.

Сколько индексов можно добавить на одну таблицу без вреда?

Жёсткого числа нет, но каждый индекс замедляет вставку, обновление и удаление и занимает место на диске. Разумный подход: индекс строится под конкретный частый запрос, а не про запас на будущее.

Код, который не стыдно передать

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

Случайный разбор