Мета-запрос работает очень медленно

23 авг. 2014 г., 22:51:15
Просмотры: 16.1K
Голосов: 11

У меня есть пользовательский 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    
1
Комментарии

Можете ли вы добавить объяснение MySQL (EXPLAIN SELECT …, на вашей системе) для полученного запроса?

David David
24 авг. 2014 г. 17:41:59
Все ответы на вопрос 5
16

Я столкнулся с этой проблемой, и похоже, что 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 (плюс оно не решало эту проблему в любом случае), поэтому изначально я попытался создать обобщенную версию вышеуказанного кода, но она оказалась слишком ненадежной, поэтому я ее удалил...

24 авг. 2014 г. 17:21:06
Комментарии

Большое спасибо за вашу помощь! Я попробую и дам вам знать.

user1706680 user1706680
24 авг. 2014 г. 21:12:49

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

user1706680 user1706680
30 авг. 2014 г. 22:53:37

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

user1706680 user1706680
31 авг. 2014 г. 01:44:24

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

bonger bonger
31 авг. 2014 г. 03:01:46

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

user1706680 user1706680
31 авг. 2014 г. 11:30:26

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

bonger bonger
31 авг. 2014 г. 17:00:57

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

user1706680 user1706680
3 сент. 2014 г. 12:09:42

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

bonger bonger
3 сент. 2014 г. 16:23:16

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

user1706680 user1706680
3 сент. 2014 г. 18:08:08

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

bonger bonger
3 сент. 2014 г. 18:38:59

Вы знаете, работает ли это в 4.1.1?

user1706680 user1706680
19 февр. 2015 г. 11:05:47

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

bonger bonger
2 мар. 2015 г. 18:26:26

Не работает на WP 4.2.4. По-прежнему очень медленно.

vee vee
13 авг. 2015 г. 17:59:51

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

bonger bonger
15 авг. 2015 г. 15:18:37

@bonger Не переживай, это не твоя вина.

vee vee
15 авг. 2015 г. 23:04:35

Мое скромное мнение: я решаю такие проблемы с помощью двух отдельных wp-запросов, а затем программно объединяю результаты через PHP — это достаточно быстро (мой недавний пример: время загрузки уменьшилось с 4 секунд до 0.3)

trainoasis trainoasis
9 дек. 2019 г. 13:03:33
Показать остальные 11 комментариев
2

Короткий ответ: метаданные в 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, вам следует рассмотреть возможность использования таксономий для хранения данных, по которым вы хотите осуществлять поиск записей.

25 авг. 2014 г. 11:37:44
Комментарии

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

bonger bonger
25 авг. 2014 г. 14:45:19

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

bonger bonger
25 авг. 2014 г. 19:24:36
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;
}

}
8 сент. 2016 г. 01:01:49
1

Таблица wp_postmeta имеет неэффективные индексы. Вот обсуждение этой проблемы и рекомендуемые решения:

http://mysql.rjweb.org/doc.php/index_cookbook_mysql#speeding_up_wp_postmeta

23 мая 2017 г. 05:10:04
Комментарии

Очень полезная информация, которая дает представление о том, что происходит и что можно сделать, хороший материал

sMyles sMyles
4 янв. 2022 г. 21:20:03
2

Операторы OR очень дорогостоящие.

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

23 авг. 2014 г. 23:07:12
Комментарии

Спасибо за подсказку – сейчас у меня только одна запись, так что изменение post_per_page ничего не изменит.

user1706680 user1706680
23 авг. 2014 г. 23:23:10

Хорошо, если ты только начинаешь, тогда, возможно, можно изменить количество ключей. Как думаешь?

Tomás Cot Tomás Cot
23 авг. 2014 г. 23:39:28