Solución:
Me he encontrado con este problema y parece que MySQL no maneja bien las múltiples uniones a la misma tabla (wp_postmeta) y OR-ed WHERE que WP genera aquí. Lo resolví reescribiendo la combinación y, como se menciona en la publicación a la que enlaza, aquí hay una versión que debería funcionar en su caso (actualizado para WP 4.1.1) (actualizado para WP 4.2.4):
function wpse158898_posts_clauses( $pieces, $query ) {
global $wpdb;
$relation = isset( $query->meta_query->relation ) ? $query->meta_query->relation : 'AND';
if ( $relation != 'OR' ) return $pieces; // Only makes sense if OR.
$prepare_args = array();
$key_value_compares = array();
foreach ( $query->meta_query->queries as $key => $meta_query ) {
if ( ! is_array( $meta_query ) ) continue;
// Doesn't work for IN, NOT IN, BETWEEN, NOT BETWEEN, NOT EXISTS.
if ( $meta_query['compare'] === 'EXISTS' ) {
$key_value_compares[] = '(pm.meta_key = %s)';
$prepare_args[] = $meta_query['key'];
} else {
if ( ! isset( $meta_query['value'] ) || is_array( $meta_query['value'] ) ) return $pieces; // Bail if no value or is array.
$key_value_compares[] = '(pm.meta_key = %s AND pm.meta_value ' . $meta_query['compare'] . ' %s)';
$prepare_args[] = $meta_query['key'];
$prepare_args[] = $meta_query['value'];
}
}
$sql=" JOIN " . $wpdb->postmeta . ' pm on pm.post_id = ' . $wpdb->posts . '.ID'
. ' AND (' . implode( ' ' . $relation . ' ', $key_value_compares ) . ')';
array_unshift( $prepare_args, $sql );
$pieces['join'] = call_user_func_array( array( $wpdb, 'prepare' ), $prepare_args );
// Zap postmeta clauses.
$wheres = explode( "n", $pieces[ 'where' ] );
foreach ( $wheres as &$where ) {
$where = preg_replace( array(
'/ +( +' . $wpdb->postmeta . '.meta_key .+) *$/',
'/ +( +mt[0-9]+.meta_key .+) *$/',
'/ +mt[0-9]+.meta_key = '[^']*"https://foroayuda.es/",
), '(1=1)', $where );
}
$pieces[ 'where' ] = implode( '', $wheres );
$pieces['orderby'] = str_replace( $wpdb->postmeta, 'pm', $pieces['orderby'] ); // Sorting won't really work but at least make it not crap out.
return $pieces;
}
y luego alrededor de su consulta:
add_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10, 2 );
$posts = new WP_Query($args);
remove_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10 );
Apéndice:
La solución para esto, el ticket 24093, no llegó a 4.0 (además, no solucionó este problema de todos modos), por lo que originalmente intenté una versión generalizada de lo anterior, pero en realidad es demasiado descabellado intentar una solución de este tipo, así que la eliminé …
La respuesta corta es que los metadatos en WordPress no están destinados a ser utilizados para datos relacionales. Obtener publicaciones por varias condiciones a sus metadatos no es la idea detrás de los metadatos. Por lo tanto, las consultas, las estructuras de las tablas y los índices no están optimizados para eso.
La respuesta más larga:
El resultado de su Meta-Consulta es algo así:
SELECT wp_4_posts.* FROM wp_4_posts
INNER JOIN wp_4_postmeta ON (wp_4_posts.ID = wp_4_postmeta.post_id)
INNER JOIN wp_4_postmeta AS mt1 ON (wp_4_posts.ID = mt1.post_id)
INNER JOIN wp_4_postmeta AS mt2 ON (wp_4_posts.ID = mt2.post_id)
INNER JOIN wp_4_postmeta AS mt3 ON (wp_4_posts.ID = mt3.post_id)
INNER JOIN wp_4_postmeta AS mt4 ON (wp_4_posts.ID = mt4.post_id)
WHERE 1=1
AND wp_4_posts.post_type="post"
AND (wp_4_posts.post_status="publish" OR wp_4_posts.post_status="private")
AND ( (wp_4_postmeta.meta_key = '_author' AND CAST(wp_4_postmeta.meta_value AS CHAR) = 'Test')
OR (mt1.meta_key = '_publisher' AND CAST(mt1.meta_value AS CHAR) = 'Test')
OR (mt2.meta_key = '_contributor_1' AND CAST(mt2.meta_value AS CHAR) = 'Test')
OR (mt3.meta_key = '_contributor_2' AND CAST(mt3.meta_value AS CHAR) = 'Test')
OR (mt4.meta_key = '_contributor_3' AND CAST(mt4.meta_value AS CHAR) = 'Test') ) GROUP BY wp_4_posts.ID ORDER BY wp_4_posts.post_date DESC
Echemos un vistazo a cómo MySQL maneja esta consulta (EXPLAIN
):
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE wp_4_posts range PRIMARY,type_status_date type_status_date 124 NULL 5 Using where; Using temporary; Using filesort
1 SIMPLE wp_4_postmeta ref post_id,meta_key post_id 8 wordpress.wp_4_posts.ID 1
1 SIMPLE mt1 ref post_id,meta_key post_id 8 wordpress.wp_4_posts.ID 1
1 SIMPLE mt2 ref post_id,meta_key post_id 8 wordpress.mt1.post_id 1 Using where
1 SIMPLE mt3 ref post_id,meta_key post_id 8 wordpress.wp_4_posts.ID 1
1 SIMPLE mt4 ref post_id,meta_key post_id 8 wordpress.wp_4_postmeta.post_id 1 Using where
Ahora lo que puede ver, MySQL haga una selección en wp_posts
y se une 5 veces a la mesa wp_postmeta
. los escribe ref
dice que MySQL tiene que examinar todas las filas en esta tabla, haciendo coincidir el índice (post_id, meta_key) comparando un valor de columna no indexado con su where
cláusula, y que para cada combinación de filas de la tabla anterior. El manual de MySQL dice: “Si la clave que se usa coincide solo con unas pocas filas, este es un buen tipo de unión”. Y ese es el primer problema: en un sistema WordPress promedio, la cantidad de post-metas por publicación puede crecer fácilmente hasta 30-40 registros o más. La otra clave posible meta_key
crece con el recuento de publicaciones. Entonces, si tienes 100 publicaciones y cada una tiene un _publisher
meta, hay 100 filas con este valor como meta_key
en wp_postmeta
, por supuesto.
Para manejar todos estos posibles resultados, mysql crea una tabla temporal (using temporary
). Si esta tabla se vuelve demasiado grande, el servidor generalmente la almacena en el disco en lugar de en la memoria. Otro posible cuello de botella.
Soluciones posibles
Como se describe en las respuestas existentes, puede intentar optimizar la consulta por su cuenta. Eso puede funcionar bien para sus inquietudes, pero puede causar problemas a medida que crecen las tablas post / postmeta.
Pero si desea usar la API de consultas de WordPress, debe considerar usar taxonomías para almacenar los datos por los que desea buscar publicaciones.
Esto puede ser un poco tarde para el juego, pero me encontré con el mismo problema. Al crear un complemento para manejar la búsqueda de propiedades, mi opción de búsqueda avanzada consultaría hasta 20 metaentradas diferentes para cada publicación para encontrar aquellas que coincidan con los criterios de búsqueda.
Mi solución fue consultar la base de datos directamente usando el $wpdb
global. Consulté cada entrada de meta individualmente y almacené el post_ids
de las publicaciones que coinciden con cada criterio. Luego hice una intersección en cada uno de los conjuntos combinados para obtener el post_ids
que cumplía con todos los criterios.
Mi caso era algo sencillo porque no tenía ninguna OR
elementos que necesitaba tener en cuenta, pero que podrían incluirse con bastante facilidad. Dependiendo de la complejidad de su consulta, esta es una solución rápida y que funciona. Aunque admito que es una mala opción en comparación con poder hacer una verdadera consulta relacional.
El siguiente código se ha simplificado en gran medida de lo que usé, pero puede obtener una idea de él.
class property_search{
public function get_results($args){
$potential_ids=[];
foreach($args as $key=>$value){
$potential_ids[$key]=$this->get_ids_by_query("
SELECT post_id
FROM wp_postmeta
WHERE meta_key = '".$key."'
AND CAST(meta_value AS UNSIGNED) > '".$value."'
");//a new operator would need to be created to handle each type of data and comparison.
}
$ids=[];
foreach($potential_ids as $key=>$temp_ids){
if(count($ids)==0){
$ids=$temp_ids;
}else{
$ids=array_intersect($ids,$temp_ids);
}
}
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
$args = array(
'posts_per_page'=> 50,
'post_type'=>'property',
'post_status'=>'publish',
'paged'=>$paged,
'post__in'=>$ids,
);
$search = new WP_Query($args);
return $search;
}
public function get_ids_by_query($query){
global $wpdb;
$data=$wpdb->get_results($query,'ARRAY_A');
$results=[];
foreach($data as $entry){
$results[]=$entry['post_id'];
}
return $results;
}
}