Мета-запрос работает очень медленно
У меня есть пользовательский meta-запрос, который работает очень медленно или вообще не загружается до конца. С тремя массивами
в 'meta_query'
запрос работает нормально, но с четырьмя и более он перестает работать.
При поиске причины я нашел этот пост, но я совершенно не знаком с пользовательскими запросами к базе данных.
Любая помощь будет очень ценной! Спасибо!
<?php
$args = array(
'post_type' => $post_type,
'posts_per_page' => -1,
'meta_query' => array(
'relation' => 'OR',
array(
'key'=>'_author',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_publisher',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_1',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_2',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_3',
'value'=> $author_single["fullname"],
'compare' => '='
)
)
);
$posts = new WP_Query($args);
if( $posts->have_posts() ) : while( $posts->have_posts() ) : $posts->the_post(); ?>
<li><a href="<?php echo get_the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php endwhile; endif; ?>
– – – – –
Обновленный код с добавлениями от boger:
page.php
<?php
$args = array(
'post_type' => $post_type,
'posts_per_page' => -1,
'meta_query' => array(
'relation' => 'OR',
array(
'key'=>'_author',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_publisher',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_1',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_2',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_3',
'value'=> $author_single["fullname"],
'compare' => '='
)
)
);
add_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10, 2 );
$posts = new WP_Query($args);
if( $posts->have_posts() ) : while( $posts->have_posts() ) : $posts->the_post(); ?>
<li><a href="<?php echo get_the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php endwhile; endif;
remove_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10 ); ?>
functions.php
function wpse158898_posts_clauses( $pieces, $query ) {
global $wpdb;
$relation = isset( $query->meta_query->relation ) ? $query->meta_query->relation : 'AND';
if ( $relation != 'OR' ) return $pieces; // Работает только для OR
$prepare_args = array();
$key_value_compares = array();
foreach ( $query->meta_query->queries as $meta_query ) {
// Не работает для IN, NOT IN, BETWEEN, NOT BETWEEN, NOT EXISTS
if ( ! isset( $meta_query['value'] ) || is_array( $meta_query['value'] ) ) return $pieces; // Пропускаем если нет значения или это массив
$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 );
$pieces['where'] = preg_replace( '/ AND[^w]+wp_postmeta.*$/s', '', $pieces['where'] ); // Удаляем условия postmeta
return $pieces;
}
– – –
$posts->request
выводит
$args = array(
'post_type' => $post_type,
'posts_per_page' => -1,
'meta_query' => array(
'relation' => 'OR',
array(
'key'=>'_author',
'value'=> "Hanna Meier",
'compare' => '='
),
array(
'key'=>'_publisher',
'value'=> "Friedhelm Peters",
'compare' => '='
)
)
);
Без пользовательского запроса
SELECT wp_vacat_posts.* FROM wp_vacat_posts INNER JOIN wp_vacat_postmeta ON (wp_vacat_posts.ID = wp_vacat_postmeta.post_id)
INNER JOIN wp_vacat_postmeta AS mt1 ON (wp_vacat_posts.ID = mt1.post_id) WHERE 1=1 AND wp_vacat_posts.post_type = 'product' AND (wp_vacat_posts.post_status = 'publish' OR wp_vacat_posts.post_status = 'private') AND ( (wp_vacat_postmeta.meta_key = '_author' AND CAST(wp_vacat_postmeta.meta_value AS CHAR) = 'Hanna Meier')
OR (mt1.meta_key = '_publisher' AND CAST(mt1.meta_value AS CHAR) = 'Friedhelm Peters') ) GROUP BY wp_vacat_posts.ID ORDER BY wp_vacat_posts.post_date DESC
С пользовательским запросом
SELECT wp_vacat_posts.* FROM wp_vacat_posts
JOIN wp_vacat_postmeta pm on pm.post_id = wp_vacat_posts.ID AND ((pm.meta_key = '_author' AND pm.meta_value = 'Hanna Meier') OR (pm.meta_key = '_publisher' AND pm.meta_value = 'Friedhelm Peters')) WHERE 1=1 AND wp_vacat_posts.post_type = 'product' AND (wp_vacat_posts.post_status = 'publish' OR wp_vacat_posts.post_status = 'private') AND ( (wp_vacat_postmeta.meta_key = '_author' AND CAST(wp_vacat_postmeta.meta_value AS CHAR) = 'Hanna Meier')
OR (mt1.meta_key = '_publisher' AND CAST(mt1.meta_value AS CHAR) = 'Friedhelm Peters') ) GROUP BY wp_vacat_posts.ID ORDER BY wp_vacat_posts.post_date DESC

Я столкнулся с этой проблемой, и похоже, что MySQL плохо справляется с множественными соединениями к одной и той же таблице (wp_postmeta) и условиями WHERE с оператором OR, которые генерирует WordPress. Я решил это, переписав JOIN и WHERE, как упоминалось в посте, на который вы ссылаетесь — вот версия, которая должна работать в вашем случае (обновлено для WP 4.1.1) (обновлено для 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; // Работает только для OR.
$prepare_args = array();
$key_value_compares = array();
foreach ( $query->meta_query->queries as $key => $meta_query ) {
if ( ! is_array( $meta_query ) ) continue;
// Не работает для 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; // Прерываем, если нет значения или оно массив.
$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 );
// Удаляем условия postmeta.
$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 = \'[^\']*\'/',
), '(1=1)', $where );
}
$pieces[ 'where' ] = implode( '', $wheres );
$pieces['orderby'] = str_replace( $wpdb->postmeta, 'pm', $pieces['orderby'] ); // Сортировка не будет работать, но хотя бы избежим ошибок.
return $pieces;
}
И затем вокруг вашего запроса:
add_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10, 2 );
$posts = new WP_Query($args);
remove_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10 );
Дополнение:
Исправление для этого, тикет 24093, не вошло в версию 4.0 (плюс оно не решало эту проблему в любом случае), поэтому изначально я попытался создать обобщенную версию вышеуказанного кода, но она оказалась слишком ненадежной, поэтому я ее удалил...

Ваш код, кажется, работает действительно хорошо! Я еще не проверил его со всеми моими записями, но с теми, что проверил - он очень быстрый! Еще раз большое спасибо - я очень ценю вашу помощь!

Шаг назад: У вас есть идеи, почему я не получаю никаких результатов при использовании вашего пользовательского запроса? Когда я использую код из моего оригинального поста только с двумя мета-запросами, я получаю ожидаемые результаты (ссылки на заголовки), но при использовании вашего запроса я не получаю ничего.

Получать другие результаты — это как десять шагов назад! Не могу понять, почему это не работает — те же ли это $args, что и выше, то есть без новых, только post_type, posts_per_page и meta_query с X записями?

Я отредактировал свой исходный пост с добавлениями, которые вы сделали. Я не вносил изменений в свой код... Если вам нужна дополнительная информация, дайте мне знать.

Всё ещё не могу понять, что идёт не так, могу только предложить отладить запрос, например, добавив print($posts->request);
(или error_log($posts->request);
) сразу после $posts = new WP_Query($args);
, чтобы увидеть, есть ли что-то явно неправильное в нём.

Спасибо за поддержку! Вы найдете выводы print($posts->request);
в моем исходном коде внизу — как с вашим пользовательским запросом, так и без него. error_log($posts->request);
используется для обоих случаев 1
.

А, понимаю — вы используете префикс, который ответ не учитывает при удалении условия WHERE — нужно заменить на '/ AND[^w]+' . $wpdb->postmeta . '.*$/s'
в preg_replace — обновлю ответ.

Ура, это работает! Могу ли я отблагодарить вас небольшим пожертвованием?

Ура, наконец-то! И это очень мило с вашей стороны предлагать - если хотите, пожертвуйте вашей любимой благотворительной организации (как говорят по телевизору) - вы тоже внесли вклад, исправив мой ответ, и я надеюсь обобщить его в не слишком отдаленном будущем (заметьте необязательную расплывчатость!), учитывая что исправление не войдет в WP 4.0.

Привет, нет, не работает, нужно игнорировать не-query записи в массиве meta_query плюс учитывать не-алиасированные ссылки в orderby - как минимум, возможно потребуются другие доработки, но я все равно обновлю этот код пока что...

@vee да, ты прав, среди прочего изменился формат условия where, поэтому оно не срабатывает. Я обновлю конкретный ответ и удалю "обобщённый" ответ, так как безнадёжно пытаться постоянно его обновлять...

Короткий ответ: метаданные в WordPress не предназначены для хранения реляционных данных. Выборка записей по нескольким условиям их метаданных противоречит изначальной концепции метаданных. Поэтому структура таблиц, запросы и индексы не оптимизированы для таких операций.
Более развернутый ответ:
Ваш мета-запрос приводит к выполнению примерно такого SQL-запроса:
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
Давайте посмотрим, как MySQL обрабатывает этот запрос (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
Здесь мы видим, что MySQL делает выборку из wp_posts
и соединяет таблицу wp_postmeta
пять раз. Тип соединения ref
означает, что MySQL должен проверить все строки в этой таблице, сопоставляя индекс (post_id, meta_key) и сравнивая значения неиндексированных столбцов с условием where
, причем для каждой комбинации строк из предыдущей таблицы. В документации MySQL сказано: «Если используемый ключ соответствует лишь нескольким строкам, это хороший тип соединения». И это первая проблема: в типичной системе WordPress количество метаданных на одну запись может легко достигать 30-40 или более. Другой возможный ключ meta_key
увеличивается вместе с количеством записей. Так, если у вас 100 записей и у каждой есть метаполе _publisher
, то в таблице wp_postmeta
будет 100 строк с этим значением в meta_key
.
Для обработки всех этих возможных результатов MySQL создает временную таблицу (using temporary
). Если эта таблица становится слишком большой, сервер обычно сохраняет ее на диск вместо памяти. Это еще одно потенциальное узкое место.
Возможные решения
Как описано в существующих ответах, вы можете попытаться оптимизировать запрос самостоятельно. Это может хорошо работать для ваших текущих нужд, но может вызвать проблемы по мере роста таблиц post/postmeta.
Но если вы хотите использовать WordPress Query API, вам следует рассмотреть возможность использования таксономий для хранения данных, по которым вы хотите осуществлять поиск записей.

Ага, значит это "объясняет", почему запрос срабатывает при первом запуске, но не при последующих немедленных запусках — отлично! Но я не могу согласиться с вашим мнением об использовании таксономий вместо этого, это кажется как минимум неестественным, если не неосуществимым. Почему бы не запрашивать метаданные реляционно? В WordPress есть много кода, который как раз предназначен для такого использования, и сам WP так делает! По мне это просто баг в WP — он должен генерировать гораздо лучший код здесь, а не тупо создавать join за join.

Есть тикет в WP (ему уже 3 года!) по этому поводу 24093, в котором есть патч, исправляющий это с помощью подзапросов. Но в 4.0 он не попадёт...

Возможно, это немного запоздалое решение, но я столкнулся с той же проблемой. При создании плагина для поиска объектов недвижимости мой расширенный поиск выполнял запросы к 20 различным мета-полям для каждого поста, чтобы найти соответствующие критериям поиска.
Моим решением был прямой запрос к базе данных с использованием глобальной переменной $wpdb
. Я запрашивал каждое мета-поле отдельно и сохранял post_ids
постов, соответствующих каждому критерию. Затем я выполнял пересечение всех найденных наборов, чтобы получить post_ids
, соответствующие всем критериям.
Мой случай был относительно простым, так как у меня не было элементов OR
, которые нужно было учитывать, но их можно было бы довольно легко добавить. В зависимости от сложности вашего запроса, это рабочее и быстрое решение. Хотя, признаю, это не лучший вариант по сравнению с выполнением истинного реляционного запроса.
Приведенный ниже код сильно упрощен по сравнению с тем, что я использовал, но он дает общее представление.
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."'
");//для каждого типа данных и сравнения нужно создать новый оператор.
}
$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;
}
}

Таблица wp_postmeta имеет неэффективные индексы. Вот обсуждение этой проблемы и рекомендуемые решения:
http://mysql.rjweb.org/doc.php/index_cookbook_mysql#speeding_up_wp_postmeta

Операторы OR очень дорогостоящие.
У вас слишком много ключей, но предположим, что сейчас это нельзя изменить. Другой вариант, который можно реализовать без большого объема кодирования, — это изменить количество получаемых записей, заменив 'posts_per_page'
на 10 или большее число, и посмотреть, как это повлияет на производительность.
