| 32 | | }}} |
| | 43 | |
| | 44 | -- 6. Мерење со индекс |
| | 45 | EXPLAIN ANALYZE |
| | 46 | SELECT * |
| | 47 | FROM customer_appointments_view |
| | 48 | WHERE customer_id = 100; |
| | 49 | }}} |
| | 50 | |
| | 51 | === Време на извршување без индекс: |
| | 52 | |
| | 53 | **7.802 ms** |
| | 54 | |
| | 55 | Дел од добиениот `EXPLAIN ANALYZE` output пред додавање на индексот: |
| | 56 | |
| | 57 | {{{ |
| | 58 | Nested Loop (cost=88.12..168.91 rows=5 width=151) (actual time=3.988..7.635 rows=6 loops=1) |
| | 59 | -> Hash Join (cost=87.68..126.58 rows=5 width=97) (actual time=3.625..4.537 rows=6 loops=1) |
| | 60 | Hash Cond: (a.service_id = s.service_id) |
| | 61 | -> Nested Loop (cost=83.41..122.30 rows=5 width=86) (actual time=3.361..4.267 rows=6 loops=1) |
| | 62 | -> Index Scan using pk_customer on customer c |
| | 63 | Index Cond: (customer_id = 100) |
| | 64 | -> Hash Join |
| | 65 | Hash Cond: (e.employee_id = a.employee_id) |
| | 66 | -> Seq Scan on employee e |
| | 67 | -> Hash |
| | 68 | -> Hash Right Join |
| | 69 | -> Seq Scan on business_location bl |
| | 70 | -> Hash |
| | 71 | -> Hash Join |
| | 72 | -> Seq Scan on business b |
| | 73 | -> Hash |
| | 74 | -> Index Scan using idx_appointment_customer_profile on appointment a |
| | 75 | Index Cond: (customer_id = 100) |
| | 76 | Filter: ((status)::text <> 'cancelled'::text) |
| | 77 | Rows Removed by Filter: 1 |
| | 78 | -> Index Scan using pk_time_slot on time_slot ts |
| | 79 | Index Cond: (slot_id = a.slot_id) |
| | 80 | |
| | 81 | Planning Time: 8.862 ms |
| | 82 | Execution Time: 7.802 ms |
| | 83 | }}} |
| | 84 | |
| | 85 | Во овој план се гледа дека за табелата `appointment` се користи индексот `idx_appointment_customer_profile`, но сепак дополнително се применува филтерот: |
| | 86 | |
| | 87 | {{{ |
| | 88 | Filter: ((status)::text <> 'cancelled'::text) |
| | 89 | }}} |
| | 90 | |
| | 91 | Ова значи дека базата ги наоѓа резервациите според `customer_id`, но потоа мора дополнително да ги филтрира оние кои имаат статус различен од `cancelled`. |
| | 92 | |
| | 93 | За да го оптимизираме ова, креираме парцијален индекс: |
| | 94 | |
| | 95 | {{{ |
| | 96 | CREATE INDEX IF NOT EXISTS idx_appointment_customer_not_cancelled |
| | 97 | ON appointment(customer_id) |
| | 98 | WHERE status <> 'cancelled'; |
| | 99 | }}} |
| | 100 | |
| | 101 | Овој индекс ги содржи само резервациите кои не се откажани. Бидејќи погледот `customer_appointments_view` ги користи токму тие резервации, индексот е помал и поефикасен. |
| 36 | | **2.756 ms** |
| 37 | | |
| 38 | | Со додавање на индексот, времето на извршување се намали од **644.544 ms** на **2.756 ms**. Ова е значително подобрување бидејќи планерот повеќе нема потреба да ја скенира целата табела `appointment`, туку директно ги наоѓа редиците за конкретниот корисник. |
| | 105 | **0.803 ms** |
| | 106 | |
| | 107 | Дел од добиениот `EXPLAIN ANALYZE` output после додавање на индексот: |
| | 108 | |
| | 109 | {{{ |
| | 110 | Nested Loop (cost=87.54..168.32 rows=5 width=151) (actual time=0.576..0.714 rows=6 loops=1) |
| | 111 | -> Nested Loop (cost=87.11..125.99 rows=5 width=97) (actual time=0.557..0.672 rows=6 loops=1) |
| | 112 | -> Index Scan using pk_customer on customer c |
| | 113 | Index Cond: (customer_id = 100) |
| | 114 | -> Hash Join |
| | 115 | Hash Cond: (e.employee_id = a.employee_id) |
| | 116 | -> Seq Scan on employee e |
| | 117 | -> Hash |
| | 118 | -> Hash Right Join |
| | 119 | -> Seq Scan on business_location bl |
| | 120 | -> Hash |
| | 121 | -> Hash Join |
| | 122 | -> Seq Scan on business b |
| | 123 | -> Hash |
| | 124 | -> Hash Join |
| | 125 | Hash Cond: (s.service_id = a.service_id) |
| | 126 | -> Seq Scan on service s |
| | 127 | -> Hash |
| | 128 | -> Index Scan using idx_appointment_customer_not_cancelled on appointment a |
| | 129 | Index Cond: (customer_id = 100) |
| | 130 | -> Index Scan using pk_time_slot on time_slot ts |
| | 131 | Index Cond: (slot_id = a.slot_id) |
| | 132 | |
| | 133 | Planning Time: 3.150 ms |
| | 134 | Execution Time: 0.803 ms |
| | 135 | }}} |
| | 136 | |
| | 137 | После додавање на индексот се гледа дека планерот го користи новиот индекс: |
| | 138 | |
| | 139 | {{{ |
| | 140 | Index Scan using idx_appointment_customer_not_cancelled on appointment a |
| | 141 | Index Cond: (customer_id = 100) |
| | 142 | }}} |
| | 143 | |
| | 144 | Овој пат нема дополнителен филтер за `status <> 'cancelled'`, бидејќи самиот индекс веќе ги содржи само редиците кои не се откажани. |
| | 145 | |
| | 146 | === Споредба на времиња === |
| | 147 | |
| | 148 | ||= Поглед =||= Пред индекс =||= После индекс =||= Подобрување =|| |
| | 149 | || customer_appointments_view || 7.802 ms || 0.803 ms || околу 9.7 пати побрзо || |
| | 150 | |
| | 151 | Со додавање на парцијалниот индекс `idx_appointment_customer_not_cancelled`, времето на извршување се намали од **7.802 ms** на **0.803 ms**. Ова покажува дека индексот е соодветен за овој поглед, бидејќи прашалникот најчесто пребарува резервации за конкретен корисник и ги исклучува откажаните резервации. |
| 66 | | }}} |
| | 189 | |
| | 190 | -- 6. Мерење со индекс |
| | 191 | EXPLAIN ANALYZE |
| | 192 | SELECT * |
| | 193 | FROM available_slots |
| | 194 | WHERE business_id = 1 |
| | 195 | AND date = CURRENT_DATE; |
| | 196 | }}} |
| | 197 | |
| | 198 | === Време на извршување без индекс: |
| | 199 | |
| | 200 | **7157.628 ms** |
| | 201 | |
| | 202 | Дел од добиениот `EXPLAIN ANALYZE` output пред додавање на индексот: |
| | 203 | |
| | 204 | {{{ |
| | 205 | Nested Loop (cost=1001.27..479269.43 rows=21 width=100) (actual time=7156.545..7157.282 rows=48 loops=1) |
| | 206 | -> Nested Loop (cost=1001.13..479265.89 rows=21 width=74) (actual time=7156.537..7157.258 rows=48 loops=1) |
| | 207 | Join Filter: ((es.employee_id = ts.employee_id) AND (es.service_id = bs.service_id)) |
| | 208 | -> Nested Loop |
| | 209 | -> Nested Loop |
| | 210 | -> Nested Loop |
| | 211 | -> Index Scan using uq_business_service_business_service on business_service bs |
| | 212 | Index Cond: (business_id = 1) |
| | 213 | Filter: is_active |
| | 214 | -> Materialize |
| | 215 | -> Index Scan using pk_business on business b |
| | 216 | Index Cond: (business_id = 1) |
| | 217 | -> Materialize |
| | 218 | -> Gather |
| | 219 | Workers Planned: 2 |
| | 220 | Workers Launched: 2 |
| | 221 | -> Parallel Seq Scan on time_slot ts |
| | 222 | Filter: (is_available AND (business_id = 1) AND (date = CURRENT_DATE)) |
| | 223 | Rows Removed by Filter: 6666651 |
| | 224 | -> Memoize |
| | 225 | Cache Key: ts.employee_id |
| | 226 | -> Index Scan using pk_employee on employee e |
| | 227 | Index Cond: (employee_id = ts.employee_id) |
| | 228 | -> Index Only Scan using uq_employee_service_employee_service on employee_service es |
| | 229 | Index Cond: (employee_id = e.employee_id) |
| | 230 | Heap Fetches: 0 |
| | 231 | -> Index Scan using pk_service on service s |
| | 232 | Index Cond: (service_id = es.service_id) |
| | 233 | |
| | 234 | Planning Time: 4.741 ms |
| | 235 | Execution Time: 7157.628 ms |
| | 236 | }}} |
| | 237 | |
| | 238 | Во овој план се гледа дека најбавниот дел е скенирањето на табелата `time_slot`: |
| | 239 | |
| | 240 | {{{ |
| | 241 | Parallel Seq Scan on time_slot ts |
| | 242 | Filter: (is_available AND (business_id = 1) AND (date = CURRENT_DATE)) |
| | 243 | Rows Removed by Filter: 6666651 |
| | 244 | }}} |
| | 245 | |
| | 246 | Ова значи дека базата мора да помине низ многу голем број редици во `time_slot` за да ги најде термините кои се достапни, се за конкретниот бизнис и се за тековниот датум. Поради тоа времето на извршување е високо. |
| | 247 | |
| | 248 | За да го оптимизираме ова, креираме парцијален индекс: |
| | 249 | |
| | 250 | {{{ |
| | 251 | CREATE INDEX IF NOT EXISTS idx_time_slot_available_business_date |
| | 252 | ON time_slot(business_id, date) |
| | 253 | WHERE is_available = TRUE; |
| | 254 | }}} |
| | 255 | |
| | 256 | Овој индекс ги содржи само достапните термини, односно редиците каде што `is_available = TRUE`. Бидејќи прашалникот секогаш бара достапни термини за конкретен `business_id` и `date`, индексот е многу погоден за овој поглед. |
| 70 | | **1.090 ms** |
| 71 | | |
| 72 | | Со овој индекс времето се намали од **429.479 ms** на **1.090 ms**. Индексот е многу корисен бидејќи прашалникот најчесто бара само достапни термини, па нема потреба да се пребаруваат сите термини во табелата `time_slot`. |
| | 260 | **0.580 ms** |
| | 261 | |
| | 262 | Дел од добиениот `EXPLAIN ANALYZE` output после додавање на индексот: |
| | 263 | |
| | 264 | {{{ |
| | 265 | Nested Loop (cost=69.25..256.19 rows=35 width=100) (actual time=0.364..0.485 rows=48 loops=1) |
| | 266 | -> Nested Loop (cost=69.10..250.31 rows=35 width=74) (actual time=0.359..0.443 rows=48 loops=1) |
| | 267 | -> Index Scan using pk_business on business b |
| | 268 | Index Cond: (business_id = 1) |
| | 269 | -> Hash Join |
| | 270 | Hash Cond: (es.service_id = bs.service_id) |
| | 271 | -> Nested Loop |
| | 272 | Join Filter: (es.employee_id = ts.employee_id) |
| | 273 | -> Hash Join |
| | 274 | Hash Cond: (ts.employee_id = e.employee_id) |
| | 275 | -> Index Scan using idx_time_slot_available_business_date on time_slot ts |
| | 276 | Index Cond: ((business_id = 1) AND (date = CURRENT_DATE)) |
| | 277 | -> Hash |
| | 278 | -> Seq Scan on employee e |
| | 279 | -> Index Only Scan using uq_employee_service_employee_service on employee_service es |
| | 280 | Index Cond: (employee_id = e.employee_id) |
| | 281 | Heap Fetches: 0 |
| | 282 | -> Hash |
| | 283 | -> Bitmap Heap Scan on business_service bs |
| | 284 | Recheck Cond: (business_id = 1) |
| | 285 | Filter: is_active |
| | 286 | -> Bitmap Index Scan on uq_business_service_business_service |
| | 287 | Index Cond: (business_id = 1) |
| | 288 | -> Index Scan using pk_service on service s |
| | 289 | Index Cond: (service_id = es.service_id) |
| | 290 | |
| | 291 | Planning Time: 2.741 ms |
| | 292 | Execution Time: 0.580 ms |
| | 293 | }}} |
| | 294 | |
| | 295 | После додавање на индексот, планерот го користи новиот индекс: |
| | 296 | |
| | 297 | {{{ |
| | 298 | Index Scan using idx_time_slot_available_business_date on time_slot ts |
| | 299 | Index Cond: ((business_id = 1) AND (date = CURRENT_DATE)) |
| | 300 | }}} |
| | 301 | |
| | 302 | Ова значи дека наместо да ја скенира целата табела `time_slot`, базата директно ги наоѓа достапните термини за бараниот бизнис и датум. Со тоа се избегнува секвенцијалното скенирање на милиони редици. |
| | 303 | |
| | 304 | === Споредба на времиња === |
| | 305 | |
| | 306 | ||= Поглед =||= Пред индекс =||= После индекс =||= Подобрување =|| |
| | 307 | || available_slots || 7157.628 ms || 0.580 ms || над 12000 пати побрзо || |
| | 308 | |
| | 309 | Со додавање на парцијалниот индекс `idx_time_slot_available_business_date`, времето на извршување се намали од **7157.628 ms** на **0.580 ms**. Ова е огромно подобрување, бидејќи прашалникот повеќе не прави `Parallel Seq Scan` врз табелата `time_slot`, туку користи директен `Index Scan` преку новиот индекс. |
| 98 | | }}} |
| | 344 | |
| | 345 | -- 6. Мерење со индекс |
| | 346 | EXPLAIN ANALYZE |
| | 347 | SELECT * |
| | 348 | FROM review_details |
| | 349 | WHERE rating = 5; |
| | 350 | }}} |
| | 351 | |
| | 352 | === Време на извршување без индекс: |
| | 353 | |
| | 354 | **360.970 ms** |
| | 355 | |
| | 356 | Дел од добиениот `EXPLAIN ANALYZE` output пред додавање на индексот: |
| | 357 | |
| | 358 | {{{ |
| | 359 | Hash Join (cost=39.52..111924.47 rows=1006495 width=35) (actual time=0.710..344.505 rows=1000001 loops=1) |
| | 360 | Hash Cond: (r.business_id = b.business_id) |
| | 361 | -> Seq Scan on review r (cost=0.00..109231.71 rows=1006495 width=27) (actual time=0.169..259.457 rows=1000001 loops=1) |
| | 362 | Filter: (rating = 5) |
| | 363 | Rows Removed by Filter: 4000000 |
| | 364 | -> Hash (cost=27.01..27.01 rows=1001 width=16) (actual time=0.528..0.528 rows=1001 loops=1) |
| | 365 | Buckets: 1024 Batches: 1 Memory Usage: 59kB |
| | 366 | -> Seq Scan on business b (cost=0.00..27.01 rows=1001 width=16) (actual time=0.020..0.311 rows=1001 loops=1) |
| | 367 | |
| | 368 | Planning Time: 2.849 ms |
| | 369 | Execution Time: 360.970 ms |
| | 370 | }}} |
| | 371 | |
| | 372 | Во овој план се гледа дека табелата `review` се скенира секвенцијално: |
| | 373 | |
| | 374 | {{{ |
| | 375 | Seq Scan on review r |
| | 376 | Filter: (rating = 5) |
| | 377 | Rows Removed by Filter: 4000000 |
| | 378 | }}} |
| | 379 | |
| | 380 | Ова значи дека базата ја чита целата табела `review`, потоа ги задржува само рецензиите со `rating = 5`, а ги отфрла останатите. Во конкретниот случај се отстранети **4 000 000** редици, што покажува дека без индекс се чита многу поголем број податоци од потребното. |
| | 381 | |
| | 382 | За да го оптимизираме ова, креираме индекс на колоната `rating`: |
| | 383 | |
| | 384 | {{{ |
| | 385 | CREATE INDEX IF NOT EXISTS idx_review_rating_details |
| | 386 | ON review(rating); |
| | 387 | }}} |
| | 388 | |
| | 389 | Овој индекс му овозможува на планерот директно да ги најде редиците со конкретна оценка, наместо да ја скенира целата табела. |
| 102 | | **340.962 ms** |
| 103 | | |
| 104 | | Со додавање на индексот времето се намали од **800.553 ms** на **340.962 ms**. Подобрувањето е забележливо, но не е толку големо како кај претходните прашалници, бидејќи вредноста `rating = 5` може да се појавува кај голем број редици. Во таков случај индексот помага, но сепак мора да се прочита голем дел од табелата. |
| | 393 | **239.802 ms** |
| | 394 | |
| | 395 | Дел од добиениот `EXPLAIN ANALYZE` output после додавање на индексот: |
| | 396 | |
| | 397 | {{{ |
| | 398 | Hash Join (cost=11246.93..73210.68 rows=1006319 width=35) (actual time=40.929..223.700 rows=1000001 loops=1) |
| | 399 | Hash Cond: (r.business_id = b.business_id) |
| | 400 | -> Bitmap Heap Scan on review r (cost=11207.40..70518.39 rows=1006319 width=27) (actual time=40.699..142.119 rows=1000001 loops=1) |
| | 401 | Recheck Cond: (rating = 5) |
| | 402 | Heap Blocks: exact=46732 |
| | 403 | -> Bitmap Index Scan on idx_review_rating_details (cost=0.00..10955.83 rows=1006319 width=0) (actual time=33.958..33.958 rows=1000001 loops=1) |
| | 404 | Index Cond: (rating = 5) |
| | 405 | -> Hash (cost=27.01..27.01 rows=1001 width=16) (actual time=0.220..0.221 rows=1001 loops=1) |
| | 406 | Buckets: 1024 Batches: 1 Memory Usage: 59kB |
| | 407 | -> Seq Scan on business b (cost=0.00..27.01 rows=1001 width=16) (actual time=0.005..0.113 rows=1001 loops=1) |
| | 408 | |
| | 409 | Planning Time: 0.593 ms |
| | 410 | Execution Time: 239.802 ms |
| | 411 | }}} |
| | 412 | |
| | 413 | После додавање на индексот се гледа дека планерот веќе не прави `Seq Scan` врз `review`, туку користи: |
| | 414 | |
| | 415 | {{{ |
| | 416 | Bitmap Index Scan on idx_review_rating_details |
| | 417 | Index Cond: (rating = 5) |
| | 418 | }}} |
| | 419 | |
| | 420 | и потоа: |
| | 421 | |
| | 422 | {{{ |
| | 423 | Bitmap Heap Scan on review r |
| | 424 | Recheck Cond: (rating = 5) |
| | 425 | }}} |
| | 426 | |
| | 427 | Ова значи дека базата прво преку индексот ги наоѓа позициите на редиците кои имаат `rating = 5`, а потоа ги чита тие редици од табелата. |
| | 428 | |
| | 429 | Подобрувањето кај овој поглед е помало во споредба со `available_slots`, бидејќи `rating = 5` се појавува кај многу голем број редици. Во конкретниот резултат има **1 000 001** рецензија со оценка 5. Кога условот враќа голем дел од табелата, индексот помага, но сепак мора да се прочитаат многу редици. |
| | 430 | |
| | 431 | === Споредба на времиња === |
| | 432 | |
| | 433 | ||= Поглед =||= Пред индекс =||= После индекс =||= Подобрување =|| |
| | 434 | || review_details || 360.970 ms || 239.802 ms || околу 1.5 пати побрзо || |
| | 435 | |
| | 436 | Со додавање на индексот `idx_review_rating_details`, времето на извршување се намали од **360.970 ms** на **239.802 ms**. Индексот е корисен затоа што го заменува секвенцијалното скенирање на целата табела со `Bitmap Index Scan`, но подобрувањето не е екстремно бидејќи условот `rating = 5` враќа голем број редици. |
| 131 | | }}} |
| | 476 | |
| | 477 | -- 6. Мерење со индекс |
| | 478 | EXPLAIN ANALYZE |
| | 479 | SELECT * |
| | 480 | FROM customer_profile_view |
| | 481 | WHERE customer_id = 100; |
| | 482 | }}} |
| | 483 | |
| | 484 | === Време на извршување без индекс: |
| | 485 | |
| | 486 | **546.242 ms** |
| | 487 | |
| | 488 | Дел од добиениот `EXPLAIN ANALYZE` output пред додавање на индексот: |
| | 489 | |
| | 490 | {{{ |
| | 491 | GroupAggregate (cost=147561.37..147562.12 rows=25 width=117) (actual time=545.746..546.153 rows=1 loops=1) |
| | 492 | Group Key: u.user_id |
| | 493 | -> Sort (cost=147561.37..147561.43 rows=25 width=77) (actual time=545.501..545.909 rows=49 loops=1) |
| | 494 | Sort Key: u.user_id, a.appointment_id |
| | 495 | Sort Method: quicksort Memory: 29kB |
| | 496 | -> Nested Loop Left Join (cost=2000.85..147560.79 rows=25 width=77) (actual time=4.673..545.848 rows=49 loops=1) |
| | 497 | -> Nested Loop Left Join (cost=1000.85..73786.27 rows=5 width=69) (actual time=4.562..382.328 rows=7 loops=1) |
| | 498 | -> Nested Loop (cost=0.85..16.89 rows=1 width=53) (actual time=1.813..1.815 rows=1 loops=1) |
| | 499 | -> Index Scan using pk_customer on customer c |
| | 500 | Index Cond: (customer_id = 100) |
| | 501 | -> Index Scan using pk_user on "user" u |
| | 502 | Index Cond: (user_id = c.user_id) |
| | 503 | -> Gather |
| | 504 | Workers Planned: 2 |
| | 505 | Workers Launched: 2 |
| | 506 | -> Parallel Seq Scan on appointment a |
| | 507 | Filter: (customer_id = 100) |
| | 508 | Rows Removed by Filter: 1666665 |
| | 509 | -> Materialize |
| | 510 | -> Gather |
| | 511 | Workers Planned: 2 |
| | 512 | Workers Launched: 2 |
| | 513 | -> Parallel Seq Scan on review r |
| | 514 | Filter: (customer_id = 100) |
| | 515 | Rows Removed by Filter: 1666665 |
| | 516 | |
| | 517 | Planning Time: 7.447 ms |
| | 518 | Execution Time: 546.242 ms |
| | 519 | }}} |
| | 520 | |
| | 521 | Во овој план се гледа дека најголемиот проблем е што табелите `appointment` и `review` се скенираат секвенцијално. За `appointment` се користи: |
| | 522 | |
| | 523 | {{{ |
| | 524 | Parallel Seq Scan on appointment a |
| | 525 | Filter: (customer_id = 100) |
| | 526 | Rows Removed by Filter: 1666665 |
| | 527 | }}} |
| | 528 | |
| | 529 | а за `review` се користи: |
| | 530 | |
| | 531 | {{{ |
| | 532 | Parallel Seq Scan on review r |
| | 533 | Filter: (customer_id = 100) |
| | 534 | Rows Removed by Filter: 1666665 |
| | 535 | }}} |
| | 536 | |
| | 537 | Ова значи дека базата чита многу голем број редици за да ги најде записите кои припаѓаат на корисникот со `customer_id = 100`. |
| | 538 | |
| | 539 | За оптимизација креираме индекс на табелата `review`: |
| | 540 | |
| | 541 | {{{ |
| | 542 | CREATE INDEX IF NOT EXISTS idx_review_customer_profile |
| | 543 | ON review(customer_id, rating); |
| | 544 | }}} |
| | 545 | |
| | 546 | Овој индекс е корисен бидејќи погледот `customer_profile_view` ги користи рецензиите на конкретен корисник, а дополнително може да ја користи и колоната `rating` при пресметки или приказ на информации поврзани со оценките. |
| 135 | | **0.767 ms** |
| 136 | | |
| 137 | | Со додавање на индексот времето се намали од **708.985 ms** на **0.767 ms**. Ова е многу големо подобрување, бидејќи податоците за конкретниот корисник се наоѓаат директно преку индексот, наместо да се пребарува целата табела `review`. |
| 138 | | |
| 139 | | Во кодот беше разгледан и индексот: |
| 140 | | |
| 141 | | {{{ |
| 142 | | -- CREATE INDEX IF NOT EXISTS idx_appointment_customer_profile |
| 143 | | -- ON appointment(customer_id, created_at); |
| 144 | | }}} |
| 145 | | |
| 146 | | Овој индекс може да биде корисен доколку во погледот или во прашалниците често се користи сортирање или филтрирање според `created_at`. Во конкретниот тест, главното подобрување е постигнато со индексот врз `review(customer_id, rating)`. |
| | 550 | **123.200 ms** |
| | 551 | |
| | 552 | Дел од добиениот `EXPLAIN ANALYZE` output после додавање на индексот: |
| | 553 | |
| | 554 | {{{ |
| | 555 | GroupAggregate (cost=73811.09..73811.84 rows=25 width=117) (actual time=122.594..123.137 rows=1 loops=1) |
| | 556 | Group Key: u.user_id |
| | 557 | -> Sort (cost=73811.09..73811.15 rows=25 width=77) (actual time=122.573..123.117 rows=49 loops=1) |
| | 558 | Sort Key: u.user_id, a.appointment_id |
| | 559 | Sort Method: quicksort Memory: 29kB |
| | 560 | -> Nested Loop Left Join (cost=1001.28..73810.51 rows=25 width=77) (actual time=3.738..123.089 rows=49 loops=1) |
| | 561 | -> Nested Loop Left Join (cost=1000.85..73785.99 rows=5 width=69) (actual time=3.437..122.737 rows=7 loops=1) |
| | 562 | -> Nested Loop (cost=0.85..16.89 rows=1 width=53) (actual time=1.866..1.868 rows=1 loops=1) |
| | 563 | -> Index Scan using pk_customer on customer c |
| | 564 | Index Cond: (customer_id = 100) |
| | 565 | -> Index Scan using pk_user on "user" u |
| | 566 | Index Cond: (user_id = c.user_id) |
| | 567 | -> Gather |
| | 568 | Workers Planned: 2 |
| | 569 | Workers Launched: 2 |
| | 570 | -> Parallel Seq Scan on appointment a |
| | 571 | Filter: (customer_id = 100) |
| | 572 | Rows Removed by Filter: 1666665 |
| | 573 | -> Materialize |
| | 574 | -> Index Scan using idx_review_customer_profile on review r |
| | 575 | Index Cond: (customer_id = 100) |
| | 576 | |
| | 577 | Planning Time: 6.138 ms |
| | 578 | Execution Time: 123.200 ms |
| | 579 | }}} |
| | 580 | |
| | 581 | После додавање на индексот се гледа дека табелата `review` повеќе не се скенира секвенцијално, туку се користи новиот индекс: |
| | 582 | |
| | 583 | {{{ |
| | 584 | Index Scan using idx_review_customer_profile on review r |
| | 585 | Index Cond: (customer_id = 100) |
| | 586 | }}} |
| | 587 | |
| | 588 | Ова значи дека базата директно ги наоѓа рецензиите за конкретниот корисник, наместо да ја чита целата табела `review`. |
| | 589 | |
| | 590 | Сепак, во планот и понатаму се гледа дека табелата `appointment` се скенира секвенцијално: |
| | 591 | |
| | 592 | {{{ |
| | 593 | Parallel Seq Scan on appointment a |
| | 594 | Filter: (customer_id = 100) |
| | 595 | Rows Removed by Filter: 1666665 |
| | 596 | }}} |
| | 597 | |
| | 598 | Затоа времето се подобрува значително, но не паѓа под 1 ms како кај некои други погледи. За дополнителна оптимизација може да се разгледа и индекс на `appointment(customer_id)`, особено ако профилот секогаш ги прикажува резервациите на конкретниот корисник. |
| | 599 | |
| | 600 | === Споредба на времиња === |
| | 601 | |
| | 602 | ||= Поглед =||= Пред индекс =||= После индекс =||= Подобрување =|| |
| | 603 | || customer_profile_view || 546.242 ms || 123.200 ms || околу 4.4 пати побрзо || |
| | 604 | |
| | 605 | Со додавање на индексот `idx_review_customer_profile`, времето на извршување се намали од **546.242 ms** на **123.200 ms**. Индексот е корисен бидејќи го заменува секвенцијалното скенирање на табелата `review` со директно индексно пребарување според `customer_id`. |
| | 606 | |
| | 607 | Иако `appointment` и понатаму се скенира секвенцијално, оваа оптимизација значително го намалува вкупното време на извршување на погледот. |
| 185 | | Индексот `idx_reschedule_request_status` помага при филтрирање на барањата според статус, особено за чести прашалници како `status = 'pending'`. |
| 186 | | |
| 187 | | Индексите `idx_reschedule_request_old_slot_id` и `idx_reschedule_request_new_slot_id` помагаат при спојување со табелата за термини, бидејќи барањата за презакажување најчесто ги поврзуваат стариот и новиот термин преку овие колони. |
| 188 | | |
| 189 | | === Време на извршување: |
| 190 | | |
| 191 | | Во овој дел треба да се внесат резултатите од `EXPLAIN ANALYZE` пред и после додавање на индексите. |
| 192 | | |
| 193 | | {{{ |
| 194 | | -- Пред оптимизација: да се внесе измереното време |
| 195 | | -- После оптимизација: да се внесе измереното време |
| 196 | | }}} |
| 197 | | |
| 198 | | == 6. Влијание на индексите врз insert/update операции == |
| 199 | | |
| 200 | | Иако индексите значително го подобруваат читањето на податоците, тие можат да имаат мало влијание врз операциите за внесување и ажурирање. Причината е тоа што при секој `INSERT`, `UPDATE` или `DELETE`, базата мора да ги ажурира и индексите, не само главната табела. |
| 201 | | |
| 202 | | За тестирање на влијанието може да се користат следните прашалници: |
| 203 | | |
| 204 | | {{{ |
| 205 | | EXPLAIN ANALYZE |
| 206 | | INSERT INTO review (customer_id, business_id, rating, comment, created_at) |
| 207 | | VALUES (100, 1, 5, 'Benchmark review', CURRENT_TIMESTAMP); |
| 208 | | }}} |
| 209 | | |
| 210 | | {{{ |
| 211 | | EXPLAIN ANALYZE |
| 212 | | UPDATE review |
| 213 | | SET rating = 4 |
| 214 | | WHERE customer_id = 100; |
| 215 | | }}} |
| 216 | | |
| 217 | | {{{ |
| 218 | | EXPLAIN ANALYZE |
| 219 | | INSERT INTO time_slot (business_id, date, start_time, end_time, is_available) |
| 220 | | VALUES (1, CURRENT_DATE, TIME '10:00', TIME '10:30', TRUE); |
| 221 | | }}} |
| 222 | | |
| 223 | | {{{ |
| 224 | | EXPLAIN ANALYZE |
| 225 | | UPDATE time_slot |
| 226 | | SET is_available = FALSE |
| 227 | | WHERE business_id = 1 |
| 228 | | AND date = CURRENT_DATE; |
| 229 | | }}} |
| 230 | | |
| 231 | | {{{ |
| 232 | | EXPLAIN ANALYZE |
| 233 | | UPDATE reschedule_request |
| 234 | | SET status = 'approved' |
| 235 | | WHERE status = 'pending'; |
| 236 | | }}} |
| 237 | | |
| 238 | | Очекувано е индексите да внесат мал трошок при запишување, но бидејќи главните сценарија во апликацијата се пребарување на слободни термини, преглед на резервации и читање на рецензии, добивката кај `SELECT` прашалниците е многу поголема од потенцијалниот трошок кај `INSERT` и `UPDATE`. |
| 239 | | |
| 240 | | == 7. Заклучок == |
| 241 | | |
| 242 | | Со додавање на соодветни индекси успеавме значително да ги подобриме перформансите на најважните прашалници во системот Rezervo. |
| | 658 | === Време на извршување без индекси: |
| | 659 | |
| | 660 | **5.051 ms** |
| | 661 | |
| | 662 | Дел од добиениот `EXPLAIN ANALYZE` output пред додавање на индексите: |
| | 663 | |
| | 664 | {{{ |
| | 665 | Merge Join (cost=124.66..141.39 rows=667 width=64) (actual time=4.200..5.002 rows=667 loops=1) |
| | 666 | Merge Cond: (ts_new.slot_id = rr.new_slot_id) |
| | 667 | -> Index Scan using pk_time_slot on time_slot ts_new |
| | 668 | -> Sort (cost=124.22..125.89 rows=667 width=56) (actual time=2.050..2.087 rows=667 loops=1) |
| | 669 | Sort Key: rr.new_slot_id |
| | 670 | Sort Method: quicksort Memory: 82kB |
| | 671 | -> Merge Join (cost=82.41..92.93 rows=667 width=56) (actual time=0.611..1.932 rows=667 loops=1) |
| | 672 | Merge Cond: (ts_old.slot_id = rr.old_slot_id) |
| | 673 | -> Index Scan using pk_time_slot on time_slot ts_old |
| | 674 | -> Sort (cost=81.31..82.98 rows=667 width=48) (actual time=0.575..0.605 rows=667 loops=1) |
| | 675 | Sort Key: rr.old_slot_id |
| | 676 | Sort Method: quicksort Memory: 77kB |
| | 677 | -> Seq Scan on reschedule_request rr |
| | 678 | Filter: ((status)::text = 'pending'::text) |
| | 679 | Rows Removed by Filter: 1335 |
| | 680 | |
| | 681 | Planning Time: 1.921 ms |
| | 682 | Execution Time: 5.051 ms |
| | 683 | }}} |
| | 684 | |
| | 685 | Во овој план се гледа дека табелата `reschedule_request` се скенира секвенцијално: |
| | 686 | |
| | 687 | {{{ |
| | 688 | Seq Scan on reschedule_request rr |
| | 689 | Filter: ((status)::text = 'pending'::text) |
| | 690 | Rows Removed by Filter: 1335 |
| | 691 | }}} |
| | 692 | |
| | 693 | Ова значи дека базата ја чита табелата `reschedule_request`, па потоа ги задржува само барањата со статус `pending`. Иако табелата во овој тест не е многу голема, ова може да стане проблем ако бројот на барања за презакажување се зголеми. |
| | 694 | |
| | 695 | За оптимизација ги креираме следните индекси: |
| | 696 | |
| | 697 | {{{ |
| | 698 | CREATE INDEX IF NOT EXISTS idx_reschedule_request_status |
| | 699 | ON reschedule_request(status); |
| | 700 | |
| | 701 | CREATE INDEX IF NOT EXISTS idx_reschedule_request_old_slot_id |
| | 702 | ON reschedule_request(old_slot_id); |
| | 703 | |
| | 704 | CREATE INDEX IF NOT EXISTS idx_reschedule_request_new_slot_id |
| | 705 | ON reschedule_request(new_slot_id); |
| | 706 | }}} |
| | 707 | |
| | 708 | Индексот `idx_reschedule_request_status` е корисен за филтрирање според статус, додека индексите `idx_reschedule_request_old_slot_id` и `idx_reschedule_request_new_slot_id` можат да помогнат при спојувањата со табелата `time_slot`, бидејќи погледот ги поврзува старите и новите термини преку овие колони. |
| | 709 | |
| | 710 | === Време на извршување со индекси: |
| | 711 | |
| | 712 | **3.988 ms** |
| | 713 | |
| | 714 | Дел од добиениот `EXPLAIN ANALYZE` output после додавање на индексите: |
| | 715 | |
| | 716 | {{{ |
| | 717 | Merge Join (cost=121.42..138.15 rows=667 width=64) (actual time=3.341..3.903 rows=667 loops=1) |
| | 718 | Merge Cond: (ts_new.slot_id = rr.new_slot_id) |
| | 719 | -> Index Scan using pk_time_slot on time_slot ts_new |
| | 720 | -> Sort (cost=120.98..122.65 rows=667 width=56) (actual time=1.460..1.490 rows=667 loops=1) |
| | 721 | Sort Key: rr.new_slot_id |
| | 722 | Sort Method: quicksort Memory: 82kB |
| | 723 | -> Merge Join (cost=79.17..89.69 rows=667 width=56) (actual time=0.712..1.359 rows=667 loops=1) |
| | 724 | Merge Cond: (ts_old.slot_id = rr.old_slot_id) |
| | 725 | -> Index Scan using pk_time_slot on time_slot ts_old |
| | 726 | -> Sort (cost=78.07..79.74 rows=667 width=48) (actual time=0.691..0.718 rows=667 loops=1) |
| | 727 | Sort Key: rr.old_slot_id |
| | 728 | Sort Method: quicksort Memory: 77kB |
| | 729 | -> Bitmap Heap Scan on reschedule_request rr |
| | 730 | Recheck Cond: ((status)::text = 'pending'::text) |
| | 731 | Heap Blocks: exact=25 |
| | 732 | -> Bitmap Index Scan on idx_reschedule_request_status |
| | 733 | Index Cond: ((status)::text = 'pending'::text) |
| | 734 | |
| | 735 | Planning Time: 0.904 ms |
| | 736 | Execution Time: 3.988 ms |
| | 737 | }}} |
| | 738 | |
| | 739 | После додавање на индексите се гледа дека планерот повеќе не користи `Seq Scan` за `reschedule_request`, туку користи: |
| | 740 | |
| | 741 | {{{ |
| | 742 | Bitmap Index Scan on idx_reschedule_request_status |
| | 743 | Index Cond: ((status)::text = 'pending'::text) |
| | 744 | }}} |
| | 745 | |
| | 746 | и потоа: |
| | 747 | |
| | 748 | {{{ |
| | 749 | Bitmap Heap Scan on reschedule_request rr |
| | 750 | Recheck Cond: ((status)::text = 'pending'::text) |
| | 751 | }}} |
| | 752 | |
| | 753 | Ова значи дека базата преку индексот прво ги наоѓа барањата кои имаат статус `pending`, а потоа ги чита само тие редици од табелата. |
| | 754 | |
| | 755 | Во овој конкретен тест подобрувањето не е многу големо затоа што табелата `reschedule_request` има релативно мал број редици. Сепак, индексот е корисен затоа што во реална апликација бројот на барања може да расте, а пребарувањето по статус би било често сценарио. |
| | 756 | |
| | 757 | === Споредба на времиња === |
| | 758 | |
| | 759 | ||= Поглед =||= Пред индекси =||= После индекси =||= Подобрување =|| |
| | 760 | || reschedule_overview || 5.051 ms || 3.988 ms || околу 1.27 пати побрзо || |
| | 761 | |
| | 762 | Со додавање на индексите, времето на извршување се намали од **5.051 ms** на **3.988 ms**. Најважниот индекс во овој случај е `idx_reschedule_request_status`, бидејќи директно се користи за условот `status = 'pending'`. |
| | 763 | |
| | 764 | Индексите на `old_slot_id` и `new_slot_id` се потенцијално корисни за спојувања со табелата `time_slot`, иако во конкретниот план најмногу се гледа користењето на индексот за `status`. |
| | 765 | |
| | 766 | == 6. Заклучок == |
| | 767 | |
| | 768 | Со додавање на соодветни индекси успеавме да ги подобриме перформансите на тестираните прашалници во системот Rezervo. |
| 246 | | ||= Поглед =||= Пред оптимизација =||= После оптимизација =|| |
| 247 | | || customer_appointments_view || 644.544 ms || 2.756 ms || |
| 248 | | || available_slots || 429.479 ms || 1.090 ms || |
| 249 | | || review_details || 800.553 ms || 340.962 ms || |
| 250 | | || customer_profile_view || 708.985 ms || 0.767 ms || |
| 251 | | |
| 252 | | Најголемо забрзување има кај `customer_profile_view`, каде што времето се намали од **708.985 ms** на **0.767 ms**, како и кај `available_slots`, каде што пребарувањето на достапни термини се намали од **429.479 ms** на **1.090 ms**. |
| 253 | | |
| 254 | | Заклучуваме дека индексите се оправдани бидејќи ги оптимизираат најчестите кориснички сценарија во апликацијата: |
| 255 | | |
| 256 | | * преглед на резервации за корисник |
| 257 | | * пребарување на достапни термини |
| 258 | | * преглед и филтрирање на рецензии |
| 259 | | * приказ на кориснички профил |
| 260 | | |
| 261 | | Иако индексите можат да имаат мал трошок при внесување и ажурирање на податоци, во овој систем читањето и пребарувањето се многу почести операции, па добивката во перформанси е значително поголема. |
| | 772 | ||= Поглед =||= Пред индекс =||= После индекс =||= Подобрување =|| |
| | 773 | || customer_appointments_view || 7.802 ms || 0.803 ms || околу 9.7 пати побрзо || |
| | 774 | || available_slots || 7157.628 ms || 0.580 ms || над 12000 пати побрзо || |
| | 775 | || review_details || 360.970 ms || 239.802 ms || околу 1.5 пати побрзо || |
| | 776 | || customer_profile_view || 546.242 ms || 123.200 ms || околу 4.4 пати побрзо || |
| | 777 | || reschedule_overview || 5.051 ms || 3.988 ms || околу 1.27 пати побрзо || |
| | 778 | |
| | 779 | Најголемо подобрување има кај `available_slots`, бидејќи пред додавање на индексот базата правеше `Parallel Seq Scan` врз табелата `time_slot` и отстрануваше над 6 милиони редици преку филтер. После додавање на парцијалниот индекс, базата директно ги наоѓа достапните термини за конкретен бизнис и датум. |
| | 780 | |
| | 781 | Кај `customer_appointments_view` подобрувањето е исто така значајно, бидејќи парцијалниот индекс ги содржи само резервациите кои не се откажани. Со тоа се избегнува дополнително филтрирање според статус. |
| | 782 | |
| | 783 | Кај `customer_profile_view` подобрувањето е значително, бидејќи индексот `idx_review_customer_profile` го заменува секвенцијалното скенирање на табелата `review` со директно индексно пребарување според `customer_id`. Сепак, бидејќи табелата `appointment` и понатаму се скенира секвенцијално, прашалникот не се намалува под 1 ms. |
| | 784 | |
| | 785 | Кај `review_details` подобрувањето е помало. Причината е тоа што условот `rating = 5` враќа многу голем број редици. Индексот го заменува целосното секвенцијално скенирање со `Bitmap Index Scan`, но бидејќи сепак мора да се прочитаат околу 1 милион рецензии, разликата не е толку голема. |
| | 786 | |
| | 787 | Кај `reschedule_overview` подобрувањето е најмало, бидејќи табелата `reschedule_request` во тестот има релативно мал број редици. Индексот `idx_reschedule_request_status` се користи во планот, но разликата меѓу времето пред и после индексот е мала. |
| | 788 | |
| | 789 | == 7. Влијание на индексите и можност за бришење на некои индекси == |
| | 790 | |
| | 791 | Индексите го подобруваат читањето на податоците, но имаат и цена. При секој `INSERT`, `UPDATE` или `DELETE`, базата мора да ги ажурира и индексите. Затоа не е секогаш оправдано да се задржи индекс кој дава многу мало подобрување. |
| | 792 | |
| | 793 | Во нашите тестови, индексите кои даваат најголема добивка и се оправдани за задржување се: |
| | 794 | |
| | 795 | {{{ |
| | 796 | idx_appointment_customer_not_cancelled |
| | 797 | idx_time_slot_available_business_date |
| | 798 | idx_review_customer_profile |
| | 799 | }}} |
| | 800 | |
| | 801 | Овие индекси значително го намалуваат времето на извршување и директно одговараат на чести сценарија во апликацијата: преглед на резервации, пребарување достапни термини и приказ на кориснички профил. |
| | 802 | |
| | 803 | Од друга страна, кај следните индекси добивката е помала: |
| | 804 | |
| | 805 | {{{ |
| | 806 | idx_review_rating_details |
| | 807 | idx_reschedule_request_status |
| | 808 | idx_reschedule_request_old_slot_id |
| | 809 | idx_reschedule_request_new_slot_id |
| | 810 | }}} |
| | 811 | |
| | 812 | Индексот `idx_review_rating_details` го намалува времето од **360.970 ms** на **239.802 ms**, но бидејќи `rating = 5` враќа многу голем број редици, подобрувањето е ограничено. Доколку филтрирањето по оценка не е често сценарио во апликацијата, може да се размисли овој индекс да се избрише. |
| | 813 | |
| | 814 | Индексот `idx_reschedule_request_status` го намалува времето од **5.051 ms** на **3.988 ms**, што е мала разлика. Дополнително, индексите `idx_reschedule_request_old_slot_id` и `idx_reschedule_request_new_slot_id` не се директно искористени во прикажаниот план на извршување. Затоа, ако овие индекси не се користат во други прашалници, може да се размисли за нивно бришење. |
| | 815 | |
| | 816 | Бришењето би се направило со: |
| | 817 | |
| | 818 | {{{ |
| | 819 | DROP INDEX IF EXISTS idx_review_rating_details; |
| | 820 | |
| | 821 | DROP INDEX IF EXISTS idx_reschedule_request_status; |
| | 822 | DROP INDEX IF EXISTS idx_reschedule_request_old_slot_id; |
| | 823 | DROP INDEX IF EXISTS idx_reschedule_request_new_slot_id; |
| | 824 | }}} |
| | 825 | |
| | 826 | Заклучуваме дека индексите треба да се задржат само ако носат значајно подобрување или ако се користат често во апликацијата. Во спротивно, подобро е да се избришат за да не создаваат непотребен трошок при внесување и ажурирање на податоците. |
| | 827 | |
| | 828 | Конечно, најоправдани индекси за задржување во оваа фаза се парцијалните индекси за `customer_appointments_view` и `available_slots`, како и индексот за `customer_profile_view`, бидејќи таму добивката е најголема и сценаријата се чести во реалната употреба на системот. |