شش سال پیش، یک سایت خبری با هشتاد هزار نوشته را به من سپردند برای بهینه‌سازی. صفحه اصلی در چهار ثانیه باز می‌شد و مدیر سایت شکایت داشت که کاربران موبایل، قبل از دیدن تیتر اول، سایت را ترک می‌کنند. وارد کد شدم و بعد از سه ساعت بررسی، متوجه شدم مشکل در یک کوئری سفارشی است که نویسنده قبلی با WP_Query نوشته بود، ولی هیچ‌کدام از پارامترهای بهینه‌سازی را رعایت نکرده بود. همان کوئری، در هر بازدید، تمام هشتاد هزار نوشته را می‌خواند تا پنج تای اول را نمایش دهد. یک هفته کار روی آن کوئری — حذف پارامترهای اضافه، اضافه کردن no_found_rows، و کش کردن نتایج — زمان پاسخ را از چهار ثانیه به نیم ثانیه رساند. آن پروژه، درس بزرگی به من داد: کوئری سفارشی، ابزار قدرتمندی است ولی اگر بی‌دقت نوشته شود، از هر افزونه سنگینی مخرب‌تر است.

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

چرا توابع کوئری سفارشی اهمیت دارند؟

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

  • صفحه اصلی با نوشته‌های منتخب و پربازدید
  • لیست محصولات با فیلتر قیمت و دسته
  • نمونه‌کارهای فیلترشده بر اساس سال و دسته
  • پروژه‌های مرتبط با هر نوشته
  • دوره‌های آموزشی با دسته‌بندی و سطح

توابع کوئری سفارشی، دقیقاً برای این نیازها طراحی شده‌اند. سه اصل در کار با این توابع:

  1. از توابع استاندارد استفاده کنید، نه SQL خام: WP_Query، get_posts، get_terms و WP_User_Query، همه لایه‌های امنیت و کش وردپرس را فعال می‌کنند. SQL خام، این لایه‌ها را دور می‌زند.
  2. پارامترها را دقیق تعریف کنید: هر پارامتر اضافه، یک JOIN یا sub-query اضافه است. در سایت‌های بزرگ، این جمع می‌شود و کوئری را از چند میلی‌ثانیه به چند ثانیه می‌رساند.
  3. نتایج سنگین را کش کنید: با set_transient یا object cache. راهنمای کامل در ترنزینت‌ها در وردپرس و افزونه‌های کش وردپرس.

راهنمای ساختار هسته و منطق کوئری در ساختار هسته وردپرس آمده است.

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

ساختار پایه WP_Query

اسکلت پایه کوئری سفارشی با WP_Query:

$args = array(
    "post_type"      => "post",
    "posts_per_page" => 10,
    "orderby"        => "date",
    "order"          => "DESC",
);
$query = new WP_Query( $args );

if ( $query->have_posts() ) :
    while ( $query->have_posts() ) : $query->the_post();
        // محتوای حلقه
    endwhile;
    wp_reset_postdata();
endif;

نکته حیاتی: پس از حلقه WP_Query، همیشه wp_reset_postdata() را فراخوانی کنید. بدون آن، متغیر جهانی $post به آخرین نوشته کوئری سفارشی اشاره می‌کند و حلقه بعدی دچار مشکل می‌شود. راهنمای کامل در توابع داده‌های نوشته و کدنویسی کوئری سفارشی.

پارامترهای کلیدی WP_Query

پارامترهای پرکاربرد WP_Query در پنج دسته قرار می‌گیرند:

دستهپارامترکاربرد
نوعpost_typeنوع محتوا (post، page، CPT سفارشی)
تعدادposts_per_pageتعداد نوشته در هر صفحه
ترتیبorderbyمعیار ترتیب (date، title، menu_order، rand، meta_value)
وضعیتpost_statuspublish، draft، private، any
وابستگیpost__inلیست مشخصی از IDها

مثال ترکیبی:

$args = array(
    "post_type"      => array( "post", "article" ),
    "posts_per_page" => 6,
    "orderby"        => array(
        "menu_order" => "ASC",
        "date"       => "DESC",
    ),
    "post_status"    => "publish",
    "post__in"       => array( 12, 34, 56, 78 ),
);

نکته: post__in با post__not_in ترکیب می‌شود. توجه: پارامتر post__in وقتی با ترتیب‌دهی عادی استفاده شود، ترتیب لیست ورودی را حفظ نمی‌کند. برای حفظ ترتیب، از orderby => "post__in" استفاده کنید. راهنمای کامل در کدنویسی کوئری سفارشی.

کوئری بر اساس متادیتا

برای فیلتر محتوا بر اساس متادیتا، از پارامتر meta_query استفاده کنید:

$args = array(
    "post_type"      => "project",
    "posts_per_page" => 12,
    "meta_query"     => array(
        "relation" => "AND",
        array(
            "key"     => "_project_year",
            "value"   => 2023,
            "compare" => ">=",
            "type"    => "NUMERIC",
        ),
        array(
            "key"     => "_project_status",
            "value"   => "completed",
            "compare" => "=",
        ),
    ),
);

پارامترهای مهم در meta_query: key: نام متادیتا. value: مقدار مقایسه. compare: عملگر مقایسه (=، !=، >، <، >=، <=، LIKE، NOT LIKE، IN، NOT IN، BETWEEN، NOT BETWEEN، EXISTS، NOT EXISTS). type: نوع داده (NUMERIC، BINARY، CHAR، DATE، DATETIME، DECIMAL، SIGNED، TIME، UNSIGNED). نکته: نوع داده را درست انتخاب کنید. اگر متادیتا عددی است و type را NUMERIC بگذارید، مقایسه عددی صحیح انجام می‌شود؛ در غیر این صورت، مقایسه رشته‌ای که در اعداد چند رقمی نتایج اشتباه می‌دهد. راهنمای متادیتا در کار با متاباکس‌ها، توابع متادیتا، و کار با User Meta.

برای کوئری‌های پیچیده‌تر، meta_query تودرتو:

"meta_query" => array(
    "relation" => "OR",
    array(
        "relation" => "AND",
        array( "key" => "_price", "value" => 1000000, "compare" => ">", "type" => "NUMERIC" ),
        array( "key" => "_in_stock", "value" => "1" ),
    ),
    array(
        "key"     => "_featured",
        "value"   => "1",
        "compare" => "=",
    ),
),

کوئری بر اساس تاکسونومی

فیلتر بر اساس تاکسونومی، با پارامتر tax_query:

$args = array(
    "post_type"      => "product",
    "posts_per_page" => 12,
    "tax_query"      => array(
        "relation" => "AND",
        array(
            "taxonomy" => "brand",
            "field"    => "slug",
            "terms"    => "apple",
        ),
        array(
            "taxonomy" => "product_cat",
            "field"    => "term_id",
            "terms"    => array( 5, 12 ),
            "operator" => "IN",
        ),
    ),
);

پارامترهای مهم در tax_query: taxonomy: نام تاکسونومی. field: نوع مقدار (term_id، slug، name، term_taxonomy_id). terms: مقدار یا آرایهٔ مقادیر. operator: عملگر (IN، NOT IN، AND، EXISTS، NOT EXISTS). include_children: شامل فرزندان در تاکسونومی سلسله‌مراتبی. راهنمای کامل در کار با تاکسونومی سفارشی، توابع دسته‌بندی، و توابع برچسب.

حلقه و بازگردانی داده

پس از اجرای کوئری، حلقه وردپرس کار می‌کند. دسترسی به داده‌های داخل حلقه:

if ( $query->have_posts() ) :
    while ( $query->have_posts() ) : $query->the_post();
        $post_id    = get_the_ID();
        $title      = get_the_title();
        $permalink  = get_permalink();
        $thumb      = get_the_post_thumbnail_url( $post_id, "medium" );
        $year       = get_post_meta( $post_id, "_project_year", true );
        $terms      = get_the_terms( $post_id, "project_cat" );
        ?>
        <article class="project-card">
            <?php if ( $thumb ) : ?>
                <img src="<?php echo esc_url( $thumb ); ?>" alt="<?php echo esc_attr( $title ); ?>" />
            <?php endif; ?>
            <h3><a href="<?php echo esc_url( $permalink ); ?>"><?php echo esc_html( $title ); ?></a></h3>
            <?php if ( $year ) : ?>
                <p>سال اجرا: <?php echo esc_html( $year ); ?></p>
            <?php endif; ?>
        </article>
        <?php
    endwhile;
    wp_reset_postdata();
endif;

سه نکته: یک — escape خروجی: esc_url، esc_html، esc_attr. دو — بررسی خالی بودن: پیش از استفاده از متادیتا یا ترم، خالی نبودن را بررسی کنید. سه — is_wp_error: پس از get_the_terms، همیشه بررسی کنید. راهنما در PHP امن در وردپرس و پاک‌سازی داده‌ها.

صفحه‌بندی در کوئری سفارشی

صفحه‌بندی در WP_Query، با پارامتر paged:

$paged = max( 1, get_query_var( "paged" ) );

$args = array(
    "post_type"      => "project",
    "posts_per_page" => 12,
    "paged"          => $paged,
);
$query = new WP_Query( $args );

// پس از حلقه
$pagination = paginate_links( array(
    "total"     => $query->max_num_pages,
    "current"   => $paged,
    "prev_text" => "قبلی",
    "next_text" => "بعدی",
) );

if ( $pagination ) {
    echo "<nav class="pagination">" . $pagination . "</nav>";
}

نکته: برای صفحه‌بندی کارآمد، نباید no_found_rows => true بگذارید؛ چون این پارامتر شمارش کل را حذف می‌کند و max_num_pages صفر می‌شود. اما اگر صفحه‌بندی ندارید، این پارامتر را برای بهینه‌سازی اضافه کنید. الگوهای صفحه‌بندی در بهینه‌سازی کد وردپرس و ساختار URL و سئو.

کوئری‌های تودرتو

گاهی نیاز به اجرای کوئری درون حلقهٔ کوئری دیگر است — مثلاً نمایش نوشته‌های مرتبط برای هر پروژه. الگوی نادرست:

// نامناسب
while ( $query->have_posts() ) :
    $query->the_post();
    $related = new WP_Query( array( "post__in" => get_related_ids() ) );
    // حلقه تودرتو
endwhile;

مشکل: هر تکرار، یک کوئری جدید می‌زند که در سایت‌های پرتعداد، فشار زیادی روی دیتابیس می‌آورد. الگوی بهینه:

// ابتدا همهٔ IDهای موردنیاز را جمع کنید
$related_ids = array();
while ( $query->have_posts() ) :
    $query->the_post();
    $related_ids = array_merge( $related_ids, get_related_ids( get_the_ID() ) );
endwhile;

// سپس یک کوئری واحد
$related_query = new WP_Query( array(
    "post__in"       => array_unique( $related_ids ),
    "posts_per_page" => -1,
) );

// نگهداری در آرایهٔ hash برای دسترسی سریع
$related_map = array();
while ( $related_query->have_posts() ) :
    $related_query->the_post();
    $related_map[ get_the_ID() ] = get_the_title();
endwhile;

این الگو، تعداد کوئری‌ها را از N به ۱ کاهش می‌دهد. راهنمای کامل در بهینه‌سازی کوئری‌ها و بهینه‌سازی کد وردپرس.

pre_get_posts و تغییر حلقهٔ اصلی

برای تغییر حلقهٔ اصلی وردپرس (بدون ساخت کوئری جدید)، از هوک pre_get_posts استفاده کنید. الگوی کلاسیک: اضافه‌کردن CPT به آرشیو جستجو:

function my_plugin_include_cpt_in_search( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }
    if ( $query->is_search() ) {
        $query->set( "post_type", array( "post", "page", "project", "product" ) );
    }
}
add_action( "pre_get_posts", "my_plugin_include_cpt_in_search" );

نکته: دو بررسی الزامی: یک — is_admin(): تا تغییرات فقط روی front-end اعمال شود. دو — is_main_query(): تا حلقه‌های جانبی تغییر نکنند. الگوهای بیشتر در هوک‌های وردپرس، نحوه استفاده از add_action، و راهنمای حرفه‌ای هوک‌ها.

کوئری خام با wpdb

در مواردی که WP_Query جواب نمی‌دهد — مثل کوئری‌های آماری یا عملیات پیچیده روی جداول اختصاصی — از $wpdb استفاده کنید. الگوی امن:

global $wpdb;

$author_id = 5;
$status    = "publish";

$sql = $wpdb->prepare(
    "SELECT ID, post_title
     FROM {$wpdb->posts}
     WHERE post_author = %d
       AND post_status = %s
     ORDER BY post_date DESC
     LIMIT 20",
    $author_id,
    $status
);

$results = $wpdb->get_results( $sql );

foreach ( $results as $row ) {
    printf(
        "<li>%s</li>",
        esc_html( $row->post_title )
    );
}

نکته حیاتی: $wpdb->prepare الزامی است. بدون آن، خطر SQL Injection جدی. سه قاعده: یک — placeholder درست: %d برای عدد، %s برای رشته، %f برای اعشار. دو — نام جدول با {$wpdb->prefix}: تا در سایت‌های با پیشوند سفارشی کار کند. سه — escape خروجی: نتایج را با esc_html نمایش دهید. راهنمای کامل در PHP امن در وردپرس، پاک‌سازی داده‌ها، و بهینه‌سازی کوئری‌های MySQL.

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

پنج تکنیک بهینه‌سازی در کوئری سفارشی: یک — محدودسازی posts_per_page: هرگز -1 بدون دلیل. دو — no_found_rows => true: اگر صفحه‌بندی ندارید، کوئری شمارش را حذف می‌کند. سه — update_post_meta_cache => false: اگر از متا استفاده نمی‌کنید. چهار — update_post_term_cache => false: اگر از ترم استفاده نمی‌کنید. پنج — fields => "ids": اگر فقط ID لازم است.

$args = array(
    "post_type"              => "project",
    "posts_per_page"         => 12,
    "no_found_rows"          => true,
    "update_post_meta_cache" => false,
    "update_post_term_cache" => false,
);

روش سنجش با Query Monitor: در صفحه‌ای که کوئری سفارشی دارد، این افزونه تعداد، زمان، و منبع هر کوئری را نشان می‌دهد. راهنمای کامل در بهینه‌سازی کوئری‌ها، بهینه‌سازی کد وردپرس، و ابزارهای تست سرعت سایت. یک نکته میدانی: در سایت‌های با بیش از ۱۰ هزار نوشته، کوئری‌های مبتنی بر meta_query سنگین می‌شوند. اگر طرح پروژه اجازه می‌دهد، به‌جای متادیتا، از تاکسونومی برای فیلتر استفاده کنید — ایندکس‌گذاری در جدول wp_term_relationships بهتر از wp_postmeta است. راهنمای تفصیلی در کار با تاکسونومی سفارشی.

کش نتایج

کوئری‌های سنگین را با Transients کش کنید:

function my_plugin_get_featured_projects() {
    $cache_key = "my_featured_projects";
    $cached = get_transient( $cache_key );

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

    $query = new WP_Query( array(
        "post_type"      => "project",
        "posts_per_page" => 6,
        "meta_key"       => "_featured",
        "meta_value"     => "1",
    ) );

    $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;
}

نکته مهم: هنگام به‌روزرسانی محتوای مرتبط، کش را باطل کنید:

add_action( "save_post_project", function() {
    delete_transient( "my_featured_projects" );
} );

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

ساختار کلاس‌محور

در پروژه‌های جدی، کوئری سفارشی در یک کلاس اختصاصی قرار می‌گیرد:

class My_Plugin_Project_Query {

    public static function get_featured( $limit = 6 ) {
        $cache_key = "my_featured_projects_" . $limit;
        $cached = get_transient( $cache_key );
        if ( $cached !== false ) {
            return $cached;
        }

        $query = new WP_Query( array(
            "post_type"              => "project",
            "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();
        while ( $query->have_posts() ) :
            $query->the_post();
            $data[] = self::format_item( get_post() );
        endwhile;
        wp_reset_postdata();

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

    public static function get_by_taxonomy( $taxonomy, $term, $limit = 12 ) {
        $query = new WP_Query( array(
            "post_type"      => "project",
            "posts_per_page" => $limit,
            "tax_query"      => array(
                array(
                    "taxonomy" => $taxonomy,
                    "field"    => "slug",
                    "terms"    => $term,
                ),
            ),
        ) );

        $data = array();
        while ( $query->have_posts() ) :
            $query->the_post();
            $data[] = self::format_item( get_post() );
        endwhile;
        wp_reset_postdata();

        return $data;
    }

    private static function format_item( $post ) {
        return array(
            "id"    => $post->ID,
            "title" => get_the_title( $post ),
            "url"   => get_permalink( $post ),
            "thumb" => get_the_post_thumbnail_url( $post, "medium" ),
            "year"  => get_post_meta( $post->ID, "_project_year", true ),
        );
    }
}

مزیت این ساختار: تمام کوئری‌ها در یک نقطه، مدیریت کش متمرکز، و شکل خروجی یکسان. الگوهای مشابه در کدنویسی اختصاصی افزونه، ساختار فایل‌های افزونه استاندارد، و استانداردهای کدنویسی وردپرس. یک تجربه میدانی: در پروژه‌ای با سه کوئری سنگین که در یک صفحه اجرا می‌شدند، تجمیع آن‌ها در یک کلاس با کش متمرکز، زمان پاسخ را از ۲.۸ ثانیه به ۱.۲ ثانیه کاهش داد.

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

جمع‌بندی

توابع کوئری سفارشی وردپرس، در چهار لایه خلاصه می‌شوند: WP_Query، get_posts، pre_get_posts، و wpdb. سه اصل را در پایان تاکید می‌کنم: اول، همیشه از توابع استاندارد استفاده کنید، نه SQL خام. دوم، پارامترهای بهینه‌سازی مثل no_found_rows و update_post_meta_cache را جدی بگیرید. سوم، نتایج سنگین را با Transients کش کنید.

اگر امروز یک کار در این مسیر انجام می‌دهید: با Query Monitor به صفحه‌ای که کوئری سفارشی دارد نگاه کنید و ببینید کدام کوئری بیشترین زمان را می‌گیرد؛ همان یک عدد، نقشه بهبود شماست. اگر تجربه‌ای از یک کوئری سنگین در پروژه‌ای دارید که با کش یا بازنویسی سبک شد، در دیدگاه‌ها بنویسید — همان گزارش‌های واقعی، این راهنما را دقیق‌تر می‌کند. 🔍