Verwaiste EAV-Werte, fehlende Store-Zuordnungen und kaputte Kategorie-Pfade mit einem eigenen CLI-Audit-Command aufdecken
Nach Jahren voller Importe, Modul-Deinstallationen und manueller Datenbank-Eingriffe sammeln sich in fast jedem gewachsenen Magento-Katalog stille Dateninkonsistenzen an, die keinen Fehler werfen, aber die Datenqualität schleichend untergraben. Ein eigenes CLI-Audit-Command macht diese Inkonsistenzen sichtbar, bevor sie als merkwürdiges Frontend-Verhalten oder falsche Suchtreffer auffallen, und ersetzt das gefährliche Reflex-Muster, bei jedem Problem erst einmal einen Reindex zu starten.
Inhaltsverzeichnis
- 1. Typische Dateninkonsistenzen im gewachsenen Katalog
- 2. Verwaiste EAV-Werte identifizieren
- 3. Fehlende Store-Zuordnungen aufspüren
- 4. Kaputte Kategorie-Pfade erkennen
- 5. Ein eigenes CLI-Audit-Command implementieren
- 6. Einen einzelnen Check als eigene Klasse implementieren
- 7. Ergebnis-Reporting: CSV-Export und CI-taugliche Ausgabe
- 8. Abgrenzung zur reinen Reindex-Problematik
- 9. Integration in die Deployment-Pipeline
- 10. Zusammenfassung
- 11. FAQ
1. Typische Dateninkonsistenzen im gewachsenen Katalog
Drei Kategorien von Inkonsistenzen tauchen in der Praxis immer wieder auf. Verwaiste EAV-Werte entstehen, wenn ein Produkt über direkte SQL-Löschungen oder einen fehlerhaften Custom-Import aus catalog_product_entity entfernt wird, die zugehörigen Wertezeilen in den Attribut-Tabellen aber bestehen bleiben, weil die referenzielle Integrität nur über Foreign Keys mit Cascade-Löschung greift, die bei direkten Bulk-Deletes häufig umgangen werden.
Fehlende Store-Zuordnungen entstehen typischerweise nach Store-View-Umstrukturierungen oder halbfertigen Migrationsskripten: Ein Produkt ist zwar in product_website korrekt zugeordnet, taucht aber in catalog_category_product für eine bestimmte Store-View gar nicht oder mit falschem Kategorie-Bezug auf. Kaputte Kategorie-Pfade schließlich entstehen, wenn der path-Wert einer Kategorie nach einem manuellen Verschieben im Baum nicht konsistent mit parent_id und level aktualisiert wurde, ein Zustand, der sich erst bei der nächsten Breadcrumb-Anzeige oder URL-Generierung als Symptom zeigt.
2. Verwaiste EAV-Werte identifizieren
Die Grundidee für die Erkennung verwaister EAV-Werte ist ein LEFT JOIN von der jeweiligen Attribut-Wertetabelle, catalog_product_entity_varchar, catalog_product_entity_int und so weiter, gegen catalog_product_entity, gefolgt von einer Prüfung auf NULL in der entity_id-Spalte der Haupttabelle. Da Magento-Attribute über mehrere Werte-Tabellen nach Datentyp verteilt sind, muss diese Prüfung für jeden relevanten Datentyp separat laufen, ein einzelner Query reicht nicht aus, um alle verwaisten Werte vollständig zu erfassen.
Bei sehr großen Katalogen ist ein naiver LEFT JOIN über die komplette varchar-Tabelle spürbar teuer, da diese Tabelle typischerweise die mit Abstand meisten Zeilen aller EAV-Wertetabellen enthält. Eine Batch-Verarbeitung über entity_id-Bereiche mit jeweils eigenem, kleinerem Query reduziert die Spitzenlast auf der Datenbank deutlich gegenüber einem einzigen, über den gesamten Katalog laufenden Join.
-- Verwaiste Werte in catalog_product_entity_varchar identifizieren
SELECT v.entity_id, v.attribute_id, v.value
FROM catalog_product_entity_varchar v
LEFT JOIN catalog_product_entity e ON e.entity_id = v.entity_id
WHERE e.entity_id IS NULL
LIMIT 500;
-- Dasselbe Muster gilt für int, decimal, text und datetime Attribut-Tabellen,
-- jede Datentyp-Tabelle muss separat geprüft werden.
SELECT i.entity_id, i.attribute_id, i.value
FROM catalog_product_entity_int i
LEFT JOIN catalog_product_entity e ON e.entity_id = i.entity_id
WHERE e.entity_id IS NULL
LIMIT 500;
3. Fehlende Store-Zuordnungen aufspüren
Für die Prüfung fehlender Store-Zuordnungen wird zunächst die Menge der Produkte ermittelt, die einer Website über product_website zugeordnet sind, und anschließend gegen die tatsächlich vorhandenen Kategorie-Zuordnungen der zugehörigen Store-Views in catalog_category_product abgeglichen. Ein Produkt, das einer Website zugeordnet ist, aber in keiner einzigen für diese Website aktiven Kategorie erscheint, ist im Frontend zwar über die Direct-URL erreichbar, aber über keine Navigation auffindbar, ein Fehlerbild, das im Live-Betrieb oft erst durch sinkende organische Sichtbarkeit auffällt.
Ein zweiter, subtilerer Fall betrifft Store-View-spezifische Attribut-Overrides ohne zugehörige globale Basis, etwa wenn ein Produktname für eine bestimmte Store-View überschrieben wurde, das Produkt selbst aber inzwischen aus dieser Store-View entfernt wurde. Diese verwaisten Store-View-Overrides sind funktional meist unschädlich, blähen aber die Attribut-Tabellen unnötig auf und sollten im selben Audit-Durchlauf mit erfasst werden.
4. Kaputte Kategorie-Pfade erkennen
Der path-Wert einer Kategorie, typischerweise eine Punkt-getrennte Kette von Kategorie-IDs von der Wurzel bis zur aktuellen Kategorie, muss exakt mit der über parent_id rekonstruierbaren Baumstruktur übereinstimmen. Eine Prüfung rekonstruiert für jede Kategorie den erwarteten Pfad rekursiv über parent_id und vergleicht ihn mit dem gespeicherten path-Feld, jede Abweichung deutet auf eine Inkonsistenz hin, die typischerweise durch ein fehlgeschlagenes oder unterbrochenes Verschieben im Kategorie-Baum entstanden ist.
Ergänzend dazu lohnt sich eine Prüfung des level-Felds gegen die tatsächliche Tiefe des rekonstruierten Pfads, da beide Werte bei einer sauberen Baumstruktur immer konsistent zueinander sein müssen. Eine Kategorie mit korrektem path, aber falschem level, oder umgekehrt, führt in der Praxis zu fehlerhaften Breadcrumbs oder falsch sortierten Kategorie-Listen im Admin-Bereich, ohne dass ein Reindex dieses grundlegende Datenproblem beheben könnte.
5. Ein eigenes CLI-Audit-Command implementieren
Für die praktische Nutzung wird ein eigenes Symfony-Console-Command über Magento\Framework\Console\Cli registriert, das die einzelnen Prüfungen als separate, unabhängig aufrufbare Checks kapselt. Jeder Check implementiert ein gemeinsames Interface mit einer einzigen execute-Methode, die eine strukturierte Ergebnisliste zurückgibt, statt Ausgaben direkt auf die Konsole zu schreiben, wodurch dieselbe Check-Logik später auch für eine CSV-Exportfunktion oder eine automatisierte CI-Prüfung wiederverwendet werden kann.
Der Command selbst orchestriert die registrierten Checks, sammelt deren Ergebnisse und gibt am Ende eine Zusammenfassung sowie, bei entsprechender Option, eine detaillierte Zeilenliste aus. Wichtig für die spätere CI-Integration ist ein korrekter Exit-Code: Findet der Audit mindestens eine kritische Inkonsistenz, muss der Command mit einem Exit-Code ungleich null terminieren, damit eine Pipeline diesen Zustand als Fehlschlag erkennt.
<?php
declare(strict_types=1);
namespace Mironsoft\CatalogIntegrity\Console\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Mironsoft\CatalogIntegrity\Model\Check\CheckPool;
/**
* Führt alle registrierten Katalog-Datenintegritäts-Checks aus
* und liefert einen für CI-Pipelines auswertbaren Exit-Code.
*/
class AuditCatalogCommand extends Command
{
/**
* @param CheckPool $checkPool Enthaelt alle registrierten Audit-Checks
*/
public function __construct(
private readonly CheckPool $checkPool,
) {
parent::__construct();
}
/**
* Konfiguriert Name, Beschreibung und Optionen des Commands.
*
* @return void
*/
protected function configure(): void
{
$this->setName('mironsoft:catalog:audit');
$this->setDescription('Prueft den Katalog auf typische Dateninkonsistenzen.');
$this->addOption('detailed', null, InputOption::VALUE_NONE, 'Zeilen-Details ausgeben');
}
/**
* Fuehrt alle Checks aus und gibt eine Zusammenfassung sowie
* bei Bedarf Details zurück.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$hasCriticalIssues = false;
foreach ($this->checkPool->getChecks() as $check) {
$result = $check->run();
$output->writeln(sprintf('%s: %d gefunden', $check->getLabel(), $result->getIssueCount()));
if ($result->hasCriticalIssues()) {
$hasCriticalIssues = true;
}
if ($input->getOption('detailed')) {
foreach ($result->getDetails() as $line) {
$output->writeln(' - ' . $line);
}
}
}
return $hasCriticalIssues ? Command::FAILURE : Command::SUCCESS;
}
}
6. Einen einzelnen Check als eigene Klasse implementieren
Jeder einzelne Check ist eine schlanke Klasse, die eine einzige fachliche Frage beantwortet, etwa ob verwaiste EAV-Werte in der varchar-Tabelle existieren, und dafür direkt gegen die ResourceConnection statt über eine vollständig geladene Collection arbeitet. Diese Entscheidung ist bewusst getroffen: Eine Collection lädt bei mehreren Millionen Zeilen unnötig viel Speicher, während eine gezielte SQL-Query mit LIMIT und Batch-Verarbeitung auch bei sehr großen Katalogen mit begrenztem Speicherverbrauch auskommt.
Die Registrierung neuer Checks erfolgt über di.xml als virtueller Type innerhalb der Checks-Argumente des CheckPool, wodurch neue Prüfungen additiv ergänzt werden können, ohne den Command selbst anzufassen. Dieses Muster folgt demselben Composite-Gedanken, der auch bei der Filter-Registrierung im FilterPool der Admin-Grids zum Einsatz kommt.
<?php
declare(strict_types=1);
namespace Mironsoft\CatalogIntegrity\Model\Check;
use Magento\Framework\App\ResourceConnection;
/**
* Prueft die Katalog-Datenbank auf verwaiste Eintraege
* in der catalog_product_entity_varchar Attribut-Tabelle.
*/
class OrphanedVarcharValuesCheck implements CheckInterface
{
/**
* @param ResourceConnection $resourceConnection Zugriff auf die Datenbankverbindung
*/
public function __construct(
private readonly ResourceConnection $resourceConnection,
) {
}
/**
* Fuehrt die Pruefung aus und liefert ein strukturiertes Ergebnis.
*
* @return CheckResult
*/
public function run(): CheckResult
{
$connection = $this->resourceConnection->getConnection();
$select = $connection->select()
->from(
['v' => $this->resourceConnection->getTableName('catalog_product_entity_varchar')],
['entity_id', 'attribute_id']
)
->joinLeft(
['e' => $this->resourceConnection->getTableName('catalog_product_entity')],
'e.entity_id = v.entity_id',
[]
)
->where('e.entity_id IS NULL')
->limit(1000);
$rows = $connection->fetchAll($select);
$details = array_map(
static fn (array $row) => sprintf('entity_id=%d attribute_id=%d', $row['entity_id'], $row['attribute_id']),
$rows
);
return new CheckResult(count($rows), $details, isCritical: count($rows) > 0);
}
/**
* Menschenlesbare Bezeichnung des Checks für die CLI-Ausgabe.
*
* @return string
*/
public function getLabel(): string
{
return 'Verwaiste EAV-Varchar-Werte';
}
}
7. Ergebnis-Reporting: CSV-Export und CI-taugliche Ausgabe
Für die menschliche Nachbearbeitung reicht die Konsolenausgabe des Commands meist nicht aus, sobald mehrere hundert oder tausend Zeilen betroffen sind. Eine zusätzliche Option exportiert die Detailergebnisse jedes Checks in eine eigene CSV-Datei, sortiert nach entity_id, sodass ein Data-Analyst oder ein Entwickler die betroffenen Datensätze gezielt nachbearbeiten kann, ohne selbst SQL-Queries schreiben zu müssen.
Für die CI-taugliche Ausgabe reicht dagegen der reine Exit-Code in Kombination mit einer kompakten Zusammenfassungszeile pro Check, da eine Pipeline in der Regel keine Detailzeilen, sondern nur einen Erfolgs- oder Fehlschlag-Status auswertet. Beide Ausgabeformen lassen sich problemlos parallel unterstützen, da sie auf demselben strukturierten CheckResult-Objekt basieren und nicht neu implementiert werden müssen.
8. Abgrenzung zur reinen Reindex-Problematik
Ein weit verbreitetes Missverständnis besteht darin, jedes merkwürdige Katalog-Verhalten reflexartig mit einem vollständigen Reindex zu behandeln. Ein Reindex leitet jedoch lediglich die für Frontend und Suche optimierten Indextabellen aus den bestehenden EAV-Rohdaten ab, er repariert oder entfernt keine fehlerhaften Rohdaten selbst. Sind die zugrunde liegenden EAV-Werte, Store-Zuordnungen oder Kategorie-Pfade bereits inkonsistent, produziert ein Reindex lediglich eine ebenso inkonsistente, aber frisch berechnete Index-Tabelle.
Die praktische Konsequenz ist eine klare Reihenfolge: Erst die Rohdaten über ein Audit-Command wie das hier beschriebene bereinigen, dann erst reindizieren. Wird diese Reihenfolge umgekehrt, wirkt ein Reindex zunächst wie eine Lösung, weil sich einzelne Symptome kurzzeitig verändern, die zugrunde liegende Dateninkonsistenz bleibt aber bestehen und produziert nach dem nächsten planmäßigen Reindex dieselben Symptome erneut.
9. Integration in die Deployment-Pipeline
Der Audit-Command lässt sich als zusätzlicher Schritt in eine GitLab-CI- oder GitHub-Actions-Pipeline einbinden, idealerweise vor einem produktiven Go-Live nach einer größeren Datenmigration oder einem umfangreichen Import. Da der Command mit einem definierten Exit-Code terminiert, lässt sich die Pipeline so konfigurieren, dass ein Deployment bei kritischen Dateninkonsistenzen automatisch gestoppt wird, statt fehlerhafte Daten stillschweigend in die Produktion zu übernehmen.
Für den laufenden Betrieb außerhalb konkreter Deployments lohnt sich zusätzlich ein regelmäßiger, cron-gesteuerter Lauf des Audit-Commands, dessen Ergebnis, ähnlich wie bei einem fehlgeschlagenen automatisierten Import, per E-Mail an das Entwicklerteam gemeldet wird. So werden schleichend entstehende Inkonsistenzen erkannt, lange bevor sie sich als sichtbares Problem im Frontend oder in der Suche zeigen.
| Inkonsistenz-Typ | Erkennungsmethode | Typische Ursache | Reindex behebt das? |
|---|---|---|---|
| Verwaiste EAV-Werte | LEFT JOIN gegen catalog_product_entity | direktes SQL-Löschen, fehlerhafter Import | Nein |
| Fehlende Store-Zuordnung | Abgleich product_website gegen catalog_category_product | Store-View-Umstrukturierung | Nein |
| Kaputter Kategorie-Pfad | Rekonstruktion via parent_id vs. path-Feld | unterbrochenes Verschieben im Baum | Nein |
| Falsches level-Feld | Vergleich Pfadtiefe vs. gespeichertem level | manuelle Datenbank-Eingriffe | Nein |
| Veraltete Index-Einträge | Vergleich Index- gegen Rohdaten | verzögerter oder fehlgeschlagener Reindex | Ja |
Mironsoft
Magento-Entwicklung, Modul-Beratung und Systemarchitektur
Magento-Projekt, das eine zweite Meinung oder erfahrene Umsetzung braucht?
Wir entwickeln individuelle Magento-Module, beraten bei Architekturentscheidungen und übernehmen komplexe Umsetzungen, von der Service-Contract-Planung bis zum produktionsreifen Deployment.
Architektur-Beratung
Modul- und Systemarchitektur vor der Umsetzung fundiert durchdenken lassen.
Custom-Modul-Entwicklung
Individuelle Magento-Module nach Best Practices sauber umsetzen.
Code-Review & Audit
Bestehende Module auf Performance, Sicherheit und Wartbarkeit prüfen lassen.
10. Zusammenfassung
Katalog-Datenintegrität in Magento 2: Das Wichtigste auf einen Blick
Kernbefund
Verwaiste EAV-Werte, fehlende Store-Zuordnungen und kaputte Kategorie-Pfade sind Rohdaten-Probleme, kein Index-Problem.
Werkzeug
Eigenes CLI-Command mit einzeln registrierten Checks, direktem SQL-Zugriff statt Collection-Iteration.
Reporting
CSV-Export für Nachbearbeitung, strukturierter Exit-Code für CI-Pipelines.
Abgrenzung
Erst Rohdaten bereinigen, dann reindizieren, niemals umgekehrt.