MySQLSeniorExperience
Как понять, что проблема связана со схемой, индексом, запросом, настройками, диском или инфраструктурой?
Диагностику ведут послойно: slow query log и Performance Schema выявляют симптом; EXPLAIN ANALYZE, SHOW STATUS, iostat и sar изолируют слой — схема, индекс, запрос, innodb_buffer_pool, диск или CPU steal на хосте.
Диагностика деградации производительности MySQL: системный подход
Когда запросы начинают работать медленнее, причина может быть в одном из шести слоёв: схема, индекс, запрос, настройки сервера, дисковая подсистема или инфраструктура. Их нужно исключать последовательно.
Шаг 1 — Установите точку отсчёта
Сначала выясните, когда всё было нормально и что изменилось. Проверьте slow query log и Performance Schema:
-- Включить slow query log (если ещё не включён)
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 0.5; -- секунды
SET GLOBAL log_queries_not_using_indexes = 1;
-- Топ-10 самых дорогих запросов за всё время
SELECT digest_text, count_star, avg_timer_wait/1e9 AS avg_sec,
sum_rows_examined, sum_rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY avg_timer_wait DESC
LIMIT 10;
Шаг 2 — Схема
Ошибки схемы проявляются при росте данных или смене паттернов доступа:
- Тип данных не совпадает с паттерном: VARCHAR(255) для UUID вместо BINARY(16) — увеличивает размер индекса в 2–3 раза.
- Отсутствие или неправильный charset: utf8mb4 vs latin1 — implicit conversion при JOIN уничтожает использование индексов.
- Ненормализованная схема с огромными JSON-полями, которые сканируются WHERE JSON_EXTRACT(...).
-- Проверить коллации — несоответствие ломает использование индексов
SELECT table_name, column_name, character_set_name, collation_name
FROM information_schema.columns
WHERE table_schema = 'your_db'
AND data_type IN ('varchar', 'char', 'text');
Шаг 3 — Индексы
-- Неиспользуемые индексы (MySQL 8.0+)
SELECT object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0
AND object_schema NOT IN ('mysql', 'performance_schema')
ORDER BY object_name;
-- Анализ конкретного запроса
EXPLAIN FORMAT=TREE
SELECT * FROM orders WHERE customer_id = 42 AND status = 'pending';
-- Смотрим: type=ALL → нет индекса, type=index → полный скан индекса, type=ref → хорошо
-- Создать составной индекс по паттерну (equality, then range)
ALTER TABLE orders ADD INDEX idx_customer_status (customer_id, status, created_at);
Шаг 4 — Сам запрос
Частые anti-patterns:
- Функция на индексируемой колонке:
WHERE YEAR(created_at) = 2024— индекс не используется. - Implicit conversion:
WHERE user_id = '123'при INT колонке — планировщик приводит типы, иногда со сканом. - SELECT * в подзапросе, где нужен только COUNT.
- OFFSET в пагинации:
LIMIT 20 OFFSET 100000— сканирует и выбрасывает 100 000 строк.
-- Вместо OFFSET используйте keyset pagination
SELECT id, title FROM articles
WHERE id > :last_seen_id
ORDER BY id
LIMIT 20;
Шаг 5 — Настройки сервера
-- Главные переменные InnoDB
SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; -- должно быть 70-80% RAM
SHOW VARIABLES LIKE 'innodb_io_capacity'; -- IOPS диска
SHOW VARIABLES LIKE 'innodb_log_file_size'; -- для write-heavy: 1–4 GB
-- Статус буферного пула
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests < 0.01 — хорошо
Если innodb_buffer_pool_size меньше размера «горячих» данных — будет постоянный physical I/O. Увеличьте до 70–80% доступной RAM.
Шаг 6 — Диск
iostat -xm 2 10 # await > 5 ms для SSD — проблема
iostat -xm 2 10 | awk 'NR>1 && /^[a-z]/{print $1, $9, $10, $16}'
# Device, r_await, w_await, %util
# Размер tablespace и фрагментация
SELECT table_name,
ROUND(data_length/1024/1024, 1) AS data_mb,
ROUND(data_free/1024/1024, 1) AS free_mb
FROM information_schema.tables
WHERE table_schema = 'your_db'
ORDER BY data_length DESC
LIMIT 10;
-- data_free большой -> OPTIMIZE TABLE или REBUILD INDEX
Шаг 7 — Инфраструктура
- CPU steal time > 5% на VPS означает конкуренцию с соседями — мигрируйте на dedicated или bare metal.
- Network latency между приложением и БД: даже 1 мс на каждый из 50 запросов в транзакции = +50 мс.
- Репликационный лаг: приложение читает с реплики, которая отстала на 30 с.
# Проверить CPU steal на Linux
top -b -n 1 | head -5
# или
sar -u 1 5 | tail -1 # %steal
Подводные камни
- EXPLAIN без ANALYZE показывает план, а не факт — строки оценочные. Используйте EXPLAIN ANALYZE (MySQL 8.0.18+) для реального времени выполнения.
- Новый индекс может замедлить запись на таблице с высоким write-throughput — всегда профилируйте INSERT/UPDATE после добавления индекса.
- innodb_buffer_pool_size применяется при рестарте, а не сразу (в MySQL 5.7+ есть динамическое изменение, но с осторожностью).
- OPTIMIZE TABLE создаёт полную копию таблицы и блокирует её — используйте pt-online-schema-change или ALTER TABLE ENGINE=InnoDB через gh-ost.
- Performance Schema сама потребляет CPU и память (5–10%) — включайте только нужные инструменты, не все сразу.
- Медленный slow log на диске может маскировать дисковую проблему: если сам лог пишется на тот же диск, что и данные, iostat покажет завышенные цифры.
- Автоматический ANALYZE TABLE при перезапуске обновляет статистику — план может поменяться без изменения кода.
- Проблема инфраструктуры (CPU steal, сетевой джиттер) часто маскируется под проблему запроса — всегда проверяйте системные метрики параллельно с MySQL.
What hurts your answer
- Сразу обвинять MySQL, не проверив соседние слои системы
- Чинить симптом без минимального воспроизведения и evidence
- Не учитывать версии, конфигурацию, окружение и recent changes
What they're listening for
- Умеет локализовать проблему вокруг MySQL
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения