| 3 | | === Преглед на шемата на AirportDB\\ |
| 4 | | |
| 5 | | Во нашите case-сценарија табелата booking со ~3.5 милиони редови е главната цел за индексирање. Повеќето сценарија ја вклучуваат.\\ |
| 6 | | |
| 7 | | === Типови на индекси во MySql |
| 8 | | **B-Tree (BTree) index — default индекс во MySql**\\ |
| 9 | | Најчест тип. Работи добро за: = , <, >, BETWEEN, LIKE 'prefix%', ORDER BY, GROUP BY.\\ |
| 10 | | \\ |
| 11 | | **Composite index** |
| 12 | | Покрива повеќе услови. Редоследот во овој тип на индекс е критичен — индексот може да даде подобри перформанси амо ако WHERE ги содржи условите одлево надесно (leftmost prefix rule). |
| | 3 | === Вовед |
| | 4 | |
| | 5 | Експериментите се изведени во MySQL 8.0.43. Оригиналната `airportdb` база се користеше само за читање, а сите експериментални индекси беа креирани во изолираната `airportdb_index_lab`. |
| | 6 | |
| | 7 | Во тест околината: |
| | 8 | * `booking` има 54,304,619 записи |
| | 9 | * `weatherdata` има 4,626,432 записи |
| | 10 | |
| | 11 | InnoDB ги организира податоците во 16 KiB pages кои се кешираат во buffer pool. PRIMARY KEY претставува clustered структура за чување на редовите, додека нормалните PRIMARY и secondary индекси се организирани како B+ tree структури погодни за пребарување по еднаквост и опсег. |
| | 12 | |
| | 13 | Secondary index содржи indexed key и PRIMARY KEY вредност. Composite index содржи повеќе колони во точно определен редослед, а covering index е индекс кој ги содржи сите податоци што му се потребни на конкретен прашалник. |
| | 14 | |
| | 15 | === 1. Secondary index - пребарување на резервации по патник |
| | 16 | |
| | 17 | Сценарио: оператор пребарува резервации за конкретен патник. |
| | 18 | |
| | 19 | {{{ |
| | 20 | SELECT booking_id, flight_id, seat, price |
| | 21 | FROM airportdb_index_lab.booking |
| | 22 | WHERE passenger_id = 10001; |
| | 23 | }}} |
| | 24 | |
| | 25 | Најпрво прашалникот беше извршен без secondary index на `passenger_id`. |
| | 26 | |
| | 27 | || '''Мерка''' || '''Без secondary index''' || '''idx_exp_booking_passenger''' || |
| | 28 | || Access type || `ALL` || `ref` || |
| | 29 | || Обработени / matching rows || ~54.3M || 1,436 || |
| | 30 | || Benchmark median || 13.62 s || 0.005 s || |
| | 31 | || Дополнителен storage / build || - || 847,233,024 B / 61.97 s || |
| | 32 | |
| | 33 | [[Image(3.PNG)]] |
| | 34 | |
| | 35 | '''Слика 1.''' Без passenger index, MySQL користи `ALL` и проценува дека треба да обработи 54,163,080 редови. |
| | 36 | |
| | 37 | Потоа беше креиран: |
| | 38 | |
| | 39 | {{{ |
| | 40 | CREATE INDEX idx_exp_booking_passenger |
| | 41 | ON airportdb_index_lab.booking(passenger_id); |
| | 42 | }}} |
| | 43 | |
| | 44 | [[Image(4.PNG)]] |
| | 45 | |
| | 46 | '''Слика 2.''' Со passenger index, пристапот станува index lookup и се добиваат 1,436 записи. |
| | 47 | |
| | 48 | Индексот значително ја намалува количината на работа за овој selective lookup. Сепак, индексот не е covering за `flight_id`, `seat` и `price`, па за тие колони се потребни дополнителни пристапи до clustered редовите. |
| | 49 | |
| | 50 | Прикажаниот benchmark median е добиен од повторени нормални извршувања на прашалникот. Времето од `EXPLAIN ANALYZE` е посебна instrumentation мерка и не се користи како директна замена за benchmark времето. |
| | 51 | |
| | 52 | === 2. Composite index - редослед на колоните |
| | 53 | |
| | 54 | Кај composite index редоследот на колоните е важен. За тестирање беше користен следниот прашалник: |
| | 55 | |
| | 56 | {{{ |
| | 57 | SELECT COUNT(*) AS measurement_count |
| | 58 | FROM airportdb_index_lab.weatherdata |
| | 59 | WHERE station = 1 |
| | 60 | AND time >= '08:00:00' |
| | 61 | AND time < '09:00:00'; |
| | 62 | }}} |
| | 63 | |
| | 64 | Беа споредени: |
| | 65 | |
| | 66 | {{{ |
| | 67 | (station, time, log_date) |
| | 68 | }}} |
| | 69 | |
| | 70 | и |
| | 71 | |
| | 72 | {{{ |
| | 73 | (time, station, log_date) |
| | 74 | }}} |
| | 75 | |
| | 76 | || '''Index''' || '''Прочитани index entries''' || '''Вратени редови''' || '''Benchmark median''' || |
| | 77 | || `(station,time,log_date)` || 48,192 || 48,192 || 19.94 ms || |
| | 78 | || `(time,station,log_date)` || 192,768 || 48,192 || 69.21 ms || |
| | 79 | |
| | 80 | [[Image(6.PNG)]] |
| | 81 | |
| | 82 | '''Слика 3.''' Equality-first редослед: прво се ограничува `station = 1`, а потоа се пребарува временскиот опсег. |
| | 83 | |
| | 84 | [[Image(7.PNG)]] |
| | 85 | |
| | 86 | '''Слика 4.''' Range-first редослед: индексот почнува од `time`, па се читаат записи за сите четири stations пред да се филтрира `station = 1`. |
| | 87 | |
| | 88 | Вториот индекс чита 4 пати повеќе index entries и има приближно 3.47 пати подолго измерено време. Ова практично го покажува значењето на leftmost-prefix правилото и пристапот equality-before-range. |
| | 89 | |
| | 90 | === 3. Covering index и однесување на optimizer-от |
| | 91 | |
| | 92 | За едномесечен weather workload беше спореден потесниот индексен пристап со covering index: |
| | 93 | |
| | 94 | {{{ |
| | 95 | (station, log_date, time, temp, humidity) |
| | 96 | }}} |
| | 97 | |
| | 98 | || '''Weather workload''' || '''Narrow / PRIMARY path''' || '''Covering index''' || |
| | 99 | || Structural rows / median || 35,712 / 12.47 ms || 8,928 / 5.62 ms || |
| | 100 | || Analytical median (31 groups) || 11.29 ms || 4.55 ms || |
| | 101 | || INDEX_LENGTH || 83,476,480 B || 109,740,032 B || |
| | 102 | |
| | 103 | Covering index-от овозможува потребните колони да се прочитаат директно од индексот и во execution plan се појавува `Using index`. Со тоа се избегнуваат дополнителни clustered-row reads, но операциите како `AVG`, `MIN`, `MAX` и `GROUP BY` сепак мора да се извршат. |
| | 104 | |
| | 105 | Експериментот покажа и дека постоење на индекс не значи дека optimizer-от мора да го избере. Кај едномесечниот weather query, `(station,log_date,time)` беше прикажан во `possible_keys`, но MySQL сепак го избра PRIMARY индексот бидејќи го процени како поевтин план. |
| | 106 | |
| | 107 | Дополнително беше тестиран и invisible index. Кога индексот е invisible, тој и понатаму физички постои, зафаќа простор и се одржува при промени, но optimizer-от го игнорира при нормален избор на план. |
| | 108 | |
| | 109 | === 4. Посложен аналитички workload |
| | 110 | |
| | 111 | Следното сценарио го анализира ефектот на индексите врз посложен аналитички прашалник: top revenue routes по ден за периодот `2015-07-01` – `2015-07-03`. |
| | 112 | |
| | 113 | Логичкиот тек е: |
| | 114 | |
| | 115 | {{{ |
| | 116 | flight departure range |
| | 117 | -> flight / booking JOIN |
| | 118 | -> COUNT / SUM / AVG |
| | 119 | -> route_metrics CTE |
| | 120 | -> DENSE_RANK() |
| | 121 | -> top 5 routes per day |
| | 122 | }}} |
| | 123 | |
| | 124 | Во тестот се обработуваат: |
| | 125 | * 14,858 flights |
| | 126 | * приближно 1.74M joined bookings |
| | 127 | * 14,858 route groups |
| | 128 | * 15 final rows |
| | 129 | |
| | 130 | Baseline без соодветен booking index беше анализиран само со `EXPLAIN`: `booking` имаше `ALL` access со проценка од приближно 54M редови. За baseline не беше мерено реално runtime. |
| | 131 | |
| | 132 | Беа споредени следните два индекси: |
| | 133 | |
| | 134 | || '''Booking index''' || '''Access''' || '''Benchmark median''' || '''INDEX_LENGTH во lab''' || |
| | 135 | || `(flight_id)` || non-covering lookup || 41.23 s || 1,694,466,048 B || |
| | 136 | || `(flight_id,price)` || covering lookup || 1.89 s || 2,007,990,272 B || |
| | 137 | |
| | 138 | Со додавање на `price` во индексот, вредностите потребни за `SUM(price)` и `AVG(price)` се достапни директно од индексот. |
| | 139 | |
| | 140 | Во оваа тест околина median времето се намали приближно 21.78 пати, но поширокиот индекс користи дополнителни 313,524,224 B во однос на потесниот. |
| | 141 | |
| | 142 | Aggregation, CTE materialization, ranking и sorting и понатаму остануваат дел од execution plan-от. |
| | 143 | |
| | 144 | [[Image(17.PNG)]] |
| | 145 | |
| | 146 | '''Слика 5.''' Посложен CTE/window query и дел од неговиот execution tree. |
| | 147 | |
| | 148 | === 5. FULLTEXT index |
| | 149 | |
| | 150 | Покрај B+ tree индексите, MySQL поддржува и други типови на пребарување. |
| | 151 | |
| | 152 | Во `airplane_type` веќе постои: |
| | 153 | |
| | 154 | {{{ |
| | 155 | FULLTEXT description_full(identifier, description) |
| | 156 | }}} |
| | 157 | |
| 14 | | {{{ |
| 15 | | |
| 16 | | CREATE INDEX idx_comp ON tabela (col_a, col_b, col_c); |
| 17 | | |
| 18 | | }}} |
| 19 | | |
| 20 | | Може да се користи за:\\ |
| 21 | | WHERE col_a = ?\\ |
| 22 | | WHERE col_a = ? AND col_b = ?\\ |
| 23 | | WHERE col_a = ? AND col_b = ? AND col_c = ?\\ |
| 24 | | WHERE col_a = ? ORDER BY col_b\\ |
| 25 | | НЕ може за:\\ |
| 26 | | WHERE col_b = ? \\ |
| 27 | | WHERE col_c = ? \\ |
| 28 | | |
| 29 | | \\ |
| 30 | | |
| 31 | | **Covering Index**\\ |
| 32 | | Индекс кој ги содржи сите колони од еден прашалник. Ако индексот ги има сите тие колони, MySQL не мора воопшто да ја чита самата табела (clustered data). |
| 33 | | Се што му треба, го зема директно од индексот. - Со овој тип на индекс се намалува бројот на И/О операции, што доведува до побрзо извршување, но е поголем индекс што зафаќа повеќе простор и ги успорува сите INSERT/UPDATE операции.\\ |
| 34 | | Пример за креирање: |
| 35 | | {{{ |
| 36 | | |
| 37 | | CREATE INDEX idx_cov ON booking (flight_id, price, seat); |
| 38 | | |
| 39 | | }}} |
| 40 | | \\ |
| 41 | | **Hash индекс**\\ |
| 42 | | Само за точна еднаквост (=). Не поддржува range, ORDER BY, GROUP BY. Во MySQL InnoDB НЕ може да се креира рачно - InnoDB го прави адаптивен hash автоматски. Може рачно само во MEMORY engine.\\ |
| 43 | | Пример за креирање:\\ |
| 44 | | {{{ |
| 45 | | |
| 46 | | CREATE TABLE temp_lookup ( |
| 47 | | id INT NOT NULL, |
| 48 | | val VARCHAR(50), |
| 49 | | PRIMARY KEY (id) |
| 50 | | ) ENGINE=MEMORY; |
| 51 | | |
| 52 | | CREATE INDEX idx_hash ON temp_lookup (vrednost) USING HASH; |
| 53 | | |
| 54 | | }}} |
| 55 | | |
| 56 | | Некои други типови: FULLTEXT индекс (за пребарување на текст), SPATIAL индекс (геолокациски).\\ |
| 57 | | \\ |
| 58 | | |
| 59 | | == Сценарија и индексирање на прашалници |
| 60 | | === Сценарио 1: Агент на шалтер го пребарува целото патничко досие за конкретен патник |
| 61 | | Табели: booking JOIN flight JOIN passengerdetails\\ |
| 62 | | Цел: B-Tree индекс на еден столбец (booking.passenger_id)\\ |
| 63 | | \\ |
| 64 | | Најпрво, за тестирање, ќе ја извршиме следната скрипта која безбедно ќе ги избрише сите предефинирани индекси кои веќе ги има во Airportdb од страна на MySql, без нарушување на foreign-key constraints:// |
| 65 | | |
| 66 | | [wiki:Script1 Погледни ја скриптата овде] |
| 67 | | // |
| 68 | | **Прашалник без индекс:** |
| 69 | | {{{ |
| 70 | | |
| 71 | | EXPLAIN ANALYZE |
| 72 | | SELECT |
| 73 | | b.booking_id, |
| 74 | | b.seat, |
| 75 | | b.price, |
| 76 | | f.flightno, |
| 77 | | f.departure, |
| 78 | | f.arrival, |
| 79 | | pd.firstname, |
| 80 | | pd.lastname, |
| 81 | | pd.country |
| 82 | | FROM booking b |
| 83 | | JOIN flight f ON b.flight_id = f.flight_id |
| 84 | | JOIN passengerdetails pd ON b.passenger_id = pd.passenger_id |
| 85 | | WHERE b.passenger_id = 10001 |
| 86 | | ORDER BY f.departure DESC; |
| 87 | | |
| 88 | | }}} |
| 89 | | |
| 90 | | |
| 91 | | |
| 92 | | |
| | 159 | |
| | 160 | {{{ |
| | 161 | SELECT type_id, identifier, |
| | 162 | MATCH(identifier, description) |
| | 163 | AGAINST('Boeing' IN NATURAL LANGUAGE MODE) AS relevance |
| | 164 | FROM airportdb.airplane_type |
| | 165 | WHERE MATCH(identifier, description) |
| | 166 | AGAINST('Boeing' IN NATURAL LANGUAGE MODE) |
| | 167 | ORDER BY relevance DESC, type_id; |
| | 168 | }}} |
| | 169 | |
| | 170 | Прашалникот врати 36 редови, а execution plan-от покажа `Full-text index search`. |
| | 171 | |
| | 172 | [[Image(16.PNG)]] |
| | 173 | |
| | 174 | '''Слика 6.''' FULLTEXT пребарување со `MATCH ... AGAINST` и `Full-text index search`. |
| | 175 | |
| | 176 | FULLTEXT индексот е наменет за пребарување на текст според зборови и relevance, а не за обично B+ tree пребарување по еднаквост или опсег. |
| | 177 | |
| | 178 | SPATIAL индексите се користат за просторни податоци. Explicit HASH индекс не е нормална user-defined опција за InnoDB табелите во овој проект; таков индекс може директно да се користи, на пример, кај MEMORY engine. |
| | 179 | |
| | 180 | === 6. Read / write / storage tradeoff |
| | 181 | |
| | 182 | Индексите го забрзуваат читањето, но имаат цена при INSERT, UPDATE и DELETE бидејќи мора да се одржуваат дополнителни index структури. |
| | 183 | |
| | 184 | За контролираниот INSERT тест: |
| | 185 | |
| | 186 | || '''Мерка''' || '''PRIMARY only''' || '''PRIMARY + passenger index''' || |
| | 187 | || Inserted rows || 197,129 || 197,129 || |
| | 188 | || Duration || 1.363 s || 1.913 s || |
| | 189 | || Additional secondary storage || - || 3,686,400 B || |
| | 190 | |
| | 191 | Со passenger secondary index истиот INSERT batch беше 40.32% побавен бидејќи InnoDB мора да ги ажурира и clustered PRIMARY и secondary B+ tree структурата. |
| | 192 | |
| | 193 | Од друга страна, за passenger lookup истиот индекс го намали времето од 13.62 s на приближно 0.005 s. Затоа користа од индекс зависи од односот меѓу read и write операции и од достапниот storage. |
| | 194 | |
| | 195 | === Заклучок |
| | 196 | |
| | 197 | Од тестовите се гледа дека индексите имаат најголем ефект кога се избрани според конкретниот начин на користење на податоците. |
| | 198 | |
| | 199 | Кај пребарувањето по `passenger_id`, secondary index го намали пристапот од околу 54.3M записи на 1,436. Кај composite index, редоследот на колоните влијаеше директно врз бројот на прочитани index entries. Covering index дополнително го намали пристапот до clustered редовите, а во дел од тестовите MySQL избра друг индекс иако постоеше дополнителен кандидат. |
| | 200 | |
| | 201 | Пошироките индекси зафаќаат повеќе простор и го зголемуваат трошокот при INSERT, UPDATE и DELETE. Затоа при избор на индекс треба да се споредат execution plan, бројот на обработени редови, времето на извршување и дополнителниот storage/write cost. |