کلاس WP_Query در وردپرس موتور اصلی تولید کوئری‌های سفارشی برای دریافت نوشته‌ها از دیتابیس است و در قلب حلقه اصلی (The Loop) و همچنین هر لیست سفارشی در قالب و افزونه قرار دارد. این کلاس جایگزین امن و استانداردی برای کوئری‌های دستی روی جدول wp_posts محسوب می‌شود.

کلاس WP_Query در وردپرس موتور اصلی ساخت کوئری‌های سفارشی برای دریافت نوشته‌ها از دیتابیس است. این کلاس امکان فیلتر بر اساس post_type، post_status، meta_query، tax_query، date_query و ده‌ها پارامتر دیگر را فراهم می‌کند. استفاده نادرست از آن — به‌ویژه فراموش کردن wp_reset_postdata — یکی از شایع‌ترین اشتباهات در توسعه قالب و افزونه وردپرس است. بهینه‌سازی کوئری‌های WP_Query تأثیر مستقیم بر سرعت سایت‌های پرترافیک دارد و می‌تواند تفاوت چند برابری در زمان بارگذاری ایجاد کند. تسلط بر این کلاس یکی از پیش‌نیازهای اصلی هر توسعه‌دهنده حرفه‌ای وردپرس محسوب می‌شود.

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

چرا WP_Query نقش کلیدی در توسعه وردپرس دارد

وردپرس تمام محتوای خود را در جدول wp_posts و روابط جانبی آن نگهداری می‌کند. برای نمایش هر نوع لیستی — آخرین نوشته‌ها، محصولات یک دسته، نوشته‌های یک نویسنده یا محتوای مرتبط — به یک لایه query نیاز است. کلاس WP_Query همین لایه را با API آرایه‌ای ساده در اختیار توسعه‌دهندگان قرار می‌دهد.

بدون این کلاس، برای دریافت محتوای سفارشی باید کوئری SQL دستی با JOINهای متعدد روی wp_postmeta، wp_term_relationships و wp_term_taxonomy نوشته شود. این کار نه‌تنها زمان‌بر است، بلکه امنیت و کارایی را نیز به خطر می‌اندازد. WP_Query تمام این پیچیدگی را پنهان می‌کند و خودش از prepared statement و cache داخلی وردپرس بهره می‌گیرد.

برای درک نقش این کلاس در کنار توابع ساده‌تر، مطلب تابع get_posts چطور کار می‌کند را مطالعه کنید. این تابع در واقع یک wrapper نازک روی همین کلاس است.

ساختار و امضای کلاس WP_Query

امضای سازنده این کلاس به شکل زیر است:

new WP_Query( array|string $query = '' )

پارامتر ورودی یک آرایه انجمنی از پارامترها یا یک رشته query string است. اگر آرایه خالی ارسال شود، کلاس به‌صورت پیش‌فرض تلاش می‌کند بر اساس URL فعلی یک کوئری بسازد — همان رفتاری که در حلقه اصلی قالب رخ می‌دهد.

پس از ساخت شیء، می‌توانید با متد have_posts() بررسی کنید که آیا نتیجه‌ای وجود دارد و با the_post() به ردیف بعدی بروید. برای دسترسی به پست جاری درون حلقه، از متغیر سراسری $post استفاده می‌شود که با setup_postdata() مقداردهی می‌شود.

برای مطالعه ساختار دقیق حلقه و رفتار آن در قالب‌ها، مطلب توابع وردپرس برای داده پست منبع خوبی است.

پارامترهای کلیدی و کاربرد هرکدام

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

پارامتر post_type

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

$q = new WP_Query( array(
    'post_type'      => array( 'post', 'product' ),
    'posts_per_page' => 10,
) );

برای ساخت post type سفارشی و آشنایی با ساختار آن، مطلب تابع register_post_type را ببینید.

پارامتر posts_per_page

تعداد پست‌های بازگشتی در هر صفحه را تعیین می‌کند. مقدار -1 تمام نتایج را برمی‌گرداند و در سایت‌های بزرگ به‌شدت مضر است. برای صفحه‌بندی سفارشی، این مقدار را با paged ترکیب کنید:

$paged = max( 1, get_query_var( 'paged' ) );
$q = new WP_Query( array(
    'posts_per_page' => 12,
    'paged'          => $paged,
) );

پارامتر post_status

وضعیت نوشته‌ها را فیلتر می‌کند. مقادیر رایج: publish، draft، pending، private، trash و any. نکته مهم این است که برخلاف حلقه اصلی، این کلاس به‌طور پیش‌فرض فقط پست‌های publish را برمی‌گرداند:

$q = new WP_Query( array(
    'post_status' => 'any',
) );

پارامتر meta_query

برای فیلتر بر اساس متادیتای سفارشی. این پارامتر از ساختار تودرتوی آرایه پشتیبانی می‌کند و می‌تواند شرط‌های AND و OR را ترکیب کند:

$q = new WP_Query( array(
    'meta_query' => array(
        'relation' => 'AND',
        array(
            'key'     => 'price',
            'value'   => 100000,
            'compare' => '>=',
            'type'    => 'NUMERIC',
        ),
        array(
            'key'     => 'in_stock',
            'value'   => '1',
            'compare' => '=',
        ),
    ),
) );

نکته‌ای که در پروژه‌های واقعی بارها دیده‌ام: هر شرط اضافه در meta_query، یک JOIN جدید به جدول wp_postmeta اضافه می‌کند. در سایت‌های بزرگ، این موضوع به‌سرعت به یک گلوگاه تبدیل می‌شود. راهکار بهینه در مطلب بهینه‌سازی WP_Query پوشش داده شده است.

پارامتر tax_query

برای فیلتر بر اساس taxonomy و termها. ساختار مشابه meta_query است:

$q = new WP_Query( array(
    'tax_query' => array(
        array(
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => array( 'news', 'reviews' ),
        ),
    ),
) );

در صورت استفاده از taxonomy سفارشی، حتماً مطلب تابع register_taxonomy را ببینید و برای دریافت خود ترم‌ها، از تابع get_terms استفاده کنید.

پارامتر date_query

فیلتر بازه زمانی. مثلاً نوشته‌های یک ماه مشخص:

$q = new WP_Query( array(
    'date_query' => array(
        array(
            'after'  => '2025-01-01',
            'before' => '2025-12-31',
        ),
    ),
) );

پارامترهای orderby و order

مرتب‌سازی نتایج. مقادیر رایج: date، title، menu_order، rand، meta_value، meta_value_num و comment_count:

$q = new WP_Query( array(
    'meta_key' => 'rating',
    'orderby'  => 'meta_value_num',
    'order'    => 'DESC',
) );

ترکیب meta_key با orderby => 'meta_value_num' روی سایت‌های بزرگ می‌تواند کند باشد چون ایندکس روی wp_postmeta.meta_value به‌صورت پیش‌فرض وجود ندارد.

پارامتر suppress_filters

اگر true باشد، تمام فیلترهای وردپرس روی کوئری نادیده گرفته می‌شوند. این کار در موارد خاص مثل اجرای کوئری خالص برای مهاجرت توصیه می‌شود، اما در قالب‌ها معمولاً باید false بماند:

$q = new WP_Query( array(
    'suppress_filters' => true,
) );

نکته مهم: تابع get_posts به‌طور پیش‌فرض این مقدار را true می‌گذارد، در حالی که WP_Query مقدار false دارد.

پارامتر no_found_rows

اگر به صفحه‌بندی نیاز ندارید و فقط می‌خواهید نتایج را بیاورید، این پارامتر را true کنید تا کوئری SELECT COUNT(*) اضافی حذف شود:

$q = new WP_Query( array(
    'no_found_rows' => true,
) );

در پروژه‌های پرترافیک، این یک بهینه‌سازی ساده اما بسیار مؤثر است.

حلقه اصلی و نقش حیاتی wp_reset_postdata

پس از ساخت شیء WP_Query، برای پیمایش نتایج از حلقه استفاده می‌شود:

$q = new WP_Query( $args );
if ( $q->have_posts() ) {
    while ( $q->have_posts() ) {
        $q->the_post();
        the_title( '<h3>', '</h3>' );
    }
    wp_reset_postdata();
}

فراخوانی wp_reset_postdata() پس از حلقه، یکی از مهم‌ترین نکاتی است که در پروژه‌های واقعی بسیار نادیده گرفته می‌شود. هر بار the_post() اجرا می‌شود، متغیر سراسری $post بازنویسی می‌شود. اگر پس از حلقه، این متغیر بازنشانی نشود، حلقه اصلی قالب به پست اشتباهی اشاره می‌کند و باعث رفتارهای عجیب در بخش‌های بعدی صفحه می‌شود.

همین مسئله در پروژه‌هایی که چند کوئری متوالی در یک صفحه دارند، به باگ‌های ظریف و سخت‌ردیابی منجر می‌شود. یکی از روش‌های عیب‌یابی این وضعیت در مطلب دیباگ عملکرد WP_Query توضیح داده شده است.

نمونه‌های عملی در پروژه واقعی

آخرین نوشته‌های یک دسته خاص

$q = new WP_Query( array(
    'category_name'  => 'news',
    'posts_per_page' => 5,
) );

محصولات مرتبط در ووکامرس

$q = new WP_Query( array(
    'post_type'      => 'product',
    'posts_per_page' => 4,
    'post__not_in'   => array( get_the_ID() ),
    'tax_query'      => array(
        array(
            'taxonomy' => 'product_cat',
            'terms'    => wp_get_post_terms( get_the_ID(), 'product_cat', array( 'fields' => 'ids' ) ),
        ),
    ),
) );

نوشته‌های یک نویسنده خاص

$q = new WP_Query( array(
    'author'         => 5,
    'posts_per_page' => 10,
) );

برای الگوهای مرتبط با کاربران و نویسندگان، مطلب تابع get_users را مطالعه کنید.

پست‌های تصادفی

$q = new WP_Query( array(
    'orderby'        => 'rand',
    'posts_per_page' => 3,
) );

ترتیب تصادفی روی جداول بزرگ به‌شدت کند است چون MySQL باید کل نتیجه را محاسبه و سپس رندوم کند. در سایت‌های بزرگ بهتر است از post__in با شناسه‌های از قبل انتخاب‌شده استفاده شود.

درج پست به‌صورت برنامه‌نویسی

اگر پس از کوئری نیاز به درج یا به‌روزرسانی پست دارید، از تابع wp_insert_post و تابع wp_update_post استفاده کنید. این دو تابع تمام پردازش‌های جانبی مثل اجرای hookها و ثبت revision را به‌درستی انجام می‌دهند.

ترکیب با wpdb برای گزارش‌های پیچیده

در برخی گزارش‌های تحلیلی، WP_Query به‌تنهایی کافی نیست و باید کوئری دستی نوشت. مطلب کدنویسی کوئری‌های سفارشی در وردپرس راهنمای این کار است. برای امنیت کوئری، همیشه از SQL Injection Prevention در وردپرس الگو بگیرید.

اشتباهات رایج در استفاده از WP_Query

فراموش کردن wp_reset_postdata

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

استفاده از posts_per_page برابر ۱-

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

عدم بررسی have_posts

اگر نتیجه‌ای وجود نداشته باشد و مستقیماً the_post() فراخوانی شود، ممکن است به پست ناقص یا خطا منجر شود. همیشه ابتدا با have_posts() بررسی کنید.

نبود escape در خروجی

عنوان و محتوای پست‌ها می‌تواند حاوی HTML باشد. هنگام چاپ در قالب، از esc_html()، esc_url() و wp_kses_post() استفاده کنید.

نبود nonce در فرم‌های سفارشی

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

نبود cache برای کوئری‌های سنگین

هرچند وردپرس نتیجه را به‌طور داخلی کش می‌کند، اما در کوئری‌های دارای متادیتا یا فیلترهای پیچیده، این کش مؤثر نیست. برای نتایج پرتکرار، از تابع wp_cache_set استفاده کنید و در بازیابی، تابع wp_cache_get را به‌کار ببرید.

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

تست‌هایی مثل «دسته خالی»، «صفحه آخر»، «بدون نتیجه» و «post_type نامعتبر» را حتماً بنویسید. این سناریوها در محیط production بدون تست، به باگ‌های پنهان تبدیل می‌شوند.

امنیت و عملکرد در WP_Query

این کلاس به‌طور داخلی از prepared statement استفاده می‌کند و در برابر SQL Injection مقاوم است. اما نکات زیر را همیشه رعایت کنید:

  • پارامترهای ورودی از URL یا فرم را با sanitize_text_field() و absint() پاک کنید
  • خروجی را با esc_html() و esc_url() escape کنید
  • سطح دسترسی را در کوئری‌های پنل مدیریت با current_user_can() بررسی کنید
  • در endpointهای عمومی، از افشای محتوای خصوصی خودداری کنید

از نظر عملکرد، سه نکته مهم:

  1. تعداد meta_query و tax_query را به حداقل برسانید
  2. از no_found_rows => true در کوئری‌های بدون صفحه‌بندی استفاده کنید
  3. نتایج کوئری‌های سنگین را در cache ذخیره کنید

برای مطالعه الگوهای بهینه‌سازی کوئری در سطح کد، مطلب بهینه‌سازی کوئری‌های وردپرس با کدنویسی و همچنین بهینه‌سازی پیشرفته دیتابیس وردپرس توصیه می‌شود.

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

تفاوت WP_Query با get_posts چیست؟

get_posts() در واقع یک wrapper روی WP_Query است که به‌طور پیش‌فرض suppress_filters => true، no_found_rows => true و posts_per_page => 5 تنظیم می‌کند و یک آرایه ساده از پست‌ها برمی‌گرداند، در حالی که WP_Query امکان استفاده از حلقه، شمارش کل نتایج و کنترل کامل‌تر را می‌دهد.

چرا wp_reset_postdata ضروری است؟

هر بار the_post() فراخوانی می‌شود، متغیر سراسری $post تغییر می‌کند. اگر این متغیر پس از حلقه سفارشی بازنشانی نشود، حلقه اصلی قالب به پست اشتباهی اشاره می‌کند و بخش‌های بعدی صفحه دچار خطا می‌شوند.

آیا WP_Query از cache استفاده می‌کند؟

بله، وردپرس نتیجه کوئری‌های پایه را در cache گروه posts ذخیره می‌کند. اما کوئری‌های دارای پارامترهای متنوع از این cache کمتر بهره می‌برند.

چگونه صفحه‌بندی را با WP_Query پیاده‌سازی کنیم؟

با پارامتر paged و استفاده از get_query_var('paged'). برای نمایش لینک‌های صفحه‌بندی از paginate_links() یا the_posts_pagination() استفاده کنید.

آیا می‌توان کوئری پیش‌فرض حلقه اصلی را تغییر داد؟

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

تفاوت meta_query با meta_key چیست؟

meta_key و meta_value برای شرط‌های ساده استفاده می‌شوند و در کوئری SQL به JOINهای کمتری نیاز دارند، در حالی که meta_query برای شرط‌های پیچیده با relation و عملگرهای متنوع طراحی شده است.

آیا WP_Query روی Multisite رفتار خاصی دارد؟

در Multisite، کوئری به‌طور پیش‌فرض فقط روی سایت جاری اجرا می‌شود. برای گرفتن پست‌های سایت‌های دیگر، باید با switch_to_blog جابه‌جا شوید و پس از پایان، با restore_current_blog بازگردید.

نگاه فنی عمیق به WP_Query

در سطح معماری، WP_Query یک abstraction نسبتاً پیچیده روی SQL است که درون خود چند مرحله پردازشی دارد: parse کردن پارامترها، تولید WHERE clauses، افزودن JOIN برای meta و taxonomy، مرتب‌سازی و در نهایت اجرای کوئری. کلید درک رفتار آن، مطالعه متد parse_query() و متد get_posts() در هسته وردپرس است.

نکته ظریف اول این است که وقتی posts_per_page را تنظیم می‌کنید و no_found_rows را false می‌گذارید، وردپرس یک کوئری SELECT COUNT(*) جداگانه اجرا می‌کند که در جداول بزرگ می‌تواند چندین برابر کوئری اصلی هزینه داشته باشد. به همین دلیل استفاده از no_found_rows => true در کوئری‌هایی که به صفحه‌بندی نیاز ندارند یک بهینه‌سازی استاندارد و پرتأثیر است.

مسئله دوم، ساختار SQL تولیدشده برای meta_query است. هر شرط اضافه، یک JOIN جدید به wp_postmeta اضافه می‌کند. برای فیلترهایی با سه شرط یا بیشتر، کوئری نهایی می‌تواند به‌سرعت به یک bottleneck تبدیل شود. راهکار درست، انتقال متادیتای پرتکرار به جدول اختصاصی و کوئری مستقیم با $wpdb است، همان‌طور که در مباحث جدول سفارشی در وردپرس توضیح داده شده.

مسئله سوم، رفتار WP_Query با کش Object است. حتی اگر Redis یا Memcached داشته باشید، این کلاس نتایج را در گروه posts کش می‌کند اما پارامترهای query را hash نمی‌کند. یعنی دو کوئری با ترتیب متفاوت پارامترها، دو کلید متفاوت خواهند داشت. راهکار استاندارد، نرمال‌سازی پارامترها قبل از ساخت کوئری است.

در نهایت، در پروژه‌های Enterprise توصیه می‌کنم لایه‌ای از Repository Pattern روی WP_Query بسازید که مسئول کش، نرمال‌سازی و تبدیل نتایج به Domain Object باشد. برای آشنایی با الگوهای حرفه‌ای کوئری، مباحث توابع وردپرس برای کوئری سفارشی نیز مفید است.

اگر در پروژه‌ای با مشکل کندی کوئری، ناسازگاری meta_query یا رفتار نامنظم حلقه مواجه شده‌اید، برای ما جالب است بدانید کدام راهکار عملاً به حل مسئله کمک کرده است. تجربه خود را در دیدگاه‌ها بنویسید تا برای سایر توسعه‌دهندگان هم مفید باشد. برای مطالعه بیشتر در مورد بهینه‌سازی و رفع مشکلات مرتبط، زبان SQL در ویکی‌پدیا نقطه شروع خوبی برای درک عمیق لایه دیتابیس است.