Производительность и частые ошибки

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

Приемы оптимизации ускоряют выборки по товарам, ценам и остаткам. Правила работы с API каталога предотвращают частые ошибки в товарных сценариях.

Базовые рекомендации по производительности инфоблоков работают и для Торгового каталога. Сначала изучите статью Производительность и частые ошибки инфоблоков, затем переходите к сценариям для каталога.

Оптимизировать производительность торговых каталогов

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

Ограничить выборку полей

Запрашивайте только те поля, которые использует шаблон или бизнес-логика. Чем меньше данных, тем быстрее работает страница.

Пример выборки только нужных полей товара:

\Bitrix\Catalog\ProductTable::getList([
            'select' => ['ID', 'QUANTITY', 'TYPE'],
            'filter' => ['=ID' => $productIds],
        ]);
        

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

Исключить N+1 запросы к ценам и остаткам

Проблема: код получает список товаров, а затем в цикле отдельно читает цену или остаток для каждого. Для каталога в 100 товаров это 101 запрос. Для каталога в 1000 товаров — 1001 запрос.

Неверный подход:

foreach ($productIds as $productId)
        {
            $price = \Bitrix\Catalog\PriceTable::getList([
                'select' => ['PRICE', 'CURRENCY'],
                'filter' => ['=PRODUCT_ID' => $productId],
                'limit' => 1,
            ])->fetch();
        }
        

Корректный подход: получите цены одним запросом по массиву идентификаторов. Перед этим получите тип цены, который участвует в выборке. Например, базовый тип цены. В примере:

  • $productIds — массив ID товаров текущей страницы или текущей выборки,

  • $basePriceType — результат получения типа цены, из которого берут поле ID.

// Получите ID нужного типа цены заранее, например, базового
        $basePriceTypeId = (int)$basePriceType['ID'];
        
        $priceMap = [];
        
        $priceIterator = \Bitrix\Catalog\PriceTable::getList([
            'select' => ['PRODUCT_ID', 'PRICE', 'CURRENCY'],
            'filter' => [
                '=CATALOG_GROUP_ID' => $basePriceTypeId,
                '=PRODUCT_ID' => $productIds,
            ],
        ]);
        
        while ($price = $priceIterator->fetch())
        {
            $priceMap[(int)$price['PRODUCT_ID']] = $price;
        }
        

Рассчитать итоговую цену

Для расчета итоговой цены с учетом скидок, прав групп и правил каталога используйте GetOptimalPrice(). Прямое чтение из PriceTable оставляйте для служебных сценариев, где не нужен расчет скидок.

В примере:

  • $productId — ID товара, для которого рассчитывают цену,

  • $quantity — количество товара в расчете,

  • $userGroups — массив групп текущего пользователя,

  • $siteId — идентификатор сайта в многосайтовом проекте.

$priceData = \CCatalogProduct::GetOptimalPrice(
            $productId,
            $quantity,
            $userGroups, // Группы текущего пользователя
            'N',
            [],          // Явный priceList, если нужно переопределить выборку
            $siteId,     // В многосайтовом проекте передавайте нужный SITE_ID явно
            false
        );
        

Организовать работу с торговыми предложениями

Метод CCatalogSKU::getOffersList() получает торговые предложения для списка товаров. Без ограничений этот вызов быстро расходует память и время.

Работайте по правилам:

  • передавайте в метод только товары текущей страницы, а не всего раздела,

  • ограничивайте состав данных торговых предложений и оставляйте только нужные поля,

  • если данных много, разбивайте productIds на части.

$offers = \CCatalogSKU::getOffersList(
            $productIds,            // массив ID товаров с текущей страницы
            0,
            ['ACTIVE' => 'Y'],      // Фильтр: только активные предложения
            ['ID', 'NAME']          // Минимальный набор полей
        );
        

Применить точные фильтры

Фильтры без точных операторов ухудшают план запроса. Для точного совпадения указывайте =.

\Bitrix\Catalog\ProductTable::getList([
            'select' => ['ID'],
            'filter' => [
                '=ID' => $productIds,
                '=AVAILABLE' => 'Y',
            ],
        ]);
        

Установить лимиты и пагинацию

Если на странице нет постраничной навигации, ограничивайте количество записей. Это снижает время запроса и потребление памяти.

\Bitrix\Catalog\ProductTable::getList([
            'select' => ['ID', 'QUANTITY'],
            'filter' => ['=AVAILABLE' => 'Y'],
            'limit' => 50,
            'order' => ['ID' => 'DESC'],
        ]);
        

Для классического API действует тот же принцип. Если общее количество элементов не нужно, используйте nTopCount вместо nPageSize, чтобы убрать лишний COUNT(*).

Закешировать стабильные выборки

Если данные каталога обновляются нечасто, их стоит кешировать. Подход полезен для блоков «популярные товары», «сопутствующие товары» и других повторных выборок.

Пример кеширования в ORM:

\Bitrix\Catalog\ProductTable::getList([
            'select' => ['ID', 'QUANTITY'],
            'filter' => ['=AVAILABLE' => 'Y'],
            'limit' => 100,
            'cache' => [
                'ttl' => 3600,
                'cache_joins' => true,
            ],
        ]);
        

Подбирайте ttl по частоте обновления данных. Если остатки меняются часто, уменьшайте ttl.

Проанализировать SQL-запросы

Откройте страницу Настройки > Производительность > Запросы SQL и проверьте:

  • запросы, которые повторяются в рамках одного запроса к серверу,

  • тяжелые сортировки и большие выборки без лимита,

  • запросы, которые не используют индексы.

Если запрос к товарам регулярно работает медленно, сократите select и уберите лишние JOIN-запросы. Затем проверьте, нужен ли странице полный набор данных.

Проверить результат оптимизации по метрикам

После каждой оптимизации фиксируйте результат по метрикам до и после изменения. Без измерений трудно понять, какое изменение ускорило страницу.

Проверяйте:

  • время генерации страницы каталога и карточки товара,

  • число SQL-запросов на один хит,

  • самые медленные SQL-запросы и их длительность,

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

Если метрики не улучшаются, вернитесь к составу выборки и к стратегии кеширования.

Отключить подсчет элементов разделов без необходимости

Для больших каталогов параметр COUNT_ELEMENTS = Y в списке разделов часто замедляет работу. Компонент считает элементы по разделам при построении страницы.

Если счетчики в текущем интерфейсе не нужны, отключите их. Если счетчики нужны, считайте их заранее и храните в отдельном поле раздела. Не пересчитывайте значения на каждом хите.

Выполнить массовые операции частями и в фоне

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

Решение:

  • разделите обработку на части фиксированного размера,

  • сохраните прогресс между частями, чтобы операция продолжилась после сбоя,

  • перенесите тяжелые операции в агент, cron-задачу или отдельный фоновый процесс,

  • предусмотрите безопасный повторный запуск: данные не должны дублироваться или повреждаться.

Применить классы пространства Model для записи

Для добавления и обновления товарных параметров и цен используйте \Bitrix\Catalog\Model\Product и \Bitrix\Catalog\Model\Price. Такой подход упрощает архитектуру и поддержку проекта. Классы из пространства Model проверяют данные и корректно обрабатывают события модуля.

Для операций записи не используйте ProductTable::update() и PriceTable::update().

Пример обновления цены:

$result = \Bitrix\Catalog\Model\Price::update($priceId, [
            'PRICE' => 12500,
            'CURRENCY' => 'RUB',
        ]);
        
        if (!$result->isSuccess())
        {
            throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
        }
        

Выполнять отложенный пересчет торговых предложений при большом объеме

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

Выполните отложенный пересчет. Он снижает пиковую нагрузку и стабилизирует время ответа в карточке товара и при массовом обновлении каталога.

  1. В онлайн-сценарии сохраните только критичные изменения.

  2. Идентификаторы товаров для пересчета запишите в очередь.

  3. Выполните пересчет торговых предложений агентом, cron-задачей или фоновым процессом.

Применить метод recountPricesFromBase для пересчета цен

Когда базовая цена меняется, зависимые типы цен нужно пересчитывать через \Bitrix\Catalog\Model\Price::recountPricesFromBase(). Метод учитывает правила перерасчета от базового типа и снижает риск рассинхронизации цен в каталоге.

Пример пересчета:

$result = \Bitrix\Catalog\Model\Price::recountPricesFromBase([
            'PRODUCT_ID' => $productId,
        ]);
        
        if (!$result->isSuccess())
        {
            throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
        }
        

Ограничить выборку остатков по складам

Запросы по остаткам на множестве складов быстро разрастаются. Особенно это заметно на страницах каталога со списком товаров, где выводятся десятки позиций одновременно.

  1. Не запрашивайте остатки по всем складам, если странице нужен только признак наличия.

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

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

Настроить сброс кеша при изменении каталога

Кеш ускоряет чтение, но при неверном сбросе показывает устаревшие цены и остатки.

Используйте три правила.

  1. После изменения товара, цены или остатков сбрасывайте только связанный кеш, а не весь кеш сайта.

  2. Для динамичных данных задавайте короткий ttl.

  3. Для данных с жестким требованием к актуальности используйте чтение без кеша.

Если сброс кеша настроен неверно, оптимизация теряет смысл: страница либо работает со старыми данными, либо постоянно пересчитывает результат.

Применить новые ключи каталога

Для фильтров и сортировки в CIBlockElement::GetList используйте актуальные ключи каталога: =AVAILABLE, =TYPE, >PRICE, <=STORE_AMOUNT и варианты с конкретным типом цены, например, >PRICE_1 или складом <=STORE_AMOUNT_17.

Старые ключи вида CATALOG_* в фильтрах и выборке часто приводят к тяжелым запросам с JOIN и росту времени выполнения.

Пример фильтра по доступным простым товарам:

$iterator = \CIBlockElement::GetList(
            [],
            [
                'IBLOCK_ID' => $catalogIblockId,
                '=AVAILABLE' => 'Y',
                '=TYPE' => 1,
            ],
            false,
            false,
            ['ID', 'NAME', 'IBLOCK_ID']
        );
        

Частые ошибки при работе с каталогами

Неправильное обращение к API каталога вызывает лишние запросы к базе, ошибки в расчете цен и сбои при массовом обновлении.

Модуль catalog или iblock не подключен

Если модули не подключены, вызовы API каталога завершаются ошибками во время выполнения.

Подключайте модули iblock и catalog до обращения к таблицам каталога:

if (!\Bitrix\Main\Loader::includeModule('iblock'))
        {
            throw new \Bitrix\Main\SystemException('Модуль iblock не установлен.');
        }
        
        if (!\Bitrix\Main\Loader::includeModule('catalog'))
        {
            throw new \Bitrix\Main\SystemException('Модуль catalog не установлен.');
        }
        

Если сценарий рассчитывает заказы, дополнительно подключайте модуль sale.

Чтение данных в цикле по товарам

Если получаете цены, остатки или свойства внутри цикла по товарам, страница будет работать медленно на реальных данных. Вместо этого соберите все ID товаров в массив, получите связанные данные одним вызовом getList() и постройте ассоциативный массив PRODUCT_ID => данные. Используйте его при выводе элементов.

Товар не активирован после создания элемента

Если элемент инфоблока создан, но не отображается как товар, ему не хватает параметров каталога. Сразу после создания элемента зарегистрируйте его в каталоге. Укажите тип товара и остаток, чтобы система начала обрабатывать его по правилам каталога:

$result = \Bitrix\Catalog\Model\Product::add([
            'ID' => $productId,
            'TYPE' => \Bitrix\Catalog\ProductTable::TYPE_PRODUCT,
            'QUANTITY' => 100,
        ]);
        
        if (!$result->isSuccess())
        {
            throw new \RuntimeException(implode('; ', $result->getErrorMessages()));
        }
        

Использование устаревших методов записи

Когда обновляете или создаете товары, не используйте методы CCatalogProduct::Add(), Update() и Delete(). Они устарели и могут работать некорректно в новых версиях платформы. Применяйте классы из пространства Model: \Bitrix\Catalog\Model\Product и \Bitrix\Catalog\Model\Price. Методы этих классов автоматически проверяют данные, корректно обрабатывают события модуля и упрощают поддержку кода.

Избыточный состав кеша компонента

Если сохраняете крупные массивы в $arResult, размер кеша быстро растет. Это замедляет чтение кеш-файлов и увеличивает нагрузку на диск.

Исправить проблему можно за три шага.

  1. Оставьте в $arResult только данные, которые используются в шаблоне.

  2. Задайте явные ключи через SetResultCacheKeys() для значений, которые нужны в component_epilog.php.

  3. Проверьте размер кеш-файла компонента после правки.

Отсутствие ограничений при массовой обработке

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

Разделяйте обработку на части фиксированного размера с помощью limit и offset. Между итерациями сохраняйте прогресс, чтобы при сбоях скрипт мог продолжить работу с нужного места.

Пример обработки каталога частями:

$limit = 500;
        $offset = 0;
        
        do
        {
            $rows = \Bitrix\Catalog\ProductTable::getList([
                'select' => ['ID', 'QUANTITY', 'TYPE'],
                'order' => ['ID' => 'ASC'],
                'limit' => $limit,
                'offset' => $offset,
            ])->fetchAll();
        
            foreach ($rows as $row)
            {
                // Обработать товар
            }
        
            $offset += $limit;
        }
        while (!empty($rows));
        

Неверная передача параметров в GetOptimalPriceList

Когда в метод CCatalogProduct::GetOptimalPriceList() передают массив идентификаторов товаров, он возвращает некорректный результат. Метод ожидает структуру товаров с данными по количествам и контексту расчета.

Если подготовленной структуры для GetOptimalPriceList() нет, получите цены напрямую через PriceTable. В точках расчета итоговой цены вызывайте GetOptimalPrice().

Страница пагинации за пределами диапазона возвращает 200 OK

Если пользователь переходит на несуществующую страницу пагинации, компонент отдает статус 200 OK и пустой список. Поисковые системы индексируют такие страницы, что вредит сайту.

Сравнивайте номер текущей страницы с NavPageCount. Если номер превышает диапазон, принудительно возвращайте статус 404 Not Found и подключайте стандартную страницу ошибки:

$currentPage = (int)($_GET['PAGEN_1'] ?? 1);
        $pageCount = (int)$arResult['NAV_RESULT']->NavPageCount;
        
        if ($currentPage > $pageCount)
        {
            \CHTTP::SetStatus('404 Not Found');
            require $_SERVER['DOCUMENT_ROOT'] . '/404.php';
            exit;
        }
        

Отсутствие проверки типа каталога перед работой с торговыми предложениями

Если загружаете торговые предложения без проверки режима инфоблока, код выполнит лишние запросы или попадет в неверную ветку исполнения. В смешанных проектах проверка типа предотвращает такие ошибки. Сначала получите тип каталога через \CCatalogSKU::getInfoByIBlock() и выполните логику для торговых предложений только при TYPE_OFFERS или TYPE_FULL:

$catalogInfo = \CCatalogSKU::getInfoByIBlock($iblockId);
        
        if (!$catalogInfo)
        {
            return;
        }
        
        if (
            $catalogInfo['CATALOG_TYPE'] === \CCatalogSKU::TYPE_OFFERS
            || $catalogInfo['CATALOG_TYPE'] === \CCatalogSKU::TYPE_FULL
        )
        {
            // Каталог с торговыми предложениями: можно выполнять логику торговых предложений.
        }
        
Предыдущая
Следующая