Перенесли базу и получили тормоза: где потерялись индексы
Перенесли базу на новый сервер, данные те же, железо новее — а запросы, которые раньше отрабатывали за миллисекунды, теперь тянутся секундами. Первая мысль обычно про диск или сеть, но в девяти случаях из десяти дело в другом: после переноса план выполнения запросов у планировщика сломался, и он честно перебирает таблицу целиком там, где раньше бил точным индексом. Разбираем, как это диагностировать и почему такое происходит именно после миграции.
Содержание
- Первая реакция: винят железо и сеть
- Реальная диагностика: EXPLAIN ANALYZE говорит правду
- Причина №1: индексы не пережили перенос
- Причина №2: планировщик работает вслепую без ANALYZE
- Причина №3: разная конфигурация СУБД между серверами
- Фикс: пересобираем индексы, статистику и параметры
- Как переносить базу правильно с первого раза
Первая реакция: винят железо и сеть
Сценарий типовой: перенесли PostgreSQL или MySQL на новый сервер — например, с VPS в России на выделенный сервер в Германии или США, — данные проверили, приложение подключилось, всё как будто работает. А потом начинают приходить жалобы: страницы грузятся дольше, отчёты, которые раньше строились за пару секунд, теперь висят по 20-30.
Первым делом смотрят на очевидное:
- Диск. Проверяют
iostat -x 1, смотрят на%utilиawait. Если новый сервер на NVMe, а старый был на SATA SSD, диск обычно не виноват — он быстрее. - Сеть. Меряют пинг до сервера, смотрят задержку между приложением и базой (
ping,mtr). Если приложение и база в одном дата-центре или даже на одной машине, сеть тоже не при чём. - CPU и память.
htop,free -h— часто видят, что ядер стало больше, а памяти минимум столько же, сколько было.
И вот тут наступает момент растерянности: железо объективно не хуже, а иногда прямо лучше — а тормозит сильнее, чем раньше. На этом этапе многие начинают винить «непонятную виртуализацию» нового провайдера или списывать всё на «просто новый сервер другой». Это тупиковый путь: если бы дело было в железе, тормозили бы все запросы примерно одинаково, а на практике обычно тормозят конкретные — как правило, те, что раньше активно использовали индекс.
Реальная причина ищется не в top, а в плане выполнения самого запроса.
Реальная диагностика: EXPLAIN ANALYZE говорит правду
Первое, что нужно сделать при подозрении на деградацию производительности после переноса — не гадать, а посмотреть, как СУБД реально выполняет медленный запрос. Для PostgreSQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE customer_id = 48213 AND status = 'pending';
Смотрите на две вещи: тип узла верхнего уровня и на то, совпадает ли оценка планировщика (rows=...) с фактическим числом строк (actual rows=...).
Если на старом сервере план выглядел так:
Index Scan using idx_orders_customer_id on orders
(cost=0.43..8.45 rows=12 width=124)
(actual time=0.021..0.034 rows=11 loops=1)
а на новом — вот так:
Seq Scan on orders
(cost=0.00..184532.00 rows=11 width=124)
(actual time=0.045..842.113 rows=11 loops=1)
— это прямое доказательство: планировщик перестал использовать индекс и делает полное сканирование таблицы (Seq Scan) вместо точечного поиска по индексу (Index Scan). Разница во времени выполнения (actual time) в сотни раз — это не погрешность и не сеть, это буквально другой алгоритм работы с данными.
Дальше нужно понять, почему план изменился. Причин обычно ровно три, и они почти всегда связаны именно с переносом, а не с самим железом.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПричина №1: индексы не пережили перенос
Самая частая причина — банальная: при переносе скопировались данные, но не индексы. Это происходит, если для миграции использовался не тот метод.
Типичные способы переноса, которые калечат индексы:
| Метод переноса | Что переносится | Риск |
|---|---|---|
pg_dump --data-only / mysqldump --no-create-info | только строки данных | индексы и ограничения не создаются вообще |
Копирование через COPY в CSV и обратно | только содержимое таблиц | структура (индексы, constraints) теряется полностью |
| ETL-инструмент "по умолчанию" (некоторые GUI-клиенты) | данные, иногда без вторичных индексов | зависит от инструмента, легко упустить |
pg_dump полный (schema + data) | схема, данные, индексы, constraints | безопасно — но восстанавливаться должно тоже полностью |
pg_basebackup / физическая репликация | всё, включая индексы, побайтово | безопасно, но не для миграции между разными версиями |
Проверить, что реально приехало на новый сервер, легко: сравните список индексов на старом и новом сервере.
Для PostgreSQL на старом сервере:
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename, indexname;
Выполните тот же запрос на новом сервере и сравните вывод (например, через diff двух файлов):
psql -h old-server -U app -d prod -c "\di+" > /tmp/idx_old.txt
psql -h new-server -U app -d prod -c "\di+" > /tmp/idx_new.txt
diff /tmp/idx_old.txt /tmp/idx_new.txt
Если в diff видно, что часть индексов (особенно составных и частичных — WHERE status = 'pending' и подобных) отсутствует на новом сервере, — вот и причина Seq Scan вместо Index Scan. Планировщику физически нечем воспользоваться.
Для MySQL аналогично: SHOW INDEX FROM orders; на обеих базах и сравнение вывода.
Причина №2: планировщик работает вслепую без ANALYZE
Вторая по частоте причина — даже более коварная, потому что индексы на месте, а план всё равно плохой. Дело в статистике планировщика.
PostgreSQL и MySQL принимают решение «использовать индекс или сканировать таблицу целиком» не наугад, а на основе статистики: сколько строк в таблице, какое распределение значений в столбцах, насколько избирательно условие WHERE. Эта статистика не переносится вместе с данными автоматически — она либо собирается заново командой ANALYZE, либо (при полном pg_dump/pg_restore с флагом статистики, что редкость) частично восстанавливается.
Если после pg_restore или ручного импорта дампа никто не выполнил ANALYZE, у PostgreSQL таблица считается пустой или с устаревшей статистикой — и планировщик может решить, что Seq Scan дешевле, чем Index Scan, просто потому что не знает реальный объём и распределение данных.
Проверить, актуальна ли статистика, можно так:
SELECT relname, n_live_tup, last_analyze, last_autoanalyze
FROM pg_stat_user_tables
WHERE relname = 'orders';
Если last_analyze и last_autoanalyze пустые (NULL) или относятся к моменту сразу после восстановления дампа (когда таблица была ещё формально пуста для планировщика), а n_live_tup не соответствует реальному числу строк — это прямое указание на проблему.
Быстрая проверка теории: выполните ANALYZE на конкретной таблице и повторите EXPLAIN ANALYZE того же запроса.
ANALYZE orders;
Если план тут же вернулся к Index Scan — вы нашли причину, и дальше нужно просто сделать это системно для всей базы (см. раздел с фиксом ниже).
Для MySQL эквивалент — ANALYZE TABLE orders;, а общая статистика по всем таблицам обновляется через mysqlcheck --analyze --all-databases.
Причина №3: разная конфигурация СУБД между серверами
Третья причина проявляется реже, но встречается — особенно если новый сервер настраивали "с нуля" вместо переноса конфига. Даже с индексами на месте и свежей статистикой планировщик может вести себя иначе из-за разных параметров памяти и стоимости операций.
Ключевые параметры PostgreSQL, которые влияют на выбор плана:
work_mem— сколько памяти планировщик готов выделить на одну операцию сортировки/хеширования до ухода на диск. Меньше, чем было — сортировки и джойны на диске, а не в памяти.effective_cache_size— оценка того, сколько данных ОС/СУБД реально держит в кэше страниц. Занижена — планировщик недооценивает выгоду от Index Scan.random_page_cost— во сколько раз случайное чтение страницы дороже последовательного. На NVMe разумно снижать (например, до1.1), но если параметр остался дефолтным4.0(рассчитан на вращающиеся диски), планировщик будет искусственно завышать стоимость Index Scan.shared_buffers— размер общего кэша страниц самой СУБД.
Сравнить текущие значения на старом и новом сервере:
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('work_mem', 'effective_cache_size', 'random_page_cost', 'shared_buffers');
Отдельно стоит проверить версию СУБД: SELECT version(); в PostgreSQL или SELECT VERSION(); в MySQL. Планировщик между мажорными версиями иногда меняет поведение по умолчанию (например, эвристики для параллельных запросов), и старый конфиг, скопированный без ревизии, может конфликтовать с новой версией.
Фикс: пересобираем индексы, статистику и параметры
Когда причина найдена, порядок действий обычно такой — сверху вниз, от самого вероятного к менее вероятному.
1. Восстановить недостающие индексы. Если по diff списка индексов видно, что чего-то не хватает, возьмите определение (indexdef) со старого сервера и выполните на новом:
CREATE INDEX CONCURRENTLY idx_orders_customer_id
ON orders (customer_id)
WHERE status = 'pending';
Флаг CONCURRENTLY важен на боевой базе — он не блокирует таблицу на запись во время построения индекса (правда, работает медленнее и не может выполняться внутри транзакции).
2. Обновить статистику планировщика по всей базе.
ANALYZE;
Без указания таблицы это пересоберёт статистику для всех таблиц в текущей базе. Если база большая и вы хотите приоритизировать конкретные горячие таблицы — сначала явно ANALYZE orders; ANALYZE order_items;, а фоновую полную ANALYZE; запустить отдельно, в период низкой нагрузки.
3. Сверить и подтянуть конфигурацию. Если random_page_cost остался дефолтным на NVMe-сервере или work_mem/effective_cache_size занижены относительно доступной памяти — поправьте в postgresql.conf и перезагрузите конфигурацию:
sudo systemctl reload postgresql
(параметры вроде shared_buffers требуют полного рестарта, а не reload).
4. Проверить автовакуум. Если autovacuum был отключён или настроен нетипично на старом сервере и это перенесли как есть, статистика может протухать снова и снова, даже после ручного ANALYZE. Убедитесь, что autovacuum = on и пороги (autovacuum_analyze_scale_factor) разумны для объёма ваших таблиц.
После каждого шага возвращайтесь к EXPLAIN ANALYZE того же проблемного запроса — это единственный надёжный способ убедиться, что план действительно изменился, а не просто "должен был".
Как переносить базу правильно с первого раза
Чтобы не повторять этот разбор при следующей миграции, стоит один раз выстроить процесс переноса так, чтобы индексы и статистика не терялись.
- Используйте полный дамп схемы и данных, а не только данных. Для PostgreSQL —
pg_dump -Fc mydb > mydb.dump(custom-формат, включает схему, данные, индексы, constraints), восстановление —pg_restore -d mydb --jobs=4 mydb.dump. Флаг--data-onlyоставьте только для узких случаев вроде синхронизации содержимого уже существующей идентичной схемы. - Сразу после восстановления — обязательный
ANALYZE. Это должно быть последним шагом в вашем чек-листе миграции, не "как-нибудь потом". Если пишете скрипт переноса — добавьтеANALYZE;последней командой послеpg_restore. - Сверяйте список индексов до и после тем же
diffиз раздела выше — это занимает минуту, а экономит часы разбора инцидента постфактум. - Переносите конфиг осознанно, а не "по умолчанию". Если старый сервер был настроен под конкретный объём памяти и диск, не запускайте новый с дефолтным
postgresql.conf— либо перенесите настроенные параметры, либо прогоните конфигурацию заново черезpgtuneили аналогичный калькулятор под новое железо. - Для больших баз рассмотрите
pg_basebackupвместо логического дампа — это физическая копия, включающая всё побайтово, без риска потерять индексы. Подходит, если версии PostgreSQL на старом и новом сервере совпадают. - Тестируйте на копии перед продакшеном. Разверните дамп на тестовом сервере, прогоните
EXPLAIN ANALYZEдля 5-10 самых тяжёлых запросов приложения — если планы совпадают со старым сервером, миграция прошла чисто.
Похожий разбор процесса миграции есть в статье про составление плана переноса на новый сервер, а более широкий обзор способов переноса баз между серверами — в материале про миграцию базы данных между серверами. Если хочется системно прогнать восстановление из бэкапа перед боевым переносом — пригодится разбор восстановления базы данных из бэкапа на практике. А если проблема оказалась именно в параметрах памяти планировщика — стоит заглянуть в материал про настройку и тюнинг PostgreSQL на VPS.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Обязательно ли делать ANALYZE вручную, если включён autovacuum?
Да, сразу после восстановления дампа — обязательно. Autovacuum сработает по своим порогам изменений данных, а сразу после pg_restore таблица для него может не выглядеть достаточно "изменившейся", чтобы триггернуть анализ немедленно. Ручной ANALYZE сразу после восстановления убирает это окно неопределённости.
Может ли дело быть одновременно в индексах и в статистике?
Да, это частая комбинация: если использовался --data-only дамп, вы теряете и индексы, и (естественно) их статистику тоже. Сначала восстановите индексы, потом обязательно выполните ANALYZE — одно без другого не даст полной картины планировщику.
Почему на старом сервере с теми же данными всё работало без явного ANALYZE?
Скорее всего там ANALYZE давно отработал через autovacuum в фоне, накопив статистику за месяцы работы базы, а после переноса вы оказались в состоянии "чистого листа" для планировщика — это нормально и ожидаемо, а не признак поломки.
Нужно ли что-то менять, если переносили через физическую репликацию (pg_basebackup), а не логический дамп?
Обычно нет — физическая копия переносит и индексы, и статистику побайтово в неизменном виде. Если тормоза всё равно есть, вероятнее причина №3 — разная конфигурация СУБД или отличающееся железо (например, другой тип диска).
Как быстро понять, какие именно запросы деградировали, если их много?
Включите логирование медленных запросов (log_min_duration_statement = 500 в postgresql.conf для запросов дольше 500 мс) и соберите список за час-два реальной нагрузки — дальше разбирайте EXPLAIN ANALYZE по каждому из топ-5-10, а не гадайте вслепую.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →