Mojí hlavní motivací při vytváření NotORM bylo nabídnout jednoduché API, které pokud možno nedovolí vytváření neefektivních dotazů (při správně navržené databázi). Často jsem totiž vídal postup, kdy se v každém průchodu cyklem kladly pořád dokola ty stejné dotazy, jen s různými parametry. Výsledek tak mohl vypadat třeba takhle (možná poznáte titulní stránku tohoto blogu):
SELECT * FROM clanek LIMIT 5;
SELECT * FROM autor WHERE id = 11;
SELECT * FROM skupina WHERE id = 21;
SELECT COUNT(*) FROM komentar WHERE clanek_id = 1;
SELECT * FROM autor WHERE id = 11;
SELECT * FROM skupina WHERE id = 22;
SELECT COUNT(*) FROM komentar WHERE clanek_id = 2;
SELECT * FROM autor WHERE id = 11;
SELECT * FROM skupina WHERE id = 23;
SELECT COUNT(*) FROM komentar WHERE clanek_id = 3;
SELECT * FROM autor WHERE id = 11;
SELECT * FROM skupina WHERE id = 22;
SELECT COUNT(*) FROM komentar WHERE clanek_id = 4;
SELECT * FROM autor WHERE id = 11;
SELECT * FROM skupina WHERE id = 21;
SELECT COUNT(*) FROM komentar WHERE clanek_id = 5;
SELECT * FROM skoleni;
SELECT * FROM skoleni_termin WHERE skoleni = 31;
SELECT SUM(pocet) FROM skoleni_prihlaska WHERE skoleni_termin_id = 51;
SELECT * FROM skoleni_termin WHERE skoleni = 32;
SELECT SUM(pocet) FROM skoleni_prihlaska WHERE skoleni_termin_id = 52;
SELECT * FROM skoleni_termin WHERE skoleni = 33;
SELECT SUM(pocet) FROM skoleni_prihlaska WHERE skoleni_termin_id = 53;
SELECT * FROM skoleni_termin WHERE skoleni = 34;
SELECT SUM(pocet) FROM skoleni_prihlaska WHERE skoleni_termin_id = 54;
SELECT * FROM skoleni_termin WHERE skoleni = 35;
SELECT SUM(pocet) FROM skoleni_prihlaska WHERE skoleni_termin_id = 55;
SELECT * FROM skoleni_termin WHERE skoleni = 36;
SELECT SUM(pocet) FROM skoleni_prihlaska WHERE skoleni_termin_id = 56;
SELECT SUM(pocet) FROM skoleni_prihlaska WHERE skoleni_termin_id = 57;
SELECT COUNT(*) FROM clanek;
SELECT * FROM prace_platnost WHERE NOW() BETWEEN platne_od AND platne_do;
SELECT * FROM prace WHERE id = 41;
SELECT * FROM prace WHERE id = 42;
SELECT * FROM skupina;
SELECT COUNT(*) FROM clanek WHERE skupina_id = 21;
SELECT COUNT(*) FROM clanek WHERE skupina_id = 22;
SELECT COUNT(*) FROM clanek WHERE skupina_id = 23;
SELECT COUNT(*) FROM clanek WHERE skupina_id = 24;
SELECT COUNT(*) FROM clanek WHERE skupina_id = 25;
SELECT COUNT(*) FROM clanek WHERE skupina_id = 26;
SELECT COUNT(*) FROM clanek WHERE skupina_id = 27;
SELECT COUNT(*) FROM clanek WHERE skupina_id = 28;
SELECT COUNT(*) FROM clanek WHERE skupina_id = 29;
SELECT * FROM clanek WHERE vyber = 1;
Na to, jak je stránka jednoduchá, se položilo příšerných 45 dotazů. Řešit se to dá dvěma způsoby:
V NotORM jsem proto zvolil druhý způsob, jehož výsledkem jsou tyto dotazy:
SELECT id, autor_id, skupina_id, nadpis, clanek FROM clanek LIMIT 5;
SELECT id, jmeno FROM autor WHERE id IN (11);
SELECT id, nazev FROM skupina WHERE id IN (21, 22, 23);
SELECT clanek_id, COUNT(*) FROM komentar WHERE clanek_id IN (1, 2, 3, 4, 5) GROUP BY clanek_id;
SELECT id, nazev FROM skoleni;
SELECT skoleni_id, datum FROM skoleni_termin WHERE skoleni_id IN (31, 32, 33, 34, 35);
SELECT skoleni_termin_id, SUM(pocet) FROM skoleni_prihlaska WHERE skoleni_termin_id IN (51, 52, 53, 54, 55, 56) GROUP BY skoleni_termin_id;
SELECT COUNT(*) FROM clanek;
SELECT id, prace_id FROM prace_platnost WHERE NOW() BETWEEN platne_od AND platne_do;
SELECT id, popis, url FROM prace WHERE id IN (41, 42);
SELECT id, nazev FROM skupina;
SELECT skupina_id, COUNT(*) FROM clanek WHERE skupina_id IN (21, 22, 23, 24, 25, 26, 27) GROUP BY skupina_id;
SELECT id, nadpis FROM clanek WHERE vyber = 1;
Počet dotazů jsme zredukovali na 13, sloučením vztahů 1:1 (v tomto případě to jsou dotazy s GROUP BY
) bychom počet dotazů mohli za cenu mírného zesložitění kódu zredukovat na 10.
Při postupu využívajícím spojování tabulek bychom se dostali nejlépe na 6 dotazů, níž už ale ne, protože těch 6 skupin (v kódu oddělených prázdným řádkem) spolu nijak nesouvisí.
Jak to dále vylepšit? Stačí vyjít z pozorování, že pro rychlost často není hlavní počet pokládaných dotazů, ale počet round-tripů. Obvykle to jde ruku v ruce, ale při využití funkce mysqli_multi_query nebo v PDO to platit nemusí – během každé komunikace lze položit dotazů několik. Takže to, co nás dosud brzdilo (počet nezávislých dotazů), můžeme využít ve svůj prospěch:
Počet dotazů tedy zůstane stejný, ale počet komunikací s databází se zredukuje na maximální počet na sobě závislých dotazů (v příkladech znázorněný odsazením):
SELECT id, autor_id, skupina_id, nadpis, clanek FROM clanek LIMIT 5; SELECT id, nazev FROM skoleni; SELECT COUNT(*) FROM clanek; SELECT id, prace_id FROM prace_platnost WHERE NOW() BETWEEN platne_od AND platne_do; SELECT id, nazev FROM skupina; SELECT id, nadpis FROM clanek WHERE vyber = 1;
SELECT id, jmeno FROM autor WHERE id IN (11); SELECT id, nazev FROM skupina WHERE id IN (21, 22, 23); SELECT clanek_id, COUNT(*) FROM komentar WHERE clanek_id IN (1, 2, 3, 4, 5) GROUP BY clanek_id; SELECT skoleni_id, datum FROM skoleni_termin WHERE skoleni_id IN (31, 32, 33, 34, 35, 36); SELECT id, popis, url FROM prace WHERE id IN (41, 42); SELECT skupina_id, COUNT(*) FROM clanek WHERE skupina_id IN (21, 22, 23, 24, 25, 26, 27) GROUP BY skupina_id;
SELECT skoleni_termin_id, SUM(pocet) FROM skoleni_prihlaska WHERE skoleni_termin_id IN (51, 52, 53, 54, 55, 56) GROUP BY skoleni_termin_id;
Skončili jsme na třech komunikacích s databází, při využití spojování tabulek bychom se mohli snadno dostat i na dvojku, za cenu opakovaného přenášení stejných dat dokonce i na jedinou komunikaci s databází.
Jak bude vypadat API, které něco takového umožní? Potřebujeme zaregistrovat dotaz, který se provede později spolu s ostatními a po jeho provedení se spustí nějaká obsluha (která si může zaregistrovat další dotazy). Inspirovat se lze u node.js. (Používám databázi ze srovnání NotORM s Doctrine 2.)
<?php NotORM::then($db->article[174], function ($article) { echo "<h1>" . htmlspecialchars($article["title"]) . "</h1>\n"; $article->article_tag()->then(function ($article_tags) { // zkratka za NotORM::then($article->article_tag(), ...) echo "<ul>\n"; foreach ($article_tags as $article_tag) { NotORM::then($article_tag->tag, function ($tag) { echo "<li>" . htmlspecialchars($tag["name"]) . "</li>\n"; }); } echo "</ul>\n"; }); }); ?>
NotORM 2 vyvíjím v branchi then.
Psát anonymní funkce je otrava. A to ani nemluvím o PHP starším než 5.3, kde by psát kód v tomto API byla ještě větší otrava. O něco by to vylepšily anonymní bloky, ty ale v oficiálním PHP nejsou. Skoro by to chtělo tento kód generovat…
Prostředí, které nám dovoluje generovat kód, je vždy po ruce a je snadno rozšiřitelné, jsou v Nette šablony. S nimi se callbacků můžeme úplně zbavit a kód psát skoro stejně jako u sekvenčního zpracování:
{* $article dostaneme *}
<h1>{$article['title']}</h1>
<ul n:inner-foraside="$article->article_tag() as $article_tag">
<li n:with="$article_tag->tag as $tag">{$tag['name']}</li>
</ul>
<p n:with="$article->category as $category">{$category['name']}</p>
Definice maker je jednoduchá:
<?php $latte = new LatteFilter; $set = new MacroSet($latte->compiler); $set->addMacro('with', function ($node, $writer) { $then = "function("; if (preg_match('~(.*\S)(\s+)as(\s+)(.+)~s', $node->args, $match)) { $then = "$match[1],$match[2]$then$match[4]"; } elseif ($node->args) { throw new CompileException("Missing ' as ' in {with} macro."); } return "NotORM::then($then) {"; }, "});"); $set->addMacro('foraside', function ($node, $writer) { if (!preg_match('~(.*\S)(\s+)as(\s+)(.+)~s', $node->args, $match)) { throw new CompileException("Missing ' as ' in {foraside} macro."); } return "$match[1]->thenForeach(function ($match[4]) {"; }, "});"); ?>
Celá myšlenka jde zobecnit na všechno, co se vyplatí provádět v dávce – zásadní přínos to bude mít také např. u Memcache.
Udělal jsem si laboratorní experiment, který by mohl ukázat, že celá rošáda s multidotazy nemá smysl, protože jejich režie je příliš vysoká. Cílem bylo získat osm řádek ze stejné tabulky na vzdáleném serveru (tedy ne localhost
) podle indexu. Záměrně jsem vybral takto jednoduchou úlohu, abych mohl porovnat čas multidotazů s jednoduššími dotazy, které by v obecném případě (dotazy do různých tabulek) ani nešly použít. Výsledky mě mile překvapily:
0.01414 s | mysqli_query(" = ") x 8 |
---|---|
0.00199 s | mysqli_query(" IN ()") |
0.00256 s | mysqli_query(" UNION ALL ") |
0.00237 s | mysqli_multi_query("; ") |
Multidotazy se zpracovaly rychleji než UNION
a skoro stejně rychle jako jeden dotaz využívající IN
, především ale řádově rychleji než osm jednotlivých dotazů.
Diskuse je zrušena z důvodu spamu.