Разработка фильтрации с автоподсчётом количества товаров 1С-Битрикс
Стандартный компонент catalog.smart.filter в 1С-Битрикс считает количество товаров через отдельный SQL-запрос к b_iblock_element с полным пересчётом при каждом изменении параметров фильтра. При каталоге в 50 000+ SKU это даёт задержку 800–1500 мс на фильтрацию — пользователь видит «зависший» интерфейс. Задача усугубляется, если в каталоге используется торговый каталог (catalog модуль) с привязкой к b_catalog_price и множественными свойствами через b_iblock_element_property.
Как работает «умный фильтр» и где он ломается
Компонент bitrix:catalog.smart.filter при включённом параметре SHOW_PRODUCTS_COUNT выполняет агрегирующий запрос вида:
SELECT COUNT(DISTINCT BE.ID)
FROM b_iblock_element BE
INNER JOIN b_iblock_element_property BEP ON BE.ID = BEP.IBLOCK_ELEMENT_ID
WHERE BE.IBLOCK_ID = ? AND BE.ACTIVE = 'Y' AND BEP.IBLOCK_PROPERTY_ID = ? AND BEP.VALUE = ?
При десяти одновременно выбранных свойствах фильтра это превращается в цепочку JOIN-ов или подзапросов, которые MySQL выполняет без использования составных индексов. EXPLAIN показывает тип ALL или index вместо ref — полный перебор таблицы.
Вторая проблема — инвалидация кеша. Стандартный тег кеша bitrix:catalog сбрасывается при любом изменении любого элемента инфоблока, включая изменение остатков. Магазины с частыми обновлениями склада получают постоянный холодный старт фильтра.
Архитектура решения с автоподсчётом
Мы переносим подсчёт из SQL-агрегации в денормализованную таблицу счётчиков и строим AJAX-механику вокруг неё.
Структура денормализации:
Создаётся отдельная таблица catalog_filter_counts (или HighLoad-блок, если требуется UI в админке):
CREATE TABLE catalog_filter_counts (
iblock_id INT NOT NULL,
prop_id INT NOT NULL,
prop_value VARCHAR(255) NOT NULL,
section_id INT NOT NULL DEFAULT 0,
cnt INT NOT NULL DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_filter (iblock_id, section_id, prop_id, prop_value)
);
Счётчики пересчитываются через агент Битрикс (CAgent) по расписанию — раз в 5–15 минут, или через обработчик события OnAfterIBlockElementUpdate для критичных изменений.
AJAX-компонент фильтра:
Вместо стандартного smart.filter подключается кастомный компонент на основе bitrix:main.ui.filter, который при изменении чекбокса отправляет запрос на компонент-роутер:
// component.php
$filterState = $this->request->getPost('filter_state');
$counts = CatalogFilterCountsTable::getList([
'filter' => [
'=IBLOCK_ID' => $ibId,
'=SECTION_ID' => $sectionId,
'@PROP_VALUE' => $filterState['values'],
],
'select' => ['PROP_ID', 'PROP_VALUE', 'CNT'],
])->fetchAll();
Ответ возвращается JSON-объектом, фронтенд обновляет счётчики в DOM без перезагрузки страницы.
Кеширование подсчётов
Денормализованная таблица сама по себе — уже кеш. Но для снижения нагрузки на БД при высоком трафике добавляется второй слой через Bitrix\Main\Data\Cache с тегом, привязанным к конкретному инфоблоку и разделу:
$cache = Cache::createInstance();
$cacheId = 'filter_counts_' . $ibId . '_' . $sectionId;
if ($cache->initCache(3600, $cacheId, '/catalog/filter/')) {
$counts = $cache->getVars();
} else {
$cache->startDataCache();
$counts = /* запрос к БД */;
$cache->endDataCache($counts);
}
Инвалидация происходит только при реальном изменении ассортимента, а не при каждом обновлении цены или остатка.
Кейс: интернет-магазин строительных материалов
Клиент — магазин с каталогом 80 000 SKU, 12 свойствами в фильтре (бренд, размер, цвет, материал и т.д.), интеграцией с 1С через d7 обменник. Стандартный smart.filter с SHOW_PRODUCTS_COUNT = Y давал среднее время ответа 2,3 с на страницах категорий. После каждого обмена с 1С (каждые 30 минут) кеш сбрасывался, и первые 5 минут сайт работал под нагрузкой без кеша.
Что сделали:
- Выключили стандартный
SHOW_PRODUCTS_COUNT - Реализовали денормализованную таблицу счётчиков с пересчётом через агент раз в 10 минут
- Разработали AJAX-компонент на основе
bitrix:catalog.section+ кастомныйbitrix:main.ui.filter - Добавили серверный кеш счётчиков с TTL 600 с, инвалидируемый только при смене ассортимента (не цен и остатков)
Результат: время ответа фильтра снизилось до 80–120 мс, нагрузка на MySQL в часы пик упала вдвое. Обмен с 1С перестал влиять на производительность фильтра.
Интеграция с торговым каталогом и множественными ценами
Отдельный случай — каталоги с несколькими типами цен (b_catalog_price) и фильтрацией по диапазону цен. Стандартный фильтр при этом добавляет JOIN к b_catalog_price с дополнительными условиями по CATALOG_GROUP_ID. Здесь мы реализуем отдельный счётчик диапазонов цен с квантованием (10 бакетов по диапазону) — это позволяет строить ползунок цены без выполнения MIN/MAX агрегации при каждом запросе.
Сроки и этапы
Разработка фильтра с автоподсчётом включает аудит текущей структуры инфоблока и свойств, проектирование схемы денормализации, разработку агента пересчёта и AJAX-компонента, настройку кеширования и нагрузочное тестирование.
| Масштаб каталога | Сложность фильтра | Срок разработки |
|---|---|---|
| до 20 000 SKU | до 8 свойств | 3–5 дней |
| 20 000–100 000 SKU | до 15 свойств | 5–10 дней |
| 100 000+ SKU / HighLoad | любая | 10–20 дней |
Нагрузочное тестирование проводится инструментами Apache Benchmark или wrk на тестовой копии продакшн-базы — без него результат непредсказуем.







