This is an update to a talk I gave three years ago at WordCamp Cape Town 2015. We’re at a point now where we have these incredibly powerful query classes in WordPress core that allow you to really tailor down to whatever criterion you want. In this talk, I take you through the outermost abstraction layers and deep into the SQL that WordPress actually uses to query for stuff on your site – it’s very much a “sky’s the limit” kind of situation.
Queries are really interesting and powerful, and a lot of people are intimidated by advanced queries, even with the abstraction layers that WordPress has put in place.
7. The Loop
if ( have_posts() ) :
while ( have_posts() ) :
the_post();
...
endwhile;
endif;
8. The Loop: Internals
if ( $wp_query->have_posts() ) :
while ( $wp_query->have_posts() ) :
$wp_query->the_post();
...
endwhile;
endif;
9. WP_Query
// Query for the 7 latest, published posts.
$query = new WP_Query( array(
'posts_per_page' => 7,
'post_status' => 'publish'
) );
10. WP_Query: SQL
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts
WHERE 1=1
AND wp_posts.post_type = 'post'
AND wp_posts.post_status = 'publish'
ORDER BY wp_posts.post_date DESC
LIMIT 0, 7
11. • WP_Query wrapper
• Defaults: Filter suppression
• Defaults: No sticky posts
• Defaults: No found rows
• Array of results vs WP_Query instance
get_posts()
12. get_posts()
// Query for the 7 latest published posts.
$query = get_posts( array(
'posts_per_page' => 7,
'post_status' => 'publish'
) );
13. get_posts(): SQL
SELECT wp_posts.ID FROM wp_posts
WHERE 1=1
AND wp_posts.post_type = 'post'
AND wp_posts.post_status = 'publish'
ORDER BY wp_posts.post_date DESC
LIMIT 0, 7
14. WP_Query: SQL
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts
WHERE 1=1
AND wp_posts.post_type = 'post'
AND wp_posts.post_status = 'publish'
ORDER BY wp_posts.post_date DESC
LIMIT 0, 7
15. • Action, not a filter
• $wp_query passed by reference
• Fires before the query actually runs
• Use query methods like $query->is_main_query()
• is_main_query() === _doing_it_wrong()
pre_get_posts
16. pre_get_posts
/**
* Display both posts and pages in the home loop.
*
* @param WP_Query $query Main WP_Query instance.
*/
function pages_in_main_query( $query ) {
if ( ! is_admin() && is_home() ) {
$query->query_vars['post_type'] = array( 'post', 'page' );
}
}
add_action( 'pre_get_posts', 'pages_in_main_query' );
17. pre_get_posts: Original SQL
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts
WHERE 1=1
AND wp_posts.post_type = 'post'
AND (
wp_posts.post_status = ‘publish'
)
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10
18. pre_get_posts: New SQL
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts
WHERE 1=1
AND wp_posts.post_type IN ( 'post', 'page' )
AND (
wp_posts.post_status = ‘publish'
)
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10
20. • Completely overrides the main query
• Very few valid use cases
• Can break pagination (and other stuff)
• wp_reset_query()
• Don’t use it
query_posts()
31. WP_Query multiple orderby: SQL
SELECT wp_posts.ID FROM wp_posts
WHERE 1=1
AND wp_posts.post_type = 'post'
AND wp_posts.post_status = 'publish'
ORDER BY wp_posts.post_date DESC, wp_posts.post_title ASC
LIMIT 0, 15
32. • Order by independent, named meta clauses
• WP_Meta_Query, WP_User_Query,
WP_Comment_Query
WP_Query orderby: meta clauses
34. WP_Query orderby: SQL
SELECT wp_posts.ID 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_postmeta.meta_key = ‘state’
AND CAST( wp_postmeta.meta_value AS CHAR ) = ‘Colorado'
)
AND mt1.meta_key = ‘city'
)
AND wp_posts.post_type = 'post'
AND ( ( wp_posts.post_status = 'publish' ) )
GROUP BY wp_posts.ID
ORDER BY
CAST( mt1.meta_value AS CHAR ) ASC,
CAST( wp_postmeta.meta_value AS CHAR ) DESC
LIMIT 0, 5
36. WP_User_Query registered on Tuesday
// Query for users registered on a Tuesday.
$query = new WP_User_Query( array(
‘count_total' => false,
‘orderby' => ‘display_name’,
'date_query' => array(
array(
'column' => 'user_registered',
'dayofweek' => 3 // Tuesday
)
),
) );
37. WP_User_Query registered on Tuesday: SQL
SELECT wp_users.*.ID FROM wp_users
WHERE 1=1
AND (
DAYOFWEEK( wp_users.user_registered ) = 3
)
ORDER BY
display_name ASC
38. WP_User_Query lifetime value
// Query for top 10 customers with a lifetime value 1,000+
$query = new WP_User_Query( array(
‘number’ => 10,
‘count_total’ => false, // No pagination.
‘meta_query’ => array(
‘lifetime_value’ => array(
‘key’ => ‘lifetime_value’,
‘compare’ => ‘>=’,
‘value’ => 1000,
‘type’ => ‘DECIMAL’,
),
),
// Order by lifetime value high to low, then name A-Z
'orderby' => array(
‘lifetime_value’ => ‘DESC’,
‘name’ => ‘ASC’,
) );
39. WP_User_Query lifetime value: SQL
SELECT wp_users.*.ID FROM wp_users
INNER JOIN wp_usermeta ON ( wp_users.ID = wp_usermeta.user_id )
WHERE 1=1
AND (
wp_usermeta.meta_key = ‘lifetime_value’
AND CAST( wp_usermeta.meta_value AS DECIMAL >= ‘1000’ )
ORDER BY
CAST( wp_usermeta.meta_value AS DECIMAL ) DESC,
display_name ASC
LIMIT 0, 10
42. WP_Term_Query name__like: SQL
SELECT t.name FROM wp_terms AS t
INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id
WHERE
tt.taxonomy IN (‘mascots’) AND t.name LIKE ‘%monster%'
ORDER BY t.name ASC
LIMIT 6
44. • Order results by a value in a custom table
• Adds an additional LEFT JOIN
WP_Query orderby with custom tables
45. Joining Custom Tables: JOIN
/**
* Join vote totals table to the 'books' custom post type query.
*
* @param string $join JOIN query clauses.
* @param WP_Query $query Current WP_Query instance.
* @return string The filtered JOIN clauses.
*/
function join_votes_table( $join, $query ) {
global $wpdb;
if ( ! is_admin() ) {
if ( isset( $query->query_vars['post_type'] )
&& 'books' == $query->query_vars[‘post_type']
) {
$votes = $wpdb->prefix . 'up_down_post_vote_totals';
$join .= "LEFT JOIN $votes ON $wpdb->posts.ID = $votes.post_id ";
}
}
return $join;
}
add_filter( 'posts_join', 'join_votes_table', 10, 2 );
46. Joining Custom Tables: ORDERBY
/**
* Order by vote totals descending, then post date descending.
*
* @param string $orderby ORDER BY query clauses.
* @param WP_Query $query Current WP_Query instance.
* @return string The filtered ORDER BY query clauses.
*/
function orderby_votes_and_date( $orderby, $query ) {
global $wpdb;
if ( ! is_admin() ) {
if ( isset( $query->query_vars['post_type'] )
&& 'books' == $query->query_vars[‘post_type']
) {
$votes = $wpdb->prefix . 'up_down_post_vote_totals';
$orderby = "$votes.vote_count_up DESC, $wpdb->posts.post_date DESC";
}
}
return $orderby;
}
add_filter( 'posts_orderby', 'orderby_votes_and_date', 10, 2 );
47. Joining Custom Tables: Original SQL
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts
WHERE 1=1
AND wp_posts.post_type = 'books'
AND (
wp_posts.post_status = ‘publish'
OR wp_posts.post_status = ‘private'
)
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10
48. Joining Custom Tables: SQL with JOIN
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts
LEFT JOIN wp_up_down_post_vote_totals
ON wp_posts.ID = wp_up_down_post_vote_totals.post_id
WHERE 1=1
AND wp_posts.post_type = 'books'
AND (
wp_posts.post_status = ‘publish'
OR wp_posts.post_status = ‘private'
)
ORDER BY
wp_up_down_post_vote_totals.vote_count_up DESC,
wp_posts.post_date DESC
LIMIT 0, 10