Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wordpress combine queries

I have two queries that I need to try to combine so that my pagination works correctly, and the posts show up in the correct order.

I have this one query:

$today = date('m/d/Y', strtotime('today'));

$args = array(
    'post_type' => 'workshops',
    "posts_per_page" => 5,
    "paged" => $paged,
    'meta_key' => 'select_dates_0_workshop_date',
    'orderby' => 'meta_value',
    'order' => 'ASC',
        'meta_query' => array(
            array(
                  'key' => 'select_dates_0_workshop_date',
                  'meta-value' => "meta_value",
                  'value' => $today,
                  'compare' => '>=',
                  'type' => 'CHAR'
             )
    )
    );

The results of this query need to come after the query above:

$args = array(
    'post_type' => 'workshops',
    "posts_per_page" => 5,
    "paged" => $paged,
    'meta_key' => 'select_dates_0_workshop_date',
    'orderby' => 'meta_value',
    'order' => 'DESC',
        'meta_query' => array(
            array(
                 'key' => 'select_dates_0_workshop_date',
                 'meta-value' => "meta_value",
                 'value' => $today,
                 'compare' => '<',
                 'type' => 'CHAR'
        )
    )
    );

The difference between the two queries is the: 'order' and the 'compare'.

I have done this in pure MYSQL Queries, but I am not sure how to do this on WordPress

like image 670
Dylan Cross Avatar asked May 09 '14 01:05

Dylan Cross


2 Answers

This is an updated version of the answer, that's more flexible then the previous one.

Here's one idea using a SQL UNION:

  • We can use the data from the posts_clauses filter to rewrite the SQL query from the posts_request filter.

  • We extend the WP_Query class to achieve our goal. We actually do that twice:

    • WP_Query_Empty: to get the generated SQL query of each sub-queries, but without doing the database query.
    • WP_Query_Combine: to fetch the posts.
  • The following implementation supports combining N sub-queries.

Here are two demos:

Demo #1:

Let's assume you have six posts, ordered by date (DESC):

CCC
AAA
BBB
CCC
YYY
ZZZ
XXX 

where the XXX, YYY and ZZZ are older than DT=2013-12-14 13:03:40.

Let's order our posts so that posts published after DT are ordered by title (ASC) and posts publisehd before DT are ordered by title (DESC):

AAA
BBB
CCC
ZZZ
YYY
XXX 

Then we can use the following:

/**
 * Demo #1 - Combine two sub queries:
 */

$args1 = array(
    'post_type'  => 'post',
    'orderby'    => 'title',
    'order'      => 'ASC',
    'date_query' => array(
        array( 'after' => '2013-12-14 13:03:40' ),
    ),
);

$args2 = array(
    'post_type'  => 'post',
    'orderby'    => 'title',
    'order'      => 'DESC',
    'date_query' => array(
        array( 'before' => '2013-12-14 13:03:40', 'inclusive' => TRUE ),    
    ),
);

$args = array( 
   'posts_per_page' => 1,
   'paged'          => 1,
   'sublimit'       => 1000,
   'args'           => array( $args1, $args2 ),
);

$results = new WP_Combine_Queries( $args );

This generates the following SQL query:

SELECT SQL_CALC_FOUND_ROWS * FROM ( 
    ( SELECT wp_posts.* 
        FROM wp_posts 
        WHERE 1=1 
            AND ( ( post_date > '2013-12-14 13:03:40' ) ) 
            AND wp_posts.post_type = 'post' 
            AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'private') 
            ORDER BY wp_posts.post_title ASC 
            LIMIT 1000
    ) 
    UNION 
    ( SELECT wp_posts.* 
        FROM wp_posts 
        WHERE 1=1 
        AND ( ( post_date <= '2013-12-14 13:03:40' ) ) 
        AND wp_posts.post_type = 'post' 
        AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'private') 
        ORDER BY wp_posts.post_title DESC 
        LIMIT 1000
    ) 
) as combined LIMIT 0, 10 

Demo #2:

Here's your example:

/**
 * Demo #2 - Combine two sub queries:
 */

$today = date( 'm/d/Y', strtotime( 'today' ) );

$args1 = array(
    'post_type'      => 'workshops',
    'meta_key'       => 'select_dates_0_workshop_date',
    'orderby'        => 'meta_value',
    'order'          => 'ASC',
    'meta_query'     => array(
        array(
            'key'         => 'select_dates_0_workshop_date',
            'value'       => $today,
            'compare'     => '>=',
            'type'        => 'CHAR',
        ),
    )
);

$args2 = array(
    'post_type'      => 'workshops',
    'meta_key'       => 'select_dates_0_workshop_date',
    'orderby'        => 'meta_value',
    'order'          => 'DESC',
    'meta_query'     => array(
        array(
            'key'         => 'select_dates_0_workshop_date',
            'value'       => $today,
            'compare'     => '<',
            'type'        => 'CHAR',
        ),
    )
);

$args = array( 
   'posts_per_page' => 5,
   'paged'          => 4,
   'sublimit'       => 1000,
   'args'           => array( $args1, $args2 ),
);

$results = new WP_Combine_Queries( $args );

This should give you a query like this one:

SELECT SQL_CALC_FOUND_ROWS * FROM ( 
    ( SELECT wp_posts.* 
        FROM wp_posts 
        INNER JOIN wp_postmeta ON (wp_posts.ID = wp_postmeta.post_id) 
        INNER JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id) 
        WHERE 1=1 
            AND wp_posts.post_type = 'workshops' 
            AND (wp_posts.post_status = 'publish' OR wp_posts.post_author = 1 AND wp_posts.post_status = 'private') 
            AND (wp_postmeta.meta_key = 'select_dates_0_workshop_date' AND (mt1.meta_key = 'select_dates_0_workshop_date' AND CAST(mt1.meta_value AS CHAR) >= '05/16/2014') ) 
            GROUP BY wp_posts.ID 
            ORDER BY wp_postmeta.meta_value ASC
            LIMIT 1000 
        ) 
    UNION 
    ( SELECT wp_posts.* 
        FROM wp_posts 
        INNER JOIN wp_postmeta ON (wp_posts.ID = wp_postmeta.post_id) 
        INNER JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id) 
        WHERE 1=1 
            AND wp_posts.post_type = 'workshops' 
            AND (wp_posts.post_status = 'publish' OR wp_posts.post_author = 1 AND wp_posts.post_status = 'private') 
            AND (wp_postmeta.meta_key = 'select_dates_0_workshop_date' AND (mt1.meta_key = 'select_dates_0_workshop_date' AND CAST(mt1.meta_value AS CHAR) < '05/16/2014') ) 
            GROUP BY wp_posts.ID 
            ORDER BY wp_postmeta.meta_value DESC 
            LIMIT 1000 
        ) 
) as combined LIMIT 15, 5

Demo #3:

We could also combine more than two sub queries:

/**
 * Demo #3 - Combine four sub queries:
 */

$args = array( 
   'posts_per_page' => 10,
   'paged'          => 1,
   'sublimit'       => 1000,
   'args'           => array( $args1, $args2, $args3, $args4 ),
);

$results = new WP_Combine_Queries( $args );

Classes:

Here are our demo classes:

/**
 * Class WP_Combine_Queries
 * 
 * @uses WP_Query_Empty
 * @link https://stackoverflow.com/a/23704088/2078474
 *
 */

class WP_Combine_Queries extends WP_Query 
{
    protected $args    = array();
    protected $sub_sql = array();
    protected $sql     = '';

    public function __construct( $args = array() )
    {
        $defaults = array(
            'sublimit'       => 1000,
            'posts_per_page' => 10,
            'paged'          => 1,
            'args'           => array(),
        );

        $this->args = wp_parse_args( $args, $defaults );

        add_filter( 'posts_request',  array( $this, 'posts_request' ), PHP_INT_MAX  );

        parent::__construct( array( 'post_type' => 'post' ) );
    }

    public function posts_request( $request )
    {
        remove_filter( current_filter(), array( $this, __FUNCTION__ ), PHP_INT_MAX  );

        // Collect the generated SQL for each sub-query:
        foreach( (array) $this->args['args'] as $a )
        {
            $q = new WP_Query_Empty( $a, $this->args['sublimit'] );
            $this->sub_sql[] = $q->get_sql();
            unset( $q );
        }

        // Combine all the sub-queries into a single SQL query.
        // We must have at least two subqueries:
        if ( count( $this->sub_sql ) > 1 )
        {
            $s = '(' . join( ') UNION (', $this->sub_sql ) . ' ) ';

            $request = sprintf( "SELECT SQL_CALC_FOUND_ROWS * FROM ( $s ) as combined LIMIT %s,%s",
                $this->args['posts_per_page'] * ( $this->args['paged']-1 ),
                $this->args['posts_per_page']
            );          
        }
        return $request;
    }

} // end class

/**
 * Class WP_Query_Empty
 *
 * @link https://stackoverflow.com/a/23704088/2078474
 */

class WP_Query_Empty extends WP_Query 
{
    protected $args      = array();
    protected $sql       = '';
    protected $limits    = '';
    protected $sublimit  = 0;

    public function __construct( $args = array(), $sublimit = 1000 )
    {
        $this->args     = $args;
        $this->sublimit = $sublimit;

        add_filter( 'posts_clauses',  array( $this, 'posts_clauses' ), PHP_INT_MAX  );
        add_filter( 'posts_request',  array( $this, 'posts_request' ), PHP_INT_MAX  );

        parent::__construct( $args );
    }

    public function posts_request( $request )
    {
        remove_filter( current_filter(), array( $this, __FUNCTION__ ), PHP_INT_MAX );
        $this->sql = $this->modify( $request );             
        return '';
    }

    public function posts_clauses( $clauses )
    {
        remove_filter( current_filter(), array( $this, __FUNCTION__ ), PHP_INT_MAX  );
        $this->limits = $clauses['limits'];
        return $clauses;
    }

    protected function modify( $request )
    {
        $request = str_ireplace( 'SQL_CALC_FOUND_ROWS', '', $request );

        if( $this->sublimit > 0 )
            return str_ireplace( $this->limits, sprintf( 'LIMIT %d', $this->sublimit ), $request );
        else
            return $request;
    }

   public function get_sql( )
    {
        return $this->sql;
    }

} // end class

You can then adjust the classes to your needs.

I use the trick mentioned here to preserve the order of UNION sub queries. You can modify it accordingly with our sublimit parameter.

This should also work for main queries, by using the posts_request filter, for example.

I hope this helps.

like image 53
birgire Avatar answered Oct 01 '22 13:10

birgire


The trouble lies in the 'order' argument: the 'meta_query' argument accepts an array of meta queries as well as the 'relation' argument (basically, "AND" or "OR"). This could be used to select posts both less-than and greater-than-or-equal-to $today (though at that point the meta query is useless as it's selecting every ("OR") or no ("AND") workshops. Unfortunately, WP_Query doesn't allow you to arbitrarily sort results (switching from closest upcoming to most recent past workshop).

If you know how to pull this off directly in MySQL, you might consider looking at the WP_Query filters, specifically the posts_orderby filter. These filters enable you to start playing with the actual SQL being generated by the WP_Query class without having to switch to totally-custom queries. Be forewarned, however, that it's important to limit the scope of these filters or risk them being applied to every query on your site (the posts_where filter documentation has some good examples on how to do so).

like image 42
Steve Grunwell Avatar answered Oct 01 '22 12:10

Steve Grunwell