Repository navigation
Product attribute Web API endpoints run one eav_attribute_option query per select/multiselect attribute when serializing options #41729
Description
Preconditions and environment
- Magento version: 2.4-develop
- PHP 8.4, MariaDB 11.4, clean install, no third-party modules
- An admin Bearer token for the REST API
Steps to reproduce
- Get an admin token:
curl -sk https://<host>/rest/V1/integration/admin/token -H 'Content-Type: application/json' -d '{"username":"<admin>","password":"<password>"}'
- Create 10 dropdown attributes with 3 options each (repeat for
i= 1..10):
curl -sk -X POST https://<host>/rest/V1/products/attributes -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"attribute":{"attribute_code":"test_select_'$i'","frontend_input":"select","default_frontend_label":"Test select '$i'","is_required":false,"options":[{"label":"a"},{"label":"b"},{"label":"c"}]}}'
- Create an attribute set based on Default (id 4) and note its id (
$SET) and the id of its first group ($GROUP, e.g. fromeav_attribute_group):
curl -sk -X POST https://<host>/rest/V1/products/attribute-sets -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"attributeSet":{"attribute_set_name":"test-set","entity_type_id":4},"skeletonId":4}'
- Enable the general query log (
SET GLOBAL general_log=1), requestGET /rest/V1/products/attribute-sets/$SET/attributes(after two warm-up requests) and count theeav_attribute_optionqueries. Repeat after assigning 2 and then all 10 of the new attributes to the set:
curl -sk -X POST https://<host>/rest/V1/products/attribute-sets/attributes -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"attributeSetId":'$SET',"attributeGroupId":'$GROUP',"attributeCode":"test_select_'$i'","sortOrder":'$((100+i))'}'
curl -sk -H "Authorization: Bearer $TOKEN" https://<host>/rest/V1/products/attribute-sets/$SET/attributes
- Do the same for
GET /rest/V1/products/attributes?searchCriteria[pageSize]=200before step 2 and after step 2.
Expected result
The options of all attributes in the response are loaded with a constant number of queries (for example one query for all option-bearing attributes), so the query count does not grow with the number of select/multiselect attributes.
Actual result
Each select/multiselect attribute that uses the default Magento\Eav\Model\Entity\Attribute\Source\Table source model adds one query of this shape, with only the attribute id changing:
SELECT `main_table`.*, `tdv`.`value` AS `default_value`, `tsv`.`value` AS `store_default_value`, IF(tsv.value_id > 0, tsv.value, tdv.value) AS `value` FROM `eav_attribute_option` AS `main_table` INNER JOIN `eav_attribute_option_value` AS `tdv` ON tdv.option_id = main_table.option_id LEFT JOIN `eav_attribute_option_value` AS `tsv` ON tsv.option_id = main_table.option_id AND tsv.store_id = '1' WHERE (`main_table`.`attribute_id` = '<id>') AND (tdv.store_id = 0) ORDER BY main_table.sort_order ASC, `value` ASC
Measured on a clean install (warm cache, HTTP 200 in every case):
| Request | Table-source select attributes | eav_attribute_option queries |
Total queries |
|---|---|---|---|
GET /V1/products/attribute-sets/{set}/attributes, set copied from Default |
2 | 2 | 16 |
| same set + 2 dropdown attributes | 4 | 4 | 18 |
| same set + 10 dropdown attributes | 12 | 12 | 26 |
GET /V1/products/attributes?searchCriteria[pageSize]=200, clean install |
3 | 3 | 34 |
| same, after creating 10 dropdown attributes | 13 | 13 | 60 |
A catalog with a few hundred dropdown attributes, which is common, issues a few hundred option queries for every call of these endpoints.
Additional information
Magento\Eav\Model\AttributeManagement::getAttributes() loads the attribute collection of the set and returns its items:
magento2/app/code/Magento/Eav/Model/AttributeManagement.php
Lines 176 to 188 in 0af2ad9
| public function getAttributes($entityTypeCode, $attributeSetId) | |
| { | |
| /** @var \Magento\Eav\Api\Data\AttributeSetInterface $attributeSet */ | |
| $attributeSet = $this->setRepository->get($attributeSetId); | |
| $requiredEntityTypeId = $this->eavConfig->getEntityType($entityTypeCode)->getId(); | |
| if (!$attributeSet->getAttributeSetId() || $attributeSet->getEntityTypeId() != $requiredEntityTypeId) { | |
| throw NoSuchEntityException::singleField('attributeSetId', $attributeSetId); | |
| } | |
| $attributeCollection = $this->attributeCollectionFactory->create(); | |
| $attributeCollection->setAttributeSetFilter($attributeSet->getAttributeSetId())->load(); | |
| return $attributeCollection->getItems(); | |
| } |
During Web API output serialization Magento\Eav\Api\Data\AttributeInterface::getOptions() is read for each item. Magento\Eav\Model\Entity\Attribute\AbstractAttribute::getOptions() falls back to getSource()->getAllOptions() when no options were set on the model:
magento2/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php
Lines 1178 to 1186 in 0af2ad9
| public function getOptions() | |
| { | |
| $options = $this->_getData(self::OPTIONS); | |
| if (!$options) { | |
| $options = $this->usesSource() ? $this->getSource()->getAllOptions() : []; | |
| } | |
| return $this->convertToObjects($options); | |
| } |
Magento\Eav\Model\Entity\Attribute\Source\Table::getAllOptions() caches options only per source instance, store and attribute id, so every attribute triggers its own option collection load:
magento2/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php
Lines 65 to 88 in 0af2ad9
| public function getAllOptions($withEmpty = true, $defaultValues = false) | |
| { | |
| $storeId = $this->getAttribute()->getStoreId(); | |
| if ($storeId === null) { | |
| $storeId = $this->storeManager->getStore()->getId(); | |
| } | |
| if (!is_array($this->_options)) { | |
| $this->_options = []; | |
| } | |
| if (!is_array($this->_optionsDefault)) { | |
| $this->_optionsDefault = []; | |
| } | |
| $attributeId = $this->getAttribute()->getId(); | |
| if (!isset($this->_options[$storeId][$attributeId])) { | |
| $collection = $this->_attrOptionCollectionFactory->create()->setPositionOrder( | |
| 'asc' | |
| )->setAttributeFilter( | |
| $attributeId | |
| )->setStoreFilter( | |
| $storeId | |
| )->load(); | |
| $this->_options[$storeId][$attributeId] = $collection->toOptionArray(); | |
| $this->_optionsDefault[$storeId][$attributeId] = $collection->toOptionArray('default_value'); | |
| } |
GET /V1/products/attributes goes through Magento\Catalog\Model\Product\Attribute\Repository::getList() and Magento\Eav\Model\AttributeRepository::getList(), which load the collection and then call get() for every attribute. The serializer ends up in the same Table::getAllOptions() path. That endpoint also issues per-attribute catalog_eav_attribute and eav_attribute lookups (16 and 10 queries respectively in the 10-attribute run above), but this report is only about the option queries.
This is a different path from #36174 / #38934 (and PRs #40934 / #39483), which cover Table::getSpecificOptions() being called repeatedly for the same attribute. Those changes do not batch getAllOptions() across attributes, so the counts above would not change.
A possible direction: after loading the attribute collection, preload options for all option-bearing attributes that use the Table source in one eav_attribute_option query (filtered by attribute_id IN (...) and the store) and set them on each attribute, or seed the Table source cache from that one query.
Release note
Product attribute Web API endpoints load select/multiselect attribute options with a constant number of queries instead of one query per attribute.
Triage and priority
- Severity: S0 - Affects critical data or functionality and leaves users without workaround.
- Severity: S1 - Affects critical data or functionality and forces users to employ a workaround.
- Severity: S2 - Affects non-critical data or functionality and forces users to employ a workaround.
- Severity: S3 - Affects non-critical data or functionality and does not force users to employ a workaround.
- Severity: S4 - Affects aesthetics, professional look and feel, “quality” or “user experience”.
Identified from production performance monitoring (New Relic) on Magento stores maintained by Fast White Cat. The root cause was verified against a clean 2.4-develop; no store data is included in this report.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsReady for Confirmation
Activity
m2-assistant commentedon Oct 27, 2026
Hi @lbajsarowicz. Thank you for your report.
To speed up processing of this issue, make sure that the issue is reproducible on the vanilla Magento instance following Steps to reproduce.
@magento I am working on thisJoin Magento Community Engineering Slack and ask your questions in #github channel.
⚠️ According to the Magento Contribution requirements, all issues must go through the Community Contributions Triage process. Community Contributions Triage is a public meeting.
🕙 You can find the schedule on the Magento Community Calendar page.
📞 The triage of issues happens in the queue order. If you want to speed up the delivery of your contribution, join the Community Contributions Triage session to discuss the appropriate ticket.
github-jira-sync-bot commentedon Oct 27, 2026
✅ Jira issue https://jira.corp.adobe.com/browse/AC-19607 is successfully created for this GitHub issue.
lbajsarowicz commentedon Oct 27, 2026
@magento I am working on this