Optimalizácia načítania dát - Bart Digital Products Optimalizácia načítania dát - Bart Digital Products

Optimalizácia načítania dát

“Ahoj Jano, stránka ide pomaly, nevieš kde by mohol byť problém? Ahoj Viktor. Áno, viem. V tvojej aplikácii.” :)

Takto častokrát v minulosti vyzerala prvotná komunikácia s adminom, keď pri náraste počtu dát v databáze začala stránka ísť pomalšie a pomalšie až to niekedy server nezvládol a jednoducho padol :). Na 99% má vždy admin pravdu a preto sa dnes pozrieme na to, ako napísať kód tak, aby bol aj z pohľadu servera optimálne napísaný a pritom prehľadný.

Určite ste sa pri vývoji webovej aplikácie nejakým spôsobom stretli s objektovo orientovaným programovaním (v skratke OOP) v prepojení na dátové úložisko, ktorým v prevažnej miere je relačná databáza (MySQL, MariaDB, PostgreSQL, …). 

Čo je OOP nebudeme rozoberať. V rámci OOP návrhu vieme elegantne zapúzdriť vlastnosti a funkcionalitu, pre jednotlivé objekty. Následne objekty inicializujeme a používame. Jednoduché že?

OOP však častokrát zvádza k síce pekne napísanému kódu ale nie vždy aj k optimálnemu z pohľadu záťaže servera. Preto je potrebné dbať na to akým spôsobom sa dáta načítajú a následne naplnia do jednotlivých objektov.

V rámci relačnej databázy máme dáta rozdelené do štruktúrovaných tabuliek, medzi ktorými máme relačný vzťah typu 1:N,. N:1 alebo N:M.

Pre konkrétnejšiu prestavu budeme pracovať s týmto dátovým modelom:

  • interest (id, name)
  • skill (id, name)
  • student (id, name, surname, sex, birth_date)
  • student_interest (student_id, nterest_id)
  • student_skill (student_id, skill_id)

V reči smrtelníka daná štruktúra hovorí o tom, že máme databázu študentov, ku ktorým evidujeme ich záujmy a programové zručnosti. Jeden študent môže mať viac záujmov a viac programových zručností. Je predpoklad, že záujmy a zručnosti sa budú opakovať pri rôznych študentoch. Preto dáta o záujmoch a zručnostiach máme v samostatných tabuľkách tzv. číselníkové tabuľky a väzbu študenta na číselník potom v prepojovacích tabuľkách.

Pri takejto dátovej štruktúre si vytvoríme OOP model:

  • Student, Student_List, Student_Search
  • Interest, Interest_List, Interest_Search
  • Skill, Skill_List, Skill_Search

V tomato OOP modeli Student, Interest, Skill predstavujú triedy pre Entity, *_List triedy pre zoznam Entít a *_Search sú triedy pre vyhľadávanie Entít. Search trieda má dostupný konektor na databázu, pomocou ktorého vie dáta z databázy načítať a vytvoriť odpoveď v potrebnej výstupnej štruktúre.

Poďme si teda ukázať ako načítať zoznam 50 študentov s ich záujmami a zručnosťami.

Tým že máme vytvorený OOP model, tak logickým krokom je použiť *_Search triedy pre načítanie potrebných údajov.

Je to veľmi prehľadné riešenie z pohľadu čítania kódu,  ale z pohľadu záťaže na server s databázou nie veľmi optimálne riešenie. Prečo? Nuž preto, že server musí vykonať 2n + 1 počet dopytov na databázu, kde n je počet nájdených študentov z prvého dopytu (v našom príklade 50).

Teraz si predstavme, že máme vysokú návštevnosť webu a v jednom čase nám naraz príde 1000 návštevníkov na stránku. V takomto prípade musí server vykonať 1000 x (2n + 1) dopytov na databázu, čo nám predstavuje exponenciálny nárast počtu dopytov nahor a server maká až sa môže stať, že to nezvládne a zhodí sa :)

Preto je vhodné sa pri vývoji na náš kód pozerať aj z tej druhej stránky a tou je výkon resp. záťaž servera. 

V našom prípade sa pokúsime zoptimalizovať naše riešenie na úrovni kódu a to tak, že si zistíme idčka študentov, ktorých sme v prvom dopyte načítali. Následne si pre tieto idečka načítame dáta z databázy v jednom dopyte a na úrovni kódu spracujeme.

Tu je výsledná zmena v logike prístupu k dátam:

Ako ste si mohli všimnúť, tak zrazu z počtu 2n + 1 dopytov sa dostávame na počet 3 a to bez závislosti na hodnote n. Zaujímavé že?

Teraz trochu výpočtov:

Pre porovnanie pri 1000 návštevníkoch a pri načítaní 50 študentov:

  • 1. Spôsob: 1000 x (2 x 50 + 1) = 101000 SQL dopytov
  • 2. Spôsob: 1000 x 3 = 3000 SQL dopytov

Ušetrili sme teda 101000 – 3000 t.j. 98000 dopytov – zrazu sa nám server fláka :)

Ako to ešte vyšperkovať?

Ak máme dáta, ktoré sa nemenia v čase príliš často, tak ich na úrovni aplikácie môžeme zapísať do CACHE vrstvy (File cache, Redis cache, …). Takouto úpravou sa vieme dostať na tieto čísla:

  • 1. Spôsob: (2 x 50 + 1) = 101
  • 2. Spôsob: 1 x 3 = 3

Ako ste si všimli, tak pri počte 1000 návštevníkov sa reálne každý dopyt odošle iba raz a zvyšným 999 návštevníkom sa dáta vrátia z CACHE vrstvy. Túto CACHE vrstvu pravidelne invalidujme, aby sme dáta mali aktuálne.

Na čo si dať pozor?

Pozor si je potrebné dať aj na to aké dáta si z databázy pýtame. Je na zváženie či nie je vhodnejšie si najskôr načítať iba idečka záznamov (menší dátový prenos) pri komplikovanejšom dopyte na databázu a následne na základe nájdených idečiek si načítať potrebné dáta. Tento dopyt bude rýchly, nakoľko dáta budeme načítať na základe primárneho kľúča.

Tu sa dostávame k alfe a omege optimalizácie databázy ako takej. Tou je indexácia tabuliek. Vhodne nastavené indexy nám vedia výrazne pomôcť pri zefektívnení načítania dát z databázy. 

Index by mal byť tvorený stĺpcami, podľa ktorých sa najčastejšie vyhľadáva. Tabuľka môže obsahovať viac indexov. Pozor, aj tu však platí pravidlo, že menej je viac. Čím viac indexov tým je pomalší INSERT a UPDATE dopyt, nakoľko sa musí index aktualizovať. Pri mohutných tabuľkách to môže byť citeľné spomalenie.

Záver

Optimalizáciu je potrebné najskôr poriadne zanalyzovať a až potom realizovať. Výsledkom optimalizácie má byť zefektívnenie načítania dát a nie ich skomplikovanie.