WordPress не «раптом починає гальмувати». Він уповільнюється в дуже конкретних, відтворюваних випадках, зазвичай у базі даних. Одного дня ваша головна сторінка завантажується за 400 мс, наступного — за 8 секунд, а PHP-процеси збираються в чергу наче багаж при скасованому рейсі.
Складність не в виборі між MySQL та MariaDB. Складність — вловити «вбивці» запитів, яких тихо додає WordPress (і плагіни): відсутні індекси, надлишок autoload, патологічні LIKE-запити, шторми блокувань, треш тимчасових таблиць і припущення по кешуванню, які ніколи не були вірними. Більшість цього можна виправити без переписування сайту і без перетворення вашої бази даних на арт-проєкт.
MySQL vs MariaDB: що насправді має значення для WordPress
WordPress — це не набір бенчмарків. Це сукупність передбачуваних шаблонів запитів (core) плюс непередбачувані шаблони (плагіни) плюс іноді самонаведені поранення (кастомний код, погані імпорти, «корисні» оптимізації).
Вибір MySQL або MariaDB важить менше, ніж люди думають. Обидві використовують InnoDB, обидві можуть працювати дуже добре, і обидві можуть зробити дуже погано, якщо дозволити кільком таблицям рости без запобіжників.
Короткі відмінності, які проявляються в продакшні
- Налаштування за замовчуванням і поведінка версій відрізняються. Значення за замовчуванням MySQL 8 (і зміни в оптимізаторі) можуть відрізнятися від MariaDB 10.x/11.x в крайніх випадках. Для WordPress більшість болю — про схему і навантаження, а не про «геніальний» оптимізатор.
- Інструменти спостереження відрізняються. Performance Schema і sys schema в MySQL 8 солідні та добре документовані. MariaDB має еквіваленти, але іноді доведеться звертатися до інших представлень або вмикати інші плагіни.
- Ностальгія за query cache — пастка. Якщо ви обираєте MariaDB, бо чули «query cache допомагає WordPress», зупиніться. Сучасний динамічний сайт не виправиш, воскресивши глобальний м’ютекс з 2009 року.
- Пул потоків може мати значення. Thread pool в MariaDB (залежно від збірки/редакції) може допомогти при великому потоці з’єднань. Але якщо ви піднімаєте 800 PHP-FPM процесів і кожен відкриває своє з’єднання — ви створили фабрику з’єднань, а не вебсайт.
- GTID/семантика реплікації відрізняються. Це більше впливає на відновлення/HA, ніж на затримку запитів, але відключення WordPress часто виглядає як «проблема з базою даних», навіть якщо тригер — затримка реплікації.
Що варто вирішити замість брендової лояльності
Для WordPress ваші реальні точки прийняття рішень:
- Чи можете ви отримати сучасну мажорну версію? Старі MySQL 5.6/5.7 і старі збірки MariaDB мають менше діагностичних інструментів і більше пасток. Запускайте щось актуальне й підтримуване.
- Чи є в командах знайомство з відповідальністю у ночі? Найкраща база даних — та, яку ваша команда може відлагодити о 02:00 без танців з інтерпретацією на живому репліці.
- Чи є план щодо дрейфу схеми/індексів? Встановлення WordPress — це сніжинка. Ваші «вбивці» запитів реальні — вони в цій сніжинці.
Цікаві факти й історичний контекст (коротко й корисно)
- MySQL був куплений Sun у 2008 році, а Sun — Oracle у 2010 — це корпоративна генеалогія, що стоїть за багатьма дебатами «MySQL vs MariaDB».
- MariaDB була започаткована оригінальними розробниками MySQL як форк, щоб зберегти спільнотний шлях після придбання Oracle.
- WordPress з’явився в еру MySQL 4, що пояснює деякі давні архітектурні рішення (наприклад, сильну залежність від meta-таблиць), які не були розраховані на сучасні аналітичні запити.
- InnoDB став дефолтним движком в MySQL 5.5; до того часто використовувався MyISAM. Деякі «оптимізаційні поради» з тих часів все ще припускають поведінку MyISAM і зараз неправильні для WordPress.
- MySQL 8 остаточно прибрав query cache; MariaDB довше зберігав його форму. Ця відмінність породила міфи про «швидкість MariaDB для WordPress», навіть коли вузьке місце — PHP або відсутні індекси.
- Performance Schema значно дозрів у MySQL 5.7 і 8; якщо ви використовували його один раз і не сподобалося, ви, ймовірно, зустріли його в «непростій» юності.
- Дизайн
wp_postmetaнавмисно універсальний (key/value), що спрощує написання плагінів і ускладнює підтримку швидкості бази даних при масштабі. - UTF8MB4 став практичним дефолтом для сучасного WordPress, щоб підтримувати повний Unicode (включно з емодзі, якими маркетингова команда точно скористається).
«Вбивці» запитів у WordPress (і чому вони шкодять)
Більшість інцидентів «продуктивності бази даних WordPress» зводиться до невеликого набору режимів відмов. Трюк — швидко їх упізнати й застосувати нудні, але ефективні виправлення.
1) Надмірний autoload у wp_options
WordPress завантажує опції з autoload='yes' на кожен запит. Кожен запит. Якщо цей набір виросте до мегабайтів серіалізованих масивів — дякуємо, плагіни — ви додали плату до кожного перегляду сторінки ще до виконання будь-якого «цікавого» запиту.
Симптоми: TTFB повільнішає, CPU зростає, запити виглядають малими, але частими, промахи об’єктного кешу стають дорогими, а адмін-сторінки ніби йдуть крізь мед.
2) Податок мета-таблиць: wp_postmeta і подібні
Мета-таблиці гнучкі, але перетворюють «знайти пости з X» на «пройти велику кількість рядків з повторами JOIN’ів». Якщо плагін будує запит з кількома мета-умовами і без індексів, це може заклинити CPU і крутити сторінки буферного пулу.
3) Відсутні або неправильні індекси (особливо на meta_key + meta_value)
Індекси в ядрі WordPress мінімальні. Плагіни часто роблять фільтрацію по колонкам без індексації (або в неправильному порядку), створюючи повні скани таблиць. З ростом даних ви переходите від «нормально» до «катастрофи» без попередження, крім повільного логу, який ви не ввімкнули.
4) ORDER BY RAND() і інші нерадісні злочини
Це заслуговує окремого зауваження: виглядає невинно й здається простим шляхом. Але це також часова бомба продуктивності на будь-якій таблиці з більше ніж «симпатичною кількістю» рядків.
Жарт #1: ORDER BY RAND() — це як просити базу даних перетасувати склад по одному ящику.
5) Блокування та конкуренція: оновлення, транзієнти, cron і поведінка в адмінці
У WordPress є патерни записів, що можуть спричинити конкуренцію: очищення транзієнтів, заплановані задачі (WP-Cron), міграції плагінів і масові операції в адмінці. Якщо у вас є трафік і адміністратор запускає масове редагування, ви можете отримати очікування блокувань та накопичення запитів.
6) Тимчасові таблиці і скидання на диск
Коли MySQL/MariaDB не може вмістити проміжні результати в пам’ять, воно викидає їх на диск. Якщо ваш tmp каталог знаходиться на повільному диску або ви насичуєте IOPS, затримка вибухне під час сортувань, групувань і складних JOIN’ів. WordPress сам може це спричинити через пошук, плагіни звітності і адмін-екрани з «фільтруй усе».
7) Плутанина кешів: object cache vs page cache vs database cache
Кешування в WordPress багаторівневе, і команди регулярно плутають їх:
- Page cache (CDN, реверсний проксі) знижує навантаження PHP та БД для анонімного трафіку.
- Object cache (Redis/Memcached) зменшує повторні читання в межах і між запитами.
- Database cache (buffer pool) тримає часто використовувані сторінки в пам’яті.
Коли ви «увімкнули кеш» і нічого не покращилося, зазвичай це тому, що ввімкнули неправильний шар або ваш hit rate поганий через autoload/мета-патерни, що генерують забагато унікальних ключів.
Швидкий план діагностики (перший/другий/третій)
Це план дій, коли сайт повільний і ви намагаєтесь не погіршити ситуацію. Мета — швидко визначити, чи вузьке місце в CPU, I/O, блокуваннях чи з’єднаннях.
Перший крок: підтвердити клас вузького місця (CPU, I/O, блоки або з’єднання)
- CPU-bound: високе завантаження процесора бази даних, багато «Sending data», численні повні скани, buffer pool не допомагає через неефективні запити.
- I/O-bound: висока затримка читання/запису, промахи buffer pool, скидання тимчасових таблиць на диск, повільне сховище або насичення тома.
- Lock-bound: багато потоків у стані «Waiting for table metadata lock» або «Waiting for row lock», довгі транзакції.
- Connection-bound: занадто багато з’єднань/потоків, контекстні переключення, досягнуто max connections, PHP-FPM stampede.
Другий крок: зафіксуйте головних порушників (не здогадуйтеся)
- Увімкніть/перевірте slow query log (коротке вікно) або використайте Performance Schema вью.
- Зробіть знімки
SHOW PROCESSLIST/information_schemaактивності. - Запустіть
EXPLAINдля найгіршого запиту та дивіться на «Using temporary; Using filesort» і повні скани.
Третій крок: застосуйте найменш ризикове виправлення, яке зніме найбільший біль
- Якщо це autoload: зменшіть
autoload, очистіть транзієнти, підтвердіть зниження розміру. - Якщо це відсутні індекси: додайте цільові композитні індекси (з планом відкату).
- Якщо це блокування: знайдіть довгу транзакцію і зупиніть/уникніть її; акуратно налаштуйте таймаути.
- Якщо це I/O: збільшіть buffer pool (якщо пам’ять дозволяє), виправте налаштування тимчасових таблиць, перемістіть tmpdir на швидке сховище.
- Якщо це з’єднання: додайте стратегію пулінгу (або принаймні зменшіть concurrency PHP-FPM) і задайте розумні ліміти.
Практичні завдання: команди, вивід, рішення (12+)
Це реальні завдання, які ви можете виконати сьогодні на типовому Linux-хості. Кожне містить: команду, що типовий вивід означає і яке рішення прийняти далі.
Завдання 1: Визначити різновид сервера й версію (не можна налаштовувати те, чого не називаєш)
cr0x@server:~$ mysql -e "SELECT VERSION() AS version, @@version_comment AS comment, @@version_compile_machine AS arch;"
+-------------------------+------------------------------+------+
| version | comment | arch |
+-------------------------+------------------------------+------+
| 8.0.36 | MySQL Community Server - GPL | x86_64 |
+-------------------------+------------------------------+------+
Що це означає: MySQL 8.0.36. Якби це була MariaDB, comment зазначав би MariaDB і формати версій часто містять -MariaDB.
Рішення: Використовуйте інструменти MySQL 8 (Performance Schema, sys schema). Якщо у вас стара версія — заплануйте оновлення, бо багато «загадкових уповільнень» легше виявити на сучасних версіях.
Завдання 2: Перевірити тиск з’єднань
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW VARIABLES LIKE 'max_connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 186 |
+-------------------+-------+
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 412 |
+----------------------+-------+
+-----------------+------+
| Variable_name | Value|
+-----------------+------+
| max_connections | 500 |
+-----------------+------+
Що це означає: Ви працюєте близько до ліміту в пікові моменти. Якщо PHP-FPM має 300 воркерів і кожний використовує своє з’єднання, ви можете досягти стелі під час сплеску трафіку.
Рішення: Якщо Threads_connected регулярно > 60–70% від max_connections, зменшіть конкаренцію додатку і/або додайте проксі/пул (або хоча б persistent connections з безпечним налаштуванням). Також перевірте thread cache.
Завдання 3: Підтвердити, чи увімкнено slow query logging (і де він)
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time'; SHOW VARIABLES LIKE 'log_output';"
+---------------------+----------------+
| Variable_name | Value |
+---------------------+----------------+
| slow_query_log | OFF |
| slow_query_log_file | /var/log/mysql/slow.log |
+---------------------+----------------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| long_query_time | 10.000000 |
+-----------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output | FILE |
+---------------+-------+
Що це означає: Slow log вимкнено і поріг занадто високий для діагностики WordPress.
Рішення: Тимчасово увімкніть його з низьким порогом (0.5–1с) під час піка, потім поверніть назад. Фіксуйте докази замість відчуттів.
Завдання 4: Увімкнути slow query log для короткого діагностичного вікна
cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.5; SET GLOBAL log_queries_not_using_indexes = 'ON';"
...output omitted...
Що це означає: Нові повільні запити будуть записані. log_queries_not_using_indexes може бути гучним; ви використовуєте його як ліхтар, не як стиль життя.
Рішення: Дайте йому працювати 10–30 хвилин при типовому трафіку, потім аналізуйте. Підніміть поріг або вимкніть після збору достатньої вибірки.
Завдання 5: Швидко підсумувати головних винуватців slow log
cr0x@server:~$ sudo mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
Reading mysql slow query log from /var/log/mysql/slow.log
Count: 212 Time=3.12s (661s) Lock=0.00s (0s) Rows=10234.0 (2169608), root[root]@localhost
SELECT SQL_NO_CACHE * FROM wp_postmeta WHERE meta_key = 'S' AND meta_value LIKE '%S%' ORDER BY meta_id DESC LIMIT N
Count: 88 Time=1.44s (126s) Lock=0.03s (2s) Rows=1.0 (88), wp[wp]@10.0.2.10
SELECT option_value FROM wp_options WHERE autoload = 'yes'
Що це означає: У вас є шаблон пошуку по мета, що робить LIKE '%...%' по meta_value (боляче), і частий запит autoload, який може тягнути величезний об’єм.
Рішення: Для мета-пошуку: виправте фічу або обмежте її; індекси не врятують leading-wildcard LIKE на довгому текстовому полі. Для autoload: виміряйте розмір autoload і обріжте.
Завдання 6: Перевірити найгірші поточні запити і стани блокувань
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;"
+-----+------+----------------+------+---------+------+------------------------------+-----------------------+
| Id | User | Host | db | Command | Time | State | Info |
+-----+------+----------------+------+---------+------+------------------------------+-----------------------+
| 821 | wp | 10.0.2.10:53214| wpdb | Query | 12 | Waiting for table metadata lock | ALTER TABLE wp_postmeta ... |
| 844 | wp | 10.0.2.11:53302| wpdb | Query | 8 | Sending data | SELECT ... FROM wp_posts ...|
| 865 | wp | 10.0.2.12:54100| wpdb | Query | 15 | Waiting for row lock | UPDATE wp_options SET ... |
+-----+------+----------------+------+---------+------+------------------------------+-----------------------+
Що це означає: Є ALTER TABLE, що блокує метадані, і UPDATE, який чекає row lock. Це інцидент конкуренції, а не «нужна ще пам’ять».
Рішення: Не робіть DDL у піковий час. Якщо плагін мігрує схему в реальному часі — призупиніть/відкотіть або робіть у вікні обслуговування. Знайдіть блокуючу транзакцію далі.
Завдання 7: Знайти очікування блокувань InnoDB і блокувальник
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,160p'
...
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 18392019, ACTIVE 14 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 865, OS thread handle 140055, query id 998201 10.0.2.12 wpdb updating
UPDATE wp_options SET option_value = '...' WHERE option_name = '_transient_timeout_wc_count_comments'
*** (2) TRANSACTION:
TRANSACTION 18392011, ACTIVE 22 sec fetching rows
...
Що це означає: Транзієнти задіяні; плагін (тут схоже на WooCommerce) штурмує wp_options і створює колізії.
Рішення: Перенести транзієнти в Redis/Memcached (persistent object cache) щоб зменшити записи в базу, і/або зменшити конкуренцію WP-Cron. Також переконайтеся, що wp_options.option_name індексований (він має бути).
Завдання 8: Виміряти тиск buffer pool (чи ви I/O-bound?)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages%';"
+----------------------------------+----------+
| Variable_name | Value |
+----------------------------------+----------+
| Innodb_buffer_pool_read_requests | 94811234 |
| Innodb_buffer_pool_reads | 2211903 |
+----------------------------------+----------+
+----------------------------------+--------+
| Variable_name | Value |
+----------------------------------+--------+
| Innodb_buffer_pool_pages_total | 524288 |
| Innodb_buffer_pool_pages_free | 120 |
| Innodb_buffer_pool_pages_dirty | 18240 |
+----------------------------------+--------+
Що це означає: Співвідношення reads/read_requests дає грубу оцінку hit ratio: 1 - reads/read_requests. Тут воно пристойне, але pages_free мізерний (buffer pool майже весь зайнятий).
Рішення: Якщо ваш робочий набір не вміщується і затримка диска висока, збільшіть innodb_buffer_pool_size (в межах безпечної пам’яті). Якщо hit ratio уже хороший — зосередьтеся на ефективності запитів і блокуваннях.
Завдання 9: Підтвердити скидання тимчасових таблиць на диск
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables'; SHOW VARIABLES LIKE 'tmp_table_size'; SHOW VARIABLES LIKE 'max_heap_table_size';"
+-------------------------+--------+
| Variable_name | Value |
+-------------------------+--------+
| Created_tmp_disk_tables | 92341 |
| Created_tmp_tables | 145220 |
+-------------------------+--------+
+-------------------+----------+
| Variable_name | Value |
+-------------------+----------+
| tmp_table_size | 16777216 |
+-------------------+----------+
+-------------------+----------+
| Variable_name | Value |
+-------------------+----------+
| max_heap_table_size | 16777216 |
+-------------------+----------+
Що це означає: Високе співвідношення disk tmp tables часто корелює з повільними сортуваннями/групуваннями. Ліміти 16MB — консервативні.
Рішення: Якщо у вас є запас пам’яті, підвищуйте обидва параметри разом (вони фактично обмежують пам’ять для тимчасових таблиць). Також знайдіть, які запити це спричиняють — не крутить ручку в надії.
Завдання 10: Проаналізувати найгірший план виконання (EXPLAIN, не молитва)
cr0x@server:~$ mysql -e "EXPLAIN SELECT p.ID FROM wp_posts p JOIN wp_postmeta pm ON pm.post_id=p.ID WHERE pm.meta_key='color' AND pm.meta_value='blue' AND p.post_type='product' AND p.post_status='publish' ORDER BY p.post_date DESC LIMIT 20;"
+----+-------------+-------+------+---------------+------+---------+-------------------+--------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+-------------------+--------+-----------------------------+
| 1 | SIMPLE | pm | ref | meta_key | meta_key | 767 | const | 980000 | Using where; Using filesort |
| 1 | SIMPLE | p | eq_ref | PRIMARY | PRIMARY | 8 | wpdb.pm.post_id | 1 | Using where |
+----+-------------+-------+------+---------------+------+---------+-------------------+--------+-----------------------------+
Що це означає: Сканується майже мільйон рядків у wp_postmeta для цього meta_key, а потім фільтрується по meta_value. filesort свідчить про додаткову роботу для сортування.
Рішення: Додайте композитний індекс, який відповідає формі фільтра (зазвичай (meta_key, meta_value(…prefix…), post_id) залежно від типів даних і селективності). Потім знову запустіть EXPLAIN і перевірте зменшення оцінки рядків.
Завдання 11: Виміряти розмір autoload у wp_options
cr0x@server:~$ mysql -e "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024,2) AS autoload_mb, COUNT(*) AS autoload_rows FROM wp_options WHERE autoload='yes';"
+-------------+--------------+
| autoload_mb | autoload_rows|
+-------------+--------------+
| 14.73 | 3129 |
+-------------+--------------+
Що це означає: ≈15МБ опцій завантажуються на кожен запит. Це не «трохи накладних витрат». Це повторюваний рахунок.
Рішення: Визначте найбільші autoload-рядки і вимкніть autoload для неважливих опцій або видаліть застарілі опції від мертвих плагінів. Ціль — менше ніж 1–2МБ для більшості сайтів; більші сайти можуть виправдати більше, але 15МБ зазвичай випадковість.
Завдання 12: Знайти найбільших «порушників» autoload (безпечно)
cr0x@server:~$ mysql -e "SELECT option_name, ROUND(LENGTH(option_value)/1024,1) AS kib FROM wp_options WHERE autoload='yes' ORDER BY LENGTH(option_value) DESC LIMIT 15;"
+-----------------------------------+--------+
| option_name | kib |
+-----------------------------------+--------+
| myplugin_big_cache_blob | 2048.4 |
| theme_mods_mytheme | 612.7 |
| rewrite_rules | 512.2 |
| widget_custom_html | 498.9 |
+-----------------------------------+--------+
Що це означає: Один плагін кладе багатомегабайтний «кеш» у autoload. Також деякі core-опції (наприклад, rewrite_rules) можуть бути великі, але зазвичай не багатомегабайтні.
Рішення: Для плагінних блобів: встановіть autoload=’no’ (якщо безпечно), мігруйте в object cache або припиніть зберігати велетенські обчислені дані. Для core-опцій: подумайте про акуратне очищення і регенерацію rewrite rules, якщо вони пошкоджені.
Завдання 13: Перевірити розміри таблиць і зростання (знайти реального монстра)
cr0x@server:~$ mysql -e "SELECT table_name, ROUND((data_length+index_length)/1024/1024,1) AS total_mb FROM information_schema.tables WHERE table_schema='wpdb' ORDER BY (data_length+index_length) DESC LIMIT 10;"
+--------------+----------+
| table_name | total_mb |
+--------------+----------+
| wp_postmeta | 8420.5 |
| wp_posts | 2150.2 |
| wp_options | 390.4 |
| wp_comments | 280.7 |
+--------------+----------+
Що це означає: wp_postmeta домінує. Це нормально на масштабі — але це означає, що вибір індексів важливий і «мета-запити скрізь» покарають вас.
Рішення: Зосередьте оптимізацію на шаблонах доступу до wp_postmeta і зменшіть мета-важкі запити на критичних ендпойнтах (пошук, фільтри, списки товарів).
Завдання 14: Перевірити довгі транзакції (приховані бомбочки блокувань)
cr0x@server:~$ mysql -e "SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND,trx_started,NOW()) AS trx_age_s, trx_mysql_thread_id FROM information_schema.innodb_trx ORDER BY trx_started ASC LIMIT 10;"
+--------+----------+---------------------+----------+---------------------+
| trx_id | trx_state| trx_started | trx_age_s| trx_mysql_thread_id |
+--------+----------+---------------------+----------+---------------------+
| 183910 | RUNNING | 2025-12-31 10:21:02 | 1843 | 512 |
+--------+----------+---------------------+----------+
Що це означає: Транзакція відкрита ≈30 хвилин. У WordPress це підозріло. Вона може блокувати очищення, викликати зростання списків історії і посилювати конкуренцію блокувань.
Рішення: Знайдіть запит/користувача, що тримає транзакцію (processlist), і виправте поведінку додатку (часто це звіт/експорт, міграція плагіна або адміністративне завдання).
Завдання 15: Перевірити затримку диска і насичення (бо фізика дата-центру ще працює)
cr0x@server:~$ iostat -xz 1 5
Linux 6.8.0 (server) 12/31/2025 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
22.1 0.0 6.4 18.8 0.0 52.7
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 980.0 420.0 52000.0 18000.0 12.4 0.8 98.7
Що це означає: Дуже високе навантаження і помітний await. Якщо тимчасові таблиці скидаються або промахи buffer pool різко ростуть, ви відчуєте це негайно як затримку сайту.
Рішення: Спочатку зменшіть роботу з диском (кращі індекси, налаштування buffer pool, уникнення tmp-сполів) перед купівлею швидших дисків. Якщо ви вже на NVMe і все ще на межі — ймовірно, ви робите погані запити на масштабі.
Гарячі точки схеми: wp_options, wp_postmeta, wp_posts
wp_options: autoload та транзієнти
Що вбиває: роздутий autoload + інтенсивні записи транзієнтів.
- Autoload: ставте його в розряд конфігурації, а не сховища кешу.
- Транзієнти: якщо у вас немає persistent object cache, транзієнти зберігаються в
wp_optionsі стають записами під час трафіку.
Стратегія виправлення:
- Виміряйте розмір autoload, скоротіть його і тримайте під контролем.
- Перенесіть транзієнти в Redis/Memcached (плагін persistent object cache + бекенд).
- Зупиніть плагіни від запису великих блобів як autoload-опції.
wp_postmeta: змусіть мета-запити «платити аренду»
Що вбиває: повторні JOIN’и і нефільтровані фільтри без індексів.
Стандартні індекси WordPress зазвичай включають post_id і meta_key. Плагіни часто фільтрують по meta_value, сортують за ним або роблять діапазонні запити по числовим значенням, збереженим як рядки. Ось як база стає «добре, поки не стала поганою».
Стратегія виправлення:
- Додайте вибіркові композитні індекси, що відповідають реальному навантаженню (не «індексувати все»).
- Для числових діапазонів: забезпечте послідовне приведення типів у запитах і розгляньте зберігання чисел в окремих таблицях, якщо матимете нагоду. Якщо ні — індексуйте префікс або переробіть фічу.
- Для текстового пошуку по
meta_value: припиніть використовувати leading-wildcard LIKE. Використовуйте належні пошукові рішення або обмежте область.
wp_posts: передбачувані шаблони, але їх легко зіпсувати
Запити, що фільтрують за post_type, post_status, post_date, та приєднання таксономій — поширені. Core-індекси часто допомагають, але кастомні запити, що сортують за неіндексованими виразами або приєднують забагато meta, усе ще можуть спричиняти тимчасові таблиці і filesort.
Жарт #2: Оптимізація бази даних, яка додає п’ять JOIN’ів, щоби уникнути одного запиту — це як летіти трьома рейсами, щоб уникнути пересадки.
Налаштування, що працюють для обох (і де відрізняється)
Ось упереджене правило: налаштовуйте за доказами, а не за блог-постовими відчуттями. Для WordPress більшість виграшів приходить зі схеми та формування навантаження. Після цього — розміри пам’яті та ліміти конкаренції.
Почніть з цих перемикачів (бо вони дійсно важливі)
1) innodb_buffer_pool_size
Якщо сервер бази присвячений MySQL/MariaDB, загальний базовий підхід — 60–75% RAM для buffer pool. На змішаних машинах будьте обережні. Для WordPress продуктивність часто поліпшується більше від поміщення «гарячих» сторінок в пам’ять, ніж від будь-якої іншої одиничної зміни.
2) innodb_log_file_size / розмір redo log
Занадто маленькі redo журнали можуть створювати тиск чекпоінтів під час сплесків записів (імпорти, cron-атаки). Занадто великі — збільшують час відновлення після збою. Розміріть свідомо за патернами записів; не ставте «як хтось в твіттері сказав».
3) Налаштування tmp таблиць та розміщення tmpdir
Якщо диск тимчасових таблиць високий, підняття tmp_table_size і max_heap_table_size (разом) може допомогти — але до певного моменту. Також переконайтеся, що tmpdir на швидкому локальному сховищі, а не на повільному мережевому шарі.
4) Конкуренція: тримайте з’єднання в розумних межах
WordPress не виграє від необмеженої конкуренції. Якщо ви насичуєте базу, затримки зростають, а пропускна здатність часто падає. Обмежте PHP-FPM воркери згідно з виміряною здатністю бази, а не «ядра CPU × 20».
5) Налаштування для спостереження (щоб можна було довести покращення)
Увімкніть те, що потрібно, коли потрібно: slow logs, певні споживачі Performance Schema (де доречно) і базові метрики ОС. Залиште собі крихти хліба.
Де MySQL і MariaDB практично відрізняються
- Різниці оптимізатора: іноді план запиту відрізнятиметься. Коли це трапляється, виправлення зазвичай таке ж: додати правильний індекс, переписати найгірший запит (часто в плагіні) або змінити шаблон доступу.
- Інструментування: в MySQL 8 опирайтесь на Performance Schema і sys schema. У MariaDB можливо зручніше використовувати
information_schemaта статус движка, плюс власні вью залежно від збірки. - Доступність thread pool: може змінювати поведінку сервера при штормі з’єднань. Це не виправдання штормів з’єднань.
Одна ідея з надійності, варта уваги і приписана John Allspaw (парафраз): «Надійність — це те, що ви робите до інциденту, а не під час нього.»
Три короткі історії з практики
Міні-історія 1: Інцидент, спричинений хибним припущенням
Вони вели завантажений контент-сайт на WordPress з популярним комерційним плагіном. Трафік не був величезний, але був сплесковим: кампанії, розсилки, іноді згадка на великому форумі.
Новий менеджер хотів стандартизувати базу. План: замінити MariaDB на MySQL 8 по всьому флоту, бо «MySQL 8 швидший і сучасніший». Це не звучало божевільно. Хибне припущення було в тому, що: база даних взаємозамінна, якщо схема однакова.
Міграція пройшла чисто, тести пройшли, і перший тиждень виглядав добре. Потім почалася розпродаж. Затримки на оформленні замовлення зросли з субсекундних до кількох секунд, і почалися переривчасті «Error establishing a database connection». Команда звинуватила новий движок і почала панічно відкатуватися.
Реальна проблема: під час міграції вони змінили SQL-моди та значення за замовчуванням. Запит плагіна, що раніше «проскакував» завдяки неявним приведенням, отримав інший план і почав використовувати жахливий шлях через індекси. Під навантаженням це спричинило скидання тимчасових таблиць і пікове I/O. Той самий запит на MariaDB «випадково» обирав менш жахливий план.
Виправлення було непоказне: додати композитний індекс, що відповідав фільтру, і змінити запит плагіна, щоб уникнути обчисленого сортування. Після цього MySQL 8 працював нормально. Урок: зміни версій виявляють приховані проблеми схеми. База не зрадила їх — вона просто перестала терпіти безлад.
Міні-історія 2: Оптимізація, що відбилася боком
Інша компанія мала класичну проблему: пошук у wp-admin і фільтри товарів були повільні. Хтось запропонував «просто додати індекси на все в wp_postmeta». Вони створили кілька широких композитних індексів, включно з довгими префіксами meta_value, і розгорнули їх під час робочого часу, бо «online DDL тепер безпечний».
Короткий період — пошуки стали швидшими. Потім латентність записів зросла. Імпорти й cron-процеси почали таймаутитись. CPU бази зріс і залишився високим. Дисковий простір зріс, бо кожний індекс напухав таблицю, і buffer pool більше не міг тримати робочий набір «гарячим».
Гірше: деякі запити не використовували нові індекси взагалі, бо форма предикатів не відповідала порядку індексу. Інші використовували їх, але платили високу вартість підтримки при записах. Вони оптимізували невелику частину читань, поклавши податок на всі записи і на кожну кеш-лінію.
Відновилися вони, зробивши те, що треба було б зробити спочатку: визначити топ-5 шаблонів запитів і індексувати тільки їх, мінімізуючи ширину індексів. Видалили зайві індекси, запланували зміни схеми належно і перенесли функції з інтенсивними транзієнтами в Redis.
Мораль: «більше індексів» — не стратегія. Це спосіб перетворити біль читання в біль записів, вичерпавши пам’ять.
Міні-історія 3: Скупа, але правильна практика, що врятувала день
Медіа-організація мала мультисайт WordPress і невелику SRE-команду, яка наполягала на двох «скупих» практиках: (1) slow query log з розумним порогом, ротований і архівований, та (2) щотижневий перегляд топ-правопорушників з власниками плагінів.
Люди зневажливо кивали, бо зазвичай все здавалося в порядку. Потім вийшла нова версія плагіна, що змінила обробку пов’язаних постів. Вона додала запит, який приєднував wp_posts до wp_postmeta кілька разів і додав сортування по неіндексованому виразу.
Оскільки slow log уже був увімкнений, команда помітила нового порушника за кілька годин. Не потрібна була «бомба» або детективний роман. У них був відбиток запиту і базова метрика «до/після».
Виправлення було хірургічним: один індекс, одна конфігурація плагіна, щоб обмежити пул пов’язаних постів, і опція відкату, якщо індекс спричинить проблеми при записах. Користувачі цього не помітили. Керівництво не дізналося — найвища похвала, яку може дати продакшн.
Ось що купує «скупа правильність»: менше героїчних вихідних.
Типові помилки: симптом → корінна причина → виправлення
1) Симптом: головна сторінка повільна, але CPU бази низький
Корінна причина: накопичення з’єднань і очікування блокувань; запити чекають, а не обчислюють.
Виправлення: перевірте SHOW PROCESSLIST на стани блокувань; знайдіть і зупиніть довгі транзакції; плануйте DDL поза піком; зменшіть concurrency PHP-FPM.
2) Симптом: адмін-сторінки (особливо «Товари» або «Замовлення») болісно повільні
Корінна причина: фільтрація та сортування з великою кількістю мета спричиняють скидання тимчасових таблиць або повні скани.
Виправлення: slow log + EXPLAIN для адмін-запиту; додайте один композитний індекс для домінуючого предикату; підніміть ліміти тимчасових таблиць при потребі; вимкніть дорогі колонки/фільтри в списках адмінки.
3) Симптом: раптові сплески записи I/O і очікувань блокувань що кілька хвилин
Корінна причина: сплески WP-Cron, очищення транзієнтів або заплановані задачі плагіна.
Виправлення: перемістіть cron в системний cron; забезпечте лише одного раннера; перенесіть транзієнти в persistent object cache; зменшіть частоту задач там, де безпечно.
4) Симптом: пошук повільний і погіршується з ростом контенту
Корінна причина: LIKE '%term%' на великих текстових полях, часто в postmeta.
Виправлення: припиніть використовувати leading-wildcard LIKE на масштабі. Використовуйте повнотекстовий пошук для контенту, де доречно, обмежте область пошуку або винесіть пошук. Індекси тут не врятують стабільно.
5) Симптом: «Error establishing a database connection» під навантаженням
Корінна причина: досягнуто max connections, накладні витрати на плануванні потоків, або БД зависла на I/O/блокуваннях і з’єднання тайм-аутяться.
Виправлення: виміряйте Threads_connected, Max_used_connections; зменшіть PHP-FPM воркерів; додайте backpressure; переконайтеся, що БД не навантажена I/O; уникайте довгих транзакцій.
6) Симптом: додавання індексу погіршило продуктивність
Корінна причина: накладні витрати на підтримку індексу, роздутий робочий набір або оптимізатор обрав невірний індекс.
Виправлення: валідируйте через EXPLAIN і статистику запитів; видаліть невикористовувані/малоефективні індекси; тримайте індекси вузькими і відповідними предикатам; розгляньте гістограми (MySQL) тільки після базових кроків.
7) Симптом: база швидка, але реплікація відстає при трафіку
Корінна причина: write-heavy навантаження (опції/транзієнти), великі транзакції або однопоточне застосування реплікації залежно від конфігурації.
Виправлення: зменшіть записи (persistent object cache), уникайте великих транзакцій, налаштуйте реплікацію відповідно до движка/версії; не використовуйте репліки для читання, якщо вони постійно відстають.
8) Симптом: періодичні зупинки під час бекапів або аналітики
Корінна причина: бекап/ETL робить I/O конкуренцію або довгі консистентні зчитування впливають на purge/undo.
Виправлення: плануйте бекапи поза піком; використовуйте неблокуючі методи бекапу; обмежуйте швидкість; забезпечте правильно налаштовані undo/redo; моніторьте I/O і вік транзакцій.
Контрольні списки / покроковий план
Покроково: стабілізувати повільну базу WordPress без переписування
- Бейзлайньте, що означає «повільно». Зніміть p95/p99 латентності сторінки, CPU бази, disk await і активні з’єднання під час поганого вікна.
- Тимчасово увімкніть slow query logging. Використайте поріг 0.5–1с на 10–30 хвилин при реальному трафіку.
- Визначте топ-3 підписів запитів. Не ганяйтеся за довгим хвостом.
- Запустіть EXPLAIN на найгіршому запиті. Дивіться на повні скани, filesort і використання тимчасових таблиць.
- Виправте найбільший «постійний» податок. Зазвичай це роздутість autoload або «гарячий» мета-запит на високонавантаженому ендпойнті.
- Додайте один цільовий індекс за раз. Перевіряйте покращення і слідкуйте за латентністю записів.
- Перенесіть транзієнти в persistent object cache. Це зменшить записову навантаженість і конкуренцію в
wp_options. - Контролюйте конкаренцію. Обмежте PHP-FPM воркерів відповідно до можливостей БД; уникайте штормів з’єднань.
- Перевірте скидання тимчасових таблиць і статистику buffer pool. Налаштуйте пам’ять тільки після виправлення форми запитів.
- Поверніть пороги slow log до нормальних. Тримайте його увімкненим на розумному значенні і ротувати логи.
- Запишіть, що змінилося. Не для бюрократії — щоб наступний інцидент не став археологією.
Чекліст: перед тим як додавати індекс
- Чи маєте точний текст запиту (або нормалізований fingerprint)?
- Ви виконали
EXPLAINі зафіксували оцінку «rows»? - Чи можете протестувати в стейджингу з даними, схожими на продукційні?
- Чи знаєте вплив на записову швидкість (імпорти, cron, оформлення замовлень)?
- Чи є план відкату (drop index) і вікно обслуговування, якщо потрібно?
Чекліст: безпечне очищення autoload
- Виміряйте поточний об’єм autoload у МБ і головних порушників.
- Підтвердіть, які опції належать деактивованим плагінам/темам.
- Змінюйте прапори autoload обережно (краще через налаштування плагіна, якщо є).
- Повторно виміряйте autoload MB і слідкуйте за латентністю фронтенду.
- Тримайте снапшот/бекап перед масовими змінами.
Питання та відповіді
1) Чи обирати MySQL або MariaDB для продуктивності WordPress?
Оберіть ту, яку ви можете запустити в сучасній підтримуваній версії і з якою ваша команда знайома. Для WordPress гігієна схеми/запитів перемагає вибір движка більшість днів.
2) Чи MariaDB «швидша для WordPress» через query cache?
Ні. Query cache не є сучасним вирішенням, як думають люди, і MySQL 8 прибрав його з причин. Використовуйте page/object кеш і виправляйте шаблони запитів.
3) Яке одне найбільш ефективне виправлення для багатьох WordPress-сайтів?
Очищення autoload у wp_options і persistent object cache для зменшення транзієнтного шуму. Це не гламурно, але часто має найвищий ROI.
4) Я увімкнув Redis object cache; чому MySQL все ще повільний?
Бо Redis не вирішує нефільтровані мета-запити, скидання тимчасових таблиць або конкуренцію через довгі транзакції. Перевірте hit rate кешу, а потім продовжуйте діагностику топ-повільних запитів.
5) Чи можна виправити повільні мета-запити без переписування плагіна?
Часто так: додайте один-два цільові композитні індекси і зменшіть область фільтрів/сортувань через конфігурацію. Якщо плагін робить LIKE '%term%' по meta_value, можливо, доведеться змінити фічу, а не індекс.
6) Чи завжди безпечно збільшувати innodb_buffer_pool_size?
Безпечно лише якщо на хості є запас RAM для ОС, файлового кешу й інших сервісів. Переміщення в swap призводить до катастрофи продуктивності.
7) Чому я бачу «Waiting for table metadata lock»?
Зазвичай тому, що хтось виконує DDL (ALTER TABLE) під час роботи трафіку, або довга транзакція блокує метадані. Плануйте DDL поза піком і уникайте довгих транзакцій.
8) Як зрозуміти, чи я I/O-bound чи CPU-bound?
Перевірте метрики ОС диска (await, %util) і промахи buffer pool у InnoDB. Високий await і багато промахів вказують на I/O. Високий CPU із великою кількістю повних сканів — на неефективні запити.
9) Чи варто запускати читання WordPress на репліках?
Тільки якщо ви готові терпіти відставання реплік і підтвердили, що навантаження на читання достатнє. WordPress має несподівані записи (опції, сесії/транзієнти через плагіни), які можуть тримати репліки позаду під сплесками.
10) Що робити, якщо повільний запит виявлено в ядрі WordPress?
Рідко ядро — головний винуватець у масштабі; зазвичай це плагін або специфічна форма даних (величезні meta/options). Якщо це справді ядро, починайте з індексів і кешування, а не з форка ядра.
Висновок: наступні кроки, які ви можете зробити цього тижня
MySQL проти MariaDB — не ваша первинна проблема. Первинна проблема: WordPress-навантаження карають неохайні схеми й необережні припущення, а плагіни професійно генерують обидва.
Зробіть це в такому порядку:
- Увімкніть slow query logging для контрольованого вікна і витягніть топ-правопорушників.
- Виміряйте розмір autoload у
wp_optionsі агресивно, але безпечно скоротіть його. - EXPLAIN для найгірших мета-запитів і додавайте цілеспрямовані композитні індекси — по одному.
- Перенесіть транзієнти в persistent object cache і серйозно поставтеся до cron (один раннер, передбачуваний графік).
- Введіть обмеження конкаренції, щоб база не була затоплена з’єднаннями до таймаутів.
- Перевірте дискові метрики і тимчасові таблиці. Якщо ви все ще скидаєте на диск — виправляйте це наступним.
Якщо ви нічого більше не зробите: припиніть гадати. Увімкніть світло (slow log + базові метрики) і дайте базі даних сказати правду. Вона зазвичай каже.