<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
WORDPRESS · JUNIOR

Як влаштована база WordPress: wp_posts, postmeta, options і чому autoload важливий?

Контент усіх типів лежить в одному wp_posts і розрізняється колонкою post_type, довільні поля в wp_postmeta за схемою EAV, налаштування в wp_options; рядки з autoload читаються одним запитом на кожен запит до сайту, тому роздутий autoload гальмує весь сайт.

Сайт віддає головну за 2 секунди без трафіку і з порожнім кешем сторінок. Куди дивитись у базі?
Де фізично лежить кастомне поле, яке додав ACF, і чому вибірка за ним повільна?
Куди WordPress кладе transients і що станеться, якщо не вказати строк життя?
Чому в wp_posts 40 тисяч рядків, хоча статей на сайті сто?
wp_posts wp_postmeta wp_options autoload transients

Схема WordPress тримається на трьох таблицях. wp_posts зберігає весь контент незалежно від його природи: звичайний пост, сторінка, вкладення медіабібліотеки, ревізія, пункт меню, перевикористовуваний блок і шаблон блокової теми лежать поруч і розрізняються колонкою post_type. Звідси й назва основного індексу таблиці, type_status_date: він складається з post_type, post_status, post_date, ID, бо майже будь-яка вибірка починається з фільтра за типом і статусом. Через ревізії сотня статей легко перетворюється на десятки тисяч рядків; обмежує це константа WP_POST_REVISIONS.

Усе, що не вписалось у фіксований набір колонок wp_posts, іде в wp_postmeta. Це класичний EAV: рядок з post_id, meta_key і meta_value типу longtext. За гнучкість доводиться платити. Індекс є на post_id і на meta_key (з префіксом 191 символ), а на самому значенні його немає, бо longtext без префікса індексувати не можна. Кожна умова в meta_query додає окремий JOIN на ту саму таблицю, а orderby => meta_value_num змушує MySQL робити CAST і сортувати без індексу. На блозі цього не видно. На каталозі в десятки тисяч товарів з фільтрами по ціні й наявності саме тут сторінка починає віддаватись секундами.

wp_options виглядає нудно: option_name з унікальним ключем, option_value типу longtext і колонка autoload. Уся сіль у третій колонці. На старті запиту wp_load_alloptions() виконує один SELECT option_name, option_value FROM wp_options WHERE autoload IN (...), серіалізує результат у масив і кладе в обʼєктний кеш під ключем alloptions. Після цього get_option() для autoload-опції в базу не йде взагалі, а для решти робить окремий запит. У WordPress 6.6 стару пару yes/no замінив набір on, off, auto, auto-on, auto-off: значення auto означає, що рішення приймає ядро, і нові опції більші за 150 КБ автоматично отримують auto-off (поріг змінюється фільтром wp_max_autoloaded_option_size).

Сам механізм дешевий, дорогим його робить сумарний обсяг. Кожен плагін, який поклав туди лог імпорту, кеш перевірки ліцензії чи серіалізований список тисячі товарів, додає байти до блоку, що читається, передається по мережі й розпаковується unserialize() при кожному хіті: на фронтенді, в admin-ajax, у REST, у кроні. Коли сума переходить за 800 КБ, Site Health показує попередження, і це вже відчутні мілісекунди до будь-якої вашої логіки. З персистентним обʼєктним кешем додається ще один ефект: запис у будь-яку autoload-опцію інвалідує ключ alloptions цілком, тому плагін, що пише лічильник на кожному перегляді, гарантує повне перечитування опцій у наступному запиті.

Transients живуть у тій самій таблиці й тому регулярно опиняються в списку винуватців. set_transient('key', $value, 900) створює _transient_key і _transient_timeout_key, обидва з вимкненим autoload. А виклик без третього аргументу створює безстроковий transient з autoload увімкненим, і кеш каталогу на пів мегабайта назавжди оселяється в alloptions. Прострочені transients видаляються при читанні через get_transient() або щоденним кроном delete_expired_transients, тож те, що ніхто більше не читає, лежить у базі до наступного запуску крону. Остання деталь, на якій ловлять: якщо на сайті працює Redis або Memcached, transients зберігаються в обʼєктному кеші, а не в wp_options, тому порада «почистіть transients у базі» там просто нічого не змінює.

global $wpdb;

// Скільки важить те, що читається при КОЖНОМУ запиті до сайту
// (після WP 6.6 значень autoload стало пʼять, старі yes/no лишились у базі)
$bytes = (int) $wpdb->get_var(
    "SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options}
     WHERE autoload IN ('yes', 'on', 'auto', 'auto-on')"
); // понад 800 КБ і Site Health уже показує попередження

// Хто саме роздув alloptions
$heaviest = $wpdb->get_results(
    "SELECT option_name, LENGTH(option_value) AS bytes FROM {$wpdb->options}
     WHERE autoload IN ('yes', 'on', 'auto', 'auto-on')
     ORDER BY bytes DESC LIMIT 20"
);

// Пастка: без expiration transient створюється з увімкненим autoload
set_transient('shop_top_products', $products);                          // autoload on
set_transient('shop_top_products', $products, 15 * MINUTE_IN_SECONDS);  // autoload off

// Лог імпорту читають раз на тиждень: третій аргумент вимикає autoload
// у ВЖЕ наявної опції; add_option() задає autoload лише при створенні
update_option('myplugin_import_log', $log, false);

// postmeta: перший get_post_meta() тягне всі мета поста одним запитом
$price = get_post_meta($postId, '_price', true);
$sku   = get_post_meta($postId, '_sku', true);   // вже з кешу, запиту немає

// А ось тут індекс не врятує: meta_value має тип longtext, кожна умова дає JOIN
$query = new WP_Query([
    'post_type'  => 'product',
    'meta_key'   => '_price',
    'orderby'    => 'meta_value_num',  // CAST(meta_value AS SIGNED) + filesort
    'meta_query' => [['key' => '_stock_status', 'value' => 'instock']],
]);
Що wp_posts зберігає весь контент одним списком: пости, сторінки, attachment, revision, nav_menu_item, wp_block і шаблони теми, а розрізняє їх колонка post_type.
Що postmeta це EAV: meta_value має тип longtext, індексу на ньому немає, тому фільтр і сортування за значенням мета-поля йдуть без індексу.
Що на початку кожного запиту wp_load_alloptions() тягне всі рядки з autoload одним SELECT і тримає їх у памʼяті процесу.
Що transient без строку життя створюється з autoload увімкненим, а з expiration вимкненим, і саме перший варіант роздуває alloptions.
Що update_option() з третім аргументом false перемикає autoload у наявної опції, а add_option() задає його лише при створенні.
Казати, що кастомні поля зберігаються в окремій таблиці для кожного поля або прямо в wp_posts.
Лікувати повільний meta_query індексом на meta_key: він і так є, проблема у фільтрі за meta_value і в JOIN на кожну умову.
Вважати, що autoload впливає на швидкість самої опції, а не на розмір блоку, який читається при кожному запиті.
Писати set_transient() без третього аргументу і потім дивуватись, звідки в wp_options мегабайт autoload-даних.
Чистити transients у базі на сайті з Redis: при персистентному обʼєктному кеші вони взагалі не потрапляють у wp_options.
Не знати, що WordPress 6.6 замінив yes/no на on, off, auto, auto-on, auto-off, і писати умову WHERE autoload = 'yes'.
ПОРАДА

Назвіть цифру: Site Health попереджає, коли autoload-опції перевищують приблизно 800 КБ, і поясніть, що це десятки мілісекунд на розпакування масиву в кожному запиті, включно з admin-ajax і REST. Одразу покажіть запит, яким ви знайдете двадцять найбільших рядків.

оновлено 3 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

wp_load_alloptions() забирає всі autoload-рядки одним SELECT і тримає їх у памʼяті; зайвий мегабайт там коштує часу в кожному запиті, включно з admin-ajax і REST.