{ }
type
GraphQL · Magento · Performance · N+1 · Batching · EAV
Magento GraphQL Performance
Warum Resolver zu langsam werden

Eine langsame GraphQL-Query ist selten GraphQLs Schuld. Die eigentlichen Probleme liegen tiefer: N+1 in Resolver-Ketten, EAV-Tabellen, die für jedes Produkt dutzende Joins erzeugen, und Queries ohne Komplexitätsgrenzen, die den Server beliebig belasten können. Dieser Artikel zeigt, wo man suchen muss und was wirklich hilft.

20 Min. Lesezeit N+1 · Batching · EAV · Complexity-Limits · Profiling · Response-Cache Magento 2.4 · PHP 8.x · GraphQL

1. Der häufigste Performance-Irrtum bei GraphQL

Wenn Magento GraphQL-Queries langsam sind, ist der erste Verdächtige meistens das falsche Ziel: „GraphQL ist langsam" oder „der Endpoint skaliert nicht". In Wirklichkeit ist der GraphQL-Layer selbst – das Parsen der Query, die Validierung gegen das Schema, die JSON-Serialisierung – extrem schnell. Was langsam wird, ist die Schicht darunter: die Datenbankabfragen, die ein Resolver auslöst, und die Art, wie Resolver-Ketten aufgebaut sind.

Dieses Missverständnis hat praktische Konsequenzen: Teams suchen Performance-Gewinne im falschen Bereich – durch Wechsel auf REST oder durch graphql-interne Optimierungen, die kaum messbar sind – statt sich auf die eigentlichen Bottlenecks zu konzentrieren. Ein gut gebauter Magento-GraphQL-Resolver, der eine einzelne, indizierte Datenbankabfrage macht, ist schnell. Ein Resolver, der 50 Produkte lädt und für jedes Produkt separat die EAV-Attribute abfragt, ist langsam – und das hat mit GraphQL nichts zu tun.

2. Das N+1-Problem: die häufigste Ursache

Das N+1-Problem ist das häufigste Performance-Antipattern in GraphQL-Systemen – und in Magento besonders leicht zu reproduzieren. Es entsteht, wenn eine Query N Elemente aus einer Liste lädt (1 Abfrage) und dann für jedes dieser Elemente ein Child-Feld mit einer eigenen Datenbankabfrage auflöst (N weitere Abfragen). Bei einer Produktliste mit 20 Artikeln, die alle related_products anfragen, entstehen so 21 Datenbankabfragen, obwohl eine einzige Batch-Abfrage ausreichend wäre.

In Magento ist das Problem besonders tückisch, weil viele Child-Resolver intern das Repository-Pattern nutzen und dadurch transparent wirken: $this->productRepository->getById($productId) sieht harmlos aus, löst aber für jeden Aufruf eine separate SELECT-Query aus. Die Lösung ist Batching: statt für jedes Element separat zu laden, sammeln die Child-Resolver alle benötigten IDs in einer Struktur und führen am Ende eine einzige Abfrage für alle IDs durch. In Magento wird das typischerweise über einen RequestContext oder eine BatchLoader-Klasse implementiert.


# ANTI-PATTERN: This query triggers N+1 if related_products resolver
# is not batched — 1 query for products + 1 query per product for related items
query ExpensiveWithN1 {
  products(search: "jacket", pageSize: 20) {
    items {
      sku
      name
      related_products {   # Each product triggers a separate DB query
        sku
        name
        price_range { minimum_price { final_price { value } } }
      }
    }
  }
}

# BETTER: Avoid deep nesting if the data is not critical for this view
# Only request what the current page component actually renders
query ProductListOptimized {
  products(search: "jacket", pageSize: 20) {
    total_count
    items {
      sku
      name
      url_key
      price_range { minimum_price { final_price { value currency } } }
      small_image { url label }
    }
  }
}

3. EAV in Magento: versteckte Datenbankkosten

Das Entity-Attribute-Value-Modell (EAV) von Magento ist der wahrscheinlich größte Performance-Faktor bei Produktabfragen über GraphQL. Magento speichert Produktattribute nicht in einer einzigen breiten Tabelle, sondern in typisierten Wertetabellen wie catalog_product_entity_varchar, catalog_product_entity_int und catalog_product_entity_decimal. Für jedes Attribut, das in einer GraphQL-Query angefragt wird, können mehrere Joins entstehen – und das multipliziert mit der Anzahl der Produkte in der Ergebnisliste.

Magento hat für dieses Problem teilweise Gegenmaßnahmen eingebaut: Der Flat-Catalog-Index materialiert die EAV-Daten in eine breite Tabelle, die deutlich effizienter abzufragen ist. Aber der Flat-Catalog ist nicht immer aktiviert, und custom Attribute, die über eigene Module hinzugefügt werden, sind oft nicht im Flat-Catalog enthalten. Für GraphQL-Resolver, die produktbezogene Daten laden, lohnt es sich, gezielt zu messen, wie viele Datenbankabfragen pro Request entstehen – mit dem Magento-Profiler oder einem Query-Logger im Debug-Modus.

4. Query-Tiefe und Komplexität ohne Limit

GraphQL erlaubt von Haus aus beliebig tiefe und komplexe Queries. Eine Query, die Produkte abfragt, für jedes Produkt verwandte Produkte lädt und für jedes verwandte Produkt wieder verwandte Produkte anfrägt, ist syntaktisch valide und würde ohne Schutzmaßnahmen ausgeführt. In Magento kann eine solche Query exponentiell viele Datenbankabfragen auslösen und den Server unter Last setzen – auch für anonyme Requests, die kein Sicherheitstoken benötigen.

Magento 2.4 hat begrenzte eingebaute Schutzmaßnahmen gegen Query-Komplexität. Für produktive Systeme empfiehlt es sich, ergänzend auf der Nginx- oder Varnish-Ebene Request-Limits zu setzen und im Resolver selbst defensive Limits für pageSize-Parameter durchzusetzen. Ein Resolver, der pageSize: 9999 akzeptiert, ohne zu begrenzen, ist eine potenzielle Ressourcenfalle. Die Prüfung sollte im Resolver früh erfolgen und mit einer GraphQlInputException bei Überschreitung des Maximums enden.


# Query complexity example — nested related_products creates exponential resolver calls
# Depth 3: products → related_products → related_products (recursive)
query DangerousDepth {
  products(search: "shoe", pageSize: 50) {
    items {
      sku
      related_products {
        sku
        related_products {
          sku
          price_range { minimum_price { final_price { value } } }
        }
      }
    }
  }
}

# SAFE alternative: flat query, controlled pageSize, no recursive nesting
query ControlledComplexity {
  products(
    search: "shoe"
    pageSize: 12    # Enforce reasonable page sizes in resolver
    currentPage: 1
  ) {
    total_count
    items {
      sku
      name
      price_range { minimum_price { final_price { value } } }
    }
  }
}

5. Performance messen: welche Tools helfen

Bevor man Performance-Probleme in Magento GraphQL behebt, muss man wissen, wo sie liegen. Der Magento-eigene Query-Logger lässt sich im Admin unter Stores > Configuration > Advanced > Developer aktivieren und zeigt alle SQL-Abfragen, die während eines Requests ausgeführt werden, mit Ausführungszeit. Für eine GraphQL-Produktlistenabfrage, die 50 SQL-Queries auslöst, ist das die schnellste Methode, das Ausmaß des Problems zu sehen.

Für tiefergehende Analyse bieten sich APM-Tools wie New Relic, Blackfire.io oder die Magento-eigene Profiling-Infrastruktur an. Blackfire ist besonders hilfreich, weil es Resolver-zu-Resolver-Profiling auf PHP-Ebene zeigt und damit sichtbar macht, in welcher Klasse und welcher Methode die meiste Zeit verbracht wird. Ein Profiling-Lauf für eine typische Produktlisten-Query zeigt oft, dass 80 % der Zeit in Repository-Calls oder EAV-Ladevorgängen verbracht wird – nicht in der GraphQL-Schicht selbst.

6. Batching-Strategien: IDs sammeln, einmal laden

Batching ist die wirksamste Strategie gegen N+1 in Magento GraphQL. Das Grundprinzip: statt jedes Child-Element einzeln aus der Datenbank zu laden, sammelt ein BatchLoader alle benötigten IDs während der Resolver-Ausführung und führt am Ende eine einzige Abfrage für alle IDs durch. Dieses Pattern erfordert eine Umstrukturierung der typischen Resolver-Implementierung, zahlt sich aber bei Listen ab 5 Elementen bereits messbar aus.

In der Praxis implementiert man Batching in Magento häufig über eine RequestContext-Klasse, die als Singleton im DI-Container lebt und während eines Requests IDs sammelt. Der erste Aufruf eines Child-Resolvers registriert eine ID; nachfolgende Aufrufe fügen ihre IDs zum BatchLoader hinzu. Nach der vollständigen Resolver-Kette werden alle gesammelten IDs in einer Abfrage geladen. Das Magento-Framework bietet keine direkte DataLoader-Abstraktion wie GraphQL.js, aber das Pattern ist mit wenig Code selbst implementierbar.

7. Query-Patterns im Performance-Vergleich

Nicht jede GraphQL-Query ist gleich teuer. Die Strukturierung der Query hat direkten Einfluss auf die Anzahl der Datenbankabfragen, die auf dem Server entstehen. Die folgende Tabelle zeigt typische Muster und ihre Performance-Implikationen.

Query-Pattern Datenbankabfragen Performance-Risiko Empfehlung
Flache Produktliste ohne Child-Felder 1–2 Queries Gering Ideal für Listen-Ansichten
Produkte + Preise (gecacht) 1–3 Queries Gering Standard-Pattern für PLPs
Produkte + related_products (unbatched) N+1 pro Produkt Sehr hoch Batching implementieren oder vermeiden
Tiefe Verschachtelung (3+ Ebenen) Exponentiell Kritisch Query-Tiefe begrenzen, Complexity-Limit setzen
pageSize: 200 ohne Limit 200× Child-Resolver Sehr hoch pageSize im Resolver auf max. 48 begrenzen

8. Response-Caching und feldspezifischer Cache

Magento unterstützt Response-Caching für anonyme GraphQL-Queries über Varnish oder den integrierten Full-Page-Cache. Wenn eine Produktlisten-Query gecacht ist, wird die gesamte Response aus dem Cache bedient, ohne dass ein einziger PHP-Request oder eine Datenbankabfrage stattfindet. Das ist die wirksamste Performance-Maßnahme für öffentlich zugängliche Produktdaten – und gleichzeitig die am häufigsten falsch konfigurierte.

Der Response-Cache greift nur, wenn die @cache-Annotation im Schema korrekt konfiguriert ist und die Identity-Klasse die richtigen Cache-Tags setzt. Fehlt die Identity-Klasse oder gibt sie leere Tags zurück, wird jeder Request neu berechnet. Für kundenbezogene Queries – Warenkorb, Kundenkonto, Wishlist – greift der Response-Cache grundsätzlich nicht. Hier hilft feldspezifisches Caching auf Service-Ebene: Produktdetails, die sich selten ändern, können im Resolver mit einem kurzen TTL gecacht werden, um wiederholte EAV-Abfragen zu vermeiden.

9. pageSize kontrollieren: kleine Grenzen, große Wirkung

Einer der einfachsten Performance-Hebel in Magento GraphQL ist die Kontrolle des pageSize-Parameters in Resolver-Implementierungen. Standardmäßig ist die Magento-API so konfiguriert, dass ein pageSize: 300-Request syntaktisch valide ist und ausgeführt wird. Das bedeutet: 300 Produkte werden geladen, ihre Preise berechnet, ihre Attribute aus EAV-Tabellen zusammengezogen, ihre Bilder aufgelöst und in die Response serialisiert.

Ein eigener Resolver sollte den pageSize-Parameter immer gegen einen konfigurierbaren Maximalwert prüfen und bei Überschreitung eine GraphQlInputException werfen. Für die meisten Use Cases sind 12–24 Produkte pro Seite ausreichend und sinnvoll. Darüber hinaus lohnt es sich, die Magento-interne pageSize-Konfiguration über den Admin zu überprüfen und bei Bedarf anzupassen. Diese einzelne Maßnahme kann die durchschnittliche Response-Zeit einer Produktlisten-Query um Faktoren reduzieren.


# Controlled product list query with explicit pagination and minimal fields
# This pattern is safe for production: bounded pageSize, no deep nesting
query ProductListSafe {
  products(
    filter: { category_id: { eq: "15" } }
    pageSize: 24
    currentPage: 1
    sort: { name: ASC }
  ) {
    total_count
    page_info {
      current_page
      page_size
      total_pages
    }
    items {
      id
      sku
      name
      url_key
      price_range {
        minimum_price {
          regular_price { value currency }
          final_price { value currency }
          discount { amount_off percent_off }
        }
      }
      small_image { url label }
      rating_summary
    }
  }
}

10. Zusammenfassung

Langsame Magento GraphQL-Resolver haben fast immer dieselben Ursachen: N+1 in Resolver-Ketten, EAV-Abfragen ohne Batching, fehlende pageSize-Limits und nicht genutzter Response-Cache. Das GraphQL-Framework selbst ist dabei nicht das Problem – der Overhead liegt in den Datenbankabfragen, die Resolver auslösen. Profiling mit dem SQL-Query-Logger oder Blackfire zeigt schnell, wo die Zeit wirklich verbracht wird.

Die wirksamsten Maßnahmen in der Praxis: pageSize im Resolver deckeln, Batching für Child-Resolver implementieren, Response-Cache für anonyme Queries aktivieren und korrekt konfigurieren sowie tiefe Verschachtelungen in Queries verhindern. Jede dieser Maßnahmen kann die Performance einer typischen Produktlisten-Query um 50–80 % verbessern – ohne die GraphQL-API grundlegend zu verändern.

Magento GraphQL Performance — Das Wichtigste auf einen Blick

N+1 identifizieren

SQL-Query-Logger aktivieren: mehr als 10 Queries pro GraphQL-Request ist ein klares Signal für N+1 oder fehlende Batching-Strategie.

EAV-Overhead

Flat-Catalog aktivieren, benutzerdefinierte Attribute auf den Flat-Catalog abbilden und EAV-Abfragen im Resolver durch gecachte Zwischenschicht entlasten.

pageSize begrenzen

Maximale pageSize im Resolver prüfen und mit GraphQlInputException begrenzen. Empfehlung: max. 24–48 für Produktlisten.

Response-Cache

Für anonyme Queries immer @cache mit Identity-Klasse konfigurieren. Der Cache-Hit macht Datenbankabfragen vollständig überflüssig.

11. FAQ: Magento GraphQL Performance

1Ist GraphQL generell langsamer als Magento REST?
Nein. GraphQL kann bei schlechtem Resolver-Design langsamer sein, aber gut implementierte Resolver sind vergleichbar schnell. Der Unterschied liegt im Design und Caching, nicht im Protokoll.
2Wie erkenne ich ein N+1-Problem?
SQL-Query-Logger aktivieren und eine GraphQL-Query ausführen. Wenn die Anzahl der SQL-Queries proportional zur Ergebnismenge steigt, liegt N+1 vor.
3Was ist Batching und wie implementiere ich es?
Alle IDs in einem BatchLoader sammeln, dann in einer einzigen Datenbankabfrage laden. In Magento über eine Singleton-Klasse im DI-Container implementierbar.
4Warum ist EAV ein Performance-Problem?
EAV speichert Attribute in typisierten Tabellen mit JOIN-Operationen. Für 20 Produkte mit 30 Attributen können hunderte SQL-Operationen entstehen. Flat-Catalog oder Caching helfen.
5Wie begrenzt man pageSize in einem Resolver?
Im resolve(): if ($args['pageSize'] > 48) { throw new GraphQlInputException(...); }. Maximalwert konfigurierbar halten.
6Für welche Queries funktioniert Response-Caching?
Nur anonyme Queries ohne Authorization-Header. Produktlisten, Kategorien, CMS. Warenkorb, Kundenkonto und Wishlist werden nie gecacht.
7Wie aktiviere ich den SQL-Query-Logger?
Admin: Stores > Configuration > Advanced > Developer > Debug > Log DB Queries. Oder: bin/magento dev:query-log:enable. Logs in var/debug/db.log.
8Hilft Flat-Catalog bei GraphQL-Performance?
Ja, erheblich. Flat-Catalog materialisiert EAV-Daten in eine breite Tabelle – ein SELECT statt dutzender JOINs. Muss aktiviert und regelmäßig reindiziert werden.
9Welche Tiefe sollte eine Query maximal haben?
Faustregel: maximal 4 Ebenen. Jede weitere Ebene multipliziert potenziell die Resolver-Aufrufe. Tiefere Verschachtelungen durch separate Queries oder Batching ersetzen.
10Kann Response-Caching mit Varnish kombiniert werden?
Ja. Varnish cacht GraphQL-Responses, wenn Cache-Tags korrekt in Response-Headern gesetzt werden. Magento setzt sie über die Identity-Klasse. Varnish muss für POST-Requests konfiguriert sein.