Jednoduché problémy mívají většinou složité řešení
Dějství druhé: K. bude dlouze trousit moudra o programování, optimalizaci databází, pomalosti dibi a další zcela zásadní životní pravdy, pak zapomene, co chtěl říct dál, ale vysolí dva stripy na dřevo, aby se neřeklo a půjde spát.
Hm.
V poslední době se střídavě vracím k jednomu triviálnímu problému, který nemá žádný praktický užitek. Představte si, že mám nějaké stránky – třeba k47čku, kde je hodně povídek a já chci zjistit, jaká konkrétní slova se v nich vyskytují, kolikrát je které použito a taky si vést tyto záznamy v rámci jedné povídky (tedy jaké slovo, kolikrát ve které povídce) a všechno to uložit nějak rozumně do databáze. V tom není žádný problém. Tedy… Vlastně je a rovnou dva. Zaprvé jak to vůbec udělat (například jak získat jednotlivá slova a co to slovo vlastně je) a jak to udělat rychlé. Právě druhý problém, ačkoli je čistě akademický – tedy zbytečný, mi nedával spát a chtěl jsem ho vyřešit a dotáhnout k dokonalosti. Chtěl jsem zkrátka aby to bylo kurva rychlý.
K dispozici mám PHP a server, kde skript může běžet 60 vteřin a může alokovat si 64MB paměti. Slabota.
Naivní řešení co slovo to dotaz je pomalou a bolestivou vraždou databáze, která vám za ty stovky tisíc nebo dokonce miliony dotazů vyloženě poděkuje (FYI: na k47čku bylo podle všeho použito 400.000 slov, no-ty-vole). Je potřeba vymyslet něco malinko sofistikovanějšího. Mohli bychom si dotazem vybrat všechny texty a ty pak najednou zpracovávat, ale narazíme na nedostatek paměti. Můžeme je zpracovávat postupně: načíst jeden text, rozdělit na slova, načíst další, rozdělit, slova spojit s předchozími atd., ale pořád je potřeba si třeba uchovávat informaci, která slova patří ke kterému článku a takže si musíme vybrat mezi variantami všechno v paměti nebo hodně SQL dotazů za sebou atd. Není to sranda. Musí se vymyslet něco víc sofistikovanějšího. Mnohem víc.
Na řešení této úplné zbytečnosti jsem (z hlediska databáze) vyzkoušel už asi šest různých přístupů, ale žádný nebyl dost rychlý. Zkoušel jsem to řešit naivně, zkoušel jsem použít prepared statements, zkoušel jsem tvořit hromadné inserty, zkoušel jsem tvořit ještě hromadnější inserty, napsal jsem si uloženou proceduru a další milion variací na podobné téma. Schválně, zkuste si podobnou věc vyřešit, naučíte se spoustu věcí, se kterými byste se nesetkali v normálních blogo-aplikacích, kde je velikost dat se kterými se pracuje tak nějak normální (fajn, neříkám, že ta moje věc je nějaký extrém, ale stejně).
Já jsem například v průběhu ladění (mimo jiné) zjisti, že databázový layer dibi je pomalý a nenažrané a táhne celý program ke dnu. Tedy takhle: dibi je samozřejmě výborná knihovna, jejíž režii v normálních situacích nepoznáte. V normálních. Ale situace, ve které ji používám já, není normální. Například generuji obrovské vícenásobné inserty a dibi nabízí možnost dotaz poskládat do pole, kdy se pak volá nějak takto: dibi::query(array('INSERT INTO [somewhere]', $data1, $data2, $data3))
. Toto použití dibi je velice pohodlné, ale při velkých datech skončí hláškou o vyčerpané paměti. Bohužel. Pohybuji se v oblasti speciálních případů: od pangejtu k pangejtu a poznávám, kde jsou mantinely. No nic, musím si odpustit ten luxus, jít níž a zrychlovat. Na milisekundách totiž záleží. Když se má provést 2000 dotazů, pak je rozdíl mezi 40ms a 80ms přesně 80 vteřin. To není problém když skript/program může běžet od nevidim do nevidim a alokovat paměť až do aleluja. Ale je to problém na omezeném serveru.
Sranda byla, když jsem zjistil, že pomalost může pramenit jinde, než jsem původně myslel. Zjistil jsem, že některé dotazy byly nečekaně skoro zadarmo a některé žraly víc času, než se mi líbilo. Profilování je v tomto případě nutné a poučné. Člověk pak zjistí, jaká kvanta času ušetří jeden starý laciný trik: před vložením velkého množství dat se z tabulky odstraní indexy a primární klíče, pak se data vloží a klíče a indexy se znovu zapnou. Je to rychlejší kvůli skutečnosti, že se nemusí kontrolovat a řadit klíče při každém insertu, ale provede se to jenom jednou na konci. Pravda někdy to nejde a nedá se použít výborná MySQL vychytávka INSERT … ON DUPLICATE KEY UPDATE ….
K tomu všemu si připočtěte, že žádná optimalizace neznamenala jenom malou změnu někde v kódu, ale kompletní (v lepším případě jenom zcela zásadní) přeformulování strategie řešení problému (například naházet všechna slova do databáze a tam je pak nějak zpracovávat). Kháááááán! Na twitteru jsem se chlubil, že v jedné iteraci pokus-omyl jsem jednu část úlohy, která by běžela celý den zoptimalizoval na 3 vteřiny, ale posléze jsem zjistil, že to stejně nebude fungovat. A zase překopat a znova.
Život je pes.
Problémy se začaly vyskytovat všude možně, ať už to byly záludnosti s porovnáním řetězců (collation) nebo zjištění, jak strašlivě může všechno táhnout k zemi PHP kód, když se v něm kopírují velká pole nebo mají nevhodnou strukturu, která se pomalu prochází atd.
Ale stejně to není rychlé. Funguje to tak nějak rozumně, ale ne žádoucí rychlostí.
Poučení z toho všeho zní: Nikdy nemůžeme vyhrát.
A na závěr nějaké stripy, abych něčím přebil pachuť tohoto článku. I když jak se na to dívám, nevím jestli 669.280 něco skutečně přebije. Při výrobě toho stripu jsem nejspíš trpěl nějakou mozkovou chorobou. Zato číslo 669.281 skutečně obsahuje humor k tématu.
PS: Když by někoho napadlo nějaké geniální řešení, jak problém řešit extrémně rychle a elegantně, ať mě urychleme kontaktuje a bude mu pomazána hlava.