Performance

EAV Attribute Debt That Slows the Catalog

Every product attribute has a running cost in reads, writes, and indexing. Here is how EAV debt slows the catalog and how to measure and reduce it.

Jason Schuman · January 9, 2026

Every product attribute adds work

Magento lets you describe a product with almost any field. That flexibility has a database cost. Each custom attribute adds product data that Magento may need to read, write, and index.

The cost can hide for years. Old attributes from imports, integrations, and removed features stay in the catalog. Product saves slow down, reindex jobs run longer, and category pages make heavier queries. Often, no single setting points to the cause.

This article explains how EAV stores product data, where attribute count adds work, how to measure the EAV footprint, and how to clean it up without deleting business data by accident.

magento eav attribute debt and catalog performance

How EAV stores product data

Magento uses the entity-attribute-value model, usually called EAV, for products. Instead of keeping every product field in one wide table, Magento stores values in tables grouped by data type: catalog_product_entity_varchar, _int, _decimal, _text, and _datetime.

Each attribute value for each product is a separate row in one of those tables. A product with fifty populated attributes has one main entity row plus many value rows. Store-view settings can add more rows for the same product and attribute.

EAV lets Magento and extensions add product fields without changing the main product table every time. The tradeoff is more rows to find, join, and assemble during product operations. Attribute count increases the database work for every operation that uses those attributes.

Why broad attribute reads get expensive

To build a product object, Magento finds its values in the EAV tables. Loading several attributes can require several joins or row lookups before Magento has one usable product object.

The classic mistake makes this much worse. Calling addAttributeToSelect('*') on a collection tells Magento to load every available attribute for every product in that collection. Code that needs a SKU, name, and price may then pull dozens of other fields it never uses.

Loading every attribute when you need three is a common catalog performance mistake. On a product collection, addAttributeToSelect('*') turns a small read into a large read every time that code runs.

Product saves fan out across EAV tables

Saving a product is not one database write. Magento writes changed values to the EAV table for each data type. Depending on the save path, it may also inspect or update many related values and indexes.

When an import updates thousands of products, Magento repeats that work thousands of times. A small amount of extra work per product becomes a large number of database writes across the whole import.

This is why bulk product operations slow down as the catalog grows. The cost of one extra attribute may be small. The same cost repeated across every product and every save is not.

Unused attributes become catalog debt

Most attribute debt comes from fields added over several years for a feature, an integration, or a temporary test. The feature disappears, but the attribute remains.

Nothing has to display an attribute for it to affect the catalog. Magento can still load it, inspect its configuration, include it in index work, or process it during an import. Unused does not mean free.

The eav_attribute table holds every attribute defined for the store. On a mature catalog, many product attributes may be legacy fields that no current developer or business process owns.

Filterable and searchable attributes create index work

Two settings add a large amount of extra work. A filterable attribute feeds the layered navigation index. A searchable attribute feeds the search index.

Every filterable attribute can add rows to catalog_product_index_eav, the index used by layered navigation. The exact row count depends on the products, values, store views, and attribute settings, but more filterable data generally means a larger index and a longer rebuild.

Searchable attributes add data to the search engine index. Marking an attribute searchable or filterable "because it might be useful" creates a reindex cost even when customers never use that field.

Layered navigation grows by multiplication

The layered navigation index needs separate attention because its size grows multiplicatively. A useful rough model is: number of filterable attributes multiplied by the number of values, multiplied by the number of products those values apply to.

For example, a store with twelve filterable attributes, many options per attribute, and a large catalog can create a very large catalog_product_index_eav table. Magento rebuilds that table during a reindex and reads it while building category filters.

Slow category pages often have this pattern. Filterable was enabled broadly, but nobody checked whether shoppers use the filters. The index then does work for navigation that the store does not need.

Reindexing exposes attribute debt

Reindexing is where attribute debt becomes easy to see. The catalog and price indexers read EAV data and write denormalized index tables that the storefront can query more quickly.

More configured and indexed data means more rows to read and write. A reindex that took minutes on a lean catalog can take much longer after years of attributes and options have accumulated. Long jobs also leave storefront data out of date for longer.

Reindex time climbs with attribute count, and filterable attributes drive the steepest part of the curve.

Imports and exports pay the same tax

Catalog imports and exports also scale with attribute count. An import row must be mapped and written across the attribute tables. An export row must gather those values before Magento can send it out.

Two catalogs can contain the same number of products and still have very different import times. The wider catalog has more data to read and write for each product.

Frequent integrations feel this cost on every sync. A feed from an ERP, PIM, marketplace, or warehouse repeats the same attribute work each time it updates the catalog.

Static storage trades flexibility for speed

Some attributes do not live in EAV tables. An attribute with a backend type of static is stored as a column on the main product entity table instead of as separate value rows.

Core fields such as SKU use static storage because Magento reads them constantly. One table column is simpler and faster to read than a value spread across EAV tables. Static storage gives up the ability to add the field through normal EAV configuration.

A custom attribute that every product reads may justify static storage, but this requires a real schema column and a module or deployment change. It is not a setting to flip on a live site. Most custom fields should stay in EAV unless the schema decision has a clear owner and a measured reason.

What the old flat catalog teaches

Older Magento versions offered a flat catalog option. It copied EAV data into wide tables so common storefront reads could avoid many joins.

Recent Magento versions have deprecated and moved away from flat catalog. If a store still has flat catalog settings from an older installation, check the Magento version and current indexer behavior before changing them.

The useful lesson is simple: storefront reads need a denormalized layer so every request does not rebuild a product from EAV rows. Fewer attributes and less indexed data reduce the work needed to build that layer.

Attribute sets decide who pays the cost

Magento groups product attributes into attribute sets. A product is assigned a set, and that set controls which attributes are available for the product.

Attribute sets become bloated when developers add fields to the default set for convenience. A field meant for a small product group then appears in the configuration and import surface for many other products. That creates extra work even when most products leave the field empty.

Review which attributes belong to each set. An attribute used by a small product group should not be assigned to the set used by every product unless there is a clear reason.

Dropdown options leave their own database trail

Dropdown and multiselect attributes carry option metadata in addition to product values. The option IDs live in eav_attribute_option, and their labels live in eav_attribute_option_value.

Those tables keep options that were defined years ago. An attribute with hundreds of old options makes admin forms, filters, imports, and other option lookups process a larger set of choices.

Cleaning stale options is separate from deleting an attribute. Before removing an option, confirm that no product value, import mapping, report, or custom code still uses its ID. Option cleanup can remove the choice without removing the attribute itself.

Price indexing multiplies the work

Pricing adds another set of combinations for Magento to calculate. The price index accounts for tier prices, special prices, catalog price rules, websites, and customer groups. Product and customer data can change the result for each combination.

Each pricing dimension expands what Magento must compute and store. Many customer groups and active catalog price rules create a larger price index, and that work runs alongside the cost of reading and indexing product attributes.

Attribute debt does not stop at the attribute tables. It reaches the price and layered-navigation indexes, where extra product data and values create more work during each reindex.

Extensions leave behind product attributes

Attribute debt often comes from third-party extensions. An extension may add product attributes during installation. Those attributes can stay after the extension is removed if its uninstall process did not clean them up.

A store that has tried several extensions may carry fields from modules that are long gone. An orphaned field can still add database and index work when no current code reads it.

During an audit, compare each attribute with the modules currently installed, import profiles, reports, API consumers, and custom templates. An attribute absent from the product page is not automatically safe to delete. Code outside the frontend may still depend on it.

The frontend pays on every uncached request

Attribute cost reaches the storefront too. The backend and indexers pay first, then product and category requests pay again when they read that data.

A product page loads the values it displays. A product listing collection loads selected attributes for every product in the list. A category page with layered navigation reads the attribute index to build filters on requests that do not come from cache.

More selected attributes mean more joins, rows, and object data. Caching can hide the work for some requests, but it does not remove the work from product saves, imports, or cache misses.

Every extra filter adds query paths

Layered navigation turns filterable attributes into requests shoppers can see. Each filter adds another way to query the attribute index, and selecting several filters adds more conditions to the category query.

A navigation with many filters and many values creates a large set of possible combinations. That breadth makes the index larger and category queries heavier. The work can grow faster than the attribute count alone suggests.

Review request logs and filter usage before keeping a filter enabled. A store often exposes more filters than shoppers use, then pays the index and query cost for navigation that receives little or no traffic.

Set a practical attribute budget

The goal is not the smallest possible catalog schema. The goal is a deliberate one. Each attribute should have a purpose, an owner, and a known consumer. Each filterable or searchable flag should match a real catalog or customer need.

Apply that rule whenever a new field is requested. Record the source system, the product types that need it, whether it is searchable or filterable, and how it will be removed later.

Review old fields on a schedule. A catalog stays easier to operate when new attributes need a reason and old attributes do not remain by default.

Measure the EAV footprint before cleanup

Start with a few database queries. Count all product attributes, then count the ones configured for layered navigation:

SELECT COUNT(*) AS product_attributes
FROM eav_attribute a
JOIN eav_entity_type e ON e.entity_type_id = a.entity_type_id
WHERE e.entity_type_code = 'catalog_product';

SELECT COUNT(*) AS filterable
FROM catalog_eav_attribute
WHERE is_filterable > 0;

SELECT table_name, engine, table_rows, data_length, index_length
FROM information_schema.tables
WHERE table_schema = DATABASE()
  AND table_name IN (
    'catalog_product_index_eav',
    'catalog_product_entity_varchar',
    'catalog_product_entity_int',
    'catalog_product_entity_decimal',
    'catalog_product_entity_text',
    'catalog_product_entity_datetime',
    'eav_attribute_option',
    'eav_attribute_option_value'
  )
ORDER BY data_length + index_length DESC;

The first query counts product attributes. The second counts filterable configuration. The third shows approximate row and storage size for the tables that carry or index product data. It also lets you check the storage engine. These tables normally use InnoDB.

For InnoDB, table_rows is an estimate, not an exact count. Use the size columns to compare tables, then use targeted counts when you need an exact answer for a cleanup decision.

Magento records applied data patches in patch_list. Check that table when you need to confirm whether a cleanup patch ran in a specific environment.

Cross-reference the attribute list with the storefront, import mappings, reports, API consumers, and scheduled jobs. A filterable or searchable attribute that no active process uses is a cleanup candidate, but confirm its business use before removing it.

Cross-referencing indexed attributes against actual storefront use exposes the ones that are pure cost.

Media rows add a second kind of catalog weight

Product images create related catalog data outside the simple EAV value tables. Magento stores image metadata in gallery tables such as catalog_product_entity_media_gallery, catalog_product_entity_media_gallery_value, and catalog_product_entity_media_gallery_value_to_entity.

Each image can have a gallery row, product links, and store-view labels or positions. A catalog with many old images accumulates those rows just as a catalog accumulates old attribute values.

Before deleting an image, check both the database record and the media file path. Remove files only after confirming that no product, store view, import, or custom code still uses them. Media cleanup reduces product weight, but it needs its own inventory.

Reduce the debt in safe stages

Cleanup can remove data, so start with an inventory. Record the attribute code, backend type, attribute set assignments, filterable and searchable flags, source module, and every known consumer.

First turn off filterable and searchable settings that the storefront does not use. This can shrink future index work without deleting attribute values, but it can change navigation and search behavior. Test it on a copy of production, reindex, and compare the result.

Next clean stale options and unused media where the ownership checks pass. Remove truly unused attributes last. Search custom code, templates, import mappings, reports, APIs, scheduled jobs, and external business systems before deleting anything.

Remove an attribute with a data patch

Removing a product attribute deletes its EAV values. That can erase business data, so do not run a direct SQL DELETE as a shortcut. The controlled path is a versioned data patch that uses Magento's EAV setup code.

Place the patch in the module Directory at a path such as app/code/<Vendor>/<Module>/Setup/Patch/Data/RemoveUnusedProductAttribute.php. Have the class implement DataPatchInterface, obtain EavSetup through EavSetupFactory, and call removeAttribute for the catalog_product entity. This records the change in the module code instead of leaving a manual database command as the only record.

Test the patch on a copy of production first. Check the storefront, import mappings, reports, ERP or PIM connections, API responses, scheduled jobs, and custom templates. An attribute that looks unused to a shopper can still feed an order process or a business report.

After review, deploy the patch and run bin/magento setup:upgrade, then reindex and clear the relevant caches. A versioned patch runs in a Reproducible way across environments, and the DIFF shows exactly what the cleanup changed.

Lean catalogs stay easier to operate

Attribute debt hides during normal work. It appears later as slow product saves, long reindexes, heavy category pages, and sluggish imports. Those symptoms can look unrelated until you count the attributes and index rows behind them.

A lean attribute model reduces the same work in several places. Saves touch less data, imports repeat less work, reindexes finish sooner, and category requests have fewer filters and rows to process.

Measure how many attributes the catalog carries, how many are indexed, and how many active systems use them. That inventory gives you a concrete cleanup plan and a way to stop new attribute debt before it spreads.