1. Universitatea „Politehnica” din București
Facultatea de Electronică, Telecomunicații și Tehnologia Informației
Universitatea Babeş-Bolyai
Facultatea de Matematică şi Informatică
Optimizarea bazelor de date MySQL
Proiect de Diplomă
Prezentat ca cerință parțială pentru obținerea titlului de Inginer în
domeniul Calculatoare și Tehnologia Informației programului de studii de
licență Ingineria Informației
Absolvent,
Stroia Laurențiu
Coordonator ştiinţific,
Lector dr. Navroschi Andreea
Promoţia 2015
2.
3. Cuprins
1. Introducere ........................................................................................................................ 13
2. Ce este MySQL................................................................................................................. 15
3. Optimizarea bazelor de date MySQL ............................................................................... 17
3.1 Privire de ansamblu ................................................................................................... 17
3.2 Optimizarea Bazei de Date........................................................................................ 18
3.2.1 Structura tabelelor .............................................................................................. 18
3.2.2 Optimizarea indexurilor tabelelor ...................................................................... 19
3.2.3 Optimizare operaţiuni......................................................................................... 22
3.2.4 Optimizare cache................................................................................................ 35
3.2.5 Monitorizare citiri și scrieri................................................................................ 39
3.3 Optimizarea server-ului MySQL............................................................................... 41
4. Implementarea bazelor de date ......................................................................................... 45
4.1 Motivația.................................................................................................................... 45
4.2 Aplicația și structura bazei de date............................................................................ 45
4.3 Optimizarea structurii bazei de date .......................................................................... 45
4.4 Interogări ................................................................................................................... 52
4.4.1 Top 5 hoteluri dintr-o ţară.................................................................................. 53
4.4.2 Top 5 hoteluri dintr-o zonă................................................................................. 54
4.4.3 Top 5 hoteluri dintr-un oraş ............................................................................... 54
4.4.4 Căutarea hotelurilor după diverse criterii........................................................... 55
4.4.5 Căutarea unor hotele după nume inclusiv în ţări,zone şi oraşe .......................... 58
4.5 Simularea bazei de date ............................................................................................. 59
5. Măsurători......................................................................................................................... 61
6. Concluzie .......................................................................................................................... 63
7. Bibliografie şi referinţe..................................................................................................... 65
8. ANEXE............................................................................................................................. 67
8.1 Anexa 1: Structura bazelor de date............................................................................ 67
8.2 Anexa 2: Trigger-ele pentru bazele de date............................................................... 69
4.
5.
6.
7.
8.
9.
10.
11. Lista Figurilor
Fig. A.1 Structura bazei de date neoptimizate
Fig. A.2 Structura bazei de date optimizate
13. 13
1. Introducere
Obiectivul prezentei lucrări este de a pune în evidenţă cât de mult contează o bază
de date bine construită, optimizată şi ce efecte poate avea aceasta asupra aplicaţiei noastre.
Pentru această lucrare am ales o bază de date utilizată foarte des în crearea de
aplicaţii web, însă nu foarte mulţi dezvoltatori o utilizează la capacitate maximă. Pentru a
demonstra eficienţa acesteia, am ales construirea a două baze de date, una optimizată și
alta neoptimizată, punând în evidenţă eficienţa răspunsurilor server-ului la interogări în
condiţiile unei activităţi abundente asupra server-ului de baze de date, care este configurat
având un singur server pe care rulează sistemul de operare Linux, prin simularea backend-
ului aplicației noastre.
Ca şi server de baze de date am ales unul foarte popular în rândul aplicaţiilor web,
acesta fiind MySQL. Pentru simulare am construit backend-ul aplicaţiei în ambele
variante, cea optimizată și cea neoptimizată, iar pentru monitorizare vom folosi două
programe de analiză. Primul captează în timp real variabilele globale ale server-ului care
ne va indica cum sunt folosite resursele de către MySQL, iar al doilea va fi un print a
variabilelor noastre, care ne va arăta rapoartele necesare pentru unele variabile.
În urma acestor simulări ne vom aştepta ca server-ul de baze de date optimizat să
fie mai puțin solicitat şi chiar mai rapid decât cel neoptimizat.
15. 15
2. Ce este MySQL
MySQL este un sistem de management pentru baze de date, produs de compania
suedeză MySQL AB şi distribuit sub Licenţa Publică Generală GNU, fiind o componentă
cheie a stivei LAMP(Linux,Apache, MySQL,PHP) . Acesta a fost lansat pentru prima dată în
23 Mai 1995. În 2008 a fost cumpărat de către compania Sun Microsystems, însă în August
2009 compania Oracle cumpără compania Sun Microsystems, iar în Decembrie 2009, Oracle
decide să dezvolte în continuare produsul MySQL ajungând la versiunea 5.7.7 în data de
8 Aprilie 2015. MySQL este scris în limbajele de programre C și C++.
Există multe scheme API disponibile pentru MySQL, ce permit scrierea aplicaţiilor în
numeroase limbaje de programare pentru accesarea bazelor de date MySQL, cum are fi: C,
C++, C#, Java, Perl, PHP, Python, FreeBasic, etc., fiecare dintre acestea folosind un tip
specific de API.
O bază de date este o colecţie structurată de date. Bazele de date MySQL sunt
relaţionale. O bază de date relaţională stochează informaţiile în tabele, aceasta fiind
organizată în fişiere fizice optimizate pentru viteză.
Modelul logic, cu obiecte precum : „database”, „tables”, „views”, „rows” şi
„columns” oferă un mediu de programare flexibil. SQL este cel mai comun limbaj standard
pentru accesarea bazelor de date.
Softul MySQL este unul Open Source , acest fapt îi oferă posibilitatea ca oricine să îl
folosească sau să îl modifice. Server-ul de date MySQL este foarte rapid, stabil, scalabil şi
uşor de folosit, acesta putând fi rulat pe Windows, sau alte sisteme de operare. MySQL este
un server multi-user, unul client/server, fiind multi-threaded. Acesta suportă diferite backend-
uri, programe client sau biblioteci.
17. 17
3. Optimizarea bazelor de date MySQL
3.1 Privire de ansamblu
Performanţa bazei de date depinde de câţiva factori la nivelul bazei de date cum ar fi :
tabele, query-uri și setările de configurare. Aceste construcţii software rezultă în CPU şi
operaţiuni de tip I/O la nivelul hardware, care ar trebui minimizate și făcute cât mai eficient
posibil.
Optimizări la nivelul bazei de date:
Structura tabelelor. Baza de date ar trebui să aibă tipuri de date
corespunzătoare pentru coloane şi structura coloanelor să fie corespunzătoare pentru
ce are nevoie aplicaţia. Un exemplu pentru structura coloanelor ar fi o aplicaţie care
face actualizări frecvente. Aplicația este mai rapidă dacă are multe tabele cu un număr
mic de coloane, pe când o aplicație care analizează datele este mai rapidă, dacă sunt
mai puţine tabele cu mai multe coloane. Un alt exemplu pentru tipurile de date ar fi
când se fac comparaţii între coloane, sau la sortări, dacă declari tipul coloanei să fie
cât mai mic vei reduce spaţiul din buffer, crescând numărul de linii pe care le poate
stoca;
Alegerea corectă a index-urilor;
Folosirea corectă a motorului de stocare pentru fiecare tabelă şi
profitarea de forţa şi caracteristicile acestuia;
Folosirea unui format corespunzător pentru forma liniilor, de exemplu
tabelele compresate folosesc spaţiu de stocare mai puţin, ceea ce implică un număr de
citiri şi scrieri mai mic;
Folosirea unei strategii bune de blocare pentru tabele. De exemplu dacă
avem programe concurente, blocarea tabelelor ar fi necesară numai în cazul unor
actualizări de date, lăsând citirea acestora deblocată;
Zonele de memorie folosite pentru cache de dimensiuni corecte.
Optimizări la nivel hardware:
Căutarea pe disc. Împărţirea datei pe mai multe discuri, deoarece teoria
spune că am putea avea maxim 100 de căutări pe secundă pe un disc;
Scrierea şi citirea de pe disc. Optimizarea scrierii şi citirii ar fi dacă am
împărţi informaţia pe mai multe discuri deoarece citirea în paralel este mult mai
rapidă;
Cicluri CPU. Dacă avem tabele mari în comparaţie cu memoria este
foarte greu să le parcurgi, dar cu tabele mici viteza nu este o problemă;
Lăţimea de bandă de memorie. Când procesorul are nevoie de mai
multe date decât încap în memoria cache , lăţimea de bandă a memoriei principale
produce o ştrangulare.
18. 18
3.2 Optimizarea Bazei de Date
3.2.1 Structura tabelelor
Dimensiunea bazei de date reduce drastic cantiatea de date scrise şi citite de pe disc.
Orice spaţiu redus dintr-o tabelă se reflectă în indexuri mici ce pot fi parcurşi mult mai
rapid. MySQL suportă diferite metode de stocare şi formatări de linii, iar pentru fiecare tabela
putem decide ce tipuri să folosească.
Putem obţine o mai bună performanţă pentru tabele şi minimizarea acestora prin
următoarele metode:
1) Coloanele Tabelelor
a) Folosirea celor mai mici tipuri de date este foarte importantă. De
exemplu folosirea tipului „smallint” , sau „mediumint” este o alegere mult mai bună
decât „int”. Dacă este posibil, alegerea corectă a tipului de dată reduce cu 25% spaţiul
şi memoria folosită în buffer pool sau din cache.
b) Declararea de coloane nenule dacă structura permite acest lucru.
Aceasta va face operaţiunile mai rapide dacă punem un index pe ele şi va elimina
testarea dacă coloana este nulă. De asemenea economisim 1 bit pentru fiecare coloană
declarată nenulă. Însă dacă este nevoie putem folosi oricând valoarea null, doar să
evităm punerea default a valorii de null în toate coloanele.
2) Formatul Linilor
a) Tabele InnoDB folosesc un mod compact de stocare. De obicei tabelele
sunt create în format compact, însă putem reveni oricând la formatul vechi redundant .
Formatul redundant conţine informaţii cum ar fi numărul de coloane , dimensiunea
acestora , chiar şi pentru cele cu dimensiune fixă. Folosirea formatului compact duce
la reducerea spaţiului cu 20% per linie, iar procesorul poate folosi restul resurselor
pentru alte operaţiuni. De asemenea modul compact permite modificarea modului cum
coloanele de tipul „CHAR” foloseşte UTF-8. Cu formatul „REDUNDANT” un UTF-
8 CHAR(N) ocupă 3xN bytes , dându-i acestuia lungimea maximă pentru un caracter
codat UTF-8 . Însă în multe limbi caracterele pot fi scrise pe un singur byte. De accea
modul „COMPACT” alocă un număr variabil de bytes între N şi 3xN.
b) Pentru minimizarea mai mult a spaţiului de stocare putem compresa
datele. Pentru InnoDB declarăm „ROW_FORMAT=COMPRESSED” la crearea tabelelor , iar
pentru tabelele de tip MyISAM folosim comanda „myisampack”. Tabelele compresate
de tip InnoDB pot fi citite şi scrise iar cele de tip MyISAM doar citite.
c) La tabelele MyISAM dacă nu sunt coloane cu lungimi variabile (
„VARCHAR”,”TEXT” sau „BLOB” ), o linie de lungime fixă va fi folosită. Aceasta
ajută la creşterea vitezei, însă este costisitoare la spaţiu. Putem folosi formatul fix
chiar şi dacă avem VARCHAR, declarând doar tipul linie de tip „FIXED”.
3) Indexuri
19. 19
a) Cheia primară trebuie să fie cât mai scurtă posibil. Aceasta face
identificarea uşoară şi rapidă pentru fiecare linie. Tabelele InnoDB duplică cheia
primară în fiecare index secundar , aceasta înseamnă că, cu cât e mai scurtă cheia
primară salvăm considerabil spaţiul de stocare, dacă avem multe indexări secundare.
b) Creează doar indexuri de care ai nevoie pentru creşterea performanţei.
Aceştia ajută la cautări, însă încetinesc adăugarea şi actualizarea coloanelor. Dacă faci
căutări dese, pe mai multe coloane, folosirea de indexuri compuşi ajută mult, prima
coloană fiind cea care este utilizată mai des şi în alte căutări.
c) Este foarte clar că, pentru un şir de caractere acesta are un unic prefix şi
este mai bine de indexat doar acel prefix. De aceea indexurile mai scurte sunt mai uşor
de găsit , pentru că îţi dau mai multe indicii şi reduc căutarea pe disk.
4) Operații de Join
a) În unele circumstanţe este de ajutor să împarţim datele în două tabele
ce vor fi scanate de mai multe ori. Este util atunci când, o parte din coloanele tabelei
sunt actualizate frecvent, dar putem folosi celelalte coloane pentru a găsi liniile dorite,
fiind mai eficient să creăm o tabelă care să conţină acele coloane indexate.
b) Declararea coloanelor cu acelaşi tip de informaţie, să aibă acelaşi tip de
dată. Acest lucru duce la creşterea vitezei operaţiei de join.
c) Numele cât mai simplu pentru coloane duce la simplificarea operațiilor
de join, iar pentru a le face portabile pe alte SQL-uri ar trebui păstrate mai mici de 18
caractere.
5) Forma Normalizată
a) Încercarea păstrării formei normale non reduntante ( formei 3 NF ).
b) Dacă spaţiul pe disc nu e o problemă, se poate duce la o relaxare a
formei, păstrând duplicate sau tabele ce păstrează informaţii sumare despre o tabelă.
Acest lucru face mult mai rapidă căutarea informaţiilor dorite.
3.2.2 Optimizarea indexurilor tabelelor
Cea mai bună cale de a optimiza interogările este de a folosi indexuri. Indexurile se
comportă ca nişte pointeri la linile tabelelor. De aceea ai zice că, dacă indexezi toate coloanele
căutarea va fi rapidă. Însă nu tot timpul se întâmplă acest lucru, deoarece dacă ai indexat toate
coloanele care nu sunt necesare este o risipă de spaţiu și timp pentru MySQL, care trebuie să
determine ce index să folosească, de asemenea costă şi executarea operațiilor de insert, update
şi delete, fiindcă MySQL updatează toţi indexii după o operaţie. De accea trebuie găsită calea
optimă de a indexa coloanele pentru a obţine performanţa maximă.
Majoritatea indexurilor din MySQL(„Primary key”, „unique”, „index” și “fulltext”)
sunt stocati în B+-arbori, cu excepţia indexurilor spaţiali (“GEOMETRY”, „POINT”,
„LINESTRING”) care folosesc R-arbori. Tabele de tip “MEMORY” suportă de asemenea şi
indexuri hash. InnoDB foloseşte liste inversate pentru indexuri de tip „FULLTEXT”.
În esență, arborele B+ este un arbore balansat , în care nodurile interne
direcționează procesul de căutare, iar nodurile frunză (terminale) conțin intrările de date.[1]
20. 20
De obicei arborii B+ asigură circa 67% grad de ocupare a spaţiului de memorie.
Pentru găsirea unei date se va căuta nodul frunză căruia îi corespunde o intrare de date. Acest
lucru presupune o căutare în interiorul nodului, ceea ce se poate realiza fie printr-o căutare
liniară, fie printr-o căutare binară.
Ideea algoritmului de inserare stă în aceea că inserăm recursiv o intrare apelând
algoritmul de inserare la nivelul nodului succesor (copie) corespunzător. De obicei,
această procedură ne conduce “jos”, la frunza căreia îi aparține intrarea, amplasând intrarea
acolo şi revenind apoi la nodul rădăcină.[1]
Uneori la o inserare nodul rădăcină este complet iar acest lucru necesită o secţionare a
nodului. Sectionarea presupune adăugarea unui pointer în nodul părinte către noul nod creat.
Secţionarea vechiului nod şi creearea unui nou nod rădăcină produce creşterea înălţimii
aroborelui cu o unitate.
Diferenţa în tratarea secţionării la nivel de frunză şi la nivel de index, este datorată
faptului că la B+ arbori toate intrările de date trebuie să fie amplasate în frunze. Această
cerinţă conduce la o uşoară redundanţă, având unele valori de cheie care apar atât la nivel de
frunză cât şi la nivel de index. În ciuda redundanţei atunci când interogările solicită valori pe
interval se poate răspunde eficient prin simpla extragere a secvenţelor de pagini frunză.
Tabel 3-1 Structura B+ arborilor
[7]
R-arborii sunt structuri de date arborescente folosite pentru metode de acces spaţiale ,
de exemplu , pentru indexarea informaţilor multi-dimensionale cum ar fi coordonate
geografice , dreptunghiuri sau poligoane. Ideea cheii este aceea de a grupa obiectele în
dreptunghiuri minimale. Acestea sunt reprezentate în cel mai mic nivel ca obiecte, crescând în
nivel acestea devin o agregare tot mai mare a numărului de obiecte ( dreptunghiuri ).
21. 21
Tabel 3-2 Structura R-arbori
[6]
În exemplul de mai sus, R-urile sunt dreptunghiurile minimale, iar literele de la A la L
sunt obiecte care conţin informaţii.
R-arbori sunt balansaţi , toate frunzele au aceeaşi înălţime, organizarea informaţiei în
pagini, dedicaţi pentru stocarea pe disc. Perfromanţa pentru umplerea paginilor a fost de 30%-
40% ( exceptând nodul rădăcină ) , iar B+-arborii deţin o performanţă de 66% , acest lucru se
datorează balansării complexe a datei spaţiale faţă de cea liniară din B+-arbori. Ideea cheii
este de a folosi cutiile de încadrare ( dreptunghiuri ) pentru a decide dacă va parcurge
subarborele. Acest lucru face ca multe dintre nodurile arborelui să nu fie parcurse, ceea ce
înseamnă că la fel ca şi B+-arborii , R-arborii sunt ideali pentru baze de date mari.
3.2.2.1 Diferenţa dintre indexuri de tip B+-arbore si cei de tip Hash
Indexuri de tip B+-arbore sunt folosiţi pentru selecturi de tip =,>,>=,<,<=, BETWEEN
şi LIKE dacă acesta nu începe cu „%”. Spre exemplu dacă avem LIKE „Patrick%” acesta face
o comparaţie de genul „Patrick” <= cheia_coloana < „Patricl”. Dacă însă folosim un şir de
caractere care începe cu „%” atunci dacă lungimea acestuia este mai mare decât 3 MySQL
foloseşte algoritmul Turbo Boyer-More pentru a găsi un pattern pentru acesta şi îl va folosi
pentru căutarea rapidă. De asemenea indexul nu va fi folosit dacă se foloşete funcţia LIKE
pentru a compara două coloane.
22. 22
Câteodata MySQL nu foloseşte indexuri, când acesta este nevoit să caute într-un set
mare de date, fiind mai rapid să parcurcă toată tabela , însă folosirea lui „LIMIT” forţează pe
acesta să folosească indexuri, fiind mai rapidă găsirea unui număr mai mic de date.
Indexurile de tip Hash sunt folosite doar pentru operaţiile de egalitate : „=” , „<=>”.
Acestea sunt extrem de rapide pentru operaţii de egalitate însă nu sunt folosite pentru alte
operaţii cum ar fi „<” , nu pot fi folosite pentru optimizarea „ORDER BY” , iar acestea
folosesc întreaga cheie pentru căutarea liniilor. Indexurile de tip B+-arbore pot folosi cel mai
din stânga prefix pentru căutare. MySQL nu poate determina dinstanţa dintre două valori iar
acest lucru poate afecta schimbarea unei tabele de tip MyISAM sau InnoDB în una de tip
MEMORY.
3.2.2.2 Cum foloseşte MySQL Indexurile
MySQL foloseşte indexurile pentru a returna rapid rezultatele dorite, fără a face o
întreagă scanare a tabelei. Acestea sunt utile atât în clauza WHERE cât şi în operaţiile de join
între tabele sau clauze precum SORT sau GROUP BY.
Folosirea unui index format din mai multe coloane este foarte util în cazurile în care
pot varia coloanele dorite în interogări. Ordinea coloanelor declarate într-un index contează
fiindcă MySQL poate folosi prefixele indexului pentru a îmbunătăţi căutarea atunci când
indexul nu se regăseşte total în clauză. Dacă am avea spre exemplu un index format din trei
coloane (col1,col2,col3) avem o căutare cu index capabilă pe (col1),(col1,col2) şi
(col1,col2,col3), de aceea este foarte important să stabilim ordinea în funcţie de utilitatea
fiecărei coloane, fiindcă dacă am utiliza mai mult col3 decât toate celelalte coloane atunci ar
fi indicat să formăm indexul de forma (col3,col2,col1).
3.2.3 Optimizare operaţiuni
3.2.3.1 Optimizarea operaţiei SELECT
Pentru a face o instrucţiune SELECT mai rapidă, primul lucru pe care trebuie să îl
avem în vedere este utilizarea indexurilor, evitarea execuţiilor de funcţii pentru că acestea ar
îngreuna extrem de mult select-ul, fiind nevoie de executarea funcţiei pentru fiecare linie din
select, sau mai rău, pentru fiecare linie din tabelă.
Evitarea scanării întregii tabele, în special pentru tabelele foarte mari.
Păstrarea statisticilor la zi pentru tabele folosind „ANALYZE TABLE” periodic ,
astfel optimizatorul are informaţiile necesare pentru construirea unui plan de execuţie rapid.
Folosirea cache-ului pentru query-uri repetate este foarte rapid , fiind mult mai rapidă
livrarea rezultatului din memorie decât căutarea acestuia pe disc , însă acesta trebuie optimizat
să nu folosească multă memorie.
3.2.3.2 Cum optimizează MySQL clauza WHERE
Multe din operaţiile de where MySQL le optimizează pentru a fi mai eficiente. Aceste
operaţii sunt:
23. 23
Ştergerea de paranteze inutile
((a AND b) AND c OR (((a AND b) AND (c AND d)))
=> (a AND b AND c) OR (a AND b AND c AND d)
Plierea de constante
(a<b AND b=c) AND a=5
=> b>5 AND b=c AND a=5
Ştergerea stării constantelor
(B >=5 AND B=5) OR (B=6 AND 5=5) OR (B=7 AND 5=6)
=> B=5 OR B=6
Expresiile constante utilizate de indexuri sunt evaluate o singură dată
Când folosim COUNT(*) pe o tabelă fără clauza WHERE acesta îşi ia
rezultatul direct din informaţiile tabelei
Detectarea rapidă a expresiilor constante invalide şi returnează 0 linii.
Dacă nu folosim GROUP BY sau funcţii agregate (COUNT(),MIN() ,
etc) HAVING este îmbinată cu clauza WHERE
Pentru fiecare operaţie de join este construită o clauză WHERE care
încearcă eliminarea cât mai rapidă a numărului de linii
Toate tabelele constante sunt citite primele din interogare. O tabelă
constantă este o tabelă care este goală sau are o singură linie , sau o tabelă care
foloseşte în clauza WHERE un PRIMARY KEY sau un index UNIQUE care este
comparat cu o constantă
3.2.3.3 Optimizare Range
Metoda de acces „range” foloseşte un index pentru a returna un subset de date care
sunt cuprinse într-un interval de unu sau mai multe valori ale indexului.
Pentru un index format dintr-o singură coloană putem vorbi mai mult de condiţie de
„range” decât de interval. Condiţia de „range” este atunci când este comparată valoarea index-
ului cu o constantă , operaţiile comune pentru indexuri de tip B+-arbore şi HASH sunt
„=”,”<=>”,IN() ,IS NULL si IS NOT NULL , pentru B+-arbori fiind acceptate şi operaţiile
„>”,”>=”,”<”,”<=”,”BETWEEN”,”!=”,”<>” sau „LIKE” comparat cu o constantă sau un
string care nu începe cu „%” , acestea putând fi combinate cu „OR” sau „AND”.
MySQL încercă să extragă condiţiile de range din clauza WHERE pentru toate
indexurile posibile. În timpul procesului de extracţie, condiţiile care nu pot fi folosite pentru
construirea condiţiei de range sunt şterse, condiţiile care produc intervale care se suprapun
sunt combinate şi condiţile care produc un interval gol sunt eliminate.
Spre exemplu luăm urmatorul query:
Select * from t1 where
(key1 < ‘abc’ AND (key1 LIKE ‘abcde%’ OR key like ‘%b’)) OR
24. 24
(key1 < ‘bar’ AND nonkey = 4) OR
(key1 < ‘uux’ AND key1 > ‘z’);
MySQL face înlocuire la nonkey = 4 şi key1 like ‘%b’ din cauză că acestea nu pot fi
folosite în condiţia de range, cu valoarea TRUE pentru a nu pierde informaţii. Reduce
condiţiile care sunt tot timpul adevărate sau false: (key1 like ‘abcde%’ or TRUE) este tot timpul
TRUE , iar (key1 < ‘uux’ AND key1 > ‘z’) este tot timpul FALSE . După înlocuirea acestora cu
TRUE şi FALSE şi reducerea lor obţinem (key1 < ‘abc’) OR (key < ‘bar’) , după această
reducere se face combinarea de intervale rezultând condiţia de range key<’bar’ .
În general, condiţia folosită pentru scanarea intervalului este mai puţin restrictivă
decât clauza WHERE . MySQL efectuează un control suplimentar pentru a filtra rândurile
care satisfac condiţia de range , dar nu întreaga condiţie WHERE.
Condiţia de range pentru indexurile compuse din mai multe coloane este o extensie a
celei cu o singură coloană, aceasta restrângând condiţia pe toate coloanele. De exemplu avem
cheia key1 formată din coloanele key_part1 , key_part2, key_part3 , condiţia key_part1 = 3
defineşte un interval (3,-inf,-inf) < (key_part1,key_part2,key_part3) < (3, +inf,+inf) .
3.2.3.4 Optimizare îmbinarea index
Această metodă preia rezultatele mai multor scanări pe intervale diferite şi le combină
într-un singur rezultat. Această combinaţie poate produce uniuni, intersecţii sau uniuni de
intersecţii.
Combinarea indexurilor prin metoda intersecţiilor apare atunci când o clauză WHERE
este convertită în câteva intervale de condiţie range având diferite chei combinate între ele cu
AND. Această metodă caută simultan rezultatele pentru toate indexurile iar la final
intersectează rezultatele acestora într-un rezultat final. Putem vedea dacă selectul nostru
foloseşte un astfel de algoritm rulând comanda EXPLAIN şi uitându-ne în coloana EXTRA
putem observa „Using index”.
SELECT COUNT(*) FROM t1 WHERE key1=1 AND key2=1;
Criteriile de aplicare a combinării indexurilor prin metoda uniunii sunt asemănătoare
cu acelea prin metoda intersecţiei doar că aceasta apare în momentul în care cheile sunt
combinate între ele cu OR.
SELECT COUNT(*) FROM t1 WHERE key1=1 OR key2=1;
Combinarea indexurilor prin metoda uniunii de intersecţii apare atunci când metoda
uniunii nu poate fi aplicată. Diferenţa între aceşti doi algoritmi este că metoda uniunii de
intersecţii prima dată aduce id-urile rândurilor şi le sortează înainte de a returna orice rând.
SELECT COUNT(*) FROM t1 WHERE key_part1=1 OR key_part2=1;
3.2.3.5 Extensia cheii primare
MySQL duplică cheia primară în restul indexurilor pentru a creşte performanţa
interogărilor. De exemplu avem cheia primară formată din coloanele key_part1 si key_part2 şi
creăm un index i1 , acesta în final va fi de forma (i1, key_part1, key_part2) iar dacă vom face
25. 25
interogări folosind în clauza WHERE coloanele i1 şi key_part1 sau i1,key_part1 şi key_part2
el foloseşte indexul amintit mai sus pentru a face rapidă căutarea. MySQL vine cu această
opţiune default activată iar pentru dezactivarea acesteia putem folosi următoarea comanda: SET
optimizer_switch = 'use_index_extensions=off'; .
Atunci când un index este format din mai multe coloane şi este utilizat într-o
interogare MySQL foloseşte optimizarea pushdown pentru a returna doar liniile care se
încadrează condiţiei indexului fără ca acesta să facă o întreagă parcurgere a tabelei, urmând ca
după să se evalueze liniile selectate cu restul condiţiilor din clauza WHERE sau operaţiei de
join.
3.2.3.6 Optimizare IS NULL
MySQL oferă câteva optimizări pentru condiţia IS NULL adresată unei coloane
indexate. MySQL foloseşte indexuri şi intervale de range pentru a găsi valorile care sunt nule.
Această optimizare apare atunci când coloana nu este declarată NOT NULL , fiind deja
optimizată acea coloană, şi nu se aplică pe coloane care pot fi nule orişicum , în cazul unui
LEFT sau RIGHT JOIN. De asemenea MySQL optimizează şi expresile de genul col_name =
expresie OR col_name IS NULL , acest lucru putând fi observat rulând comanda EXPLAIN , în
coloana type având valoarea ref_or_null.
3.2.3.7 LEFT si RIGHT JOIN optimizator
MySQL foloseşte un optimizator pentru query care decide ordinea optimă pentru a fi
executate operaţiile de join. Executarea forţată a interogării cu STRAIGHT_JOIN poate duce
la creşterea vitezi reducând efortul optimizatorului de a face permutările necesare ordonării
interogării, mai ales în cazurile unde sunt multe operaţii de join. Pentru un LEFT JOIN unde
clauza WHERE este tot timpul falsă pentru coloane generate nule această operaţie se
transformă într-un join normal , fiind mai rapid şi mai sigur să convertim interogarea într-un
join normal, exemplu:
SELECT * FROM t1 LEFT JOIN t2 ON (column1) WHERE t2.column=5;
=> SELECT * FROM t1,t2 WHERE t2.column2=5 AND t1.column1 =t2.column1
3.2.3.8 Nested Join Optimization
Algoritmul pentru un simplu Nested-Loop Join citeşte fiecare linie din prima tabelă
urmând ca apoi să citească fiecare linie din următoarea tabelă. Un exemplu simplu este să
avem trei tabele în join : t1,t2,t3 care pentru fiecare join să avem următorii algoritmi: range,
ref si ALL , un algoritm pentru Nested-Loop Join arată în felul următor:
for each row in t1 matching range {
for each row in t2 matching reference key {
for each row in t3 {
if row satisfies join conditions,
send to client
}
}
26. 26
}
[2]
Pentru că acest algoritm ar presupune citirea de prea multe ori a unei singure tabele,
MySQL permite folosirea unui algoritm de tip Block Nested-Loop Join(BNL) , acesta
foloseşte un buffer pentru a păstra câteva elemente din tabela anterioară şi făcând apoi
selectul pe întreaga tabelă, astfel reduce citirea tabelei pentru fiecare linie din tabela
anterioară. MySQL foloseşte BNL doar dacă acesta este nevoit să parcurcă întreaga tabelă în
lipsa unui index sau în cazul în care MySQL nu poate folosi un index pentru join. Variabila
join_buffer_index ne spune cât este de mare buffer-ul pentru lotul de cheii(coloane) necesare
joinului. Un buffer este alocat ori de câte ori este nevoie pentru un join între 2 tabele , cu alte
cuvinte pot exista simultan mai multe buffere. Doar coloanele necesare join-ului sunt păstrate
în buffer , nu întreaga linie , astfel bufferul încearcă păstrarea cât mai multor rânduri din
tabela principală reducând numărul de citiri a tabelei următoare.
Algoritmul pentru această metoda de join este:
for each row in t1 matching range {
for each row in t2 matching reference key {
store used columns from t1, t2 in join buffer
if buffer is full {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions,
send to client
}
}
empty buffer
}
}
}
if buffer is not empty {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions,
send to client
}
27. 27
}
}
[2]
Astfel dacă notăm fiecare combinaţie t1,t2 cu S şi fiecare combinaţie din buffer cu C
atunci numărul de câte ori este scanată tabela t3 este dat de relatia: (S*C)/join_buffer_size
+1[2].
Concluzia este că cu cât este mai mare dimensiunea variabilei join_buffer_size cu atât
reducem numărul de citiri a tabelei t3.
Atunci când clauza WHERE poate fi reprezentată printr-o formulă conjunctivă de
forma C1(t1) AND C2(t2) AND C3(t3) atunci MySQL aplică algoritmul „pushed-down” , mutând
fiecare conjuncţie la cel mai apropiat ciclu, algoritmul fiind:
FOR each row t1 in T1 such that C1(t1) {
FOR each row t2 in T2 such that P1(t1,t2) AND C2(t2) {
FOR each row t3 in T3 such that P2(t2,t3) AND C3(t3) {
IF P(t1,t2,t3) {
t:=t1||t2||t3; OUTPUT t;
}
}
}
}
[3]
Pentru outer join algoritmul pushed-down este aplicat doar dacă s-a găsit o linie care
îndeplineşte condiţiile de join, cele din WHERE nefiind verificate dacă nu există vreo linie
din tabela înterioară. Algoritmul arată de forma:
FOR each row t1 in T1 such that C1(t1) {
BOOL f1:=FALSE;
FOR each row t2 in T2 such that P1(t1,t2) AND (f1?C2(t2):TRUE) {
BOOL f2:=FALSE;
FOR each row t3 in T3 such that P2(t2,t3) AND (f1&&f2?C3(t3):TRUE) {
IF (f1&&f2?TRUE:(C2(t2) AND C3(t3))) {
t:=t1||t2||t3; OUTPUT t;
}
f2=TRUE;
f1=TRUE;
28. 28
}
IF (!f2) {
IF (f1?TRUE:C2(t2) && P(t1,t2,NULL)) {
t:=t1||t2||NULL; OUTPUT t;
}
f1=TRUE;
}
}
IF (!f1 && P(t1,NULL,NULL)) {
t:=t1||NULL||NULL; OUTPUT t;
}
}
[3]
3.2.3.9 Simplificarea Outer Join
Atunci când optimizatorul evaluează planul pentru operaţiile de join exterioare, el ia in
considerare doar acele operaţii care se execută înaintea operaţiilor interioare de join. Opţiunile
optimizatorului sunt limitate deoarece sunt doar câteva planuri de execuţie care pot fi aplicate
pentru optimizare, de aceea MySQL încearcă convertirea interogărilor în altele asemănătoare
fără outer join-uri dacă condiţia WHERE este null-rejected.O condiţie null-rejected pentru o
operaţie de join externă ( outer join ) este dacă aceasta este evaluată la FALSE sau
UNKNOWN pentru oricare linie complementară nulă construită de operaţia de join. O
condiţie este null-rejected în următoarele cazuri :
Dacă este de forma A IS NOT NULL , unde A este un atribut al tabelei
Dacă este un predicat care conține o trimitere la un tabel interior care se
evaluează la UNKNOWN atunci când unul dintre argumentele sale este NULL
Dacă este o conjuncţie care conţine o conjuncţie null-rejected
Dacă este o disjuncţie de condiţii null-rejected
„O operaţie poate fi null-rejected pentru o operaţie de join exterioară dar nu poate fi
pentru alta.
SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
LEFT JOIN T3 ON T3.B=T1.B
WHERE T3.C > 0
Primul WHERE este null-rejected pentru al doilea join exterior dar nu şi pentru
primul”[4]
Dacă o condiţie null-rejected este doar pentru o operaţiune de join exterioară atunci
aceasta este înlocuită cu un inner join. O convertire a unei operaţii de join exterioare poate
29. 29
declanşa o conversie a altei operaţii de join exterioare. Câteodată se reuseşte înlocuirea
operaţiunilor de join exterioare , însa nu se reuşeşte convertirea acestora , când se încearcă
convertirea lor trebuie avut grijă la condiţile de join exterioare să poata fi puse la un loc cu
cele de WHERE. Un exemplu ar fi:
SELECT * FROM T1 LEFT JOIN
(T2 LEFT JOIN T3 ON T3.B=T2.B)
ON T2.A=T1.A AND T3.C=T1.C
WHERE T3.D > 0 OR T1.D > 0
Care este convertit la:
SELECT * FROM T1 LEFT JOIN
(T2, T3)
ON T2.A=T1.A AND T3.C=T1.C AND T3.B=T2.B
WHERE T3.D > 0 OR T1.D > 0
3.2.3.10 Optimizare ORDER BY
În unele cazuri MySQL poate folosi un index pentru a satisface clauza ORDER BY
fără a face sortări suplimentare. Acesta poate folosi indexul chiar dacă coloanele folosite în
ORDER BY nu sunt toate corespunzătoare indexului, atât timp cât toate părţile neutilizate ale
indexului şi celelalte coloane din clauza ORDER BY sunt constante în clauza WHERE.
În unele cazuri MySQL nu poate folosi index-ul pentru sortare chiar dacă acesta este
folosit pentru a găsi linile care corespund clauzei WHERE. Acestea sunt cazurile când nu
poate folosi indexul:
Folosirea clauzei ORDER BY cu diferite chei: SELECT * FROM t1 ORDER
BY key1, key2;
Folosirea în clauza ORDER BY a unor părţi neconsecutive din cheie:
SELECT * FROM t1 WHERE key2=constant ORDER BY key_part2;
Folosirea unui mix ASC şi DESC
Cheia folosită pentru găsirea liniilor nu este identică cu cea folosită în
clauza ORDER BY
Folosirea în clauza ORDER BY a expresilor care folosesc alti termeni
decât numele cheii: SELECT * FROM t1 ORDER BY ABS(key);
Folosirea în clauza ORDER BY de coloane care nu provin din prima
tabelă neconstantă folosită pentru a găsi liniile într-un join cu multe tabele.
Existenţa a diferitelor expresii în clauza ORDER BY si GROUP BY
Indexarea unui prefix din coloana folosită în clauza ORDER BY. De
exemplu dacă avem o coloană de tip VARCHAR(20) şi indexăm numai primii 10 biţi.
Tipul indexului folosit nu păstrează în ordine liniile. Un exemplu bun
este indexul de tipul HASH dintr-o tabelă de tip MEMORY.
Sortarea folosind un index poate fi afectată şi de aliasul coloanelor date în select , dacă
acestea corespund cu numele coloanei indexate, în acest caz MySQL este nevoit să folosească
30. 30
sortarea.Putem verifica dacă MySQL foloseşte indexul prin comanda EXAPLAIN query ,iar
în coloana Extra nu ar trebui să apară „Using filesort”.
MySQL are doi algoritmi de sortare, cel original prin care foloseşte doar coloanele din
clauza ORDER BY şi metoda modificată prin care foloseşte toate coloanele implicate în
query nu doar cele din ORDER BY.
„Algoritmul original de filesort funcţionează în felul următor:
Citeşte toate rândurile în conformitate cu cheia sau prin scanarea
tabelei. Sare peste rândurile care nu corespund clauzei WHERE.
Pentru fiecare linie stochează o pereche de valori(valoarea cheii de
sortare şi ID rândului) în sort buffer
Dacă toate perechile încap în sort buffer nici un fişier temporar nu este
creat. În caz contrar rulează un quicksort în memorie şi le scrie într-un fişier temporar,
salvând un pointer la blocul sortat.
Repetă paşii precedenţi până când toate liniile au fost citite
Face un multi-merge de până la MERGEBUFF(7) regiuni într-un
singur bloc în alt fişier temporar. Repetă acest lucru până când toate blocurile din
prima pagină se află în a doua
Repetă urmatoarele blocuri până când sunt mai puţin de
MERGEBUFF2(15) blocuri rămase
La ultimul multi-range doar ID-urile rândurilor sunt scrise în fişierul
rezultat
Citeşte rândurile într-o ordine sortată folosind ID-ul rândurilor. Pentru
a optimiza aceasta citeşte într-un bloc mare de rânduri , le sortează, şi le foloseşte
pentru a citi într-o ordine sortată într-un row buffer. Dimensiunea row buffer-ului este
dată de variabila de sistem read_rnd_buffer_size ”[5]
O problemă cu acest algoritm ar fi faptul că el citeşte de două ori rândurile: prima dată
când evaluează clauza WHERE iar a doua oară după sortarea perechiilor de valori.
Algoritmul modificat pentru filesort încorporează o optimizare pentru a preveni citirea
de două ori a rândurilor: acesta înregistrând valoarea cheii de sortare, dar în loc de ID-ul
rândului, păstrează coloanele referenţiate de interogare.
„Algoritmul modificat de filesort funcţionează în felul următor:
Citeşte rândurile care se potrivesc clauzei WHERE
Pentru fiecare , înregistrează un tuplu format din valoarea cheii de
sortare şi coloanele referite de interogare
Când bufferul devine plin, sortează tuplele după valoarea cheii de
sortare în memorie şi le scrie într-un fişier temporar
După merge-sorting-ul fişierului temporar, preia liniile în ordinea
sortată, dar citeşte doar coloanele necesare direct din tuplele sortate pentru a nu
accesa tabela a doua oară”[6]
31. 31
Deoarece algoritmului modificat presupune păstrarea unor tuple mai mari decât a
algoritmului original s-ar putea să fie nevoie să facă mai multe operaţii de citire şi scriere pe
disc decât cel original , devenind mai lent. De accea MySQL decide să folosească algoritmul
modificat doar atunci când dimensiunea totală a coloanelor din tuplele sortate nu depăşeşte
valoarea variabilei de sistem max_length_for_sort_data . Încercarea de a seta această variabilă
prea mare poate duce la o activitate mare a discului şi o activitate redusă a procesorului.
Pentru a vedea dacă un ORDER BY foloseşte vreun algoritm de filesort putem folosi
comanda EXPLAIN iar în coloana Extra ar trebui să avem „Using filesort” . Pentru detalii
exacte ale algoritmului de filesort putem folosi trace-ul optimizatorului iar filesort_summary
ne dă informaţiile necesare.
Exemplu de trace:
SET optimizer_trace="enabled=on";
select * from t order by b asc;
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";
Iar pentru filesort_sumpary avem un output de forma:
"filesort_summary": {
"rows": 100,
"examined_rows": 100,
"number_of_tmp_files": 0,
"sort_buffer_size": 25192,
"sort_mode": "<sort_key, additional_fields>"
}
Unde sort_mode ne dă informaţii despre algoritmul folosit:
<sort_key, rowid>: Tuplele din sort buffer fiind formate din valoarea
cheii de sortare şi ID-ul rândului ( algoritmul original)
<sort_key, aditional_fields>: Tuplele din sort buffer fiind formate din
valoarea cheii de sortare şi coloanele referite de interogare ( algoritmul modificat)
Pentru a creşte viteza sortării pentru cazurile când nu pot fi folosite indexuri există
câteva strategii:
Creşterea valorii variabilei sort_buffer_size (cât de mare este bufferul
pentru sortare)
Creşterea valori variabilei sort_rnd_buffer_size ( cât de mare este
bufferul pentru citirea rândurilor)
32. 32
Folosirea cât mai puţină a memoriei RAM per rând , declarând coloane
de dimensiuni suficiente cât să stocheze datele . De exemplu VARCHAR(16) este mai
bine decât VARCHAR(100)
Schimbarea valorii variabilei tmpdir să pointeze către un sistem de
fişiere dedicat cu spaţiu mare de stocare. Fişierul ar trebuie sa fie pe alt disc fizic nu pe
acelaşi disc .
Folosirea funcţiei LIMIT dacă este posibil poate duce la optimizare
reducând procesele de scriere şi citire pe disc.
3.2.3.11 Optimizare GROUP BY
În general pentru a satisface clauza GROUP BY trebuie parcursă întreaga tabelă şi
creată o tabelă temporară în care sunt salvate liniile pentru fiecare grup care este consecutiv ,
după care se foloseşte tabela pentru a descoperi grupurile şi de a aplica funcţiile de agregare.
MySQL ne oferă o soluţie de a evita crearea unei tabele temporare prin folosirea
indexurilor. O precondiţie pentru aceasta este ca toate atributele din clauza GROUP BY să
facă parte din acelaşi index , iar indexul să păstreze cheiile în ordine ( B+-arbori, nu indexuri
de tip HASH ). De asemenea utilizarea indexurilor în locul tabelei temporare depinde şi de ce
părţi ale indexului au fost folosite într-o interogare , condiţiile specificate pentru acestea şi
funcţiile agregate selectate. MySQL are 2 modalităţi de executare a unui GROUP BY prin
intermediul indexurilor: primul aplică acţiunea de grupare împreună cu toate predicatele din
interval, iar al doilea prima dată execută o cautare pe intervale , iar după grupează tuplele
rezultate.
3.2.3.11.1 Loose Index Scan
Această metodă foloseşte doar o parte a cheii pentru a grupa rezultatele. Dacă nu
există clauza WHERE aceasta ia toate cheiile care fac parte din grupul respectiv, care este mai
avantajos decât a citi toate cheiile. Dacă clauza WHERE conţine predicate de tip range, atunci
Loose Index Scan caută doar prima cheie din fiecare grup care satisface condiţia de
range.Acest tip de scanare este posibil dacă:
Interogarea este asupra unei singure tabele
Coloanele din clauza GROUP BY trebuie să fie cel mai din stânga
prefix al indexului şi să nu fie alte coloane. De exemplu avem indexul t1(c1,c2,c3) ,
GROUP BY c2,c3 sau GROUP BY c2,c3,c4 nu sunt bune , primul nu este cel mai din
stânga prefix ( c1,c2 ,.. ) iar al doilea conţine coloane care nu fac parte din index.
Singurele funcţii agregate folosite în SELECT pot fi MIN() şi MAX() ,
şi oricare dintre ele trebuie să facă referinţă la aceeaşi coloană , coloana trebuie sa fie
în index şi să urmeze coloanele din GROUP BY
Orice alte părţi ale indexului care nu sunt folosite în GROUP BY
trebuie să fie constante ( sau să fie comparate cu constante ) , cu excepţia
argumentului funcţiilor MIN() si MAX()
Coloanele din index trebuie să fie indexate în totalitate. De exemplu c1
VARCHAR(20) nu poate fi folosit dacă indexăm doar o parte INDEX (c1(10))
33. 33
Dacă Losse Index Scan este aplicat putem observa în rezultatul EXPLAIN „Using
index for group-by” în coloana Extra.
Loss Index Scan este aplicat şi în cazul altor funcţii de agregare folosite în select-ul
interogării dacă:
AVG(DISTINCT) şi SUM(DISTINCT) au un singur argument,
COUNT(DISTINCT) poate avea mai multe argumente.
Nu trebuie să apară clauzele GROUP BY si DISTINCT în query
Limitările enunţate mai sus se aplică şi în aceste cazuri
3.2.3.11.2 Tight Index Scan
O scanare de tip Tight Index Scan face o scanare a indexului întreg sau o scanare
range a index-ului , depinde de condiţiile query-ului.
Atunci când condiţiile algoritmului Loose Index Scan nu pot fi îndeplinite şi pentru a
evita o creare temporară de tabelă MySQL foloseşte condiţiile de range pe index-ul din clauza
WHERE pentru a găsi doar cheile care satisfac condiţia, caz contrar, când nu găseşte condiţii
de range în clauza WHERE face o scanare pe întreg indexul.
Pentru ca acest tip de scanare să funcţioneze este de ajuns să avem condiţii de egalitate
cu o constantă pe părţiile cheii care nu se regăsesc în GROUP BY. Nişte exemple bune pentru
care acest algoritm se aplică sunt:
SELECT c1, c2, c3 FROM t1 WHERE c2 = 'a' GROUP BY c1, c3;
SELECT c1, c2, c3 FROM t1 WHERE c1 = 'a' GROUP BY c2, c3;
3.2.3.12 Optimizare DISTINCT
În cele mai multe cazuri clauza DISTINCT poate fi considerată un caz special de
GROUP BY, iar acest lucru înseamnă că optimizările enumerate pentru GROUP BY se aplică
şi în acest caz. Să presupunem că avem indexul key(c1,c2,c3) , o optimizare pe index se
aplică şi în cazul unei interogări de forma:
select distinct b,c,d from t where c >=2;
În EXPLAIN putem regăsi în coloana extra „Using index” şi acelaşi algoritm aplicat
ca şi în cazul unei interogări de forma:
select b,c,d from t where c >=2 group by b;
Folosirea clauzei LIMIT face ca MySQL să oprească interogarea atunci când va găsi
numărul de coloane distincte cerute , nefiind nevoie de o întreagă scanare.
3.2.3.13 Optimizare LIMIT
MySQL oferă nişte optimizări pentru cazurile când este folosită clauza LIMIT şi nu
clauza HAVING:
La selectarea a câtorva linii dintr-o tabelă, MySQL foloseşte indexul în
unele cazuri în care optimizatorul preferă o întreagă scanare
34. 34
Dacă este combinat LIMIT row_count cu ORDER BY , MySQL
returnează doar primele row_count linii sortate. Dacă ORDER BY se face prin
intermediul unui index este şi mai rapid, însă dacă este făcut prin intermediul unui
filesort atunci MySQL selectează toate rândurile care corespund clauzelor interogării
fără clauza LIMIT , iar sortarea se face pe primele row_count linii, restul ne mai fiind
nevoie să fie sortate se va opri.
Dacă este combinat LIMIT cu DISTINT, MySQL se opreşte când se
găseşte numărul de linii distincte dorite.
În cazurile când GROUP BY foloseşte un index el calculează
rezultatele când se schimbă valoarea indexului. Însă dacă este folosită clauza LIMIT
acest lucru nu se mai întamplă.
După ce trimite numărul de linii precizate în LIMIT MySQL opreşte
interogarea dacă nu foloseşte SQL_CALC_FOUND_ROWS. Numărul de linii putând
fi preluat după cu SELECT FOUND_ROWS() .Această metodă returnează numărul de
linii care s-ar potrivi clauzelor interogării fară a mai fi nevoie să mai faca un select.
LIMIT 0 este foarte util atunci când vrem să verificăm validitatea unei
interogări.
Dacă interogarea noastră foloseşte tabele temporare atunci MySQL
foloseşte clauza LIMIT pentru a calcula cât de mult spaţiu are nevoie.
MySQL optimizează mai eficient interogările de forma:
SELECT ... FROM single_table ... ORDER BY non_index_column [DESC] LIMIT [M,]N;
Dacă elementele care trebuie sortate pentru N(M+N dacă e specificat M) linii nu
încap în sort buffer atunci el foloseşte o sortare pe fişiere(merge-file) pentru a le sorta, însă
pentru a evita folosirea merge-file poate folosi sort buffer-ul ca o coadă cu priorităţi.
Costurile pentru aceşti algoritmi sunt:
Metoda cozii cu priorităţi foloseşte mult procesorul pentru a introduce
linii în coadă ordonate.
Metoda merge-file foloseşte operaţii I/O ale discului pentru scrierea şi
citirea din fişier şi operaţiile ale procesorului pentru sortare.
Optimizatorul ia în considerare un echilibru între aceşti factori pentru valorile
particulare ale lui N şi dimensiunea liniei.
3.2.3.14 Prevenirea scanării întregii tabele
O scanare a întregii tabele poate apărea în următoarele cazuri:
Tabela este foarte mică şi optimizatorul consideră mai rapid o scanare a
acesteia decât folosirea cheii pentru a găsi rândurile. Acest lucru se întamplă în
momentul în care tabela are mai puţin de 10 rânduri şi dimensiunea rândului este
foarte mică
Nu sunt restricţii în clauza WHERE sau ON pentru coloanele indexate
35. 35
Comparăm un index cu o constantă care cuprinde o mare parte din
tabelă şi atunci optimizatorul decide că este mai rapidă o întreagă scanare.
Folosim o cheie cu cardinalitate mică şi MySQL decide că va avea
nevoie de multe căutări de chei şi atunci consideră mai rapidă întreaga scanare a
tabelei.
O scanare întreagă pentru o tabela mică este asemănătoare cu cea a unui index , însă
pentru tabele mari este foarte costisitoare. Pentru a preveni întreaga scanare pentru tabele mari
putem încerca următoarele variante:
Folosirea comenzii ANALYZE TABLE tabe_name pentru a actualiza
distribuţiile de chei pentru scanarea tabelei
Folosirea FORCE INDEX pentru scanarea tabelei, aceasta forţează să
folosească indexul dat în căutare: SELECT * FROM t1, t2 FORCE INDEX (index_for_column)
WHERE t1.col_name=t2.col_name;
Pornirea mysqld cu opţiunea --max-seeks-for-key=1000 sau folosind
SET max_seeks_for_key=1000 . Acesta îi spune optimizatorului că o căutare fără
cheie cauzează mai mult de 1000 de chei căutate.
3.2.4 Optimizare cache
3.2.4.1 InnoDB Buffer Pool
InnoDB menţine o zonă de depozitare numită „buffer pool” pentru date cache şi
indexuri în memorie. Întelegerea cum funcţionează acesta şi păstrarea cât mai multor date în
memorie este un important aspect al MySQL-ului.
Ideal ar fi să setăm valoarea pentru buffer cât mai mare fără să afectăm memoria
folosită de celalte procese. Astfel vom păstra cât mai multe date în memorie, citim o dată de
pe disc şi apoi luăm datele din memorie pe baza citirii anterioare. Buffer pool cachează chiar
şi datele schimbate în urma unei operaţiuni de insert sau update, reducând citirea de pe disc.
În funcţie de volumul de muncă de pe sistem putem regla ce părţi să păstreze în memorie
pentru accesări frecvente , în ciuda operaţiunilor bruşte precum backup sau rapoarte. Dacă
avem un sistem pe 64 de biţi cu multă memorie putem împărţi buffer-ul în mai multe părţi
astfel reducem lupta pentru structurarea memoriei pentru operaţiuni concurente.
MySQL tratează pool buffer-ul ca o listă, folosind o variaţie a algoritmului „Least
Recently Used” (LRU). Când acesta trebuie sa adauge un nou bloc în buffer pool , InnoDB
evacuează cel mai puţin recent bloc folosit şi adaugă noul bloc în mijlocul listei. Strategia
„midpoint inseration” tratează lista ca pe două subliste: „capul” , o sublistă a blocurilor „noi”
( sau „tinere”) accesate recent, şi „coada” , o sublistă a blocurilor „vechi” care au fost mai
puţin accesate recent.
Algoritmul păstrează blocurile puternic utilizate în lista nouă , iar lista veche conţine
doar blocuri mai puţin utilizate, aceste blocuri fiind candidate pentru evacuare.
MySQL are configuraţia standard pentru LRU în felul următor:
36. 36
3/8 din buffer pool este dedicat listei vechi
Punctul de mijloc fiind limita unde capul listei vechi întâlneşte coada
listei noi
Când InnoDB citeşte un bloc din buffer pool acesta îl introduce în
punctul de mijloc. Citirea poate fi una efectuată de user sau una a algoritmului citire-
înainte.
Accesarea unui bloc din lista veche îl transformă pe acesta într-un bloc
tânăr , mutându-l în capul listei noi. Dacă citirea s-a produs pentru că acesta a avut
nevoie, acesta ajunge imediat în capul listei, însă dacă blocul a fost citit de algoritmul
citire-înainte acesta nu ajunge imediat în capul listei (s-ar putea să nu apară deloc)
Blocurile care nu sunt accesate îmbătrânesc, mutându-se la coada listei
şi urmând să fie evacuate.
O scanare de tabelă ( operaţia mysqldump sau un select fără where) duce la adăugarea
unui bloc foarte mare în lista nouă , forţând eliminarea altor blocuri din lista veche respectiv
nouă şi acest lucru face ca să se introducă în buffer un bloc care niciodată nu va mai fi
accesat eliminând celelalte blocuri accesate frecvent.
MySQL ne ofera nişte variabile pentru controlul dimensiunii pool buffer-ului şi nişte
variabile pentru optimizarea LRU:
Innodb_buffer_pool_zise , acesta dă dimeniunea buffer-ului , o
dimensiune mai mare poate reduce I/O sistemului
Innodb_buffer_pool_instances, acesta împarte buffer-ul în regiuni
specifice, fiecare având propriul lui LRU şi structură pentru a reduce citirea şi scrierea
memoriei concurente. Această opţiune are efect doar dacă innodb_buffer_pool_size
este mai mare de 1 GB . Pentru o eficienţă mai bună trebuie să avem o combinaţie
între innodb_buffer_pool_size si innodb_buffer_pool_instances astfel încât fiecare
instanţă să aibă cel putin 1 GB per buffer.
Innodb_old_blocks_pct, specifică cât % din buffer este alocat listei
vechi, acesta poate lua valori între 5 si 95 , default este 37.
Innodb_old_block_time, specifică în milisecunde cât timp un bloc
introdus în sublista veche trebuie să rămână acolo după prima accesare înainte de a fi
mutat în sublista nouă. Default această variabilă are valoarea 0 , aceasta înseamnă că
un bloc introdus în sublista veche se mută în sublista nouă numai după ce InnoDB a
evacuat ¼ din paginile blocului inserat , nu contează cât de repede va fi folosit acesta
după inserare. De exemplu dacă dăm valoarea 1000 atunci blocul va ramâne 1000 de
milisecunde în lista veche iar acesta va fi evacuat după dacă nu devine candidat la
lista nouă , adică să fie accesat.
Output-ul de la monitorizarea standard InnoDB conţine câteva fielduri în secţiunea
BUFFER POOL AND MEMORY care aparţin algoritmului LRU pentru buffer pool:
Old database pages: numărul de pagini din sublista veche a buffer pool-
ului
37. 37
Pages made young, not young: numărul de pagini mutate în sublista
nouă, şi numărul de pagini care nu au putut fi mutate în sublista nouă
youngs/s non-youngs/s: numărul de accesări al paginilor vechi care au
fost sau nu făcute tinere. Acesta diferă de fieldul amintit mai sus prin două lucruri: se
referă numai la pagini vechi, şi acesta se bazează doar pe numărul de accesări al
paginilor nu pe numărul paginilor
young-making rate: accesările care au făcut ca un bloc să treacă în
capul listei noi
not: accesările care nu au făcut ca un bloc să treacă în capul listei noi
3.2.4.2 Query cache
Query cache stochează textul interogării SELECT împreună cu rezultatul acesteia care
a fost trimis clientului. Dacă o interogare identică va fi cerută mai târziu acesteia i se va livra
rezultatul direct din cache fără a mai face interogarea efectivă. Cache-ul este împărţit între
sesiuni, ceea ce face ca unui alt user care face o interogare identică din altă sesiune să i se
livreze acelaşi rezultat cache din sesiunea precedentă.
Cache-ul este util atunci când tabelele nu se schimbă foarte des şi interogările sunt
identice. Acesta este benefic în unele cazuri însă poate fi un dezastru în altele, el depune un
efort de 13% după interogare pentru a construi cache-ul şi pentru a căuta, însă dacă spre
exemplu facem interogări dese în care ne trebuie doar o linie din tabelă acest lucru poate duce
la o muncă în plus a procesorului de a găsi cache-ul şi de a-l livra, însă dacă nu facem foarte
multe interogări pentru o singură linie, un rezultat livrat din cache poate fi de 238% mai rapid
decât interogarea în sine. Dimensiunea cache-ului este dată de variabila de sistem
query_cache_size, aceasta poate fi o problemă dacă va fi prea mare va depune prea mult efort
pentru găsirea interogării noastre, însă dacă este prea mic poate duce la eforturi mari ale
procesorului care trebuie să şteargă vechile interogări din cache pentru a face loc celor noi şi
acest lucru duce la blocarea tuturor thread-urilor din baza de date pentru actualizarea acestuia.
Pentru o bază de date care face frecvent insert-uri şi update-uri salvarea interogărilor în cache
nu este o alternativă bună deoarece MySQL şterge toate interogările care ţin de tabele pe care
se aplică operaţiunii de insert,update sau delete, pentru astfel de situaţii în care trebuie să
păstrăm un rezultat costisitor putem folosi alte tehnologii cum ar fi memcached sau redis.
MySQL verifică bit cu bit interogarea pentru a o găsi în cache, ceea ce face ca 2
interogări să fie identice ca valoare dar diferite semantic, una cu majuscule şi una fără
majuscule, să fie văzute ca interogări diferite. Totodată MySQL nu ţine în cache
subinterogările unei interogări exterioare sau interogările din triggere, proceduri sau
evenimente. Dacă o interogare este returnată din cache atunci variabila de sistem Qcache_hits
este incrementată cu 1, iar variabila Com_select nu este incrementată.
De asemeanea un query nu este salvat în cache dacă:
Se referă la o funcţie definită de user sau la o procedură stocată
Se referă la variabile definite de user sau sistem
38. 38
Se referă la tabelele mysql, INFORMATION_SCHEMA sau
performance_schema
Se referă la tabelele partiţionate
Interogarea este de forma:
SELECT ... LOCK IN SHARE MODE
SELECT ... FOR UPDATE
SELECT ... INTO OUTFILE ...
SELECT ... INTO DUMPFILE ...
SELECT * FROM ... WHERE autoincrement_col IS NULL
Foloseşte tabele de tip TEMPORARY
Nu foloseşte nici o tabelă
Interogarea generează alerte
Utilizatorul are un privilegiu pe o coloană pentru una din tabelele
invocate
Toate variabilele de sistem pentru cache încep cu „query_cache_”. În continuare vom
detalia variabilele utile pentru reglarea cache-ului interogării:
query_cache_size : ne dă dimensiunea cache-ului. Aceasta trebuie să
fie mai mare de 40 kb, având nevoie de spaţiu pentru a stoca şi textul interogării
query_cache_type: influenţează comportamentul cache-ului,
schimbarea acestuia în timpul rulării face ca noul tip să se aplice doar pentru clienţii
conectaţi după schimbare, însă aceasta poate fi setată şi din variabila de sesiune SET
SESSION query_cache_type = OFF; şi se va aplica doar pentru clientul respectiv. Însă dacă
dorim să aplicăm tuturor clienţilor putem porni server-ul mysqld cu --
query_cache_type=1 .Această variabila putând lua 3 valori:
o 0 sau OFF previne cache-ul sau returnarea rezultatelor din
cache
o 1 sau ON activează cache-ul cu excepţia interogărilor care
conţin SQL_NO_CACHE
o 2 sau DEMAND activează cache-ul doar pentru interogările ce
conţin SQL_CACHE
query_cache_limit: specifică dimensiunea maximă a unui select care
poate fi cache-uit. Default are valoare 1 MB
query_cache_min_res_unit: dimensiunea minimă alocată unui bloc
pentru un select care se mută în cache. Această variabilă are valoarea default de 4 KB
care se adaptează multor cazuri, însă dacă avem multe selecturi cu o valoare mai mică
acest lucru ar putea face ca să avem o memorie defragmentată din cauza numărului
mare de spaţiu liber rămas în blocuri. Fragmentarea memoriei poate duce la ştergerea
cache-ului interogărilor din cauza lipsei de memorie. Numărul de blocuri libere si
interogări şterse prin eliberarea spaţiului sunt date de variabilele de sistem
Qcache_free_blocks respectiv Qcache_lowmem_prunes. Însa dacă avem multe
interogări cu rezultate de dimensiuni mai mari decât minimul putem creşte această
variabilă pentru a evita alocarea unui alt bloc de dimensiune mai mare.
39. 39
Numărul total de interogări din cache este dat de variabila Qcache_queries_in_cache,
respectiv numărul de blocuri din cache de către variabila Qcache_total_blocks. Numărul total
de interogări poate fi dat de formula Com_select + Qcache_hits + interogările găsite cu erori
de parsare iar variabila Com_select este dată de formula Qcache_inserts +
Qcache_not_cached + interogările găsite cu erori de parsare. După executarea comenzii
FLUSH QUERY CACHE doar un singur bloc liber va mai rămâne.
Caching-ul permite server-ului să fie mai eficient deoarece reduce regăsirea declaraţiei
unei structuri şi convertirea acesteia. Convertirea şi cache-ul apar atunci când:
Pregătirea declaraţiilor, atât procesele de nivel SQL ( folosind
declaraţia PREPARE ) cât şi procesul folosit de protocolul binar client-server (
folosind mysql_stmt_prepare() , funcţia C API ). Variabila de sistem
max_prepared_stmt_count controlează numărul total de declaraţii pe care server-ul le
cache-uieşte.
Programe stocate (proceduri stocate, funcţii, triggere şi evenimente), în
acest caz server-ul converteşte şi cache-uieşte întregul corp al funcţiei. Variabila de
sistem stored_program_cache indică numărul aproximativ de programe stocate de
server într-o sesiune.
Atunci când server-ul foloseşte cache-ul intern pentru o declaraţie trebuie avut grijă ca
structura să nu se schimbe. Modificarea unei metadate pentru un obiect folosit în cache-ul
declaraţiei duce la o nepotrivire între declaraţia actuală a obiectului şi declaraţia cache-uită.
Pentru a preveni problema schimbării metadatelor tabelelor sau view-urilor referite de o
declaraţie, server-ul detectează aceste schimbări şi automat fixează declaratia când va fi
utilizată data viitoare. Acesta este reanaliza declaraţiei şi reconstruirea structurii interne.
Reanaliza apare şi atunci când după ce cache-ul definirii tabelelor sau view-urilor referite
sunt şterse, implicit prin a face loc pentru noi intrări în cache, sau explicit prin FLUSH
TABLES.
Acelaşi lucru se întâmplă şi pentru programele stocate. De asemenea acesta poate
detecta metadata-urile schimbate dintr-o expresie. Acest lucru este foarte util pentru
programele stocate care folosesc declaraţii de CURSOR sau de controlul flow-ului cum ar fi
IF, CASE şi RETURN. Acesta fiind capabil să repare doar părţile expirate fără să
reconstruiască întrega declaraţie sau procedură.
Reanaliza este automată, dar în măsură ce aceasta apare, diminuează performanţa
pregătirii declaraţiilor şi programelor stocate. Variabila de sistem Com_stmt_reprepare
urmăreşte numărul reanalizelor.
3.2.5 Monitorizare citiri și scrieri
Pentru monitorizarea citirilor și scrierilor de pe disk sau din memorie există nişte
variabile de sistem care ne vor fi utile pe parcurs:
40. 40
Handler_read_first: ne indică de câte ori a fost citită prima înregistrare
a unui index. Dacă aceasta este prea mare înseamnă că avem prea multe scanări
complete de index.
Handler_read_key: numărul de requesturi prin care se citeşte o linie
bazându-se pe un index. Dacă valoarea este mare înseamnă că tabela noastră este
indexată corect.
Handler_read_next: numărul de requesturi pentru a citi următoarea linie
în ordinea cheilor. Această valoare se incremnetează dacă folosim o condiţie de range
sau o scanare pe indexuri.
Handler_read_prev: numărul de requesturi pentru a citi linia anterioară
în ordinea cheilor.
Handler_read_rnd: numărul de requesturi pentru a citi o linie dintr-o
poziţie fixă. Acesta este mare dacă facem multe interogări ce necesită o sortare.
Handler_read_rnd_next: numărul de requesturi pentru a citi linia
următoare din fişier. Dacă are valoare mare poate sugera că nu este indexat
corespunzător sau interogarea noastră nu foloseşte avantajele unui index. Valoarea
acestuia este formată din numărul de linii citite + 1 care reprezintă serealizarea
acestora.
Innodb_buffer_pool_pages_data: numărul de pagini ce conţin date.
Innodb_buffer_pool_pages_dirty: numărul de pagini în prezent
„murdare”.
Innodb_buffer_pool_pages_flushed: numărul de requesturi flush pentru
pagini din buffer pool.
Innodb_buffer_pool_pages_free: numărul de pagini goale.
Innodb_buffer_pool_read_request: numărul de cereri de citire logice
Innodb_buffer_pool_read: numărul de citiri logice care nu au putut fi
satisfăcute de buffer pool şi trebuie citite de pe disc
Innodb_buffer_pool_waite_free: acest contor numără instanţele citirilor
sau scrierilor de pagini din buffer pool după care este necesar să aştepte. Aceste citiri
şi scrieri în mod normal se fac în background dar, dacă această valoare este mare
înseamnă că dimensiunea buffer pool-ului nu este setată corespunzător.
Pentru a vedea cum „lucrează” o interogare vom folosi:
SHOW SESSION STATUS LIKE 'Handler%';
SELECT * FROM ts; --interogarea noastra
SHOW SESSION STATUS LIKE 'Handler%';
Apoi comparăm rezultate variabilelor handler. Însă această metodă nu ne dă un
răspuns la cache vs non-cache însă ne dă indicii interesante despre interogare. Însă putem
folosi variabilele globale innodb pentru a face o comparaţie mai exactă, însă trebuie să le
luăm înainte şi după executarea interogării, fiind globale le poate modifica şi alte
interogări.
SHOW SESSION STATUS LIKE 'Innodb_buffer_pool%';
41. 41
SELECT * FROM ts;
SHOW SESSION STATUS LIKE 'Innodb_buffer_pool%';
3.3 Optimizarea server-ului MySQL
Acest subcapitol prezintă câteva sugestii de îmbunătăţire a server-ului MySQL atunci
când toate optimizările au fost aplicate asupra InnoDB. Printre acestea se numără:
Căutările pe disc sunt o adevarată ştrangulare a performanţelor atunci
când cache-ul nu mai poate să ţină toate datele şi sunt foarte multe operaţii de
executat. Pentru bazele de date mari în care accesul la date este mai mult sau mai puţin
random, putem folosi un disc pentru a citi sau pentru a scrie.
Creşterea numărului de axe disponibile ale discului prin adăugarea de
fişiere cu legături simbolice către alte discuri sau prin spargerea datelor în blocuri care
pot fi scrise pe discuri diferite.
o Linkuri simbolice: pentru tabele de tip MyISAM se creează
legături simbolice pentru fişierul indexurilor sau a datelor de la locaţia lor, de
obicei către un alt disc. Pentru InnoDB nu există astfel de legături, însă se
poate apela comanda create pentru tabela file-per-table cu specificarea locaţiei
din afara directorului datelor MySQL folosind următoarea comandă:
DATA DIRECTORY = absolute_path_to_directory , în declaraţia tabelei.
o Spargerea datelor: acest lucru presupune să ai mai multe discuri
şi să pui primul bloc pe primul disc, al doilea pe al doilea disc şi tot aşa
(nr_de_blocuri MOD nr_de_discuri). Împărţirea datelor pe discuri diferă în
funcţie de sistemul de operare şi de dimensiunea împărţirii acestora, astfel
putem obţine rezultate diferite. Pentru aceasta va trebui găsit numărul de
discuri optime în care se pot împărţi.
Pentru mai multă siguranţă, putem folosi tehnologia Redundant Array
of Independent Disks(RAID) 0+1, însă aceasta necesită 2xN unităţi pentru a ţine N
unităţi de date.
O bună alegere ar fi să variem RAID în funcţie de cât de critic este
tipul de date. De exemplu putem stoca datele semi-importante în RAID 0 şi cele
importante în RAID 0+1 sau RAID N. RAID N, însă s-ar putea sa fie o problemă dacă
ai multe scrieri datorită timpului de care are nevoie pentru a actualiza biţii de pe
partiţii.
Pe Linux putem folosi hdparm pentru configurarea interfeţei discului.
Performanţa şi siguranţa când folosim această comandă depinde de componentele
hardware.
Putem seta câţiva parametri pe fişierele de sistem pe care baza de date
le foloseşte:
o Dacă nu ne interesează când au fost accesate ultima dată
fişierele, putem monta fişierele de sistem cu opţiunea –o noatime .
42. 42
o Unele sisteme de operare pot seta fişierele de sistem să fie
actualizate asincron prin montarea acestora cu opţiunea –o async .
Unele arhitecturi hardware/ sisteme de operare suportă pagini de memorie mai mari
decât parametri iniţiali (de obicei 4KB). În MySQL paginile sunt folosite de către InnoDB
pentru a aloca memorie pool buffer-ului şi altor zone de memorie suplimentare. MySQL
suporă de asemenea implementarea paginilor mari în Linux, numite HugeTLB. Pentru a folosi
HugeTLB în Linux trebuie să vedem dacă kernelul suportă acest tip, acesta putând fi verificat
prin comanda cat /proc/meminfo | grep -i huge. Dacă kernelul nostru necesită o reconfigurare
pentru HugeTLB trebuie să consultăm documentaţia din Documentation/vm/hugetlbpage.txt.
Iniţial MySQL are dezactivat suportul pentru paginile mari, pentru activarea acestuia trebuie
pornit MySQL-ul cu parametrul --large-pages sau adăugarea acestuia în fişierul my.cnf. Dacă
InnoDB nu poate folosi aceste pagini el le va folosi înapoi pe cele default returnând un mesaj
de alertă în log-uri.
45. 45
4. Implementarea bazelor de date
4.1 Motivația
A nu acorda atenție suficientă pentru implementarea bazelor de date ale unei aplicaţii poate
avea efecte dezastruoase în timp: scăderea vitezei de răspuns ale acestora, creşterea costurilor
pentru întreţinerea şi îmbunătăţirea acestora, chiar şi posibilitatea pierderii integrităţii datelor.
Toate acestea se întâmplă din cauză că acestea nu sunt concepute, reparate şi îmbunătăţite la
timp, numai în momentul în care se ajunge la o dimensiune considerabilă. Atunci este foarte
greu să le îmbunătăţeşti, iar costurile sunt de două ori mai mari datorită faptului că va fi
nevoie de o transformare a datelor din baza de date veche în cea nouă. De asemenea timpul
alocat pentru aceste schimbări este şi el dublu datorită faptului că trebuie făcute scripturi de
migrare.
4.2 Aplicația și structura bazei de date
Am luat ca exemplu o aplicație web pentru o agenție de turism. Aceasta trebuie să ofere
utilizatorilor o căutare rapidă a hotelurile dorite, respectiv oferte ale hotelurilor în diferite ţări,
zone sau orașe. De asemenea agenția trebuie să poată adauga constant și actualiza ofertele,
hotelurile, ţările, zonele sau oraşele şi să vadă rezervările făcute de utilizatori. Utilizatorii mai
pot acorda note hotelurilor pe diverse criterii. Această aplicaţie, pentru fiecare ţară, zonă sau
oraş va deţine o pagină cu detalii şi un top 5 cele mai apreciate hoteluri de către utilizatori.
Aplicaţia este construită şi cu o posibilitate de a face căutări după un şir de caractere dat, iar
căutarea trebuie să returneze toate hotelurile ale căror nume, numele ţării, numele zonei sau
numele oraşului conţin şirul respectiv de caractere.
Această aplicație are nevoie de o bază de date rapidă ale căror date să nu îşi piardă
integritatea. Chiar dacă se folosesc alte tehnologii pentru cache, ce se întâmplă cu prima
cautare ? Cineva va fi nevoit să aştepte după acea interogare, şi chiar şi cu cronuri, ar însemna
să ţii o mare parte a select-urilor în memorie, iar pentru o aplicaţie în care căutarea este
dinamică acest lucru înseamnă că ai avea nevoie de GB de memorie pentru a salva toate
combinaţiile de căutări.
O primă structură a acestei baze de date este reprezentată în Fig. A.1. În aceasta se
observă des folosirea numelui entităţii pe post de cheie primară sau chiar chei primare
compuse. Tot în aceasta putem observa că se pierde uşor integritatea unui oraş datorită
faptului că acesta depinde de zonă şi ţară, neexistând o ierarhie bine definită care să păstreze
integritatea. Tabela hotel şi conditi_camera conţin foarte multe fielduri neindexate care sunt
folosite pentru diverse filtrări ale hotelelor, acest lucru face ca o interogare să fie foarte lentă.
De asemenea se observă şi faptul că sunt folosite tipuri de dată mai mari care vor ocupa
spaţiul în buffer-ul de sortare sau în pool buffer producând mai multe citiri şi scrieri pe disc.
4.3 Optimizarea structurii bazei de date
Pentru optimizare prima dată schimbăm cheile primare ale tabelelor tara, zona şi oras să
fie de tipul int unsigned not null , acestea fiind şi mai rapide în căutarea liniilor dorite,
46. 46
datorită faptului că înainte era indexată toată lungimea cheii şi unele erau chei compuse, în
tabelele zona şi oras cheia era formată din cheile primare ale tabelei precedente acesteia, în
plus not null ajută la creşterea cardinalităţii indexului şi evitarea verificării dacă este nulă
coloana, iar prin declararea unei singure chei primare suntem siguri că păstrăm integritatea
datelor iar la o schimbare de zonă sau oraş nu este nevoie de reconstruirea indexului în urma
actualizării coloanei cu numele zonei sau oraşului. Din cauză că un oraş nu este obligatoriu să
fie încadrat într-un sezon am decis să creez o tabela de legătură între tabelele oras şi sezon iar
ca şi cheie primară să fie id-ul oraşului; astfel eliminăm verificarea dacă este null id-ul
sezonului fiind mult mai rapid la interogări. Iar cheile primare vechi le-am transformat în
indexuri unice formate din id-ul tabelei anterioare şi coloana nume.
Fig. 4-1 Tabelele tara,zona, oras, sezon neoptimizate
Fig. 4-2 Tabelele tara, zona, oras, sezon optimizate
Pentru tabelele de detalii am schimbat cheile străine într-o coloana de tipul int unsigned
not null care face referire la tabelele respective, am adăugat un nou index format din
47. 47
coloanele: id, categorie şi vizibil fiind un criteriu de selectare tot timpul pentru afisarea unor
detalii. Deşi aceste detalii trebuie ordonate după prioritate nu am adăugat index pe această
coloană fiind luate în considerare informaţiile detaliate în capitolul 2.3.2.10 .
Fig. 4-3 Tabelele de detalii pentru entitati neoptimizate
Fig. 4-4 Tabelele de detalii pentru entitati optimizate
Şi tabelele tag, obiective,oras_tag,oras_obiective au suferit schimbări ale cheii primare
fiind adăugat o nouă coloană tot de tipul int unsigned not null, făcându-le mai rapide la
operaţiile de join. Această modificare a ajutat şi la păstrarea integrităţii datelor, acest id fiind
unic şi nemodificabil, cum era cazul anterior când numele unui oraş era cheia primară.
48. 48
Fig. 4-5 Tabelele obiective,tag neoptimizate
Fig. 4-6 Tabelele obiective, tag optimizate
O tabelă care a avut multe schimbări a fost tabela hotel. Pe lângă schimbarea tipurilor de
date pentru coloane a fost creată şi o nouă tabelă unde au fost mutate mai multe coloane din
tabela hotel, iar în tabela hotel rămânând doar o coloană info_has care ne va ajuta mult la
interogări. Dat fiind faptul ca utilizatorii făceau continuu operaţii de căutare cu diferite
combinaţii ale coloanelor has_sauna, has_piscina, has_parcare, has_restaurant, has_fitness,
aceste coloane pot lua doar valori de 1 si 0, să creăm indexuri pentru toate combinaţiile
posibile nu era o soluţie optimă, cu atât mai mult cu cât actualizările pe aceste coloane erau
relativ frecvente. De aceea am decis să le mut, totodată şi o mare parte a informaţiilor despre
hotel în altă tabelă, aici au intrat şi informaţii care nu sunt actualizate frecvent şi au doar
caracter informativ precum codul API-urilor, map_x, map_y, distanta_obiectiv şi chiar
coloana intern. Am construit o regulă pentru coloanele: has_sauna, has_piscina, has_parcare,
has_restaurant şi has_fitness, în tabela principală păstrez doar o coloana, info_has, care se
actualizează prin intermediul unui trigger şi care ne dă date despre coloanele noastre după
următoarea formulă:
49. 49
$coloane_has = array(
‘has_sauna’ => 1,
‘has_piscina’ => 2,
‘has_parcare’ => 3,
‘has_restaurant’ => 4,
‘has_hitness’ => 5,
);
$info_has =0;
foreach($coloane_has $coloana => $valoare){
$info_has = $info_hotel[$coloana] * POW(2,$valoare);
}
Orice combinaţie a puterilor lui 2 începând cu o putere nenula dacă o însumăm obţinem o
sumă unică tot timpul. Acest lucru ne ajută enorm din cauză că vom avea doar 1 index după
care vom efectua filtrarea, iar la actualizări frecvente va fi nevoie de reconstruirea unui singur
index. Mutarea celorlalte coloane ajută interogărilor prin faptul că nu va mai ocupa foarte
mult spaţiu o linie a tabelei hotel în buffer-ul de sortare, acest lucru fiind specificat în
capitolul 2.3.2.10. Adăugarea de indexuri pe coloanele nota_hotel, nota_restaurant,
nota_locatie, nota_conditi ajută foarte mult creşterea vitezei sortării rezultatelor interogării
conform informaţiilor din capitolul 3.2.3.13 .
Fig. 4-7 Hotel neoptimizat
50. 50
Fig. 4-8 Hotel optimizat
Pentru tabela conditii_camera vom proceda similar tabelei hotel, mutând coloanele
has_hav, hav_pat_infant, has_balcon , has_internet, has_vedere_mare, nr_paturi, parter şi
ultim_etaj într-o altă tabelă şi creăm în tabela conditi_camera o coloană info_conditi_camera
care se va comporta la fel precum coloana info_has din tabela hotel. Puterea pentru fiecare
coloană fiind: has_tv = 1, has_pat_infant = 2, has_balcon = 3, has_internet=4,
has_vedere_mare=5,parter=6,ultim_etaj=7.
Am adăugat un index unic pe coloanele info_conditi_camera,id_hotel,id_camera deoarece
aceeaşi cameră cu aceleaşi condiţii nu are voie să existe de mai multe ori pentru acelaşi hotel,
iar cu acest index transformăm indexul nostru în alte 3 posibile indexuri conform
informaţiilor din capitolul 3.2.2.2 .
51. 51
Fig. 4-9 conditi_camera neoptimizat
Fig. 4-10 conditi_camera optimizat
Pentru tabela oferta scoatem cheia straină către tabelul hotel pentru a păstra integritatea,
legătura cu hotelul fiind făcută prin intermediul tabelei conditi_camera, la fel şi cu celelalte
chei primare, le-am transformat în coloane de tip int unsigned not null , în această tabelă a
52. 52
fost format şi un index care cuprinde coloanele vizibil,id_conditie,id_tip_oferta , coloana
vizibil fiind în permanenţă folosită şi în alte interogări informaţiile din capitolul 3.2.2.2 se
aplică şi pentru acest index.
Fig. 4-11 Tabela oferta neoptimizată
Fig. 4-12 Tabela oferta optimizată
Restul tabelelor suferind doar nişte modificări ale tipurilor de dată pentru a fi mai optimă
ordonarea sau sortarea acestora conform capitolului 2.3.2.10 .
4.4 Interogări
53. 53
4.4.1 Top 5 hoteluri dintr-o ţară
Selectarea primelor 5 hoteluri dintr-o ţară, ştiind cheia primară a tabelei tara.
4.4.1.1 Selectul neoptimizat
Interogare:
select * from hotel where nume_tara=? order by nota_hotel desc limit 5
Explain-ul interogării:
Id Select_type Table Type Possible_key Key Key_len Ref Rows Extra
1 Simple hotel ALL NULL NULL NULL NULL 13759 Using
Where;
Using
filesort
Se poate observa clar că pentru un astfel de select care face o întreagă scanare a tabelei
cea mai bună optimizare ar fi adăugarea unui index pe coloana nume_tara, însă lăsarea
coloanei numelui ţării în tabela hotel va duce la pierderea integrităţi tabelei hotel. Filesort-
ul este cel original, ceea ce face ca atunci când tipurile id-ului linilor şi coloanei de sortare
sunt minime performanţa va fi maximă.
4.4.1.2 Selectul optimizat
Interogare:
select h.* from tara t
inner join zona z on t.id=? and z.id_tara = t.id
inner join oras o on o.id_zona = z.id
inner join hotel h on h.id_oras = o.id
order by h.nota_hotel desc limit 5
Explain-ul interogării:
I
d
Select_ty
pe
Tabl
e
Type Possible_
key
Key Key_l
en
Ref Row
s
Extra
1 Simple t cons
t
PRIMARY PRIMA
RY
4 const 1 Using
index;
Using
temporar
y; Using
filesort
1 Simple z ref PRIMARY,
id_tara
id_ta
ra
4 const 12 Using
index
1 Simple o ref PRIMARY,
id_zona
id_zo
na
4 licenta_optimizat.
z.id
1 Using
index
1 Simple H ref id_oras Id_or
as
4 licenta_optimizat.
o.id
1 NULL
Se poate observa folosirea intensă a indexurilor pentru o performanţă considerabilă a
operaţiilor de join, acest select nu va fi mai rapid precum căutarea directă în cazul în care
nu ar conta integritatea datelor şi am avea id-ul ţării în tabela hotel. Deşi sunt patru
operaţii de join putem observa că MySQL creează o tabelă temporară în care salvează
rezultatele pentru a le putea sorta folosind buffer-ul pe post de coadă cu priorităţi,
algoritmul filesort modificat. Tipul de dată şi dimensiunea buffer-ului joacă un rol foarte
important conform informaţiilor din capitolul 3.2.3.10 .
54. 54
4.4.2 Top 5 hoteluri dintr-o zonă
Selectarea primelor 5 hoteluri dintr-o zonă, ştiind cheia primară a tabelei zona.
4.4.2.1 Selectul neoptimizat
Interogarea:
select * from hotel where nume_tara=? and nume_zona=?
order by nota_hotel desc limit 5
Explain-ul interogării:
Id Select_type Table Type Possible_key Key Key_len Ref Rows Extra
1 Simple hotel ALL NULL NULL NULL NULL 13759 Using
Where;
Using
filesort
Precum interogarea precedentă neoptimizată şi aceasta face o întreagă scanare şi pentru a
o optimiza va trebui să adăugăm un index format din coloanele nume_tara respectiv
nume_zona, însă şi acum tabela hotel îşi va pierde integritatea. Filesort-ul este cel original
şi în acest caz.
4.4.2.2 Selectul optimizat
Interogarea:
select h.* from zona z
inner join oras o on z.id=1 and o.id_zona = z.id
inner join hotel h on h.id_oras = o.id
order by h.nota_hotel desc limit 5
Explain-ul interogării:
I
d
Select_ty
pe
Tabl
e
Type Possible_
key
Key Key_l
en
Ref Row
s
Extra
1 Simple Z cons
t
PRIMARY PRIMA
RY
4 const 1 Using
Index;
Using
temporar
y; Using
filesort
1 Simple o ref PRIMARY,
id_zona
id_zo
na
4 const 5 Using
index
1 Simple h ref id_oras id_or
as
4 Licenta_optimizat.
o.id
1 NULL
Observăm un număr mai scăzut ale operaţiilor de join faţă de anterioara interogare,
aceasta, precum precedenta foloseşte indexurile pentru găsirea rapidă a liniilor şi filesort-
ul modificat pentru sortarea lor, însă are un avantaj faţă de interogarea anterioară prin
faptul că tuplele salvate în buffer sunt mai mici, ceea ce înseamnă un număr mai mare de
linii sortate în memorie şi un număr ale operaţiilor de I/O mai mic.
4.4.3 Top 5 hoteluri dintr-un oraş
Selectarea primelor 5 hoteluri dintr-un oraş, ştiind cheia primară a tabelei oraş.
4.4.3.1 Selectul neoptimizat
Interogarea:
55. 55
select * from hotel where nume_tara=? and nume_zona=? and nume_oras=? order by nota_hotel
desc limit 5
Explain-ul interogări:
Id Select_type Table Type Possible_key Key Key_len Ref Rows Extra
1 Simple hotel ref nume_oras nume_oras 309 const,
const,
const
13 Using
index
condition,
Using
Where;
Using
filesort
Putem observa că această interogare foloseşte cheia străină pentru a găsi liniile şi conform
teoriei de la capitolul 3.2.3.5 aceasta foloseşte algoritmul pushdown pentru a evita o întreagă
scanare. Aceasta nu este din păcate cea mai rapidă interogare datorită faptului că lungimea
indexului este destul de mare şi comparaţia acestuia este costisitoare în comparaţie cu un
index de lungime 4 ( tipul de data int ).
4.4.3.2 Selectul optimizat
Interogarea:
select * from hotel where id_oras=? order by nota_hotel desc limit 5
Explain-ul interogări:
Id Select_type Table Type Possible_key Key Key_len Ref Rows Extra
1 Simple hotel ref id_oras id_oras 4 const 14 Using
Where;
Using
filesort
Comparaţia unui index cu o constantă este cea mai rapidă metodă de a obţine linile dorite,
aceasta se deosebeşte de interogarea neoptimizată prin faptul că indexul este o singură
coloană cu tipul de dată minim, int, ceea ce face să obţinem o maximă performanţă pentru
un astfel de select.
4.4.4 Căutarea hotelurilor după diverse criterii
O căutare a hotelurilor după următoarele criterii:
Toate hotelurile din sezonul curent
Opţional: doar dintr-o ţară, zonă sau oraş
Opţional: doar cele cu un oarecare numar de stele, sau doar cele care au anumite
condiţii
Opţional: doar hotelurile care au un anume tip de cameră sau/şi anume condiţii în
cameră
Opţional: doar hotelurile care au o anumită ofertă
Ordonate dupa nota_hotel descrescător şi limitate 15
4.4.4.1 Selectul neoptimizat
Interogarea:
select h.* from sezon z
inner join oras o
56. 56
on data_inceput<=?
and data_sfarsit >=?
and o.nume_sezon = z.nume
inner join hotel h
on h.nume_oras = o.nume
and h.nume_zona=o.nume_zona
and h.nume_tara=o.nume_tara
and h.nume_tara=? and h.nume_zona=? and h.nume_oras=?
and h.has_restaurant=?
and h.stele=?
inner join conditi_camera c_c
on c_c.id_hotel = h.id and c_c.nume_camera=?
and has_tv = ? and has_pat_infant= ? and has_balcon = ? and has_internet = ?
and has_vedere_mare = ? and parter = ? and ultim_etaj = ?
inner join oras_tag o_t
on h.nume_oras=o_t.nume_oras
and h.nume_zona=o_t.nume_zona
and h.nume_tara=o_t.nume_tara
and o_t.nume_tag = ?
inner join oferta of
on of.nume_tip_oferta=?
and h.id=of.id_hotel and o.vizibil = ?
group by h.id order by h.nota_hotel DESC limit 15
Explain-ul interogării:
I
d
Selec
t_typ
e
Ta
bl
e
Type Possible_key Key Key
_le
n
Ref Ro
ws
Extra
1 Simpl
e
o ref PRIMARY,
nume_sezon,
nume_tara,
nume_zona
nume_
sezon
306 const, const,
const
1 Using temporaly,
Using filesort
1 Simpl
e
z ALL PRIMARY PRIMA
RY
102 const 1 NULL
1 Simpl
e
o_
t
index
_merg
e
nume_oras,
nume_tag
nume_
oras,
nume_
tara
309
,10
3
NULL 1 Using
intersect(nume_ora
s,nume_tag);Using
where; Using index
1 Simpl
e
c_
c
ref PRIMARY,
nume_camera
nume_
camer
a
103 const 3 Using index
condition; Using
where
1 Simpl
e
h eq_re
f
PRIAMRY,
hotel_nume_i
ndex,
nume_oras,
nume_comisio
n
PRIMA
RY
4 licenta_neopt
imizat.c_c.id
1 Using where
57. 57
1 Simpl
e
of ref nume_tip_ofe
rta,id_hotel
id_ho
tel
5 licenta_neopt
imizat.c_c.id
6 Using where
Se observă faptul că optimizatorul întâi filtrează oraşele prin intermediul cheilor străine,
iar în momentul când ajunge la tabela oras_tag face filtrarea where care reduce drastic
oraşele. După trierea oraşelor se observă încercarea optimizatorului de a alege prima dată
condiţiile şi făcând join cu tabela hotel pentru a găsi rapid hotelurile după cheia primară şi
a verifica clauza where de la oraş pe ele. Un lucru important de remarcat este că pentru
sortare se foloseşte algoritmul modificat, ceea ce înseamnă că va salva tuplele pentru a le
sorta, iar dimensiunea acestora este foarte mare datorită cheilor compuse din mai multe
coloane sau a celor unde tipul de dată este dat exagerat de mare, acest lucru va face ca
pentru o bază de date mare să avem foarte multe scrieri pe disc din cauză că buffer-ul se
va umple foarte rapid.
4.4.4.2 Selectul optimizat
Interogarea:
select h.* from sezon s
inner join oras_sezon os
on s.data_inceput<=?
and s.data_sfarsit >=?
and os.id_sezon = s.id
inner join oras o on o.id=os.id_oras and o.id=?
inner join hotel h on h.id_oras = o.id and h.info_has=? and h.stele=?
inner join conditi_camera c_c on h.id=c_c.id_hotel and c_c.info_conditi_camera=? and
c_c.id_camera=?
inner join oferta of on of.id_conditie = c_c.id and of.id_tip_oferta=? and of.vizibil=?
inner join oras_tag o_t on h.id_oras=o_t.id_oras and o_t.id_tag = ?
group by h.id order by h.nota_hotel DESC limit 15;
Explain-ul interogării:
I
d
Sele
ct_t
ype
Ta
bl
e
Type Possible_key Key Ke
y_
le
n
Ref R
o
w
s
Extra
1 Simp
le
os cons
t
PRIMARY,
id_sezon
PRIAMRY 4 const 1 Using temporary;
Using filesort
1 Simp
le
o cons
t
PRIAMRY PRIMARY 4 const 1 Using index
1 Simp
le
o_
t
cons
t
PRIAMRY,
id_tag
PRIMARY 8 const,
const
1 Using index
1 Simp
le
s cons
t
PRIMARY,
data_inceput_s
farsit_index
PRIMARY 4 const 1 NULL
1 Simp
le
h inde
x_me
rge
PRIMARY,
hotel_nume_ind
ex, id_oras,
stele_index,
info_has_index
,
id_oras,
info_has_ind
ex
4,
2
NULL 1 Using
intersect(id_ora
s,info_has_index
);Using where
58. 58
nota_hotel_ind
ex,
nota_restauran
t_index,
nota_locatie_i
ndex,
nota_conditii_
index,
id_comision
1 Simp
le
c_
c
eq_r
ef
PRIMARY,
info_conditi_h
otel_camera_in
dex,
id_camera,
id_hotel
info_conditi
_hotel_camer
a_index
10 const,
licenta_op
timizat.h.
id, const
1 Using index
1 Simp
le
of ref id_tip_oferta,
id_conditie,
id_vizibil_con
ditie_tip_ofer
ta_index
id_conditie_
vizibil_inde
x
5 licenta_op
timizat.c_
c.id,
const
2 Using where
De remarcat este faptul că tipurile de join sunt „const” cea ce înseamnă că acestea vor
returna doar 1 linie bazându-se pe comparaţia indexului cu o constantă, aceste tipuri de
join sunt cele mai rapide. După schimbarea structurii cheilor primare se observă o utilizare
mult mai intensă a acestora. Eficienţa cheilor şi a indexurilor se poate observa din coloana
de rows, dacă o comparăm cu cea neoptimizată observăm că cea neoptimizată face câteva
citiri de linii în plus. Totodată acesta foloseşte algoritmul filesort modificat, însă avantajul
acestuia este numărul de linii redus drastic prin join şi tipul de date a coloanelor din tuple
foarte redus ceea ce îl face mult superior interogării anterioare reducând considerabil
scrierile şi citirile de pe disc a tabelei temporare.
4.4.5 Căutarea unor hotele după nume inclusiv în ţări,zone şi oraşe
Căutarea tuturor hotelurilor în al căror nume, numele ţării, numele zonei sau în numele
oraşului există şirul de caractere dat.
4.4.5.1 Selectul neoptimizat
Interogarea:
select * from licenta_neoptimizat.hotel where nume_tara like '%?%' or nume_zona like '%?%'
or nume_oras like '%?%' or nume like '%?%' order by nota_hotel desc limit 5
Explain-ul interogării:
Id Select_type Table Type Possible_key Key Key_len Ref Rows Extra
1 Simple hotel ALL NULL NULL NULL NULL 13759 Using
Where;
Using
filesort
Pentru un astfel de select un index pus pe toate cele 4 coloane: nume, nume_tara,
nume_zona, nume_oras nu ajută deloc, va face aceaşi parcurgere doar că pe arborele
tabelei , iar un index separat pe fiecare nu ajuta fiindcă ar trebui să parcurgă iar toată
tabela pentru toate cele 4 cazuri, nu e optim deloc pentru MySQL o asemenea variantă.
Sortarea este cea originală, aceasta înseamnă că o singură optimizare ar fi creşterea
dimensiunii bufferului.
4.4.5.2 Selectul optimizat
Interogarea:
select h.* from tara t
59. 59
inner join zona z on t.id = z.id_tara
inner join oras o on z.id=o.id_zona
inner join hotel h on o.id=h.id_oras
where t.nume like '%?%' or z.nume like '%?%' or o.nume like '%?%' or h.nume like '%?%'
order by h.nota_hotel limit 5
Explain-ul interogării:
I
d
Select
_type
Table Type Possible_
key
Key Key_l
en
Ref Row
s
Extra
1 Simple t inde
x
PRIMARY nume_ind
ex
102 NULL 568
5
Using
index;
Using
temporary;
Using
filesort
1 Simple z ref PRIMARY,
id_tara
id_tara 4 licenta_optimi
zat.t.id
1 NULL
1 Simple o ref PRIMARY,
id_zona
id_zona 4 licenta_optimi
zat.z.id
1 NULL
1 Simple h ref id_oras id_oras 4 licenta_optimi
zat.o.id
1 WHERE
Observăm că aici cu index diferenţa nu este prea mare, singurul lucru este că acesta face
parcurgerea pe structura arborelui indexului nume_index al tabelei tari. Însă o diferenţă
majoră se vede la sortare, acesta folosind filesortul modificat, aceasta înseamnă că acesta
le va filtra şi sorta în memorie evitând I/O discului foarte mult, ceea ce îl face drastic mai
rapid pentru tabelele mari.
4.5 Simularea bazei de date
Pentru a vedea eficienţa server-ului va trebui să simulăm backend-ul aplicaţiei noastre iar
apoi să facem măsurători.
Pentru simularea backend-ului aplicaţiei am creat un program care va face inserări,
actualizări, ştergeri şi interogări într-un mod aleator, păstrând un raport al numărului de
interogări faţă de numărul operaţiilor insert, update şi delete. Acest program este construit în
limbajul PHP, acesta fiind cel mai popular limbaj în care este construit backend-ul aplicaţiilor
web. Tot cu acest simulator putem măsura timpii de execuţie a celor 5 tipuri de interogări
enumerate la capitolul anterior. Acesta măsoară timpii pentru interogări cu aceleaşi condiţii în
ambele cazuri, neoptimizat şi optimizat.
Pentru a face măsurătorile vom folosi programul open source innotop pentru a vedea în
timp real fluctuaţiile bazei de date, iar cu scriptul tuning-primer.sh putem capta într-un punct
statusul bazei de date.
61. 61
5. Măsurători
Tabel 5-1 Marimea bazelor de date
Dimensiunea bazelor de date
Neoptimizata Optimizata
108 MB 90 MB
Fiecare interogare a fost testată de 101 ori cu aceleaşi date atât cea optimizată cât şi cea
neoptimizată pentru obţinerea vitezei de răspuns a server-ului. În urma testării pentru acelaşi
răspuns al ambelor interogări obţinem următoarele rezultate:
Tabel 5-2 Viteza de răspuns a interogărilor
Interogarea
din
capitolul
NEOPTIMIZAT OPTIMIZAT
MIN
(secunde)
MAX
(secunde)
MEDIA
(secunde)
MIN
(secunde)
MAX
(secunde)
MEDIA
(secunde)
4.4.1 0.0176901 0.0277528 0.0183300 0.0022449 0.0073809 0.0025844
4.4.2 0.0197269 0.0205760 0.0200286 0.0010988 0.0233998 0.0015261
4.4.3 0.0005710 0.0007810 0.0006113 0.0003349 0.0007369 0.0004857
4.4.4 0.0017921 0.0048320 0.0020381 0.0017590 0.0043690 0.0021350
4.4.5 0.0286619 0.0409371 0.0291289 0.1926510 0.3733401 0.1980776
În urma simulării unui trafic perfect aleator pe server în ambele cazuri timp de 10 minute
obţin:
Neoptimizat: un total de 8244 de operaţiuni insert, update, delete şi select. Operaţii de
I/O un total de 3457, dimensiunea pool buffer-ului a urcat până la 82% iar rata
tabelelor scanate este de 10291:1
Optimizat: un total de 8470 de operaţiuni insert, update, delete şi select.Operatii de I/O
un total de 3485, dimensiunea pool buffer-ului a urcat până la 66% iar rata tabelelor
scanate este de 7264:1
63. 63
6. Concluzie
O primă concluzie ar fi: după optimizarea bazei de date aceasta scade în dimensiuni,
deşi conţine aceleaşi date precum cea neoptimizată. Acest lucru este un mare avantaj
pentru eficienţa server-ului nostru pentru că va fi capabil să stocheze în pool buffer mai
multe tabele reducând drastic operaţiile de I/O ale serverului şi viteza de răspuns a
interogărilor. Un alt aspect pozitiv este şi faptul că în cazul filesort-urilor dimensiunile
liniilor sunt clar mai mici şi eficienţa sortării creşte.
În urma testelor făcute pe interogări s-a observat că deşi unele interogări ale tabelelor
neoptimizate au avut un timp de răspuns maxim mai optim decât cele ale interogărilor
optimizate, media de răspuns ale celor optimizate este cu mult mai mică decât media celor
neoptimizate. Acesta lucru înseamnă că în cazul interogărilor concurente serverul
optimizat va răspunde mai eficient decât cel neoptimizat.
După monitorizarea server-elor într-o perioadă de timp pentru nişte operaţiuni
aleatorii observăm eficienţa server-ului optimizat. Din punct de vedere a operaţiunilor
SELECT, s-a redus rata tabelelor scanate iar dimensiunea pool buffer-ului fiind mult mai
scăzută în cazul celor optimizate. Pentru un server unde avem la dispoziţie resurse RAM
care le putem pune în slujba pool buffer-ului vom observa pentru baze de date de
dimensiuni mult mai mari o eficienţă foarte mare a interogărilor, fiind capabil să stocheze
mai multă informaţie de pe disc în memorie.
În opinia mea, o bază de date bine optimizată de la început poate reduce foarte mult
costul unei aplicaţii soft pe termen lung, chiar dacă pe termen scurt necesită mult efort
pentru obţinerea acesteia într-o formă optimă pentru interogări şi pentru păstrarea
integrităţii datelor.
65. 65
7. Bibliografie şi referinţe
[1] http://www.bazededate.org/PBD_Indexare1.pdf accesat la data de 06.03.2015 , ora
12:00
[2] http://dev.mysql.com/doc/refman/5.6/en/nested-loop-joins.html Nested-Loop Join
Algorithms accesat la data de 11.04.2015, ora 14:20
[3] http://dev.mysql.com/doc/refman/5.6/en/nested-join-optimization.html Nested Join
Optimization accesat la data de 11.04.2015, ora 19:01
[4] http://dev.mysql.com/doc/refman/5.6/en/outer-join-simplification.html Outer Join
Simplification accesat la data de 16.04.2015, ora 10:45
[5] http://dev.mysql.com/doc/refman/5.6/en/order-by-optimization.html ORDER BY
Optimization accesat la data de 16.04.2015 ora 17:50
[6] http://blog.jcole.us/2013/01/10/btree-index-structures-in-innodb B+Tree index
structures in InnoDB accesat la data de 13.06.2015 ora 15:10
[7] http://sysnet.ucsd.edu/~cfleizac/cse262/R-Trees.ppt accesat la data de 13.06.2015
ora 15:10
69. 69
8.2 Anexa 2: Trigger-ele pentru bazele de date
Trigger-e comune la ambele baze de date:
delimiter
Create trigger hotel_nota_insert
BEFORE INSERT ON nota_hotel FOR EACH ROW
BEGIN
DECLARE nota_restaurant double;
DECLARE nota_locatie double;
DECLARE nota_conditi double;
select
COALESCE(AVG(nota_restaurant),0),COALESCE(AVG(nota_locatie),0),COALESCE(AVG(nota_conditi),0)
into @nota_restaurant,@nota_locatie,@nota_conditi
from nota_hotel where id_hotel=NEW.id_hotel;
update hotel
set nota_hotel = (@nota_restaurant+@nota_locatie+@nota_conditi)/3,
nota_restaurant = @nota_restaurant,
nota_locatie = @nota_locatie,
nota_conditii = @nota_conditi
where id = NEW.id_hotel;
END
delimiter ;
delimiter
Create trigger hotel_nota_update
BEFORE UPDATE ON nota_hotel FOR EACH ROW
BEGIN
DECLARE nota_restaurant double;
DECLARE nota_locatie double;
DECLARE nota_conditi double;
select
COALESCE(AVG(nota_restaurant),0),COALESCE(AVG(nota_locatie),0),COALESCE(AVG(nota_conditi),0)
into @nota_restaurant,@nota_locatie,@nota_conditi
from nota_hotel where id_hotel=NEW.id_hotel;
update hotel
set nota_hotel = (@nota_restaurant+@nota_locatie+@nota_conditi)/3,
nota_restaurant = @nota_restaurant,
nota_locatie = @nota_locatie,
nota_conditii = @nota_conditi
where id = NEW.id_hotel;
END
delimiter ;
delimiter
Create trigger hotel_nota_delete
BEFORE DELETE ON nota_hotel FOR EACH ROW
BEGIN
DECLARE nota_restaurant double;
DECLARE nota_locatie double;
DECLARE nota_conditi double;
select
COALESCE(AVG(nota_restaurant),0),COALESCE(AVG(nota_locatie),0),COALESCE(AVG(nota_conditi),0)
into @nota_restaurant,@nota_locatie,@nota_conditi
from nota_hotel where id_hotel=OLD.id_hotel;
update hotel
set nota_hotel = (@nota_restaurant+@nota_locatie+@nota_conditi)/3,
nota_restaurant = @nota_restaurant,
nota_locatie = @nota_locatie,
nota_conditii = @nota_conditi
where id = OLD.id_hotel;
END
delimiter ;
Doar pentru baza de date optimizata: