Auto-Count Product Filter Development for 1C-Bitrix

We've seen firsthand how a standard `catalog.smart.filter` in 1C-Bitrix turns catalog navigation with 50,000+ SKUs into a nightmare. Each checkbox click triggers a separate SQL query against `b_iblock_element` with a full recount — users wait 800–1500 ms per change. The interface appears frozen, and

Our competencies:

Frequently Asked Questions

We've seen firsthand how a standard catalog.smart.filter in 1C-Bitrix turns catalog navigation with 50,000+ SKUs into a nightmare. Each checkbox click triggers a separate SQL query against b_iblock_element with a full recount — users wait 800–1500 ms per change. The interface appears frozen, and they leave. The problem worsens when the catalog uses the catalog module with b_catalog_price joins and multiple properties via b_iblock_element_property. As a result, filtering becomes a bottleneck, and every 1C exchange resets the cache, causing further downtime.

The typical fix — enabling SHOW_PRODUCTS_COUNT in the component — only makes it worse: each request executes an expensive aggregate query. We take a different approach: move the counting into a denormalized table and build AJAX mechanics around it. This yields a 10–15x speedup, and significant server resource savings.

Why does the standard smart filter lag?

The bitrix:catalog.smart.filter component with SHOW_PRODUCTS_COUNT enabled runs an aggregating query like:

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 = ? 

With ten simultaneously selected properties, this becomes a chain of JOINs or subqueries that MySQL executes without using composite indexes. EXPLAIN shows type ALL or index instead of ref — a full table scan.

The second issue is cache invalidation. The standard cache tag bitrix:catalog is reset on any change to any infoblock element, including stock updates. Stores with frequent warehouse updates experience constant cold starts of the filter.

How we solve the auto-count problem?

We move the counting from SQL aggregation to a denormalized counter table and build AJAX mechanics around it. This proven approach gives a 10–15x speedup over the standard component.

Denormalization structure: We create a separate table catalog_filter_counts (or a HighLoad block if an admin UI is needed):

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) ); 

Counters are recalculated via a Bitrix agent (CAgent) on a schedule — every 5–15 minutes, or via the OnAfterIBlockElementUpdate event for critical changes.

AJAX filter component:

Instead of the standard smart.filter, we attach a custom component based on bitrix:main.ui.filter that sends a request to a router component when a checkbox changes:

// 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(); 

The response returns a JSON object; the frontend updates counters in the DOM without page reload.

What does denormalized counters give us?

The denormalized table itself is already a cache. But to reduce database load under high traffic, we add a second layer using Bitrix\Main\Data\Cache with a tag tied to the specific infoblock and section:

$cache = Cache::createInstance(); $cacheId = 'filter_counts_' . $ibId . '_' . $sectionId; if ($cache->initCache(3600, $cacheId, '/catalog/filter/')) { $counts = $cache->getVars(); } else { $cache->startDataCache(); $counts = /* database query */; $cache->endDataCache($counts); } 

Invalidation happens only when the assortment actually changes, not on every price or stock update.

Case study: an online building materials store (from our practice)

A client with an 80,000 SKU catalog, 12 filter properties (brand, size, color, material, etc.), and 1C integration via a d7 exchange. The standard smart.filter with SHOW_PRODUCTS_COUNT = Y gave an average response time of 2.3 seconds on category pages. After each 1C exchange (every 30 minutes), the cache was cleared, and the first 5 minutes the site worked under load without cache.

Implemented solutions:

  • Disabled the standard SHOW_PRODUCTS_COUNT
  • Implemented a denormalized counter table with recalculation via an agent every 10 minutes
  • Developed an AJAX component based on bitrix:catalog.section + custom bitrix:main.ui.filter
  • Added server-side counter cache with 600-second TTL, invalidated only on assortment changes (not prices or stock)

Results: filter response time dropped to 80–120 ms — 20x faster than the standard approach. MySQL load during peak hours halved. The 1C exchange stopped affecting filter performance. Server resource savings were significant, and conversion growth added another 30% to revenue.

Integration with trade catalog and multiple prices

A special case is catalogs with several price types (b_catalog_price) and price range filtering. The standard filter adds a JOIN to b_catalog_price with additional conditions on CATALOG_GROUP_ID. Here we implement a separate price range counter with quantization (10 buckets per range) — this allows building a price slider without running MIN/MAX aggregation on each request.

What's included in the work for creating a filter

  1. Audit of the current structure — analysis of infoblocks, properties, data volume, and current filter performance.
  2. Denormalization schema design — determining the counter table composition, indexes, and agent schedules.
  3. Component development — creating a custom AJAX filter based on bitrix:main.ui.filter with server-side caching.
  4. Load testing — verifying on a test copy of the production database using Apache Benchmark or wrk.
  5. Integration with existing 1C exchange — configuring counter invalidation upon new data arrival.
  6. Documentation and training — handing over source code, agent descriptions, instructions for adding new properties.

Timelines and stages

Developing a filter with auto-count includes auditing the current infoblock and properties structure, designing the denormalization schema, developing the recalculation agent and AJAX component, configuring caching, and load testing. We guarantee transparency at every stage. Estimated timelines depend on catalog size and complexity; contact us for a precise estimate.

Catalog Scale Filter Complexity Development Time
Up to 20,000 SKU Up to 8 properties 3–5 days
20,000–100,000 SKU Up to 15 properties 5–10 days
100,000+ SKU / HighLoad Any 10–20 days

Load testing is performed with Apache Benchmark or wrk on a test copy of the production database — without it, results are unpredictable.

Comparison of approaches: standard vs. ours

Characteristic Standard smart.filter Our solution
Response time at 80,000 SKU 2.3 s 80–120 ms
Dependence on 1C exchange Full cache reset Works independently
Indexes used None (full scan) Composite indexes (ref)
Customization possibilities Limited Full

Over 5 years of work, we have completed more than 50 projects on optimizing filtering in Bitrix. If your catalog suffers from lag, get a free consultation and project assessment. Order development, and we will find a solution tailored to your data volume.

According to the official 1C-Bitrix documentation: using custom indexes in property tables reduces query execution time by an order of magnitude.

More about indexing in databases can be found in the article Index (database).