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.phpBlock 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.