Performance-Optimierung
Performance-Optimierung
~17 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
Bevor wir in Kapitel 48 deployen, ein systematischer Blick auf Performance – insbesondere das N+1-Query-Problem, eine der häufigsten Ursachen für langsame Symfony/Doctrine-Anwendungen.
Den Symfony Profiler nutzen
In der dev-Umgebung (Kapitel 4) zeigt die Debug-Toolbar am unteren Bildschirmrand JEDE Anfrage im Überblick – ein Klick öffnet den vollständigen Profiler mit Details zu:
- Gesamt-Ausführungszeit UND Aufschlüsselung nach Phase (Routing, Controller, Rendering).
- ALLEN ausgeführten Datenbank-Queries, inklusive tatsächlicher Ausführungszeit JEDER einzelnen Query.
- Speicherverbrauch.
- Geladenen Services UND deren Erzeugungsreihenfolge.
Das N+1-Query-Problem erkennen
Ein KLASSISCHES Performance-Problem, demonstriert an einer Projektliste mit Aufgaben-Anzahl:
{% for projekt in projekte %}
<li>{{ projekt.name }} ({{ projekt.tasks|length }} Aufgaben)</li>
{% endfor %}Achtung: Sieht harmlos aus, ist es aber NICHT: projekt.tasks löst pro Schleifendurchlauf eine EIGENE Datenbank-Query aus (Doctrines "Lazy Loading", Kapitel 22) – bei 50 Projekten sind das EINE Query für die Projektliste PLUS 50 WEITERE Queries für die jeweiligen Aufgaben, statt EINER einzigen, effizienten Abfrage. Der Profiler aus diesem Kapitel macht GENAU dieses Muster als "51 Queries" sofort sichtbar.
Die Lösung: Eager Loading mit JOIN
/**
* @return Project[]
*/
public function findAlleMitAufgabenAnzahl(): array
{
return $this->createQueryBuilder('p')
->leftJoin('p.tasks', 't')
->addSelect('t')
->getQuery()
->getResult()
;
}addSelect('t') ist der ENTSCHEIDENDE Unterschied zum reinen leftJoin aus Kapitel 24: OHNE addSelect würde Doctrine den JOIN nur zum FILTERN nutzen, die Task-Objekte selbst aber weiterhin SEPARAT nachladen. MIT addSelect('t') werden Projekte UND ihre Aufgaben in EINER EINZIGEN Query geladen – aus 51 Queries werden 1.
Die Query-Anzahl im Profiler vergleichen
Tipp: Rufen Sie die Projektliste VOR und NACH der Umstellung im Profiler auf und vergleichen Sie die Anzahl der Datenbank-Queries – DIESE messbare Zahl, nicht nur das subjektive "fühlt sich schneller an", ist der verlässliche Nachweis einer echten Verbesserung.
Weitere häufige Performance-Fallen
| Falle | Beschreibung |
|---|---|
| Fehlende Datenbank-Indizes | Ein Feld, nach dem HÄUFIG gefiltert/sortiert wird (z. B. status in Kapitel 24), aber OHNE Index, führt zu langsamen Full-Table-Scans bei wachsender Datenmenge. |
| Ungenutzte Ergebnisse laden | findAll() nutzen, wenn nur 10 von 10.000 Zeilen gebraucht werden – IMMER Pagination oder gezielte WHERE-Bedingungen verwenden. |
| Fehlendes Caching | Kapitel 45/46 wiederholt berechnete/abgefragte Werte NICHT nutzen, obwohl sie sich SELTEN ändern. |
Einen fehlenden Index ergänzen
#[ORM\Entity(repositoryClass: TaskRepository::class)]
#[ORM\Index(columns: ['status'], name: 'idx_task_status')]
class Task
{
// ...
}php bin/console make:migration
php bin/console doctrine:migrations:migrateGENAU derselbe Workflow aus Kapitel 20 – ein Index ist letztlich eine Datenbank-Struktur-Änderung wie jede andere, versioniert per Migration.