4

This is my current query:

query_posts(array_merge(array('tag' => $pagetag,'meta_key'=>priority,'orderby' =>meta_value, 'order' =>'ASC','paged' => get_query_var('paged'))));

My problem is that the query shows me only the post that has values for my 'meta_key' meaning that 'priority' is not NULL. How can I improve this query so that it will still orderby my meta_key but will show all the posts that aren't NULL as well?

Thanks in advance!

nimi
  • 917
  • 2
  • 14
  • 28

4 Answers4

6

The problem is that WordPress adds an INNER JOIN to the wp_postmeta table as soon as you mention meta_key in your conditions. One way around the problem is to add a filter on the order by clause, something like this:

function so_orderby_priority($original_orderby_statement) {
    global $wpdb;

    return "(SELECT $wpdb->postmeta.meta_value
               FROM $wpdb->postmeta
              WHERE $wpdb->posts.ID = $wpdb->postmeta.post_id
                AND $wpdb->postmeta.meta_key = 'priority') ASC";
}

add_filter('posts_orderby', 'so_orderby_priority');

query_posts(
    array(
        'tag' => $pagetag,
        'paged' => get_query_var('paged')
    )
);

remove_filter('posts_orderby', 'so_orderby_priority');

Note MySQL sorts NULLs first - if you want them sorted last, try something like this (assuming all your priorities come before ZZZZZ alphabetically):

function so_orderby_priority($original_orderby_statement) {
    global $wpdb;

    return "IFNULL(
               (SELECT $wpdb->postmeta.meta_value
                  FROM $wpdb->postmeta
                 WHERE $wpdb->posts.ID = $wpdb->postmeta.post_id
                   AND $wpdb->postmeta.meta_key = 'priority'),
                'ZZZZZ') ASC";
}

Edit

Here's a bit more explanation, which assumes you understand SQL at least a bit.

Your original query_posts resulted in the following query running against the database:

SELECT wp_posts.*
FROM   wp_posts
       INNER JOIN wp_term_relationships ON ( wp_posts.id = wp_term_relationships.object_id )
       INNER JOIN wp_postmeta ON ( wp_posts.id = wp_postmeta.post_id )
WHERE  1 = 1
       AND ( wp_term_relationships.term_taxonomy_id IN ( 3 ) )
       AND wp_posts.post_type = 'post'
       AND ( wp_posts.post_status = 'publish'
              OR wp_posts.post_status = 'private' )
       AND ( wp_postmeta.meta_key = 'priority' )
GROUP  BY wp_posts.id
ORDER  BY wp_postmeta.meta_value ASC
LIMIT  0, 10; 

That INNER JOIN wp_postmeta is what removed any posts without a priority from your results.

Removing the meta_* related conditions from your query_posts:

query_posts(
    array(
        'tag' => $pagetag,
        'paged' => get_query_var('paged')
    )
);

solved that problem, but the sort order is still wrong. The new SQL is

SELECT wp_posts.*
FROM   wp_posts
       INNER JOIN wp_term_relationships ON ( wp_posts.id = wp_term_relationships.object_id )
WHERE  1 = 1
       AND ( wp_term_relationships.term_taxonomy_id IN ( 3 ) )
       AND wp_posts.post_type = 'post'
       AND ( wp_posts.post_status = 'publish'
              OR wp_posts.post_status = 'private' )
GROUP  BY wp_posts.id
ORDER  BY wp_posts.post_date DESC
LIMIT  0, 10; 

The posts_orderby filter allows us to change the ORDER BY clause: wp_posts.post_date DESC gets replaced by what the filter returns. The final SQL becomes:

SELECT wp_posts.*
FROM   wp_posts
       INNER JOIN wp_term_relationships ON ( wp_posts.id = wp_term_relationships.object_id )
WHERE  1 = 1
       AND ( wp_term_relationships.term_taxonomy_id IN ( 3 ) )
       AND wp_posts.post_type = 'post'
       AND ( wp_posts.post_status = 'publish'
              OR wp_posts.post_status = 'private' )
GROUP  BY wp_posts.id
ORDER  BY (SELECT wp_postmeta.meta_value
           FROM   wp_postmeta
           WHERE  wp_posts.id = wp_postmeta.post_id
                  AND wp_postmeta.meta_key = 'priority') ASC
LIMIT  0, 10 

which does what you're after.

Hobo
  • 7,536
  • 5
  • 40
  • 50
  • Thank you! Worked amazing... nice code...But I could be really happy if you could explain a bit more what did you do. Im novice in php and didn't really fully get it... What's the different from the function query to the original query? And what did the filter exactly do? – nimi Jul 04 '12 at 07:43
  • Glad it helped. The solution is more MySQL than PHP, as `query_posts` generates SQL to run against the database behind the scenes. The filter completely replaces the sort order in the SQL with code that will sort the results the way you needed. I'll edit my answer to add a little more detail. – Hobo Jul 04 '12 at 08:03
1

I needed to perform a similar task on the users.php page for a custom column I added and used the following code that I modified from Hobo

add_action('pre_user_query', 'qd_users_column_orderby');

function qd_users_column_orderby($userquery){
  if('my_last_login'==$userquery->query_vars['orderby']) { //check if my cusomt meta_key is the column being sorted

    global $wpdb;

    $userquery->query_orderby = " ORDER BY(SELECT $wpdb->usermeta.meta_value
        FROM $wpdb->usermeta
        WHERE $wpdb->users.ID = $wpdb->usermeta.user_id
        AND $wpdb->usermeta.meta_key = 'my_last_login') ".($userquery->query_vars["order"] == "ASC" ? "asc " : "desc ")." , wp_users.user_login ".($userquery->query_vars["order"] == "ASC" ? "asc " : "desc ");
  }
}

Hopefully this helps somebody in need of this information.

In an effort of being throughout, the remainder of the necessary code to complete my individual task is below.

add_filter('manage_users_columns', 'qd_add_user_login_column');

function qd_add_user_login_column($columns) {
  $columns['my_last_login'] = 'Last Logged In';
  return $columns;
}

add_action('manage_users_custom_column',  'qd_show_user_login_column_content', 10, 3);

function qd_show_user_login_column_content($value, $column_name, $user_id) {
  $user = get_userdata( $user_id );
  if ( 'my_last_login' == $column_name ){
    $lastLogin = get_the_author_meta('my_last_login', $user_id);
    if(!$lastLogin){
      return "Never";
    }else{
      date_default_timezone_set(get_option('timezone_string'));
      return date('m/d/y g:ia', $lastLogin); 
    }
  }
  return $value;
}

add_filter( 'manage_users_sortable_columns', 'qd_users_table_sorting' );

function qd_users_table_sorting( $columns ) {
  $columns['my_last_login'] = 'my_last_login';
  return $columns;
}
Eric K
  • 689
  • 9
  • 26
  • I know that this answer doesn't exactly fit the individual question, but I wanted to simply document my answer here since I was able to solve my problem using the marked answer – Eric K Feb 14 '14 at 15:51
  • 1
    This actually helped me by stumbling into it here. I have last_login date stored as longtext in user meta area. I also was adding 3 new columns to users.php table. This was only method (pre_user_query) that actually worked to sort a date user column pulled from meta area. Thanks for your thoughtful answer. – B. Shea Sep 02 '15 at 14:23
1

The Issue: Sorting by a custom field without excluding posts that don't have a value set for that custom field.

Hobo's answer explains this well. Here I'm just going to offer a simpler alternative that ended up being easier in my case (Please note this won't work correctly if pagination is needed).

I decided to do the sorting in PHP after the query is made. The nice part is that I have better control over where the posts with null values end up (I wanted them to show up last).

$query = new WP_Query($args); 

//sort by rank
function customSort($a, $b)
{
    $a = get_field("sort_ranking", $a);
    $b = get_field("sort_ranking", $b);

    //handle nulls
    $a = is_numeric($a) ? $a : 9999;
    $b = is_numeric($b) ? $b : 9999;

    if ($a == $b) return 0;
    return ($a < $b) ? -1 : 1;
 }
 usort($query->posts, "customSort");

Here I have a numeric custom field called sort_ranking and I'm using it to sort ASC. Posts with a null value for this field are assigned 9999 so that they end up at the end. (Note: I'm using ACF, hence the get_field function)

Hope this helps someone!

saudes
  • 202
  • 2
  • 15
-1

The easiest way to do this is to insert the custom field using save_post action, so every post published will have its own meta_key with a default value.

Use a MySQL query for add post_meta to all posts has not the meta. Thats it.

If you/anyone need code help on this, just reply :)

UPDATE

As Timusan asked, add this code in your functions.php file after changing the meta-key name :

add_action('save_post', 'sidati_post_views_metakey');

function sidati_post_views_metakey ($post_id){

    /*
    * $post_id = is the post ID
    * 'sidati_post_views' => is your metakey name (sidati is prefix always nice to add your prefix)
    * 0 => the inital value
    * true => (bool) add true if you want this metakkey become unique
    */

    add_post_meta($post_id, 'sidati_post_views', 0, true);

}


// THIS ACTION MUST RUN ONLY ONE TIME
add_action('init', 'sidati_allposts_must_have_this');

function sidati_allposts_must_have_this(){

    /* Call the WordPress DataBase class */
    global $wpdb; 

    /* This Query will get us all the posts and pages without "sidati_post_views" metakey. */
    $ids = $wpdb->get_row("SELECT ID FROM wpdb->posts WHERE post_type IN ('post', 'page') AND post_status = 'publish' AND ID NOT IN (SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'sidati_post_views')");

    /* After get all posts/pages, now you need to add the meta keys (this may take a few munites if you have many posts/pages)*/
    foreach ($ids as $post_id) {
        add_post_meta($post_id, 'sidati_post_views', 0, true);
    }
}
Sidati
  • 1
  • 1
  • Adding a few code samples from the get-go (when posting your answer) might make it more clear what you mean. – Timusan Jan 16 '15 at 05:01