web-dev-qa-db-fra.com

Amélioration des performances de WP_Query pour plusieurs taxonomies

J'ai un site avec plusieurs taxonomies personnalisées et j'ai constaté que l'une des parties les plus lentes du site tente d'interroger un OR sur plusieurs d'entre elles à la fois. J'utilise un WP_Query comme suit:

array(
  'tax_query' => array(
    'relation' => 'OR',
    array('taxonomy' => 'tax1', 'field' => 'slug', 'terms' => 'term1'),
    array('taxonomy' => 'tax2', 'field' => 'slug', 'terms' => 'term2'),
    array('taxonomy' => 'tax3', 'field' => 'slug', 'terms' => 'term3'),
    array('taxonomy' => 'tax4', 'field' => 'slug', 'terms' => 'term4'),
  )
)

Le SQL qu'il génère prend 6 secondes inacceptables à exécuter:

SELECT SQL_CALC_FOUND_ROWS wp_posts.* FROM wp_posts  
INNER JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id) 
INNER JOIN wp_term_relationships AS tt1 ON (wp_posts.ID = tt1.object_id) 
INNER JOIN wp_term_relationships AS tt2 ON (wp_posts.ID = tt2.object_id) 
INNER JOIN wp_term_relationships AS tt3 ON (wp_posts.ID = tt3.object_id) 
WHERE 1=1 AND wp_posts.ID NOT IN (70) 
AND (wp_term_relationships.term_taxonomy_id IN (23) 
  OR tt1.term_taxonomy_id IN (5)
  OR tt2.term_taxonomy_id IN (11)
  OR tt3.term_taxonomy_id IN (10) ) 
AND (wp_posts.post_status = 'publish') 
GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 500

Mais cette requête équivalente prend 0,29 seconde:

SELECT SQL_CALC_FOUND_ROWS  wp_posts.* FROM wp_posts
INNER JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id)
WHERE 1=1 AND wp_posts.ID NOT IN (70)
AND (wp_term_relationships.term_taxonomy_id IN (23, 5, 11, 10)) 
AND (wp_posts.post_status = 'publish')
GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 500

Il est clair que les jointures multiples le rendent plus lent que nécessaire. Le SQL ne se soucie pas que les termes viennent de taxonomies différentes, mais WP_Query le fait parce qu'ils sont recherchés par slug. Existe-t-il un moyen de persuader WP_Query de générer quelque chose de plus proche du second?

(Notez que ce qui précède a été anonymisé pour protéger mon client)

2
Marcus Downing

J'ai une solution, mais c'est vraiment laide. J'aimerais en entendre un meilleur, mais je ne suis pas sûr que ce soit possible.

WP_Query::get_posts() appelle parse_tax_query() deux fois: d'abord vers le début, puis de nouveau juste avant d'obtenir le code SQL. Il n'y a pas de hook unique qui me permette d'intercepter et d'ajuster la valeur de $tax_query à temps pour ajuster le code SQL. J'ai donc dû le faire en deux parties.

  • Une action sur pre_get_posts, près du début de get_posts(), reconnaît les requêtes de taxonomie avec 'relation' => 'OR' et les simplifie afin qu'elles ne génèrent qu'une seule jointure sur wp_term_relationships. En même temps, il stocke la liste résolue d'ID de toutes les taxonomies à l'intérieur de l'objet 'WP_Query' à utiliser ultérieurement.

  • Un filtre sur posts_where_paged, beaucoup plus tard dans query_posts(), recherche une liste enregistrée d'identifiants et remplace la condition sur la jointure.

Voici le code:

add_action('pre_get_posts', 'wp_query__pre');
function wp_query__pre ($wp_query) {
  if (!isset($wp_query->query['tax_query'])) return;
  if ($wp_query->query['tax_query']['relation'] != 'OR') return;

  $allterms = array();
  foreach ($wp_query->tax_query->queries as $query) {
    $tax = $query['taxonomy'];
    $terms = $query['terms'];
    $wp_query->tax_query->_transform_terms($terms, $query['taxonomy'], $query['field'], 'term_taxonomy_id');
    $allterms = array_merge($allterms, $terms);
  }

  $tax_query = array(array(
      'taxonomy' => $tax,
      'terms' => $terms,
      'operator' => 'IN',
      'include_children' => 0,
      'field' => 'term_taxonomy_id',
    ));

  $wp_query->query['tax_query'] = $tax_query;
  $wp_query->query_vars['tax_query'] = $tax_query;
  $wp_query->tax_query = new WP_Tax_Query($tax_query);
  $wp_query->saved_tax_terms = $allterms;
}

add_filter('posts_where_paged', 'wp_query__where', 10, 2);
function wp_query__where ($where, $wp_query) {
  if (!empty($wp_query->saved_tax_terms)) {
    $terms = implode(", ", $wp_query->dft_tax_terms);
    $where = preg_replace("!term_taxonomy_id IN \([^)]*\)!", "term_taxonomy_id IN ($terms)", $where);
  }
  return $where;
}

Notez que le code n'a pas encore été testé et qu'il contient sans aucun doute toutes sortes de bogues. Je ne vais probablement pas faire face à des requêtes plus compliquées.

L'avantage de cette approche est qu'elle n'exige pas que le reste de votre code soit au courant. Il suffit de coder une requête de taxonomie avec OR pour la récupérer et l’optimiser. La vitesse n’a pas été aussi rapide que je l’espérais, mais c’est une nette amélioration.

Je me demande si l'équipe WordPress ne devrait pas inclure quelque chose comme ceci dans le code principal.


Mise à jour: Comme il sied à un hack aussi laid, ceci est interrompu sous WordPress 3.2. Je cherche une solution.

2
Marcus Downing

Vous pouvez faire en sorte que Wordpress utilise votre requête plus efficace en utilisant les filtres posts_join et posts_where, comme suit:

add_filter( 'posts_join', 'tax_posts_join', 10, 2 );
add_filter( 'posts_where', 'tax_posts_where', 10, 2 );
add_filter( 'posts_request', 'tax_posts_request' );

function tax_posts_join( $sql, $wp_query ){
    if( $tax_ids = $wp_query->get('term_taxonomy_ids_in') )
        $sql .= " INNER JOIN wp_term_relationships ON ( wp_posts.ID = wp_term_relationships.object_id )";

    return $sql;
}

function tax_posts_where( $sql, $wp_query ){
    if( $tax_ids = $wp_query->get('term_taxonomy_ids_in') ){
        $tax_ids = implode( ', ', $tax_ids );
        $sql .= " AND ( wp_term_relationships.term_taxonomy_id IN (".$tax_ids.") ) ";
    }   

    return $sql;
}

function tax_posts_request( $sql ){
    //var_dump( $sql );
    return $sql;
}


$args = array(
    'term_taxonomy_ids_in' => array(23, 5, 11, 10)
);

$tax_posts = new WP_Query( $args );

Cela peut être un peu nettoyé, nettoyez probablement les identifiants de taxonomie avant de créer le SQL, vous aurez peut-être aussi besoin du filtre posts_groupby, mais vous devriez vous mettre dans la bonne direction. http://codex.wordpress.org/Custom_Queries

1
postpostmodern