Praktický návod

Jak vyřešit N+1 problém v Doctrine

Nezačínej slepým přidáváním JOINů. Nejdřív změř dotazy a potom načti přesně ta data, která daný případ použití potřebuje.

20 minut · Doctrine ORM

Nejdřív stručně

Proč jeden seznam spouští desítky dotazů?

Doctrine ORM načte hlavní entity jedním dotazem. Když potom v cyklu sáhneš na lazy vazbu každé entity, spustí další dotaz pro každý řádek. Jeden dotaz plus N dalších je problém N+1.

Správná oprava závisí na tvaru dat. Vazbu na jednu entitu často vyřeší fetch JOIN, ale kolekce mohou násobit řádky a rozbít stránkování. U nich bývá bezpečnější stránkovat hlavní entity a související data načíst druhým dávkovým dotazem.

Připrav si

Co budeš potřebovat

Potřebuješ konkrétní pomalou stránku nebo use case. Bez měření snadno optimalizuješ vztah, který problém nezpůsobuje.

  • Symfony projekt s Doctrine ORM a reprezentativními testovacími daty.
  • Symfony profiler nebo SQL logger, ve kterém uvidíš počet a podobu dotazů.
  • Konkrétní repository metodu a místo, které prochází entity nebo jejich kolekce.
  • Test očekávaného výsledku; optimalizace nesmí změnit oprávnění, pořadí ani počet položek.

Kroky 1 až 3

Najdi a odstraň nadbytečné dotazy

Cílem není jediný dotaz za každou cenu. Cílem je malý a předvídatelný počet dotazů bez zbytečně velkého výsledku.

1. Potvrď N+1 měřením

  1. Otevři problémovou stránku v dev prostředí a v profileru projdi Doctrine dotazy.
  2. Hledej jeden dotaz na seznam a potom opakovaný dotaz se stejným SQL, ve kterém se mění jen identifikátor.
  3. Najdi řádek aplikace, který lazy loading spouští. Často je to getter asociace uvnitř šablony, serializeru nebo mapování na DTO.
  4. Zapiš si výchozí počet dotazů a velikost výsledku. Bez této hodnoty opravu spolehlivě neověříš.
php bin/console debug:config doctrine dbal
Oficiální Symfony dokumentace k profileru

2. Zvol načtení podle typu vazby

  1. Pro vazbu many-to-one nebo one-to-one přidej JOIN a zahrň alias vazby do SELECTu. Doctrine ji pak načte jako fetch JOIN bez dalšího dotazu.
  2. Pro čtecí obrazovku, která nepotřebuje celé entity, zvaž DTO projekci. Vybere jen potřebné sloupce a neaktivuje lazy vazby později.
  3. U kolekce one-to-many nepovažuj fetch JOIN za univerzální řešení. Násobí řádky hlavní entity, zvyšuje paměť a komplikuje LIMIT i OFFSET.
  4. Stránkuj nejdřív identifikátory hlavních entit. Kolekce pro danou stránku načti druhým dotazem s IN (:ids) a seskup je v aplikaci.
SELECT o, c FROM App\Entity\Order o JOIN o.customer c WHERE o.id = :id
Oficiální Doctrine dokumentace k JOINům v DQL

3. Uzavři načítání do repository

  1. Vytvoř repository metodu pojmenovanou podle use case, například findOrdersForOverview(). Tvar načtení tak nebude schovaný v šabloně.
  2. Vrať entity s explicitně načtenými vazbami nebo samostatné read DTO. Nekombinuj oba přístupy bez jasného důvodu.
  3. Pro stránkovaný fetch JOIN kolekce použij Doctrine Paginator a ověř jeho nastavení; u složitých dotazů raději použij dvoufázové načtení.
  4. Přidej integrační test repository a rozumný limit počtu dotazů v testu obrazovky, která už jednou regresi způsobila.
php bin/phpunit --filter OrderOverview
Oficiální Doctrine dokumentace ke stránkování

Krok 4

Ověř počet dotazů i správnost dat

Rychlejší SQL nestačí. Výsledek musí zůstat úplný, seřazený a správně stránkovaný.

  1. Porovnej profiler před a po změně

    Pro stejnou stránku a stejná data ověř, že zmizela opakovaná SQL a počet dotazů už neroste s počtem položek.

  2. Otestuj více velikostí stránky

    Vyzkoušej první, prostřední i poslední stránku. Žádná hlavní entita nesmí kvůli JOINu zmizet ani se objevit dvakrát.

    php bin/phpunit --filter Pagination
  3. Změř objem a čas dotazů

    Sleduj nejen počet SQL, ale i počet vrácených řádků, paměť a čas hydratace. Jeden obrovský JOIN může být horší než dva malé dotazy.

Když to zlobí

Nejčastější chyby

JOIN je v DQL, ale další dotazy stále vznikají

Samotný JOIN nemusí asociaci načíst. U fetch JOINu přidej alias asociace do SELECTu a ověř v profileru skutečný výsledek.

Po JOINu chybí položky nebo nesedí stránkování

To-many kolekce násobí SQL řádky před aplikací limitu. Stránkuj hlavní entity odděleně a kolekce načti dávkově, případně správně použij Doctrine Paginator.

Jeden dotaz spotřebuje příliš paměti

Nenačítej celý graf entit. Použij menší DTO projekci nebo dva cílené dotazy místo širokého JOINu několika kolekcí.

N+1 vzniká až při serializaci

Serializer prochází lazy vazby, které repository nepřipravilo. Vrať explicitní výstupní DTO a mapuj jen pole patřící do kontraktu odpovědi.

Hotovo

Počet dotazů je teď předvídatelný.

Doctrine ORM načítá data podle konkrétního use case, ne náhodou při průchodu objekty. Stejné měření přidej ke každé důležité seznamové obrazovce.

Zavolejte mi

Zavolám vám následující pracovní den mezi 9:00 a 17:00.

Můžete mi také zavolat rovnou.

+420 605 181 728

Nechte mi telefonní číslo a pošlete žádost o zpětné zavolání.

Odesláním souhlasíte se zpracováním údajů pro vyřízení žádosti.