Skip to content

Batch GraphQL stock_status and stop reloading configurables for only_x_left_in_stock (#40709) - #41216

Open
lbajsarowicz wants to merge 1 commit into
magento:2.4-developfrom
lbajsarowicz:fix/40709-graphql-stock-status-n-plus-one
Open

Batch GraphQL stock_status and stop reloading configurables for only_x_left_in_stock (#40709)#41216
lbajsarowicz wants to merge 1 commit into
magento:2.4-developfrom
lbajsarowicz:fix/40709-graphql-stock-status-n-plus-one

Conversation

@lbajsarowicz

Copy link
Copy Markdown
Contributor

Description (*)

Two GraphQL resolvers in CatalogInventoryGraphQl issue per-product queries for every item of a products response.

  • stock_status is a plain per-field resolver; StockStatusProvider::resolve() calls StockStatusRepository::get() for each product, which goes straight to the database and never consults StockRegistryStorage. 24 items, 24 queries.
  • only_x_left_in_stock reloads every configurable item through ProductRepositoryInterface::get($product->getSku()) before it checks the "Only X left Threshold" setting. That reload was added for configurable cart items, where Configurable::getSku() returns the selected child's SKU through the simple_product custom option. In a products query there is no such option, so the resolver reloads the product it already holds, at roughly 13 queries per configurable, and then returns null because the threshold is 0 by default.

The fix:

  • A new Magento\CatalogInventoryGraphQl\Model\Resolver\StockStatus implements BatchResolverInterface and is now the @resolver of ProductInterface.stock_status. It preloads the statuses of all plain product requests in one query through the existing StockRegistryPreloader and maps them to IN_STOCK / OUT_OF_STOCK the same way as before (a product without a stock status row stays OUT_OF_STOCK). Requests carrying a cart_item are delegated unchanged to StockStatusProvider::resolve(), so the bundle and configured-variant logic for cart items is untouched. StockStatusProvider itself is not modified.
  • OnlyXLeftInStockResolver reads the threshold first and returns null before touching the registry or the repository. For a configurable it takes the selected child from the simple_product custom option when present, otherwise it uses the product it was given; the repository is only consulted if the reported SKU differs from the entity SKU.

Performance impact

Measured on a vanilla 2.4-develop install (performance toolkit small profile: 1200 products, legacy CatalogInventory, MSI disabled), request:

{ products(filter:{}, pageSize: 24) { total_count items { sku stock_status only_x_left_in_stock } } }

24 items, 16 of them configurable. Wall time is the median of 7 fresh requests without the profiler; call counts and memory are from one PHP SPX run of the same request. The response body is byte-identical before and after.

Metric Before After Delta
SQL queries 312 16 -95%
cataloginventory_stock_status queries 40 2
cataloginventory_stock_item queries 16 0
Wall time (median of 7) 168.9 ms 81.2 ms -52%
PHP function calls (SPX) 585,735 128,250 -78%
Peak memory (SPX) 16.8 MB 13.0 MB -22%

Fixed Issues (if relevant)

  1. Fixes ⚡ Performance: Stock registry N+1 on every product load in CatalogInventory module #40709

Manual testing scenarios (*)

  1. Enable the DB query log (bin/magento dev:query-log:enable) and send the GraphQL query above against a catalog with a few configurable products.
  2. Count cataloginventory_stock_status queries in var/debug/db.log: one per response instead of one per item, and no product loads by SKU.
  3. Set Stores > Configuration > Catalog > Inventory > Stock Options > Only X left Threshold to a value above the qty of a simple product and query only_x_left_in_stock for it: the remaining qty is returned as before.
  4. Add a configurable product to a cart and query cart { items { product { stock_status only_x_left_in_stock } } }: values match the selected variant, as before.

Questions or comments

Gates run locally: unit (16 tests, 98 assertions), PHPCS, PHPStan and the Static Tests LiveCodeTest on the changed files are clean. The module has no integration suite. The new unit test for OnlyXLeftInStockResolver fails without the fix and passes with it.

The Semantic Version Checker will report the new public class and the changed @resolver class on ProductInterface.stock_status; the GraphQL schema surface is unchanged. Third-party plugins on StockStatusProvider::resolve() keep applying to cart items but no longer to plain product items, since those are resolved in the batch.

Contribution checklist (*)

  • Pull request has a meaningful description of its purpose
  • All commits are accompanied by meaningful commit messages
  • All new or changed code is covered with unit/integration tests (if applicable)
  • README.md files for modified modules are updated and included in the pull request if any README.md predefined sections require an update
  • All automated tests passed successfully (all builds are green)

…x_left_in_stock (magento#40709)

stock_status was a per-field resolver issuing one cataloginventory_stock_status
query per product; it now runs as a BatchResolverInterface that preloads every
product of a response through StockRegistryPreloader in a single query, while
cart-item requests keep delegating to StockStatusProvider unchanged.
OnlyXLeftInStockResolver checked the Only X Left threshold only after loading a
configurable by SKU through the product repository; it now returns early when
the threshold is 0 and reads the selected child from the simple_product custom
option instead of reloading the product it already holds.
@m2-assistant

m2-assistant Bot commented Sep 4, 2026

Copy link
Copy Markdown

Hi @lbajsarowicz. Thank you for your contribution!
Here are some useful tips on how you can test your changes using Magento test environment.
❗ Automated tests can be triggered manually with an appropriate comment:

  • @magento run all tests - run or re-run all required tests against the PR changes
  • @magento run <test-build(s)> - run or re-run specific test build(s)
    For example: @magento run Unit Tests

<test-build(s)> is a comma-separated list of build names.

Allowed build names are:
  1. Database Compare
  2. Functional Tests CE
  3. Functional Tests EE
  4. Functional Tests B2B
  5. Integration Tests
  6. Magento Health Index
  7. Sample Data Tests CE
  8. Sample Data Tests EE
  9. Sample Data Tests B2B
  10. Static Tests
  11. Unit Tests
  12. WebAPI Tests
  13. Semantic Version Checker

You can find more information about the builds here
ℹ️ Run only required test builds during development. Run all test builds before sending your pull request for review.


For more details, review the Code Contributions documentation.
Join Magento Community Engineering Slack and ask your questions in #github channel.

@lbajsarowicz

Copy link
Copy Markdown
Contributor Author

@magento run all tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⚡ Performance: Stock registry N+1 on every product load in CatalogInventory module

1 participant