در وردپرس، Meta Query (کوئری متادیتا) ابزار اصلی برای واکشی داده‌های سفارشی است، اما در پروژه‌های حجیم، بدون ایندکس‌گذاری هدفمند، به بزرگ‌ترین منبع کندی پنهان تبدیل می‌شود. هر meta_query یک JOIN به جدول wp_postmeta اضافه می‌کند و اگر جدول ایندکس مناسب نداشته باشد، MySQL مجبور به Full Table Scan می‌شود که زمان پاسخ را به چند ثانیه می‌رساند. انتخاب نوع داده صحیح در meta_query، تفاوت بین استفاده از ایندکس و عدم استفاده از آن است و نادیده گرفتن آن، هزینه‌ای پنهان ایجاد می‌کند. کاهش تعداد شرط‌ها، اجتناب از LIKE و REGEXP، و در صورت امکان، جایگزینی با جدول سفارشی، سه لایه ضروری برای مقیاس‌پذیری هستند. تست Meta Query در سه سطح زمان اجرا، Plan اجرایی و رفتار MySQL انجام می‌شود و بدون آن، انتشار به تولید ریسک بالایی دارد. در این راهنما از ساختار پایه تا استقرار تولیدی بهینه‌سازی Meta Query را با نگاه مهندسی و کد عملی پوشش می‌دهیم.

در پروژه‌های واقعی، اولین بار که با یک فروشگاه بزرگ مواجه می‌شوم، کوئری محصولات با meta_query بیش از سه ثانیه طول می‌کشد. راه‌حل معمولاً در دو تغییر کوچک است: ایندکس مناسب و حذف شرط اضافه. این راهنما از همان نقطه‌ای شروع می‌کند که تجربه می‌گوید بیشترین ارزش را دارد.

Meta Query چیست و چرا کند می‌شود؟

Meta Query یک پارامتر در WP_Query است که امکان فیلتر نتایج بر اساس متادیتا را می‌دهد. هر شرط در meta_query، یک JOIN اضافه به جدول wp_postmeta ایجاد می‌کند.

سه دلیل اصلی کندی meta_query:

  1. نبود ایندکس روی ستون meta_key.
  2. نبود ایندکس ترکیبی روی meta_key و meta_value.
  3. استفاده از LIKE یا REGEXP که اجازه استفاده از ایندکس را نمی‌دهد.

پیش از ادامه، راهنمای WP_Query Optimization و بهینه‌سازی کوئری را مطالعه کنید. اگر با ساختار جدول wp_postmeta آشنایی ندارید، راهنمای فیلد سفارشی در قالب و افزونه نقطه شروع مناسبی است.

نشانه‌هایی که Meta Query کند است

اگر کوئری فروشگاه بیش از ۵۰۰ms طول می‌کشد، اگر EXPLAIN از Full Scan استفاده می‌کند، یا اگر تعداد رکوردهای wp_postmeta از چند صد هزار عبور کرده، به سراغ ایندکس بروید.

آناتومی کوئری wp_postmeta

جدول wp_postmeta شامل چهار ستون است: meta_id، post_id، meta_key و meta_value. ساختار key/value برای انعطاف طراحی شده، اما برای کوئری بهینه نیست.

SELECT p.ID, p.post_title
FROM wp_posts p
INNER JOIN wp_postmeta pm1 ON pm1.post_id = p.ID
INNER JOIN wp_postmeta pm2 ON pm2.post_id = p.ID
WHERE pm1.meta_key = 'price'     AND pm1.meta_value >= 1000000
  AND pm2.meta_key = 'stock'    AND pm2.meta_value = 'instock'
  AND p.post_type = 'product';

در این کوئری، دو JOIN به wp_postmeta وجود دارد و اگر ایندکس مناسب نباشد، MySQL هر ردیف را اسکن می‌کند.

ساختار ایندکس پیش‌فرض وردپرس

وردپرس به‌طور پیش‌فرض سه ایندکس روی wp_postmeta ایجاد می‌کند:

  1. PRIMARY KEY روی meta_id.
  2. KEY post_id روی post_id.
  3. KEY meta_key روی meta_key.

اما هیچ ایندکس ترکیبی روی (meta_key, meta_value) وجود ندارد و همین باعث کندی می‌شود.

ایندکس‌گذاری wp_postmeta

افزودن ایندکس ترکیبی روی (meta_key, meta_value) بزرگ‌ترین بهبود را ایجاد می‌کند.

-- ایندکس ترکیبی روی meta_key و meta_value
ALTER TABLE wp_postmeta
ADD INDEX idx_key_value (meta_key, meta_value(20));

توجه: طول meta_value می‌تواند زیاد باشد و ایندکس روی ستون کامل امکان‌پذیر نیست. با محدودسازی به ۲۰ کاراکتر اول، ایندکس بهینه می‌شود.

ایندکس برای کوئری‌های خاص

-- ایندکس ترکیبی برای پست و کلید
ALTER TABLE wp_postmeta
ADD INDEX idx_post_key (post_id, meta_key);

این ایندکس برای واکشی همه متادیتای یک پست مفید است و N+1 را کاهش می‌دهد.

ایندکس سفارشی بر اساس نوع داده

در پروژه‌های خاص، ممکن است نیاز به ایندکس روی مقدار عددی داشته باشید. این کار با CAST امکان‌پذیر است.

-- ایندکس تابعی (MySQL 8.0+)
ALTER TABLE wp_postmeta
ADD INDEX idx_numeric_price ((CAST(meta_value AS UNSIGNED)));

انتخاب نوع داده در meta_query

در meta_query، پارامتر type تعیین می‌کند MySQL مقدار را به چه نوعی Cast کند. انتخاب اشتباه، ایندکس را بی‌استفاده می‌کند.

<?php
$query = new WP_Query( array(
    "post_type"  => "product",
    "meta_query" => array(
        array(
            "key"     => "price",
            "value"   => 1000000,
            "compare" => ">=",
            "type"    => "NUMERIC",  // مهم
        ),
    ),
) );

انواع پشتیبانی‌شده عبارت‌اند از: NUMERIC، BINARY، CHAR، DATE، DATETIME، DECIMAL، SIGNED، TIME، UNSIGNED.

اثر type بر ایندکس

اگر ایندکس شما روی meta_value به‌صورت CHAR است، استفاده از type => NUMERIC می‌تواند ایندکس را نادیده بگیرد. بنابراین، ایندکس باید با نوع کوئری هماهنگ باشد.

مقایسه عملگرها و انتخاب سریع‌ترین

ترتیب سرعت عملگرها:

  1. = سریع‌ترین، اجازه استفاده از ایندکس.
  2. IN سریع، با ایندکس.
  3. >=، <=، >، < سریع، با ایندکس.
  4. BETWEEN متوسط، با ایندکس.
  5. LIKE 'prefix%' کند، گاهی ایندکس.
  6. LIKE '%suffix' کند، بدون ایندکس.
  7. REGEXP کندترین، بدون ایندکس.

هرگز از LIKE با wildcard در ابتدا استفاده نکنید.

مثال: جایگزینی LIKE با Exact Match

// اشتباه
array(
    "key"     => "sku",
    "value"   => "ABC",
    "compare" => "LIKE",
)

// درست
array(
    "key"     => "sku",
    "value"   => "ABC",
    "compare" => "=",
)

کاهش تعداد شرط‌ها و JOIN

هر شرط در meta_query، یک JOIN اضافه می‌کند. اگر ۵ شرط داشته باشید، ۵ JOIN و در نتیجه اسکن پنج‌باره جدول خواهید داشت.

// اشتباه: ۵ شرط
"meta_query" => array(
    "relation" => "AND",
    array( "key" => "price",    "value" => 1000000, "compare" => ">=" ),
    array( "key" => "stock",    "value" => "instock" ),
    array( "key" => "brand",    "value" => "Apple" ),
    array( "key" => "color",    "value" => "black" ),
    array( "key" => "shipping", "value" => "free" ),
)

راه‌حل: ادغام چند کلید در یک کلید ترکیبی، یا استفاده از جدول سفارشی.

ادغام متادیتا

// جایگزین: ذخیره یک آرایه در meta_key واحد
update_post_meta( $post_id, "wpk_attributes", array(
    "price"    => 1000000,
    "stock"    => "instock",
    "brand"    => "Apple",
    "color"    => "black",
    "shipping" => "free",
) );

البته این روش، کوئری‌گیری را محدود می‌کند. برای فیلترهای پیچیده، جدول سفارشی انتخاب بهتری است.

کش کردن نتایج متادیتا

اگر متادیتا تکرارشونده است، می‌توانید نتایج را در Object Cache ذخیره کنید.

<?php
function wpk_get_products_by_price( $min_price ) {
    $cache_key = "wpk_products_price_" . $min_price;
    $cached    = wp_cache_get( $cache_key, "wpk" );

    if ( false !== $cached ) {
        return $cached;
    }

    $query = new WP_Query( array(
        "post_type"      => "product",
        "posts_per_page" => 20,
        "fields"         => "ids",
        "meta_query"     => array(
            array( "key" => "price", "value" => $min_price, "compare" => ">=", "type" => "NUMERIC" ),
        ),
    ) );

    wp_cache_set( $cache_key, $query->posts, "wpk", 15 * MINUTE_IN_SECONDS );
    return $query->posts;
}

راهنمای Object Cache در وردپرس نقطه شروع مناسبی است.

پاک‌سازی کش بعد از تغییر محصول

add_action( "save_post_product", function() {
    wp_cache_flush();
} );

جایگزینی با جدول سفارشی

اگر کوئری‌های متادیتا سنگین هستند، جدول سفارشی انتخاب درستی است. جدول سفارشی امکان ایندکس‌گذاری مستقیم روی ستون‌های عددی و رشته‌ای را می‌دهد.

CREATE TABLE wp_wpk_product_meta (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    product_id BIGINT UNSIGNED NOT NULL,
    price DECIMAL(15,2) NOT NULL,
    stock VARCHAR(32) NOT NULL,
    brand VARCHAR(64) NOT NULL,
    PRIMARY KEY (id),
    KEY idx_price (price),
    KEY idx_stock (stock),
    KEY idx_brand (brand)
) ENGINE=InnoDB;

راهنمای Custom Table و ساختار داده سفارشی نقطه شروع مناسبی است.

مقایسه سرعت

رویکردزمان کوئریمقیاس‌پذیری
meta_query بدون ایندکس2-5 ثانیهضعیف
meta_query با ایندکس ترکیبی200-500msمتوسط
جدول سفارشیزیر 50msبالا

Meta Query در پست‌تایپ سفارشی

در پست‌تایپ سفارشی، meta_query رایج‌ترین پارامتر فیلتر است. توصیه می‌کنم برای هر پست‌تایپ، ایندکس اختصاصی تعریف کنید.

ALTER TABLE wp_postmeta
ADD INDEX idx_portfolio_price (meta_key(20), meta_value(20));

راهنمای ساخت نوع نوشته سفارشی را ببینید.

Meta Query در ACF Pro و Metabox.io

افزونه‌های فیلد سفارشی مثل ACF Pro و Metabox.io از همین ساختار wp_postmeta استفاده می‌کنند. بهینه‌سازی meta_query در این افزونه‌ها نیز ضروری است. راهنمای راهنمای ACF Pro و راهنمای Metabox.io را ببینید.

تست و پایش کوئری

برای تحلیل Plan اجرایی، از EXPLAIN ANALYZE استفاده کنید (MySQL 8.0.18+).

EXPLAIN ANALYZE
SELECT p.ID
FROM wp_posts p
INNER JOIN wp_postmeta pm ON pm.post_id = p.ID
WHERE pm.meta_key = 'price'
  AND pm.meta_value >= 1000000
  AND p.post_type = 'product';

خروجی نشان می‌دهد که آیا ایندکس استفاده شده یا Full Scan رخ داده است.

Query Monitor برای کوئری‌های وردپرس

Plugin Query Monitor، همه کوئری‌های صفحه را با زمان و Plan نشان می‌دهد. راهنمای Query Monitor برای دیباگ وردپرس نقطه شروع مناسبی است.

اشتباهات رایج در تست

اشتباه اول، نبود تست با داده حجیم. اشتباه دوم، نبود تست با EXPLAIN. اشتباه سوم، نبود تست با کوئری‌های ترکیبی. اشتباه چهارم، نبود تست کش. اشتباه پنجم، نبود تست با جدول سفارشی.

پرسش‌های پرتکرار درباره Meta Query

چرا meta_query کند است؟

چون ساختار key/value به‌صورت پیش‌فرض ایندکس ترکیبی ندارد و هر شرط یک JOIN اضافه می‌کند.

آیا افزودن ایندکس به wp_postmeta امن است؟

بله، اما حتماً قبل از تغییر، بکاپ بگیرید. راهنمای بکاپ دیتابیس و بازیابی را ببینید.

آیا type => NUMERIC همیشه سریع است؟

خیر، بستگی به ایندکس دارد. اگر ایندکس CHAR باشد، NUMERIC ممکن است ایندکس را نادیده بگیرد.

آیا LIKE همیشه کند است؟

LIKE با wildcard در ابتدا کند است. LIKE 'prefix%' می‌تواند از ایندکس استفاده کند.

چه زمانی به جدول سفارشی مهاجرت کنم؟

اگر کوئری‌ها بیش از ۵۰۰ms طول می‌کشند یا تعداد شرط‌ها بیش از ۳ است، مهاجرت را جدی بگیرید.

آیا meta_query روی فیلدهای ACF هم کند است؟

بله، چون ACF هم در wp_postmeta ذخیره می‌کند. راهنمای راهنمای ACF Pro را ببینید.

نتیجه و مسیر ادامه

Meta Query یکی از بزرگ‌ترین منابع کندی پنهان در وردپرس است. کلید موفقیت، ایندکس‌گذاری هدفمند، انتخاب نوع داده صحیح، کاهش تعداد شرط‌ها و در صورت امکان، جایگزینی با جدول سفارشی است. اگر این لایه‌ها با دقت طراحی شوند، کوئری‌ها در طول سال‌ها سریع باقی می‌مانند.

پیشنهاد می‌کنم مسیر یادگیری را با WP_Query Optimization و بهینه‌سازی کوئری ادامه دهید و سپس Custom Table و ساختار داده سفارشی را به‌عنوان رویکرد پیشرفته مطالعه کنید. همچنین مفهوم Database Index را در ویکی‌پدیا مرور کنید.

اگر روی پروژه واقعی خود Meta Query را بهینه کرده‌اید، برایم جالب است بدانید کدام بخش — ایندکس‌گذاری یا مهاجرت به جدول سفارشی — بیشترین تأثیر را داشته است. تجربه خودتان را در دیدگاه‌ها بنویسید.