B2BPRO.KZ | Рекламное агентство в Алматы

Оптимизация базы данных WordPress: почему сайт тормозит со временем

Схема базы данных WordPress: слои таблиц, забитые лишними данными, и очистка

Почему сайт, который год назад летал, теперь думает по три секунды

Свежая установка WordPress работает быстро почти всегда. Десяток страниц, пять плагинов, база данных в пару мегабайт, тут трудно что-то испортить. Проблемы начинаются на втором или третьем году жизни проекта, когда контента стало в сто раз больше, плагины ставили и удаляли, дизайн меняли дважды, а хостинг остался тот же. И вот админка открывается по восемь секунд, каталог подтормаживает, а владелец бизнеса пишет подрядчику: «Сайт тормозит, сделайте что-нибудь».

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

Как устроена база WordPress и где она обычно распухает

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

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

В wp_postmeta хранятся метаполя к этим записям. Соотношение обычно кратное: одна запись товара в WooCommerce тянет за собой два-три десятка строк метаданных. Это самая объёмная таблица на большинстве коммерческих сайтов.

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

Последняя пара, wp_comments и wp_commentmeta, накапливает комментарии вместе со спамом, который никто не удалял с момента запуска.

Спам заслуживает отдельного слова. На сайте с открытыми комментариями и без работающего фильтра за пару лет накапливаются десятки тысяч записей, помеченных как спам. Посетителям они не показываются, но лежат в тех же таблицах и раздувают индексы наравне с настоящими комментариями.

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

wp_options и автозагрузка: главный подозреваемый

У таблицы wp_options есть колонка autoload. Опции, помеченные для автозагрузки, WordPress вытягивает одним запросом при каждом обращении к сайту, до того как станет ясно, какая именно страница нужна посетителю. Механизм разумный: держать в памяти настройки темы и активных плагинов дешевле, чем ходить за каждой настройкой отдельно.

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

В WordPress 6.6 это поведение поправили на уровне ядра. Колонка autoload перестала быть бинарной: вместо прежних yes и no появились пять значений, on, off, auto, auto-on и auto-off. Первые два означают явное решение разработчика, остальные три решение, принятое ядром автоматически. Функция wp_autoload_values_to_autoload() определяет, какие из этих значений действительно приводят к автозагрузке.

Вместе с новыми значениями появилась эвристика: если размер опции превышает 150 000 байт и разработчик не потребовал автозагрузку явно, WordPress запишет её как auto-off. Порог настраивается фильтром wp_max_autoloaded_option_size. Тогда же в «Здоровье сайта» добавили проверку, которая помечает критическую проблему, если суммарный объём автозагружаемых опций превысил 800 КБ.

Оговорка тут существенная: механизм работает для новых опций. Всё, что накопилось в базе до обновления, так и осталось помеченным на автозагрузку. Поэтому на медленном сайте с историей я в первую очередь смотрю раздел «Инструменты → Здоровье сайта → Информация» и данные о размере автозагружаемых опций. Если там сотни килобайт или мегабайты, дальше можно не искать.

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

Ревизии, автосохранения и корзина

WordPress по умолчанию сохраняет копию записи при каждом сохранении. Для редакции это удобно, можно откатиться к любой версии текста. Для сайта с длинной историей это означает, что одна страница «О компании», которую правили сорок раз за три года, лежит в базе сорок один раз.

Поведение регулируется константой WP_POST_REVISIONS в wp-config.php. Её можно отключить полностью, а можно ограничить числом и оставить, скажем, последние пять версий. Второй вариант практичнее: страховка на случай неудачной правки сохраняется, а бесконечное накопление прекращается.

Рядом работает AUTOSAVE_INTERVAL, интервал автосохранения черновика, по умолчанию 60 секунд. Для команды, которая пишет длинные материалы прямо в админке, увеличивать его не стоит: потерять час работы дороже, чем хранить лишние строки.

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

Отдельная история со статусом auto-draft. Такие черновики создаются каждый раз, когда кто-то нажал «Добавить запись» и передумал. Ядро их подчищает, но на сайтах, где редакторы часто открывают и закрывают редактор, счёт идёт на тысячи.

Метаполя, пережившие свои плагины

Здесь начинается самая грязная часть работы. Плагины пишут данные в wp_postmeta, wp_usermeta, wp_termmeta и в собственные таблицы. Когда плагин удаляют через админку, WordPress удаляет его файлы. Данные в базе он не трогает, если только автор плагина не написал специальный обработчик деинсталляции, а пишут его далеко не все.

В результате на трёхлетнем сайте лежат метаполя SEO-плагина, которым пользовались первые полгода. Настройки конструктора страниц, от которого отказались при редизайне. Служебные поля системы бронирования, которую тестировали неделю. Каждая такая строка привязана к записи и попадает в выборку, когда WordPress собирает метаданные поста.

Ещё хуже метаполя-сироты: строки, у которых поле post_id ссылается на давно удалённую запись. Они не используются никогда и ни при каких условиях, но занимают место и раздувают индексы.

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

Транзиенты: кэш, который не всегда убирает за собой

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

Когда на сайте не установлен постоянный объектный кэш, транзиенты хранятся в wp_options под именами с префиксами _transient_ и _transient_timeout_, а сетевые варианты с _site_transient_ и _site_transient_timeout_. Каждый транзиент занимает две строки: само значение и метку времени.

Просроченные транзиенты не исчезают в момент истечения срока. Официальная документация формулирует это осторожно: WordPress очищает их нечасто. Функция delete_expired_transients() существует и удаляет просроченные пары, но у неё есть особенность. При подключённом внешнем объектном кэше она по умолчанию ничего не делает, пока её не вызвать принудительно.

Отсюда практика: на сайте с активными плагинами интеграций в wp_options могут лежать десятки тысяч строк транзиентов, часть из которых просрочена месяцы назад. Удалять их безопаснее, чем что-либо ещё в базе. Транзиент по определению временный, и любой корректно написанный плагин переживёт его пропажу, просто сходит за данными заново.

Как отличить проблему базы от проблемы хостинга

Прежде чем чистить, стоит убедиться, что чистить нужно именно базу. На неё указывают три признака.

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

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

Сайт заметно проседает в моменты, когда идёт индексация роботами. Роботы ходят по страницам, которых нет в кэше, и каждая такая страница это полный цикл обращений к базе.

Для точной диагностики нужны инструменты, а не ощущения. Плагин Query Monitor показывает список запросов конкретной страницы с временем выполнения каждого и указанием, какой плагин его инициировал, этого достаточно, чтобы найти виновника за один заход. На стороне сервера помогает журнал медленных запросов MySQL: он покажет, какие запросы стабильно выходят за порог, даже если в браузере проблема воспроизводится не всегда.

Порядок действий, который не ломает работающий сайт

Оптимизация базы данных WordPress обратима ровно до того момента, пока есть бэкап. Дальше всё зависит от дисциплины.

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

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

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

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

После этого ревизии. Проредить накопленное и сразу прописать WP_POST_REVISIONS в wp-config.php, чтобы через полгода не возвращаться к тому же.

Шестой шаг это дефрагментация. После массовых удалений таблицы остаются с «дырами». Команда WP-CLI wp db optimize запускает mysqlcheck с ключом оптимизации, что на уровне MySQL означает OPTIMIZE TABLE и дефрагментацию данных и индексных файлов. Делать это стоит в часы низкого трафика.

И последнее, до чего доходят немногие, хотя на нагруженных проектах оно даёт больше остальных пунктов вместе взятых: индексы и объектный кэш. Если после чистки конкретные запросы всё ещё медленные, дело в отсутствующих индексах на больших таблицах метаданных. А постоянный объектный кэш на Redis или Memcached снимает с базы повторные чтения настроек и транзиентов вообще, и тогда wp_options перестаёт быть узким местом.

Есть ещё один пункт, актуальный для сайтов с длинной историей переездов: движок таблиц. MySQL использует блокировку на уровне таблицы для MyISAM, позволяя обновлять такую таблицу только одной сессии за раз, и построчную блокировку для InnoDB, что даёт одновременный доступ на запись нескольким сессиям. На проекте, который переносили с хостинга на хостинг несколько раз, часть таблиц может до сих пор оставаться на MyISAM, и под нагрузкой это выливается в очередь из ждущих запросов. Проверить движок по каждой таблице стоит заодно с замером их размеров.

Отдельно про аварийный инструмент. В wp-config.php можно объявить константу WP_ALLOW_REPAIR, после чего по адресу /wp-admin/maint/repair.php откроется страница восстановления базы, доступная без авторизации. Это средство для ситуации, когда сайт уже упал с ошибкой соединения с базой. В обычной оптимизации оно не участвует, а константу после использования нужно убрать, иначе страница останется открытой для всех.

Типичный сценарий на живом проекте

Собирательный пример, такие проекты приходят к нам регулярно. Интернет-магазин на WooCommerce, около двенадцати тысяч товаров, работает четвёртый год. Жалоба: админка открывается по десять-двенадцать секунд, редактирование товара стало мучением, публичная часть терпимая за счёт кэширования, но карточки товаров тоже подтормаживают. Хостинг менялся дважды, каждый раз с временным улучшением.

При разборе выясняется знакомая картина. Объём автозагружаемых опций несколько мегабайт, из них львиная доля приходится на две опции: кэш конструктора страниц и накопленный лог плагина синхронизации с 1С, который писал историю обменов прямо в wp_options. В wp_posts десятки тысяч ревизий товаров, потому что цены обновлялись импортом, и каждый импорт создавал ревизию на каждый товар. В wp_postmeta метаполя трёх SEO-плагинов, сменявших друг друга.

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

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

Регламент вместо разовой чистки

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

Минимальный регламент, который окупается на любом коммерческом проекте, выглядит так. Раз в квартал: проверка «Здоровья сайта» на предмет автозагружаемых опций, замер размеров ключевых таблиц, чистка спама и просроченных транзиентов. Раз в полгода: ревизия установленных плагинов, что реально используется, а что осталось от прошлых экспериментов и продолжает писать в базу. При каждом удалении плагина проверка, остались ли после него данные, и решение, что с ними делать, пока помните контекст.

Плюс два постоянных правила в wp-config.php: ограничение ревизий и разумный срок жизни корзины. Их прописывают один раз, и половина будущих проблем просто не возникает.

Если внутри компании нет специалиста, который возьмёт это на себя, регламент проще передать подрядчику вместе с остальной поддержкой. Разработка и поддержка сайтов на WordPress у нас включает именно такое сопровождение, а не только доработки по заявкам. Сайт, за базой которого следят, не требует спасательной операции раз в два года.

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

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

Можно ли доверить чистку плагину оптимизации?
Для простого сайта на стандартной теме да, с обязательным бэкапом перед запуском. Для магазина, портала или сайта с самописной логикой автоматическая чистка опасна: плагин не знает, какие метаполя использует ваша кастомная функциональность, и может удалить нужное. Там, где цена ошибки высока, разбор делают вручную.

Как часто нужно оптимизировать базу данных WordPress?
Полноценный разбор раз в год или при появлении жалоб на скорость. Рутинная чистка спама, транзиентов и корзины раз в квартал. Ограничение ревизий ставится один раз и работает постоянно.

Что делать, если сайт тормозит, а база в порядке?
Смотреть дальше по цепочке: количество и качество активных плагинов, тему, внешние скрипты, версию PHP, ресурсы хостинга, наличие кэширования. База частая причина, но далеко не единственная. Query Monitor и журнал медленных запросов покажут, где именно теряется время, и избавят от гадания.

Стоит ли отключать ревизии полностью?
Как правило, нет. Ревизии много раз спасали ситуацию, когда автор переписал текст и захотел вернуть предыдущую версию. Разумный компромисс это ограничить их числом от трёх до десяти в зависимости от того, как часто правят контент.

Опасно ли удалять транзиенты?
Это самая безопасная категория. Транзиент по своей природе временный, и плагин, который его создал, обязан корректно отработать ситуацию, когда значения нет, просто получит данные заново. Единственное последствие это небольшая дополнительная нагрузка сразу после чистки, пока кэш наполняется.

Прокрутить вверх