<? phpukraine СПІВБЕСІДИ
Пошук по платформі
WORDPRESS · MIDDLE ЧАСТО ПИТАЮТЬ

Як проєктувати custom post types і таксономії, і коли краще власна таблиця?

CPT — це рядок у wp_posts із власним post_type, таксономія — нормалізована багато-до-багатьох звʼязка через term_relationships; таксономія годиться для фільтрів і категоризації, meta — для атрибутів запису, а власна таблиця потрібна, коли даних мільйони або потрібні складні фільтри й свої індекси.

Чому після реєстрації CPT сторінки записів віддають 404?
У нас 300 тисяч записів з двадцятьма meta-полями і фільтр по них вішає базу — що не так із моделлю даних?
Чим таксономія відрізняється від meta-поля, і як обрати між ними?
Коли ви б відмовились від post type і зробили окрему таблицю?
CPT таксономії wp_postmeta register_post_type моделювання даних

Custom post type не створює жодної таблиці. register_post_type('property', …) лише каже ядру, що в спільній wp_posts бувають рядки зі значенням post_type = 'property', і що для них треба показати пункт меню, застосувати правила перезапису й підключити потрібні шаблони. Тому питання «скільки типів реєструвати» майже не має ціни: у стандартній схемі є індекс type_status_date по (post_type, post_status, post_date, ID), і вибірка по типу лишається індексованою. Ціна ховається в іншому — усі типи ділять одну wp_posts і одну wp_postmeta, тож роздутий meta одного типу гальмує запити до решти.

Таксономія — це нормалізована звʼязка багато-до-багатьох через три таблиці: terms (назва й слаг), term_taxonomy (термін у контексті конкретної таксономії, з parent і count) і term_relationships (object_id, term_taxonomy_id). Остання має первинний ключ по обох колонках і окремий індекс по term_taxonomy_id, тобто запит «дай усі записи цього терміна» — це звичайний індексований JOIN. Meta влаштована інакше: wp_postmeta — це EAV з індексами лише по post_id і по meta_key (перші 191 символ), а по meta_value індексу немає взагалі. Кожна умова в meta_query додає ще один JOIN до тієї самої таблиці, тому фільтр каталогу з пʼяти meta-умов на 300 тисячах записів — це пʼять самозʼєднань по колонці без індексу.

Звідси практичне правило проєктування. Якщо за значенням фільтрують, воно повторюється між записами й для нього доречний архів з власним URL — це таксономія (місто, бренд, тип матеріалу). Якщо значення описує один конкретний запис і читається лише разом із ним — це meta (SKU, вага, координати). Ознака помилки очевидна: якщо термінів буде приблизно стільки ж, скільки записів, таксономія вироджується й тільки роздуває term_taxonomy. Реєструвати обидва треба на init — раніше ядро ще не готове, пізніше типу вже не побачить частина функцій — і робити це в плагіні, а не в темі: після зміни теми записи лишаться в базі, але без реєстрації типу стануть недоступними. Правила перезапису WordPress кешує в опції rewrite_rules, тому після появи нового типу з has_archive або власним rewrite потрібен один flush_rewrite_rules() у register_activation_hook — саме його відсутність дає 404 на сторінках записів. Викликати flush на кожному init не можна: це запис в options на кожен запит.

Власну таблицю варто робити за чотирма ознаками, не за однією. Обсяг: сотні тисяч рядків, що ростуть, особливо коли це не контент, а події — перегляди, ціни постачальників, лог доставок. Інтенсивність запису: wp_posts тягне за собою revisions, autosave, save_post і чужі плагіни на кожен UPDATE. Потреба у власних складених індексах під конкретні фільтри й сортування, чого над EAV зробити неможливо. І відсутність потреби в інфраструктурі поста: ні редактора, ні статусів, ні коментарів, ні прав. Схему створюють через dbDelta() у хуку активації, зберігаючи версію схеми в опції для наступних міграцій, а запити пишуть через $wpdb->prepare().

Компроміс тут прямий і його варто назвати вголос: із власною таблицею ви втрачаєте WP_Query, кеш обʼєктів, адмінку, REST, права, ревізії, багатомовність і сумісність з плагінами, що працюють з постами. Усе це доведеться писати руками — список через WP_List_Table, ендпоїнти через register_rest_route. Тому в реальних проєктах частіше виграє гібрид: дані й фільтрація живуть у власній таблиці, а CPT лишається вітриною зі шаблоном, URL і SEO, звʼязаною по post_id. А перед тим, як іти в цей бік, дешевший крок — перевести фільтровані атрибути з meta в таксономії й перевірити, чи цього вже досить.

add_action('init', function (): void {
    // Службовий тип: без фронтенду, керується лише з адмінки
    register_post_type('property', [
        'labels'       => ['name' => 'Обʼєкти', 'singular_name' => 'Обʼєкт'],
        'public'       => true,
        'has_archive'  => true,          // потребує flush після реєстрації
        'rewrite'      => ['slug' => 'nerukhomist', 'with_front' => false],
        'supports'     => ['title', 'editor', 'thumbnail', 'custom-fields'],
        'show_in_rest' => true,          // без цього не працює Gutenberg і REST
        'menu_icon'    => 'dashicons-building',
    ]);

    // Фільтрований атрибут -> таксономія, бо значення повторюються
    register_taxonomy('property_city', ['property'], [
        'hierarchical' => true,          // як категорії, а не як теги
        'public'       => true,
        'show_in_rest' => true,
        'rewrite'      => ['slug' => 'misto'],
    ]);
});

// Flush лише при активації плагіна, ніколи не на кожному запиті
register_activation_hook(__FILE__, function (): void {
    do_action('init');                   // типи мають бути вже зареєстровані
    flush_rewrite_rules();
});

// Так робити не треба: три JOIN до wp_postmeta без індексу по meta_value
$slow = new WP_Query(['post_type' => 'property', 'meta_query' => [
    ['key' => 'city', 'value' => 'Львів'],
    ['key' => 'rooms', 'value' => 3, 'type' => 'NUMERIC'],
    ['key' => 'floor', 'value' => 5, 'compare' => '<=', 'type' => 'NUMERIC'],
]]);

// Те саме через таксономію + одну meta: звʼязка йде по term_relationships
$fast = new WP_Query([
    'post_type' => 'property',
    'tax_query' => [['taxonomy' => 'property_city', 'terms' => 'lviv', 'field' => 'slug']],
    'meta_query' => [['key' => 'rooms', 'value' => 3, 'type' => 'NUMERIC']],
]);
Що CPT не створює таблицю: всі записи лежать в одному wp_posts, а post_type це просто колонка, тому кількість типів на продуктивність не впливає, а кількість рядків впливає.
Що таксономія вже індексована під запит «дай усі записи терміна» (term_relationships з ключем по term_taxonomy_id), а meta — ні, бо wp_postmeta має індекс лише по meta_key(191) і post_id.
Що register_post_type і register_taxonomy треба викликати на init, а не раніше й не пізніше, і після зміни rewrite-правил потрібен flush_rewrite_rules — саме звідси 404 на сторінках записів.
Що meta_query по кількох ключах перетворюється на кілька JOIN до wp_postmeta по одній таблиці, і на сотнях тисяч записів це головна причина повільного каталогу.
Що критерій для власної таблиці — це обсяг, частота запису, потреба у власних складених індексах і в тому, щоб дані не тягли за собою revisions, autosave й адмінку wp_posts.
Реєструвати post type у файлі теми поза хуком init або на after_setup_theme і потім дивуватись, що частина функцій ядра типу не бачить.
Викликати flush_rewrite_rules() на кожному завантаженні сторінки замість register_activation_hook — це перезапис опції rewrite_rules на кожен запит.
Тримати CPT у темі: після зміни теми записи лишаються в базі, але стають недоступними, бо тип більше ніде не зареєстрований.
Робити таксономію з даних, які не групують записи: ціна, дата, SKU, рейтинг — це meta, бо термінів буде стільки ж, скільки записів.
Будувати фільтр каталогу на meta_query з п'яти умов і лікувати повільність кешем сторінки, замість того щоб винести атрибути в таксономії або власну таблицю.
Забувати 'public' => false для службових типів і отримувати сторінки-порожняки в sitemap та у видачі.
ПОРАДА

Скажіть просте правило: якщо за значенням будуть фільтрувати або потрібен архів і URL — це таксономія; якщо значення описує один конкретний запис і його лише читають разом із записом — це meta; якщо рядків мільйони або потрібен свій складений індекс — це власна таблиця з $wpdb, а CPT лишається лише вітриною.

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

Кожен CPT — це рядки в спільному wp_posts з іншим значенням post_type. Таксономії живуть в окремих term_* таблицях з індексованою звʼязкою, тому фільтр по них дешевший за meta_query.