Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

EAV Performance Pitfalls and How to Avoid Them

EAV Performance Pitfalls and How to Avoid Them

~7 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Chapter 10 deliberately ended the EAV decision for rewards with an open point: flexibility has a price, and up to now that price was only asserted, not demonstrated. This last chapter of block 2 makes it concrete - and closes the block with the same kind of summary chapter 9 used to close block 1.

Pitfall 1: the N+1 problem

The most expensive and most common EAV mistake of all: fetching a list of entity IDs and then loading each entity separately via load(), instead of using a single collection with the needed attributes.

// BAD: one collection query plus N further load() calls,
// each of those with several JOINs of its own.
$ids = $this->rewardCollectionFactory->create()->getAllIds();
foreach ($ids as $id) {
    $reward = $this->rewardFactory->create();
    $this->rewardResource->load($reward, $id);
    // ... $reward->getTitle() ...
}

// GOOD: a single query with exactly the attributes needed.
$collection = $this->rewardCollectionFactory->create();
$collection->addAttributeToSelect(['title', 'points_cost']);
foreach ($collection as $reward) {
    // ... $reward->getTitle() ...
}

Achtung: With 100 rewards and three needed attributes, the bad variant means up to 100 × 3 = 300 extra queries instead of a single collection query with three JOINs - a difference that stays invisible with small test data sets and only becomes painfully obvious with real production data.

Pitfall 2: addAttributeToSelect() without narrowing

Chapter 15 already touched on this: addAttributeToSelect('*') joins all six attributes across four value tables, even when a specific view only displays two of them. The rule of thumb: always narrow the attribute list explicitly to what the specific caller actually needs - a frontend catalog tile needs different fields than the full admin form.

Pitfall 3: store scope and row multiplication

Without a dedicated, additional attribute table (like catalog_eav_attribute provides for products), the reward entity has no convenient website/store scope selector in the admin - every attribute value technically exists per store_id, by default as a single row with store_id = 0 ("default", applies to all stores). If a separate row per store view gets written by accident, instead of consistently saving to store_id = 0, the row count in every value table multiplies by the number of store views - with two store views (DE/EN) it doubles, without a single additional attribute having been added.

Pitfall 4: using EAV for something that needs no variation at all

The tie-back to chapter 10: the points ledger from block 1 is deliberately not an EAV entity. It's a high-frequency-written, append-only log with fixed, never-varying columns - exactly the opposite of the use case EAV was built for. An EAV version of the ledger would trigger seven separate INSERTs (main table plus six attribute values) on every single points transaction instead of one - a sevenfold write load for zero benefit, since the ledger's columns never change anyway.

  • EAV pays off when attributes vary heavily between individual entities, when new attributes need to be addable without a deployment, and when read frequency (admin forms, occasional catalog queries) clearly exceeds write frequency.
  • A flat table pays off when the column count is fixed, when high write frequency (every order, every page view) is the priority, or when the same fields are needed on every query anyway - then EAV only costs joins without giving back flexibility nobody uses.

Magento's own answer: flat indexes

Magento itself doesn't resolve this exact trade-off by avoiding EAV, but through additional, indexer-maintained flat tables for the most read-heavy paths: the frontend product catalog doesn't read directly from catalog_product_entity_*, but from indexer-built flat structures; customer_grid_flat feeds the customer grid in the admin (chapter 16 already raised the same consideration for the reward grid). The EAV structure stays the "source of truth" for writes and flexibility, while the index speeds up the read-heavy paths - a pattern that would translate 1:1 to a growing reward catalog, should it ever exceed the scale mentioned in chapter 16.

Tipp: The cache type from chapter 8 is the pragmatic middle ground between "do nothing" and "build a full indexer": it caches the result of the already narrowed-down collection from chapter 15, avoiding repeated JOINs for unchanged data, without the complexity of a dedicated indexer - entirely sufficient for the scale assumed in this series.

Block 2 complete

Nine chapters, one complete EAV entity: six tables, a model/resource model pair built on AbstractEntity, two data patches registering six attributes, a source model, a filterable collection, an admin grid, and two layers of validation. The complete directory structure after this chapter extends block 1 without changing anything in it.

Mironsoft\Loyalty, complete after block 2

app/code/Mironsoft/Loyalty/
├── registration.php
├── composer.json
├── etc/
│   ├── module.xml                          (chapter 11: + Magento_Eav)
│   ├── di.xml
│   ├── acl.xml                              (chapter 16: + Mironsoft_Loyalty::rewards)
│   ├── cache.xml
│   ├── config.xml
│   ├── db_schema.xml                        (chapter 11: + 6 reward tables)
│   └── adminhtml/
│       ├── system.xml
│       └── menu.xml                         (chapter 16)
├── Api/
│   ├── PointsLedgerRepositoryInterface.php
│   └── Data/
│       └── PointsLedgerInterface.php
├── Controller/
│   └── Adminhtml/
│       └── Reward/
│           └── Index.php                    (chapter 16)
├── Model/
│   ├── PointsLedger.php
│   ├── PointsLedgerRepository.php
│   ├── Reward.php                            (chapters 12, 17)
│   ├── Reward/
│   │   └── Source/
│   │       └── RewardType.php                (chapter 13)
│   ├── Cache/
│   │   └── Type/
│   │       └── LoyaltyCatalog.php
│   ├── Config/
│   │   └── LoyaltyConfig.php
│   ├── ResourceModel/
│   │   ├── PointsLedger.php
│   │   ├── PointsLedger/
│   │   │   └── Collection.php
│   │   ├── Reward.php                        (chapter 12)
│   │   └── Reward/
│   │       └── Collection.php                (chapter 15)
│   └── Service/
│       └── PointsCalculator.php
├── Setup/
│   └── Patch/
│       └── Data/
│           ├── InstallRewardEntityType.php   (chapter 13)
│           └── InstallRewardAttributes.php   (chapter 13)
├── view/
│   └── adminhtml/
│       └── ui_component/
│           └── reward_listing.xml            (chapter 16)
└── Console/
    └── Command/
        └── RecalculatePointsCommand.php

Block 3 picks up exactly at the question this block deliberately set aside: no new EAV entity, but custom attributes on already existing Magento core entities - product, category, customer, company, and order - starting with chapter 19 and the product attribute loyalty_points_multiplier.