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

EAV-Performance-Fallstricke und wie man sie vermeidet

EAV-Performance-Fallstricke und wie man sie vermeidet

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

Kapitel 10 hat die EAV-Entscheidung für Rewards bewusst mit einem offenen Punkt beendet: Flexibilität hat einen Preis, und dieser Preis war bis hierhin nur behauptet, nicht belegt. Dieses letzte Kapitel von Block 2 macht ihn konkret - und schließt den Block mit derselben Art Zusammenfassung, mit der Kapitel 9 Block 1 beendet hat.

Fallstrick 1: das N+1-Problem

Der teuerste und häufigste EAV-Fehler überhaupt: eine Liste von Entity-IDs holen und dann jede einzelne Entität separat per load() nachladen, statt eine einzige Collection mit den benötigten Attributen zu verwenden.

// SCHLECHT: eine Collection-Abfrage plus N weitere load()-Aufrufe,
// jeder davon wieder mit mehreren JOINs.
$ids = $this->rewardCollectionFactory->create()->getAllIds();
foreach ($ids as $id) {
    $reward = $this->rewardFactory->create();
    $this->rewardResource->load($reward, $id);
    // ... $reward->getTitle() ...
}

// GUT: eine einzige Abfrage mit genau den benötigten Attributen.
$collection = $this->rewardCollectionFactory->create();
$collection->addAttributeToSelect(['title', 'points_cost']);
foreach ($collection as $reward) {
    // ... $reward->getTitle() ...
}

Achtung: Bei 100 Prämien und drei benötigten Attributen bedeutet die schlechte Variante bis zu 100 × 3 = 300 zusätzliche Abfragen statt einer einzigen Collection-Abfrage mit drei JOINs - ein Unterschied, der bei kleinen Testdatenmengen unsichtbar bleibt und erst mit echten Produktionsdaten schmerzhaft auffällt.

Fallstrick 2: addAttributeToSelect() ohne Eingrenzung

Kapitel 15 hat es bereits angerissen: addAttributeToSelect('*') joint alle sechs Attribute über vier Wertetabellen, selbst wenn eine konkrete Ansicht nur zwei davon anzeigt. Die Faustregel: die Attributliste immer explizit auf das eingrenzen, was der jeweilige Aufrufer tatsächlich braucht - eine Katalogkachel im Frontend braucht andere Felder als das vollständige Admin-Formular.

Fallstrick 3: Store-Scope und Zeilenvervielfachung

Ohne eine eigene, zusätzliche Attribut-Tabelle (wie catalog_eav_attribute sie für Produkte bereitstellt) kennt die Reward-Entity keinen komfortablen Website/Store-Scope-Auswahl im Admin - jeder Attributwert liegt technisch pro store_id vor, standardmäßig als eine einzige Zeile mit store_id = 0 ("Default", gilt für alle Stores). Wird versehentlich pro Store View eine eigene Zeile geschrieben, statt konsequent auf store_id = 0 zu speichern, vervielfacht sich die Zeilenzahl in jeder Wertetabelle um die Anzahl der Store Views - bei zwei Store Views (DE/EN) verdoppelt sich sie, ohne dass ein einziges zusätzliches Attribut hinzukam.

Fallstrick 4: EAV für etwas verwenden, das gar keine Variation braucht

Der Rückbezug auf Kapitel 10: der Punkte-Ledger aus Block 1 ist absichtlich keine EAV-Entity. Er ist ein hochfrequent geschriebenes, append-only Protokoll mit festen, niemals variierenden Spalten - genau das Gegenteil des Anwendungsfalls, für den EAV gebaut wurde. Eine EAV-Version des Ledgers hätte bei jeder einzelnen Punktebuchung sieben separate INSERTs (Haupttabelle plus sechs Attribut-Werte) statt eines einzigen ausgelöst - eine siebenfache Schreiblast für keinerlei Gegenwert, weil sich an den Ledger-Spalten ohnehin nie etwas ändert.

  • EAV lohnt sich, wenn Attribute sich zwischen einzelnen Entitäten stark unterscheiden, wenn neue Attribute ohne Deployment hinzukommen sollen, und wenn die Lesehäufigkeit (Admin-Formulare, gelegentliche Katalogabfragen) die Schreibhäufigkeit deutlich übersteigt.
  • Eine flache Tabelle lohnt sich, wenn die Spaltenzahl fest steht, wenn hohe Schreibfrequenz (jede Bestellung, jeder Seitenaufruf) im Vordergrund steht, oder wenn dieselben Felder ohnehin bei jeder Abfrage gebraucht werden - dann kostet EAV nur Joins, ohne Flexibilität zurückzugeben, die niemand nutzt.

Magentos eigene Antwort: flache Indizes

Magento selbst löst genau diesen Zielkonflikt nicht durch den Verzicht auf EAV, sondern durch zusätzliche, indexer-gepflegte flache Tabellen für die lesehäufigsten Pfade: der Produktkatalog im Frontend liest nicht direkt aus catalog_product_entity_*, sondern aus vom Indexer aufbereiteten, flachen Strukturen; customer_grid_flat versorgt das Kunden-Grid im Admin (Kapitel 16 hat dieselbe Überlegung schon für das Reward-Grid angesprochen). Die EAV-Struktur bleibt dabei die "Quelle der Wahrheit" für Schreibvorgänge und Flexibilität, während der Index die lesehäufigen Pfade beschleunigt - ein Muster, das sich 1:1 auf einen wachsenden Prämienkatalog übertragen ließe, sollte er einmal die in Kapitel 16 genannte Größenordnung überschreiten.

Tipp: Der Cache-Typ aus Kapitel 8 ist die pragmatische Zwischenstufe zwischen "nichts tun" und "einen vollständigen Indexer bauen": Er cacht das Ergebnis der bereits auf das Nötigste eingegrenzten Collection aus Kapitel 15 und vermeidet damit wiederholte JOINs bei gleichbleibenden Daten, ohne die Komplexität eines eigenen Indexers - für die in dieser Serie angenommene Größenordnung völlig ausreichend.

Block 2 abgeschlossen

Neun Kapitel, eine vollständige EAV-Entity: sechs Tabellen, ein Model/ResourceModel-Paar auf Basis von AbstractEntity, zwei Data Patches mit sechs registrierten Attributen, ein Source Model, eine filterbare Collection, ein Admin-Grid und zweifache Validierung. Die vollständige Verzeichnisstruktur nach diesem Kapitel erweitert Block 1, ohne etwas daraus zu verändern.

Mironsoft\Loyalty, vollständig nach Block 2

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

Block 3 setzt bei genau der Frage an, die dieser Block bewusst ausgeklammert hat: keine neue EAV-Entity mehr, sondern eigene Attribute auf bereits bestehenden Magento-Kernentitäten - Produkt, Kategorie, Kunde, Firma und Bestellung -, beginnend mit Kapitel 19 und dem Produkt-Attribut loyalty_points_multiplier.