WordPress: выбор случайных постов

Владимир | | MySQL, PHP, Web разработка, WordPress.

wordpress sql rand

Идея написать этот пост у меня появилась после выхода статьи
Random Redirection In WordPress Перевод в Smashing magazine.

В ней рассказывается о том, как выполнить редирект на случайный пост. Но точно такой же подход можно использовать и для отображения случайных постов, например, в каком-нибудь виджете.

Сразу хочу пояснить. Я ничего не имею против решения, описанного в Smashing magazine, это встроенный в WP способ выборки случайных записей, просто при его использовании можно ощутимо снизить скорость формирования страниц.

Для начала рассмотрим, как работает стандартный вариант.

Для этого немного перепишем функцию, приведённую в Smashing magazine.

function show_random_posts($count = 3) {
     $start = microtime(true);
    
     $args = array(
         'numberposts' => $count,
         'orderby' => 'rand',
         'post_type' => 'any',
     );
    
     $rnd_posts = get_posts( $args );

     foreach ( $rnd_posts as $post ) {
          echo '<p><a href="'.get_permalink($post->ID).'">'.$post->post_title.'</a></p>';
     }

     $stop = microtime(true);
     echo '<p>Затраченное время: '.($stop - $start).'</p>';
}

Здесь мы выбираем заданное количество случайных записей ($count) с помощью встроенной функции get_posts. Эта функция в качестве параметра получает массив с настройками поиска. В данном случае это:
numberposts – количество записей, которые должен вернуть запрос;
orderby – сортировка, в данном случае – rand – случайным образом;
post_type – типы постов – any – все.

Затем в цикле (строки 12-14) просто выводим список полученных постов.

Кроме того, в переменных $start и $stop сохраняем время начала и завершения кода.

Использовать эту функцию не сложно, достаточно просто вставить её в шаблон нужной страницы.

Но что именно происходит при вызове get_posts?

WP делает один запрос к БД, который возвращает все необходимые данные.

SELECT   wp_posts.* FROM wp_posts  WHERE 1=1  AND wp_posts.post_type IN ('post', 'page', …) AND (wp_posts.post_status = 'publish')  ORDER BY RAND() DESC LIMIT 0, 3;

Просто и элегантно 🙂

Но известно, что функция RAND() снижает скорость запроса.

Попробуем переписать нашу функцию так, чтобы получить тот же результат, но обойтись без RAND().

У меня получился следующий код

function show_random_posts_optimized($count = 3) {
     $start = microtime(true);
    
     $args = array(
         'numberposts' => -1,
         'fields' => 'ids',
         'post_type' => 'any',
     );
    
     $random_posts_ids = get_posts($args);
     $rnd_posts = array_rand($random_posts_ids, $count);

     foreach ( $rnd_posts as $post_index ) {
          $id = (int)$random_posts_ids[$post_index];
          $post = get_post($id);
          echo '<p><a href="'.get_permalink($post->ID).'">'.$post->post_title.'</a></p>';
     }

     $stop = microtime(true);
     echo '<p>Затраченное время: '.($stop - $start).'</p>';
}

Идея следующая. Сначала получаем ID всех постов. Обратите внимание, что если вы не укажите параметр 'fields' => 'ids', WordPress «вытянет» все посты целиком. Очень неприятная ошибка, в разы увеличивает потребление памяти.

На этом этапе выполняется запрос

SELECT   wp_posts.ID FROM wp_posts  WHERE 1=1  AND wp_posts.post_type IN ('post', 'page', ...) AND (wp_posts.post_status = 'publish')  ORDER BY wp_posts.post_date DESC ;

Затем выбираем случайным образом заданное количество постов, с помощью array_rand.

И в цикле получаем данные постов с помощью get_post. На этом этапе выполняются запросы вида

SELECT * FROM wp_posts WHERE ID = 23 LIMIT 1;

Т.е. если мы выводим три случайных поста, то функция show_random_posts_optimized будет выполнять 4 запроса для получения информации о постах, а show_random_posts — всего 1.

Но проверим время выполнения.

Первый эксперимент я провел на блоге с ~40 постами.
Результаты:

show_random_posts - 0.021620035171509 с
show_random_posts_optimized - 0.0076930522918701 с

В абсолютном выражении цифры небольшие, но разница заметная — в 2.8 раза.

Теперь посмотрим, что произойдет на реальном блоге с 1700+ постами.

Результаты:

show_random_posts - 0.49169611930847 с
show_random_posts_optimized - 0.039859056472778 с

Как видите, теперь первая функция работает в 12.3 раза медленнее.

Выводы сделать несложно.

Второй вариант работает быстрее и выдаёт тот же результат, но дело не в этом. Подобные проблемы могут быть практически не заметны во время разработки и привести к очень не приятным результатам при запуске на реальном ресурсе. Или ещё хуже, скорость будет падать постепенно, по мере добавления новых постов.

А найти проблемный запрос довольно просто. Установите BlackBox Debug Bar или аналогичный плагин и он вам выведет список всех выполненных запросов и затраченное на них время.

Что делать дальше, зависит от ситуации. В данном случае, оказалось достаточно переделать алгоритм выбора случайных постов. В других, возможно, придется использовать кеширование или, в крайнем случае, вообще отказаться от какой-то возможности. Главное, понять причину проблемы, тогда и выбранное решения будет обоснованным и эффективным… скорее всего 🙂

Happy coding 😉

Интересно почитать:

Скачайте договор безвозмездного пользования нежилым помещением от грамотных юристов

Заполним декларацию 3-НДФЛ при покупке квартиры в 2012 году

  • by1

    Можно еще больше оптимизировать, если не делать отдельный запрос под каждый пост, а использовать WHERE ID IN (1,2,3,…). Тогда останется только 2 запроса и скорость и для 2 и для 10 случайных постов будет одинакова.

    • Полностью согласен.
      Вообще есть есть еще несколько возможностей оптимизировать код. Дело в том, что get_permalink может работать довольно медленно (зависит от структуры URL). И при большом количестве записей в подобных виджетах может создать существенную нагрузку на сервер. Самое простое решение в таком случае — использовать кеширование, например, с помощью transients api

  • Руслан

    Интересный пост, я первым способом получал рендомные посты + также рандомно анкор цеплялся. Спасибо за работу над анализом скорости выполнения, буду второй способ юзать

  • Yurr

    Спасибо, отлично работает, но есть одно НО. А именно невозможно вывести только один случайный пост, т.е. если $count = 1 функция возвращает ошибку. Возможно как-то подправить?

  • Yurr

    А вот стандартный неоптимизированный вариант работает без ошибок при $count = 1.

    • А какую ошибку вы получили при использовании
      $count = 1 с оптимизированным вариантом?

      • yurr

        Ошибка такая:
        Warning: Invalid argument supplied for foreach() in /home/…/functions.php
        в строке где foreach.
        вылазит только при $count = 1

        • Ясно. Эту проблему можно решить так.

          if (!is_array($rnd_posts)) {
          $rnd_posts = array($rnd_posts);
          }

        • yurr

          Спасибо! Так работает )
          правда пытался еще прикрепленное к посту изображение получить, не получилось, но то другая песня.

        • Попробуйте так

  • Зачем делать 3 запроса в базу если можно одним? У Вас есть массив id, в php есть ф-ция «array_rand» (wordpress не знаю, так что пишу псевдокод):

    $rand_ids = array();
    foreach(array_rand($post_ids, 3) as $index) {
    $rand_ids[] = $post_ids[$index];
    }

    $random_rows = $db->query('SELECT * FROM wp_posts WHERE post_id IN(?)', $rand_ids);

    Еще есть вариант:
    $count = $db->query('SELECT COUNT(*) FROM wp_posts');

    $random_rows = $db->query('SELECT * FROM wp_posts LIMIT ? OFFSET ?', 3, mt_rand(0, $count));

    • Да, все правильно. Просто я хотел показать как заменить RAND в запросе. Дальнейшая оптимизация — отдельная тема.

      Дело в том, что в WP есть особенность, связанная с получением ссылки на пост. Движок поддерживает произвольную структуру URL, поэтому URL в базе не хранится, а сформировать его можно с помощью get_permalink($post->ID), а вызов этой функции может быть довольно ресурсоемким (зависит от структуры URL). Поэтому особенно выиграть на использовании оператора IN в запросе не получится (если количество постов небольшое). Но в целом вы правы, лучше использовать запрос с IN.

  • Дмитрий

    Подскажите,
    как вот здесь заменить 'orderby'=> 'rand' ?

    $categories = get_the_category($post->ID);
    if ($categories) {
    $category_ids = array();
    foreach($categories as $individual_category) $category_ids[] = $individual_category->term_id;
    $args=array(
    'category__in' => $category_ids,
    'post__not_in' => array($post->ID),
    'orderby'=> 'rand', //sort by random
    'showposts'=>5, // Number of related posts that will be shown.
    'caller_get_posts'=>1
    );
    // Rest is the same as the previous code
    $my_query = new wp_query($args);
    if( $my_query->have_posts() ) {
    echo 'Related Posts:';
    while ($my_query->have_posts()) {
    $my_query->the_post();
    ?>
    <a href="» rel=»bookmark» title=»»>
    <?php
    }
    echo '';
    }
    wp_reset_query();
    }

    • У вас будет 2 запроса.

      Первый запрос ваш. Но в его параметры нужно:
      1) добавить 'fields' => 'ids',
      2) убрать 'orderby'=> 'rand'
      3) изменить 'showposts'=>-1

      В результате получите массив с id нужных постов.

      Затем с помощью array_rand случайным образом сортируете этот массив и выбираете из него первые 5 id-шников.

      Вторым запросом получаете посты с этими id.

      • Дмитрий

        Вы не могли бы набросать этот код как это должно правильно выглядеть, мне не хватает знаний php,
        вот весь код, а то там часть обрезало pastebin.com/c5yL0iL3
        Заранее, спасибо.

        • Примерно так, но я не проверял 😉

        • Дмитрий

          Спасибо, но не работает, точнее работает, но не так как надо, он просто выводит 10 последних записей, игнорируя категории.

        • Боюсь, тут вам придется самостоятельно проверить запросы. Установите плагин для отладки WP и посмотрите какие запросы выполняются. Скорее всего, ошибка где-то в параметрах.

        • Дмитрий

          Спасибо, но не работает, он просто выводит 10 последних записей игнорируя категории, видимо что то надо ещё добавить или переставить.

    • Роман

      ID);
      if ($categories) {
      $category_ids = array();
      foreach($categories as $individual_category) $category_ids[] = $individual_category->term_id;
      $args=array(
      'category__in' => $category_ids,
      'post__not_in' => array($post->ID),
      'showposts'=>12,
      'orderby'=>rand,
      'caller_get_posts'=>1);
      $my_query = new wp_query($args);
      if( $my_query->have_posts() ) {
      echo »;
      while ($my_query->have_posts()) {
      $my_query->the_post();
      ?>
      <a href="» rel=»bookmark» title=»»><img src="ID); ?>» alt=»» />
      <?php
      }
      echo '';
      }
      wp_reset_query();
      }
      ?>

      тут работает с выводом картинок подскажите как правильно сделать код ?

      • Я правильно понимаю, что вы хотите заменить rand в запросе? Если да, то вы может использовать пример из этой статьи. Вместо

        'orderby'=>rand, (кстати, rand нужно взять в кавычки)

        у вас будет

        'post_in'=>$rnd_posts //массив сформированный способом, показанным в этой статье

        • Роман

          rand очень сильно грузит базу вот незнаю как от этого уйти
          по статье я незнаюкак правильно сформировать код

        • Роман

          А если ранд оставить как можно сократить запросы к базеветь он перечитывает всю базу чтобы выдать новости

        • Я не могу написать решение именно для вашего случая, т.к. не вижу остального вашего кода. Попробуйте запустить пример из статьи и посмотреть отладчиком как он выполняется, думаю, вы сможете адаптировать его для себя без особых изменений.

        • Роман

          Владимир а можно ваши контакты мнеочень нужно решить проблему оплата гарантированна

        • Отправьте, пожалуйста, мне письмо на почту vladimirsta@yandex.ru

  • Роман

    ID);
    if ($categories) {
    $category_ids = array();
    foreach($categories as $individual_category) $category_ids[] = $individual_category->term_id;
    $args=array(
    'category__in' => $category_ids,
    'post__not_in' => array($post->ID),
    'showposts'=>12,
    'orderby'=>rand,
    'caller_get_posts'=>1);
    $my_query = new wp_query($args);
    if( $my_query->have_posts() ) {
    echo »;
    while ($my_query->have_posts()) {
    $my_query->the_post();
    ?>
    <a href="» rel=»bookmark» title=»»><img src="ID); ?>» alt=»» />
    <?php
    }
    echo '';
    }
    wp_reset_query();
    }
    ?>
    вывод с картинками как правильно сделатькод по 12 запросов

  • VRS

    Благодарю. Переписала свой код вывода похожих страниц по Вашему примеру. Время генерации страницы снизилось с 2,9 до 0,21 сек.

  • Замените
    'post_type' => 'any',
    на
    'post_type' => 'post',

  • ааа

    А у меня с 300 000 постами первая функция быстрее срабатывает чем вторая оптимизированная, но я ещё выборку по таксономиям делаю

    • А как выглядит весь запрос целиком?