کندی سایت وردپرسی، در بیشتر موارد نه از هاست، نه از قالب، که از کوئری‌های سنگین می‌آید. در سال‌ها کار با وردپرس، الگویی تکراری دیده‌ام: سایت روی هاست قوی اجرا می‌شود، قالب سبک است، افزونه‌های زیادی نصب نیستند، ولی صفحه در سه ثانیه بالا نمی‌آید. مقصر معمولاً یک کوئری سنگین در قالب یا افزونه است که در هر بازدید، جدول wp_postmeta یا wp_options را می‌کاود. این مقاله، بهینه‌سازی کوئری‌های وردپرس را از پایه تا الگوهای حرفه‌ای مرور می‌کند. برای درک پیش‌نیازها، افزونه وردپرس چیست، کدنویسی کوئری سفارشی، و تأثیر دیتابیس بر سرعت سایت را پیش از ادامه ببینید.

هزینهٔ واقعی یک کوئری سنگین

هر کوئری، سه هزینهٔ اصلی دارد: یک — زمان CPU سرور دیتابیس: پارس و اجرای کوئری. دو — زمان I/O دیسک: خواندن صفحات از دیسک. سه — زمان انتقال داده: ارسال نتایج به PHP. کوئری‌های سنگین در وردپرس معمولاً از سه منبع می‌آیند: یک — جستجو در wp_postmeta با meta_query چندشرطه. جدول postmeta ایندکس محدودی دارد و کوئری‌های ترکیبی، آن را به اسکن کامل می‌کشانند. دو — کوئری‌های تکراری درون حلقه. الگوی N+1 که در بخش‌های بعدی باز می‌کنم. سه — کوئری روی wp_options با autoload=yes سنگین. هر بازدید، این optionها لود می‌شوند و اگر حجم زیادی داشته باشند، بار قابل‌توجهی روی MySQL می‌گذارند. سه نشانه که سایت شما کوئری‌سنگین است: یک — کندیِ پیشخوان. دو — بالا رفتن CPU هاست در ساعات پربازدید. سه — تفاوت زمان پاسخِ first-byte (TTFB) بین صفحه‌های مختلف. راهنمای TTFB در تأثیر TTFB بر سرعت.

هر کوئری اضافه در هر بازدید، یک سکه از بودجهٔ سرعت سایت شما خرج می‌کند؛ صد کوئری اضافه، صد سکه در ثانیه.

سنجش قبل از بهینه‌سازی

پیش از هر بهینه‌سازی، باید بدانید کدام کوئری مقصر است. سه ابزار اصلی: یک — Query Monitor. افزونهٔ رایگانی که تعداد کوئری‌ها، زمان اجرا، منبع (افزونه/قالب/هسته)، و کوئری خام را نشان می‌دهد. در صفحه‌ای که کند است، پس از فعال‌سازی، نوار پایین صفحه خلاصه‌ای از کوئری‌ها نشان می‌دهد. با کلیک روی آن، فهرست کامل کوئری‌ها به ترتیب زمان ظاهر می‌شود. دو — New Relic یا ابزار مشابه. برای پایش Production، پروفایلینگ Transaction مفید است. سه — Slow Query Log در MySQL. در cPanel یا phpMyAdmin، می‌توانید کوئری‌های بالای چند صد میلی‌ثانیه را ثبت کنید.

-- در phpMyAdmin، بررسی کوئری‌های کند
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';

الگوی عیب‌یابی: در Query Monitor، به کوئری‌هایی با زمان بالای ۵۰ میلی‌ثانیه نگاه کنید. مقصر معمولاً یک افزونه یا کوئری قالب است که در هر بازدید اجرا می‌شود. راهنمای کامل در ابزارهای تست سرعت سایت، تست و دیباگ پروژه‌های وردپرس، و بهینه‌سازی کد وردپرس. یک نکتهٔ میدانی: در Query Monitor، تب Queries by Caller عملاً اسم افزونه یا تابع را نشان می‌دهد. همین تب، در پروژه‌های بزرگ، جای ساعت‌ها عیب‌یابی دستی را گرفته است.

پارامترهای سبک‌ساز WP_Query

چهار پارامتر WP_Query که بدون تغییر منطق، سرعت را بهبود می‌دهند:

$args = array(
    'post_type'              => 'post',
    'posts_per_page'         => 10,
    'no_found_rows'          => true,           // حذف کوئری شمارش
    'update_post_meta_cache' => false,          // حذف لود متا
    'update_post_term_cache' => false,          // حذف لود ترم
    'fields'                 => 'ids',           // فقط ID
);
$query = new WP_Query( $args );

توضیح هرکدام: یک — no_found_rows: به‌طور پیش‌فرض، WP_Query دو کوئری می‌زند — یکی برای داده، یکی برای شمارش کل. اگر صفحه‌بندی ندارید، پارامتر دوم بی‌فایده است. با no_found_rows => true، کوئری شمارش حذف می‌شود. صرفه‌جویی معمول: ۲۰ تا ۴۰ درصد زمان کوئری. دو — update_post_meta_cache: وقتی حلقه لود می‌شود، وردپرس به‌طور خودکار تمام متای تمام نوشته‌ها را در یک کوئری می‌گیرد. اگر از متا استفاده نمی‌کنید، این کوئری حذف می‌شود. سه — update_post_term_cache: مشابه، برای ترم‌ها. چهار — fields => 'ids': اگر فقط ID لازم دارید، WP_Query تنها ستون ID را برمی‌گرداند. تفصیل کامل در کدنویسی کوئری سفارشی، توابع کوئری سفارشی، و بهینه‌سازی کوئری‌های MySQL. یک الگوی تکمیلی برای صفحه‌های با لیست بلند: با posts_per_page معقول شروع کنید و اگر صفحه‌بندی ندارید، مقدار را در حد نیاز نگه دارید. بارگذاری ۵۰۰ نوشته در یک صفحه، نه به سرعت، نه به تجربهٔ کاربر کمک نمی‌کند.

مشکل N+1 query و راه‌حل

الگوی N+1 query، شایع‌ترین نوع کوئری سنگین در وردپرس است. مثال نادرست:

$query = new WP_Query( array( 'post_type' => 'project', 'posts_per_page' => 20 ) );
while ( $query->have_posts() ) :
    $query->the_post();
    // کوئری اضافه در حلقه
    $author_meta = get_user_meta( get_the_author_meta( 'ID' ), '_company_name', true );
endwhile;

در این کد، برای هر پروژه، یک کوئری به wp_usermeta زده می‌شود. اگر ۲۰ پروژه باشد، ۲۰ کوئری اضافه. الگوی بهینه:

$query = new WP_Query( array( 'post_type' => 'project', 'posts_per_page' => 20 ) );

// جمع‌آوری IDها
$author_ids = wp_list_pluck( $query->posts, 'post_author' );
$author_ids = array_unique( $author_ids );

// یک کوئری برای همه
$authors = array();
foreach ( $author_ids as $author_id ) {
    $authors[ $author_id ] = get_userdata( $author_id );
}

while ( $query->have_posts() ) :
    $query->the_post();
    $author = $authors[ get_the_author_meta( 'ID' ) ];
endwhile;

سه الگوی مشابه: یک — کاربران: با get_users و post__in، همه را یک‌جا بگیرید. دو — ترم‌ها: با get_terms و فیلتر object_ids. سه — متادیتای نوشته‌ها: با update_meta_cache یا get_post_meta که به‌طور خودکار cache می‌شود. یک نکتهٔ فنی: اگر حلقهٔ شما ۲۰۰ نوشته دارد و برای هر نوشته یک متا لازم دارد، حتی با cache، بار سنگین است. در چنین مواردی، طراحی صفحه را بازبینی کنید: آیا واقعاً ۲۰۰ نوشته باید در یک صفحه نمایش داده شود؟ راهنما در بهینه‌سازی کد وردپرس و بهینه‌سازی کوئری‌ها. یک مثال میدانی: در پروژه‌ای با فهرست ۵۰ پروژه که برای هر کدام فیلد «نام مشتری» لازم بود، الگوی بالا تعداد کوئری‌ها را از ۵۲ به ۳ کاهش داد و زمان صفحه از ۲.۴ به ۰.۹ ثانیه رسید.

بهینه‌سازی کوئری‌های متادیتا

کوئری‌های meta_query ذاتاً سنگین‌اند، چون جدول wp_postmeta ساختار key-value دارد. چهار تکنیک برای سبک‌سازی:

یک — کاهش تعداد شرط‌ها: هر شرط اضافه، یک JOIN دیگر است. اگر می‌توانید شرطی را در PHP پس از کوئری اعمال کنید (روی حجم کوچک)، کوئری را سبک‌تر کنید. مثال:

// نامناسب
'meta_query' => array(
    array( 'key' => '_year', 'value' => 2023 ),
    array( 'key' => '_status', 'value' => 'published' ),
    array( 'key' => '_featured', 'value' => '1' ),
),

// بهتر - اگر یکی از شرط‌ها نادر است، به PHP منتقل کنید
'meta_query' => array(
    array( 'key' => '_year', 'value' => 2023, 'type' => 'NUMERIC' ),
),
// سپس در PHP فیلتر بر اساس status و featured

دو — انتخاب درست type: همیشه type را صریح بگذارید (NUMERIC، DATE، DATETIME) تا MySQL بتواند ایندکس را به‌کار گیرد.

سه — استفاده از EXISTS و NOT EXISTS: برای فیلتر بر اساس وجود داشتن متادیتا:

'meta_query' => array(
    array(
        'key'     => '_featured',
        'compare' => 'EXISTS',
    ),
),

این الگو سریع‌تر از value => '1' است، چون نیازی به مقایسه مقدار ندارد.

چهار — انتقال داده‌های پرمصرف به تاکسونومی: اگر فیلتری مکرر روی یک متادیتا اعمال می‌شود، شاید باید آن را به تاکسونومی منتقل کنید. جدول wp_term_relationships ایندکس بهتری دارد و کوئری‌های تاکسونومی سریع‌ترند. راهنمای انتقال در کار با تاکسونومی سفارشی. یک آسیب‌پذیری عملکردی که در سایت‌های بزرگ زیاد دیده‌ام: کوئری روی متادیتای عددی که type ندارد. نتیجه: مقایسه رشته‌ای که در اعداد چند رقمی نتایج اشتباه و اسکن کامل می‌دهد. همیشه type را بگذارید.

بهینه‌سازی کوئری تاکسونومی

کوئری‌های tax_query، به‌طور پیش‌فرض سریع‌تر از meta_query هستند، چون جدول wp_term_relationships ایندکس بهتری دارد. سه تکنیک بهبود: یک — انتخاب درست field: term_id سریع‌تر از slug است. اگر slug دارید، در PHP به ID تبدیل کنید و کوئری را با ID بزنید. دو — کاهش include_children: در تاکسونومی‌های سلسله‌مراتبی، include_children => true (پیش‌فرض) کوئری سنگین‌تری می‌زند. اگر نیازی به فرزندان ندارید، false بگذارید. سه — ترکیب با no_found_rows: همان‌طور که پیش‌تر گفتم، حذف کوئری شمارش، اثر محسوسی دارد.

$args = array(
    'post_type'      => 'product',
    'posts_per_page' => 12,
    'no_found_rows'  => true,
    'tax_query'      => array(
        array(
            'taxonomy'         => 'product_cat',
            'field'            => 'term_id',
            'terms'            => 5,
            'include_children' => false,
        ),
    ),
);

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

ایندکس‌گذاری دیتابیس

وردپرس به‌طور پیش‌فرض روی جدول wp_postmeta، ایندکس ترکیبی روی (post_id, meta_key) دارد. ولی اگر کوئری‌های شما به شکل خاصی هستند، ایندکس اختصاصی می‌تواند سرعت را چند برابر کند.

-- افزودن ایندکس روی meta_key (برای کوئری‌های پرتکرار)
ALTER TABLE wp_postmeta ADD INDEX meta_key_value (meta_key, meta_value(20));

-- ایندکس روی wp_options برای فیلتر autoload
ALTER TABLE wp_options ADD INDEX autoload_name (autoload, option_name);

سه تذکر حیاتی: یک — پیش از افزودن ایندکس، بکاپ کامل بگیرید. راهنمای بکاپ در بکاپ دیتابیس و افزونه‌های بکاپ. دو — ایندکس، هزینه هم دارد: هر INSERT، UPDATE، DELETE کندتر می‌شود. برای سایت‌هایی که نوشتن زیاد دارند، این هزینه باید در تصمیم لحاظ شود. سه — ایندکس اضافه، حذف می‌شود: در آپدیت‌های وردپرس، ایندکس‌های سفارشی روی جدول‌های هسته ممکن است حذف شوند. بنابراین یا در mu-plugins، کدی بگذارید که در آپدیت، ایندکس را بازسازی کند، یا ایندکس را روی جدول‌های اختصاصی خود بسازید. راهنمای ایندکس‌گذاری در ایندکس‌گذاری در دیتابیس، ایندکس در MySQL، و بهینه‌سازی جداول MySQL. یک نکتهٔ فنی: در سایت‌های با بیش از ۱۰۰ هزار نوشته، افزودن ایندکس اختصاصی روی meta_key، تفاوت بین کوئری ۵ ثانیه‌ای و ۲۰۰ میلی‌ثانیه‌ای است.

کش نتایج سنگین

کش کردن نتایج کوئری‌های سنگین، بیشترین اثر را در زمان کم دارد. سه سطح کش: یک — Transients: برای کوئری‌های پرهزینه:

function my_plugin_get_featured_posts() {
    $cache_key = 'my_featured_posts';
    $cached = get_transient( $cache_key );
    if ( $cached !== false ) {
        return $cached;
    }

    $query = new WP_Query( array(
        'post_type'              => 'post',
        'posts_per_page'         => 10,
        'meta_key'               => '_featured',
        'meta_value'             => '1',
        'no_found_rows'          => true,
        'update_post_meta_cache' => false,
        'update_post_term_cache' => false,
    ) );

    $data = array();
    while ( $query->have_posts() ) :
        $query->the_post();
        $data[] = array(
            'id'    => get_the_ID(),
            'title' => get_the_title(),
            'url'   => get_permalink(),
            'thumb' = get_the_post_thumbnail_url( get_the_ID(), 'medium' ),
        );
    endwhile;
    wp_reset_postdata();

    set_transient( $cache_key, $data, HOUR_IN_SECONDS );
    return $data;
}

دو — Object Cache (Redis/Memcached): با فعال‌سازی Redis Object Cache، نتایج کوئری‌های تکراری بین درخواست‌ها به اشتراک گذاشته می‌شوند. در سایت‌های پرترافیک، این لایه به‌تنهایی تعداد کوئری‌های دیتابیس را چند برابر کاهش می‌دهد. سه — Cache Invalidation: پس از هر تغییر در محتوای مرتبط، کش را باطل کنید:

add_action( 'save_post', function( $post_id ) {
    delete_transient( 'my_featured_posts' );
} );

راهنمای کامل در ترنزینت‌ها در وردپرس، افزونه‌های کش وردپرس، و بهینه‌سازی کد وردپرس. یک نکتهٔ عملی: در پروژه‌ای با کوئری سنگین روی جدول postmeta، کش ۲۴ساعته زمان پاسخ را از ۲.۵ به ۰.۴ ثانیه رساند. کل تغییر، ۱۵ خط کد بود.

بهینه‌سازی کوئری خام با wpdb

وقتی WP_Query کافی نیست، از $wpdb استفاده می‌کنید. چهار تکنیک سبک‌سازی: یک — انتخاب ستون‌های مشخص: به‌جای SELECT *، ستون‌های لازم را انتخاب کنید.

global $wpdb;

// نامناسب
$sql = "SELECT * FROM {$wpdb->posts} WHERE post_type = 'post' LIMIT 20";

// بهتر
$sql = $wpdb->prepare(
    "SELECT ID, post_title, post_date FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s ORDER BY post_date DESC LIMIT 20",
    'post',
    'publish'
);
$results = $wpdb->get_results( $sql );

دو — استفاده از LIMIT: بدون آن، تمام نتایج بار می‌شوند. سه — INNER JOIN به‌جای کوئری‌های تودرتو: اگر به دادهٔ چند جدول نیاز دارید، JOIN یک کوئری، سریع‌تر از چند کوئری است. چهار — آماده‌سازی الزامی: $wpdb->prepare را هرگز فراموش نکنید.

نکته: در کوئری‌های خام، به prefix جدول توجه کنید. همیشه از {$wpdb->posts}، {$wpdb->postmeta} و مشابه استفاده کنید، نه نام‌های hardcode. راهنمای کامل در PHP امن در وردپرس و بهینه‌سازی کوئری‌های MySQL. یک نکتهٔ تکمیلی: در کوئری‌های خام با حجم بالا، ممکن است نیاز به cache_results => false باشد که در WP_Query هم قابل تنظیم است. این پارامتر، cache داخلی وردپرس را غیرفعال می‌کند — مناسب برای کوئری‌های آماری که cache وردپرس باری اضافه است.

کوئری‌های options و autoload

یکی از کم‌توجه‌شده‌ترین منبع‌های کندی سایت، جدول wp_options است. در هر بازدید، تمام optionهای با autoload=yes در حافظه لود می‌شوند. اگر این حجم بالا باشد، بار قابل‌توجهی روی هر ریکوئست می‌آید. بررسی حجم:

SELECT SUM( LENGTH( option_value ) ) AS total_size
FROM wp_options
WHERE autoload = 'yes';

اگر عدد بالای ۱ مگابایت است، زمان بازبینی رسیده. سه اقدام: یک — انتقال optionهای سنگین به autoload=no:

$value = get_option( 'my_large_cache' );
delete_option( 'my_large_cache' );
add_option( 'my_large_cache', $value, '', 'no' );

دو — حذف optionهای بی‌استفاده: گاهی افزونه‌های حذف‌شده، optionهایشان را باقی گذاشته‌اند. سه — استفاده از object cache: با Redis، optionها از کش سراسری لود می‌شوند. راهنمای کامل در کار با Options API، پاک‌سازی دیتابیس، و تأثیر دیتابیس بر سرعت سایت.

ابزارهای عیب‌یابی

پنج ابزار ضروری در عیب‌یابی کوئری‌ها: یک — Query Monitor: تعداد، زمان، و منبع کوئری‌ها در هر صفحه. دو — WP-CLI: با دستور wp db query، کوئری‌های دلخواه را در ترمینال اجرا کنید. سه — EXPLAIN در MySQL: برای تحلیل کوئری‌های کند:

EXPLAIN SELECT ID, post_title FROM wp_posts
WHERE post_type = 'post' AND post_status = 'publish'
ORDER BY post_date DESC LIMIT 20;

خروجی EXPLAIN نشان می‌دهد MySQL از کدام ایندکس استفاده می‌کند یا اگر type = ALL باشد، یعنی اسکن کامل جدول. چهار — پروفایل Xdebug: در محیط لوکال، پروفایلینگ دقیق کد را نشان می‌دهد. پنج — Log Queries در wpdb: با SAVEQUERIES در wp-config.php، تمام کوئری‌ها ذخیره می‌شوند:

// در wp-config.php
define( 'SAVEQUERIES', true );

// در قالب، پشت سد دسترسی ادمین
global $wpdb;
if ( current_user_can( 'manage_options' ) ) {
    echo '<pre>' . esc_html( print_r( $wpdb->queries, true ) ) . '</pre>';
}

راهنمای کامل در ابزارهای تست سرعت سایت، تست و دیباگ پروژه‌ها، و دیباگ کد سفارشی وردپرس. نکته: SAVEQUERIES روی Production فعال نکنید. بار اضافه دارد و حافظه مصرف می‌کند. فقط در محیط توسعه یا استیجینگ استفاده کنید. راهنمای محیط لوکال در توسعه با محیط لوکال.

الگوهای ساختاری

بهینه‌سازی سطح کد، بدون ساختار درست، پایدار نمی‌ماند. چهار الگو که در پروژه‌های بزرگ به‌کار می‌برم: یک — لایهٔ Query جداگانه: تمام کوئری‌های سنگین در یک کلاس اختصاصی، با کش متمرکز:

class My_Plugin_Queries {

    public static function get_featured_posts( $limit = 6 ) {
        $key = 'featured_posts_' . $limit;
        $data = get_transient( $key );
        if ( $data !== false ) {
            return $data;
        }

        $query = new WP_Query( array(
            'post_type'              => 'post',
            'posts_per_page'         => $limit,
            'meta_key'               => '_featured',
            'meta_value'             => '1',
            'no_found_rows'          => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        ) );

        $data = array_map( array( __CLASS__, 'format_post' ), $query->posts );
        set_transient( $key, $data, HOUR_IN_SECONDS );

        return $data;
    }

    private static function format_post( $post ) {
        return array(
            'id'    => $post->ID,
            'title' => get_the_title( $post ),
            'url'   => get_permalink( $post ),
            'thumb' = get_the_post_thumbnail_url( $post, 'medium' ),
        );
    }
}

دو — Cache Invalidation مرکزی: تمام کش‌های مرتبط با یک نوع محتوا، در یک نقطه باطل شوند:

add_action( 'save_post', function( $post_id, $post ) {
    if ( 'post' === $post->post_type ) {
        delete_transient( 'featured_posts_6' );
        delete_transient( 'featured_posts_10' );
    }
}, 10, 2 );

سه — بودجهٔ کوئری در CI: در تست‌های خودکار، تعداد کوئری هر صفحه را اندازه بگیرید و در فایل config، بودجه تعیین کنید. اگر تعداد از بودجه گذشت، تست شکست می‌خورد. الگو در CI/CD در وردپرس و تست و دیباگ پروژه‌ها. چهار — مستندسازی کوئری‌های سنگین: در مستندات پروژه، فهرست کوئری‌های سنگین و کش‌های مرتبط را نگه دارید. راهنمای ساختاربندی در ساختاربندی پروژهٔ وردپرس و بهینه‌سازی کد وردپرس. یک تجربهٔ میدانی: در پروژه‌ای با سه کوئری سنگین روی صفحهٔ اصلی، تجمیع آن‌ها در یک کلاس با کش متمرکز، زمان پاسخ صفحه را از ۲.۸ به ۱.۱ ثانیه کاهش داد — بدون تغییر در سرور.

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

بهینه‌سازی کوئری‌های وردپرس، مسیری روشن دارد: سنجش با Query Monitor، سبک‌سازی با پارامترهای WP_Query، رفع الگوی N+1، بهینه‌سازی meta_query و tax_query، ایندکس‌گذاری هدفمند، کش نتایج سنگین، بهینه‌سازی کوئری خام، مدیریت optionهای autoload، و ساختار کلاس‌محور. هر یک از این گام‌ها، به‌تنهایی می‌تواند زمان پاسخ را محسوس کاهش دهد. اگر امروز یک کار در این مسیر انجام می‌دهید: Query Monitor را نصب کنید و در صفحهٔ اصلی سایت، فهرست کوئری‌ها را به ترتیب زمان مرور کنید؛ اولین سه کوئری، فهرست اقدام شما هستند. اگر تجربه‌ای از یک کوئری سنگین دارید که با کش یا بازنویسی حل شد، در دیدگاه‌ها بنویسید؛ همان گزارش‌های واقعی، این راهنما را دقیق‌تر می‌کند. ⚡