Jak psát kód: Udělejte práci najednou

Školení, která pořádám

Jde o jeden z nejčastějších problémů s výkonností aplikace a zároveň obvykle jde o low hanging fruit, který lze snadno vyřešit. Jde o to, že žádost o provedení práce obvykle něco stojí a často to může být víc, než kolik stojí samotná práce. Platí to zejména tehdy, když práce obsahuje nějakou komunikaci po síti, např. databázový dotaz. Takže nemusí až tolik záležet na tom, jaké databázové dotazy kladete, ale kolik jich kladete.

Problém s tímto pravidlem je, že je obvykle mnohem jednodušší napsat přímočarý kód, který provádí práci hned, než kód, který práci dělá najednou. Porovnejte následující dvě ukázky:

<?php
// provádění práce hned
$articles = $db->query('SELECT * FROM article LIMIT 10');
foreach ($articles as $article) {
    $author = $db->query('SELECT * FROM author WHERE id = ?', $article['author_id'])->fetch();
}

// provádění práce hromadně
$articles = $db->query('SELECT * FROM article LIMIT 10');
$author_ids = array();
foreach ($articles as $article) {
    $author_ids[$article['author_id']] = null;
}
$authors = array();
if ($author_ids) {
    foreach ($db->query('SELECT * FROM author WHERE author_id IN (?)', array_keys($author_ids)) as $author) {
        $authors[$author['id']] = $author;
    }
}
foreach ($articles as $article) {
    $author = $authors[$article['author_id']];
}
?>

Správným řešením je opět použít vhodnou abstrakci, která špinavou práci provede za nás. Tento problém byl hlavní důvod, proč jsem vymyslel NotORM, kde se stejně efektivní kód dá napsat přímočarým a ještě jednodušším způsobem:

<?php
$articles = $db->article()->limit(10);
foreach ($articles as $article) {
    $author = $article->author; // provede jen jeden dotaz za celý cyklus
}
?>

Tento problém se netýká zdaleka jen provádění databázových dotazů. Pokud třeba indexujete data do Elasticsearch, tak můžete index obnovit při každém zaindexovaném dokumentu nebo až v okamžiku, kdy zaindexujete všechny. Může jít třeba jen o jeden přesunutý řádek, který kód zrychlí tisícinásobně.

Stejně tak pokud třeba v JavaScriptu vytváříte HTML kód z fragmentů, tak můžete mnohokrát změnit innerHTML (např. v jQuery metodou .after()). Nebo si fragmenty můžete naskládat do pole a to na konci zřetězit a do dokumentu vložit najednou. Opět jde o jednoduchou změnu, která výkonnost dramaticky vylepší.

Nevýhoda tohoto přístupu spočívá ve větším množství zabrané paměti, té je ale obvykle dost. Navíc když už je kód napsán správně, tak se to dá obvyle snadno regulovat, např. prostou změnou LIMIT 1000 na LIMIT 100.

Přijďte si o tomto tématu popovídat na školení Výkonnost webových aplikací.

Jakub Vrána, Dobře míněné rady, 3.6.2013, diskuse: 7 (nové: 0)

Diskuse

ikona Martin:

Je tohle vazne nejlepsi zpusob jak vytvorit mnozinu id autoru?

foreach ($articles as $article) {
    $author_ids[$article['author_id']] = null;
}

ikona Jakub Vrána OpenID:

V PHP 5.5 jde použít http://php.net/array_column, do té doby je tohle asi nejlepší.

kolemjdoucí:

Nešlo by provést dotaz jedním volání přes join, místo toho, abych volal databázi vícekrát? Neznám moc MySQL, ale jinde by to určitě šlo - včetně případného stránkování (tj. chci vrátit informace k autorům pro x záznamů počínaje y).

Pokud by tabulka s autory byla skutečně široká, pak bych při vytvoření složeného indexu (id, jmeno, prijmení) šel pouze do indexu bez zbytečného čtení tabulky. Vše jedním dotazem na minimum čtení.

PS: Nemyslím to jako flame MySQL vs. zbytek platforem - skutečně mě to zajímá.

ikona Jakub Vrána OpenID:

Šlo by to, samozřejmě i v MySQL. Nevýhoda je, že se data o autorech budou přenášet opakovaně. Psal jsem o tom třeba v http://php.vrana.cz/srovnani-dotazu-do-zavislych-tabulek.php.

ikona Jakub Vrána OpenID:

Lepší příklad by byl třeba s nálepkami (tagy). Každý článek může mít libovolné množství nálepek. Přenášeli bychom celý text článku kvůli každé nálepce? Asi ne, spíš bychom položili dva až tři dotazy.

kolemjdoucí:

Nevím, jestli to je zrovna lepší příklad. Pokud bych měl tabulku ČLÁNKY (id, abstrakt, text, atd.), TAGY (id článku, id tagu) a TAG_POPIS (id tagu, popisek), pak jsem schopen načíst seznam článků (včetně popisků tagů), aniž bych musel nutně načítat text. Samozřejmě lze předpokládat, že úvodní stranu bych asi "cachoval", protože ji zobrazí většina čtenářů.

Pokud už má něco charakter číselníku (popisky tagů), pak to mohu rovnou mít "cachováno" lokálně.

ikona Jakub Vrána OpenID:

Potřebuji zobrazit jak články (včetně kompletního textu), tak jejich tagy.

Kešování není alternativa k rozumnému pokládání dotazů do databáze, ale jen doplněk. Keš je potřeba invalidovat a když se tak stane, tak je potřeba ji vhodně položeným dotazem zase obnovit.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.