How to Sort WordPress Query Results by Multiple Meta Keys

I’m building a website that publishes reviews of hosting companies. For each review I store Price, Rating, and a “Top Pick” flag as post meta. Visitors can sort the review listings by Price, Rating, or Post Date.

The client wants “Top Picks” to always appear first. That means when a user sorts by Price (Low to High), the results should show all Top Picks sorted by price low-to-high first, followed by the remaining reviews sorted by price low-to-high.

In WordPress 4.0, you could pass multiple orderby parameters and specify the order for each, but you were limited to a single meta-based orderby because of the need for a top-level meta_key parameter. For example:

 'be_rating',
	'orderby'  => array(
		'meta_value_num' => 'DESC',
		'post_date'      => 'ASC',
	),
) );

With WordPress 4.2 the query system was improved to allow ordering by multiple meta keys, which makes implementing the “Top Picks first” behavior straightforward. Below is the code I use to build the sort options shown in the screenshot.

 'review',
	'posts_per_page'  => 10,
	'paged'           => get_query_var( 'paged', false ),
	'meta_query'      => array(
		'relation'    => 'AND',
		'be_top_pick' => array(
			'key'     => 'be_top_pick',
			'compare' => 'EXISTS',
		),
		'be_price'    => array(
			'key'     => 'be_price',
			'type'    => 'NUMERIC',
			'compare' => 'EXISTS',
		),
		'be_rating'   => array(
			'key'     => 'be_rating',
			'type'    => 'NUMERIC',
			'compare' => 'EXISTS',
		),
	)
);

// Sort Results
$current_sort = isset( $_GET['hosting-sort'] ) ? esc_attr( $_GET['hosting-sort'] ) : 'most-recent';

switch( $current_sort ) {

	case 'most-recent':
		$args['orderby']  = array(
			'be_top_pick' => 'DESC',
			'post_date'   => 'DESC',
		);
		break;

	case 'price-high':
		$args['orderby'] = array(
			'be_top_pick' => 'DESC',
			'be_price'    => 'DESC',
		);
		break;

	case 'price-low':
		$args['orderby'] = array(
			'be_top_pick' => 'DESC',
			'be_price'    => 'ASC',
		);
		break;

	case 'rating-high':
		$args['orderby'] = array(
			'be_top_pick' => 'DESC',
			'be_rating'   => 'DESC',
		);
		break;

	case 'rating-low':
		$args['orderby'] = array(
			'be_top_pick' => 'DESC',
			'be_rating'   => 'ASC',
		);
		break;
}

$loop = new WP_Query( $args );

How it works

  • I start by constructing the base query arguments: post type, posts per page, and pagination.
  • I include a meta_query entry for every meta key used for sorting. Each entry has 'compare' => 'EXISTS' because I want posts that include those meta keys without restricting the query to specific values.
  • For numeric metadata (Price and Rating) I specify 'type' => 'NUMERIC' so WordPress uses numeric comparison and sorts using meta_value_num instead of meta_value. That prevents lexical ordering when sorting numbers.
  • In the theme template, a dropdown adds a GET parameter (hosting-sort) to indicate the user’s chosen sort. I read that GET variable and fall back to a default sort of “most recent” if it’s not present.
  • Depending on the selected sort, I set the orderby argument to an ordered array where be_top_pick is always listed first with a descending order so Top Picks appear first. The second orderby key handles the requested secondary sort (price, rating, or post date).

Notes and best practices

  • Use the same key names in your meta_query entries as you reference in the orderby array.
  • Specify numeric types for numerical meta to avoid incorrect ordering of values like “10” vs “2”.
  • Because WordPress 4.2 allows multiple meta-based orderby clauses, this pattern keeps Top Picks at the top while still honoring the user’s chosen sorting preference for the rest of the results.