SlideShare a Scribd company logo
1 of 70
Download to read offline
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
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
Lista Figurilor
Fig. A.1 Structura bazei de date neoptimizate
Fig. A.2 Structura bazei de date optimizate
12
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.
14
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.
16
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
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
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
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
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
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
 Ş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
(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
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
}
[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
}
}
[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
}
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
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
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
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
 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
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
 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
 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
 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
 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
 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
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
 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
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
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.
43
44
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
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
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
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
$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
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
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
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
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
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
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
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
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
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
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.
60
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
62
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.
64
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
66
67
8. ANEXE
8.1 Anexa 1: Structura bazelor de date
Fig. A. 1 Structura bazei de date neoptimizate
68
Fig. A. 2 Structura bazei de date optimizate
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:
Stroia_Laurentiu

More Related Content

Similar to Stroia_Laurentiu

Nosql Movement Budai Steliana Gorea Alexandra Diana
Nosql Movement Budai Steliana Gorea Alexandra DianaNosql Movement Budai Steliana Gorea Alexandra Diana
Nosql Movement Budai Steliana Gorea Alexandra Dianasteliana
 
Impactul saa s in institutiile publice – andreea sandu ap1
Impactul saa s in institutiile publice – andreea sandu ap1Impactul saa s in institutiile publice – andreea sandu ap1
Impactul saa s in institutiile publice – andreea sandu ap1silviu_cojocaru
 
Graduation projects in Crispico
Graduation projects in CrispicoGraduation projects in Crispico
Graduation projects in Crispicostagiipebune
 
Programarea aplicațiilor distribuite
Programarea aplicațiilor distribuiteProgramarea aplicațiilor distribuite
Programarea aplicațiilor distribuite Dumitru Maros
 
Plan Proiect E.Pausan
Plan Proiect E.PausanPlan Proiect E.Pausan
Plan Proiect E.PausanEmilia Pausan
 
Baze date cap_1
Baze date cap_1Baze date cap_1
Baze date cap_1relu27
 
baze-de-date-access-laborator-de-ioan-mocian
baze-de-date-access-laborator-de-ioan-mocianbaze-de-date-access-laborator-de-ioan-mocian
baze-de-date-access-laborator-de-ioan-mocianCristina Timofte
 
Medii de dezvoltare node.js npm
Medii de dezvoltare node.js  npmMedii de dezvoltare node.js  npm
Medii de dezvoltare node.js npmDmitrii Stoian
 
Reteaua internet
Reteaua internetReteaua internet
Reteaua internetliviupilot
 
Procesarea RDF pentru platforma Java
Procesarea RDF pentru platforma JavaProcesarea RDF pentru platforma Java
Procesarea RDF pentru platforma JavaRalucaGheorghita
 
Notiuni despre retele_de_calculatoare
Notiuni despre retele_de_calculatoareNotiuni despre retele_de_calculatoare
Notiuni despre retele_de_calculatoareSima Sorin
 

Similar to Stroia_Laurentiu (20)

Nosql Movement Budai Steliana Gorea Alexandra Diana
Nosql Movement Budai Steliana Gorea Alexandra DianaNosql Movement Budai Steliana Gorea Alexandra Diana
Nosql Movement Budai Steliana Gorea Alexandra Diana
 
NoSql
NoSqlNoSql
NoSql
 
Irina Cureraru
Irina CureraruIrina Cureraru
Irina Cureraru
 
Impactul saa s in institutiile publice – andreea sandu ap1
Impactul saa s in institutiile publice – andreea sandu ap1Impactul saa s in institutiile publice – andreea sandu ap1
Impactul saa s in institutiile publice – andreea sandu ap1
 
Graduation projects in Crispico
Graduation projects in CrispicoGraduation projects in Crispico
Graduation projects in Crispico
 
Programarea aplicațiilor distribuite
Programarea aplicațiilor distribuiteProgramarea aplicațiilor distribuite
Programarea aplicațiilor distribuite
 
Plan Proiect E.Pausan
Plan Proiect E.PausanPlan Proiect E.Pausan
Plan Proiect E.Pausan
 
Ghid ro
Ghid roGhid ro
Ghid ro
 
Baze date cap_1
Baze date cap_1Baze date cap_1
Baze date cap_1
 
baze-de-date-access-laborator-de-ioan-mocian
baze-de-date-access-laborator-de-ioan-mocianbaze-de-date-access-laborator-de-ioan-mocian
baze-de-date-access-laborator-de-ioan-mocian
 
Mahout Balaoi Bogdan
Mahout Balaoi BogdanMahout Balaoi Bogdan
Mahout Balaoi Bogdan
 
planif_9_zi.doc
planif_9_zi.docplanif_9_zi.doc
planif_9_zi.doc
 
Music Finder
Music FinderMusic Finder
Music Finder
 
Medii de dezvoltare node.js npm
Medii de dezvoltare node.js  npmMedii de dezvoltare node.js  npm
Medii de dezvoltare node.js npm
 
Reteaua internet
Reteaua internetReteaua internet
Reteaua internet
 
MOBILI-AR.pptx
MOBILI-AR.pptxMOBILI-AR.pptx
MOBILI-AR.pptx
 
Procesarea RDF pentru platforma Java
Procesarea RDF pentru platforma JavaProcesarea RDF pentru platforma Java
Procesarea RDF pentru platforma Java
 
Notiuni despre retele_de_calculatoare
Notiuni despre retele_de_calculatoareNotiuni despre retele_de_calculatoare
Notiuni despre retele_de_calculatoare
 
Capitolul 3
Capitolul 3Capitolul 3
Capitolul 3
 
Tice usb 1
Tice usb 1Tice usb 1
Tice usb 1
 

Stroia_Laurentiu

  • 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
  • 12. 12
  • 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.
  • 14. 14
  • 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.
  • 16. 16
  • 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.
  • 43. 43
  • 44. 44
  • 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.
  • 60. 60
  • 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
  • 62. 62
  • 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.
  • 64. 64
  • 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
  • 66. 66
  • 67. 67 8. ANEXE 8.1 Anexa 1: Structura bazelor de date Fig. A. 1 Structura bazei de date neoptimizate
  • 68. 68 Fig. A. 2 Structura bazei de date optimizate
  • 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: