Skip to content

Perf: Compute the category products once for all facet counts - #1347

Open
LameuleFR wants to merge 7 commits into
PrestaShop:devfrom
LameuleFR:perf/faceted-search-2
Open

LameuleFR wants to merge 7 commits into
PrestaShop:devfrom
LameuleFR:perf/faceted-search-2

Conversation

@LameuleFR

Copy link
Copy Markdown
Contributor
Questions Answers
Description? Follow-up of #1346, only the last commit is new. Uncached category pages got the same number of queries, but the facet queries no longer each recompute the category population: it is read once and reused.. Same results, 30 to 75 % faster on large catalogs.
Type? improvement
BC breaks? no
Deprecations? no
Fixed ticket?
How to test? See below.
Sponsor company

What changes

  • Every facet query used to embed the whole category population as a subquery. The counting queries now read its product IDs once and reuse them (WHERE id_product IN (…)). Above 250k products, or when the population carries more than product columns, nothing changes.
  • The fields the listing adds to the population (combination, sort fields) are left out of the facet queries, which don't use them.
  • The three availability counts are done in one query (new countOperations() in the adapter).
  • Facets on product-level values (brand, condition, category…) no longer repeat the stock and attribute joins.

Benchmark

Official PrestaShop 9.2 Docker image, MariaDB 11.4, 100k products with 3 combinations each, facet cache emptied before each request.

Page #1346 This PR Gain
Root category (100k products) 10.1 s 4.5 s −55 %
Root, "In stock" 27.9 s 8.2 s −70 %
Root, one color 18.0 s 4.7 s −74 %
Category (6.5k products) 957 ms 703 ms −27 %
Same, 2 features 1 206 ms 611 ms −49 %
Same, color + "In stock" 1 984 ms 948 ms −52 %
Same, sorted by price 1 042 ms 594 ms −43 %
Small category (550 products) 483 ms 479 ms −1 %

Pages with cached facets run exactly the same queries as before. Rendered pages and facet counts were compared with #1346 on 30 URLs: identical.

How to test

Empty the facet cache, then browse categories with various filters and sort orders: products and counts must match the current behaviour, and with an attribute filter the listing must still show the matching combination.

With only nleft >= and nright <= on the browsed category, the range read on
the category index covers every following category, so MySQL computed the
population from all the shop products and filtered the subtree last.
Bounding nleft by the category nright is redundant in the nested set but
makes the index range match the subtree.
The stock_available join went through product_attribute_combination, which
repeated each combination stock row once per attribute. Joining it on
product_attribute selects the same rows without the duplicates.

The quantity summed to order the out of stock products last no longer counts
a combination once per attribute: it is now the actual stock of the product.
Each attribute group and each feature facet ran its own count query, and
each one recomputed the population of the category. The facets without
selected value, and the AND features, share the current filters: their
values are now counted in one query per facet type. A facet with a selected
value keeps its own query, as it ignores its own selection.

The values of the displayed features are also loaded in one query.
The "Not available" count filters on quantity and out_of_stock in the same
operation, and each item got its own stock_available join on the same row.
The stock fields are now flagged to share the join of their operation: the
unique key of stock_available guarantees a single row. Operations on other
tables keep one join per item, needed when they require several values.
@github-project-automation github-project-automation Bot moved this to Ready for review in PR Dashboard Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready for review

Development

Successfully merging this pull request may close these issues.

1 participant