Сегодня займёмся оптимизацией сниппета pdoPage. Опытные разработчики знают, что это самый медленный сниппет из всего набора сниппетов библиотеки pdoTools. И это особенно становится заметно на больших сайтах. Встроенные возможности кэширования в некоторых случаях могут помочь. Но если вы выводите список статей с дополнительными характеристиками (количество просмотров, комментариев и т.п.), то вам придётся отказаться от кэширования.

Немного теории

Сниппет pdoPage реализует пагинацию для списка элементов. Он должен разбить список на страницы согласно указанному количеству элементов на странице. Т.е. он должен знать сколько всего элементов в списке и вывести только часть.

Общее количество подсчитывается с помощью встроенной функции MySql. В оператор SELECT добавляется директива SQL_CALC_FOUND_ROWS, а после выполнения запроса нужно выполнить ещё один запрос для получения общего количества.

SELECT FOUND_ROWS();

Таким образом, MySql выполняет 2 запроса — основной с ограничением limit и второй для получения общего количества строк без лимита. Но общее количество MySql определяет во время первого запроса.

Несколько лет назад в pdoTools директива SQL_CALC_FOUND_ROWS использовалась для каждого запроса во всех сниппетах. Даже тех, где общее количество считать не нужно. Соответственно, такие запросы отрабатывались дольше. Затем автор оптимизировал класс pdoFetch и добавил возможность управления этим функционалом, а также отключил подсчёт количества по-умолчанию для всех сниппетов кроме pdoPage.

Собственно, вы можете спросить, а в чём проблема? Дело в том, что в некоторых случаях этот встроенный механизм MySql работает гораздо хуже, чем 2 обычных запроса — один с лимитом, другой с COUNT(*). Вот статья, где подробно объясняется как, что и почему. Да, в ней рассматривается простая таблица, правильно оптимизированная. Это больше подойдёт для индивидуального проекта, разрабатываемого на фреймворке. Но тенденция развития MySql поддерживает такой подход. В восьмой версии MySql этот функционал объявлен устаревшим. Т.е. вместо таких запросов

SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name WHERE id > 100 LIMIT 10;
SELECT FOUND_ROWS();

советуют использовать такие

SELECT * FROM tbl_name WHERE id > 100 LIMIT 10;
SELECT COUNT(*) FROM tbl_name WHERE id > 100;

Из своего опыта могу сказать, что для MODX в большинстве случаев использование директивы SQL_CALC_FOUND_ROWS оптимальнее запроса с COUNT(*). Как правило запросы гораздо сложнее, чем в примере выше с одной таблицей и индексом. А запрос с COUNT(*) выполняется практически столько же, сколько и запрос с лимитом. В то время как вариант с директивой даёт оверхеда всего на 30-40%. Но это не значит, что тут нет предмета для оптимизации. Мы можем ускорить выполнение запроса на эти самые 30-40%. Для этого мы будем кэшировать общее количество записей. А значит мы будем обходится только одним запросом вместо двух.

Библиотека pdoTools переведена автором в статус архивной и развиваться не будет. Поэтому задачу по оптимизации придётся брать на себя. К счастью, автор заложил в библиотеку возможность расширения (что мы уже использовали в прошлой статье). Т.е. опять придётся рефакторить класс pdoFetch. Можно взять оптимизированный класс из прошлой статьи или создать свой аналогичный и добавить в него изменённый метод setTotal.

public function setTotal()
{
    if ($this->config['setTotal'] && !in_array($this->config['return'], array('sql', 'ids'))) {
        $total = $this->getCache($this->config);  // Запрашиваем из кэша.
        if (empty($total)) {
            $time = microtime(true);

            $q = $this->modx->prepare("SELECT FOUND_ROWS();");

            $tstart = microtime(true);
            $q->execute();
            $this->modx->queryTime += microtime(true) - $tstart;
            $this->modx->executedQueries++;

            $total = $q->fetch(PDO::FETCH_COLUMN);
        }
        $this->modx->setPlaceholder($this->config['totalVar'], $total);
        $this->setCache((int)$total, $this->config); // Сохраняем в кэш.
        $this->addTime("Total rows: <b>$total</b>", microtime(true) - $time);
    }
}

Теперь остаётся только вызвать сниппет с параметром cacheKey, в котором указать имя файла для кэша. Я советую также указывать id страницы, на которой вызывается сниппет, если таких страниц несколько.

{'!pdoPage' | snippet : [
    'cacheKey' => 'pdoTools/pdoPage/' ~ $_modx->resource.id ~'_pageTotal',
    ...
]}

Если заглянуть в кэш, то в папке core/cache/default/pdoTools/pdoPage/ увидим файл кэша с количеством для страницы, на которой вызывается сниппет.

Но так как количество может меняться, необходимо решить задачу инвалидации кэша при добавлении или удалении ресурса (или другого объекта системы, для которого используется сниппет). Как обычно делается это в плагине.

// Отмечаем события "OnDocFormSave" и "OnDocFormDelete"
if ($modx->event->name === 'OnDocFormSave' && $mode === 'new' || $modx->event->name === 'OnDocFormDelete'} {
    array_map('unlink', glob($modx->getOption(xPDO::OPT_CACHE_PATH) . 'default/pdoTools/pdoPage/*.cache.php'));
}

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

После удаления кэша на первый запрос страницы придётся дополнительная нагрузка (как и раньше), но следующие запросы уже будут работать с кэшем.

Конечно, всё это имеет значение для больших сайтов, где выполнение pdoPage может занимать секунды. Для небольших сайтов с невысокой посещаемость это не так актуально.

В следующей статье мы разберём некоторые моменты, связанные с безопасностью pdoTools.

0   559

Комментарии ()

  1. shock 28 июля 2021, 11:27 # +1
    Спасибо! Актуальная тема.
    1. Sentinel 29 июля 2021, 22:35 # 0
      А для mFilter2 это будет работать? он ведь тоже на основе pdoPage
      1. Сергей Шлоков 30 июля 2021, 06:52 # 0
        Чтобы ответить на этот вопрос нужно купить данное дополнение и проверить. Для mFilter нужно скорее кэширование результатов. Но эта задача нетривиальная.

      Вы должны авторизоваться, чтобы оставлять комментарии.

      Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.