| 11 | | === 1. Анализа на поглед 1, достапни возења по град на поаѓање === |
| 12 | | |
| 13 | | Прашалникот кој го тестираме: |
| 14 | | {{{ |
| 15 | | SELECT * FROM v_available_rides WHERE origin_city = 'Скопје' LIMIT 50; |
| 16 | | }}} |
| 17 | | |
| 18 | | ==== Време на извршување без индекси: |
| 19 | | |
| 20 | | **2 s 92 ms** |
| 21 | | |
| 22 | | {{{ |
| 23 | | Limit (cost=473.63..24708.84 rows=50 width=223) |
| 24 | | (actual time=2040.xxx..2092.xxx rows=0 loops=1) |
| 25 | | -> Hash Join |
| 26 | | Hash Cond: (ud.id = vdp.driver_user_id) |
| 27 | | -> Seq Scan on rides r |
| 28 | | Filter: ((status)::text = 'scheduled'::text) |
| 29 | | Rows Removed by Filter: 4750535 |
| 30 | | -> Hash |
| 31 | | -> Finalize HashAggregate |
| 32 | | Group Key: ratings.reviewee_user_id |
| 33 | | -> Gather |
| 34 | | Workers Planned: 3 |
| 35 | | -> Partial HashAggregate |
| 36 | | -> Parallel Seq Scan on ratings |
| 37 | | (603662 rows — full scan за сите 15000 возачи) |
| 38 | | Planning Time: 5.xxx ms |
| 39 | | Execution Time: 2092.xxx ms |
| 40 | | }}} |
| 41 | | |
| 42 | | Постојат два тесни грла: (1) `Seq Scan on rides` скенира сите 5 милиони редови за да ги филтрира само 255,667 со `status = 'scheduled'`; (2) `JOIN v_driver_profile` предизвикува целосна агрегација на табелата `ratings` за сите 15,000 возачи, иако за секој ред во резултатот се потребни оцените само на еден возач. Затоа погледот е реструктуриран — `JOIN v_driver_profile` е заменет со `LEFT JOIN LATERAL` кој ги агрегира оцените само за возачот на тековниот ред, а потоа се додаваат следните индекси: |
| 43 | | |
| 44 | | {{{ |
| 45 | | CREATE INDEX IF NOT EXISTS idx_rides_status_dep |
| | 11 | === 1. Анализа на поглед 1, достапни возења по град на поаѓање и пристигнување === |
| | 12 | |
| | 13 | Прашалникот кој го тестираме: |
| | 14 | {{{ |
| | 15 | SELECT * FROM v_available_rides |
| | 16 | WHERE origin_city = 'Skopje' AND destination_city = 'Bitola'; |
| | 17 | }}} |
| | 18 | |
| | 19 | ==== Време на извршување без индекси: |
| | 20 | |
| | 21 | **72 s 559 ms** |
| | 22 | |
| | 23 | {{{ |
| | 24 | Nested Loop Left Join (cost=38175.80..423608.72 rows=9 width=643) |
| | 25 | (actual time=178.534..72559.294 rows=547.00 loops=1) |
| | 26 | -> Gather (cost=1023.28..89235.80 rows=9 width=767) |
| | 27 | (actual time=1.669..2.593 rows=547.00 loops=3) |
| | 28 | Workers Planned: 2 |
| | 29 | Workers Launched: 2 |
| | 30 | -> Nested Loop |
| | 31 | -> Parallel Seq Scan on rides r |
| | 32 | Filter: ((status)::text = 'scheduled'::text) |
| | 33 | Rows Removed by Filter: 1583512 |
| | 34 | -> Hash Join |
| | 35 | Hash Cond: (r.route_id = ro.id) |
| | 36 | -> Index Scan using cities_name_country_key on cities orig_city |
| | 37 | Index Cond: ((name)::text = 'Skopje'::text) |
| | 38 | -> Index Scan using cities_pkey on cities dest_city |
| | 39 | Filter: ((name)::text = 'Bitola'::text) |
| | 40 | Rows Removed by Filter: 1 |
| | 41 | -> Aggregate |
| | 42 | -> Seq Scan on ratings |
| | 43 | Filter: (reviewee_user_id = ud.id) |
| | 44 | Rows Removed by Filter: 1871226 |
| | 45 | Planning Time: 2.672 ms |
| | 46 | Execution Time: 72559.907 ms |
| | 47 | }}} |
| | 48 | |
| | 49 | Постојат две тесни грла: (1) `Parallel Seq Scan on rides` скенира сите 5 милиони редови за да ги филтрира само оние со `status = 'scheduled'`; (2) `Seq Scan on ratings` се извршува 547 пати (по еднаш за секој возач во резултатот) и секојпат скенира сите 1,871,226 редови за да ги филтрира оцените по `reviewee_user_id`. Ова е резултат на `LEFT JOIN LATERAL` — агрегацијата се врши по возач, но без индекс секој пат се скенира целата табела. Ги додаваме следните индекси: |
| | 50 | |
| | 51 | {{{ |
| | 52 | CREATE INDEX idx_rides_status_dep |
| 54 | | **1 s 821 ms** (беше 2 s 92 ms) |
| 55 | | |
| 56 | | {{{ |
| 57 | | Limit (cost=473.63..24708.84 rows=50 width=223) |
| 58 | | (actual time=1315.180..1315.194 rows=0 loops=1) |
| 59 | | -> Nested Loop Left Join |
| 60 | | -> Nested Loop |
| 61 | | -> Seq Scan on rides r |
| 62 | | Filter: ((status)::text = 'scheduled'::text) |
| 63 | | Rows Removed by Filter: 4750535 |
| 64 | | -> Index Scan using routes_pkey on routes ro |
| 65 | | -> Index Scan using locations_pkey on locations orig |
| 66 | | -> Materialize |
| 67 | | -> Seq Scan on cities orig_city |
| 68 | | Filter: ((name)::text = 'Скопје'::text) |
| 69 | | Rows Removed by Filter: 20 |
| 70 | | -> Aggregate |
| 71 | | -> Bitmap Heap Scan on ratings |
| 72 | | Recheck Cond: (reviewee_user_id = ud.id) |
| 73 | | -> Bitmap Index Scan on idx_ratings_reviewee |
| 74 | | Index Cond: (reviewee_user_id = ud.id) |
| 75 | | Planning Time: 5.712 ms |
| 76 | | Execution Time: 1315.531 ms |
| 77 | | }}} |
| 78 | | |
| 79 | | Со `LEFT JOIN LATERAL` и `idx_ratings_reviewee` агрегацијата на оцените се врши само за возачот на тековниот ред наместо за сите 15,000 возачи. Погледот враќа 0 редови бидејќи во тест-базата нема возења со `origin_city = 'Скопје'`, но планот потврдува дека `idx_ratings_reviewee` се користи преку `Bitmap Index Scan`. |
| | 61 | **3 s 980 ms** (беше 72 s 559 ms) |
| | 62 | |
| | 63 | {{{ |
| | 64 | Nested Loop Left Join (cost=4373.93..71603.67 rows=9 width=643) |
| | 65 | (actual time=628.137..3979.211 rows=547.00 loops=1) |
| | 66 | -> Gather (cost=3902.09..67356.78 rows=9 width=767) |
| | 67 | (actual time=122.653..127.176 rows=547.00 loops=1) |
| | 68 | Workers Planned: 2 |
| | 69 | Workers Launched: 2 |
| | 70 | -> Parallel Bitmap Heap Scan on rides r |
| | 71 | Recheck Cond: ((status)::text = 'scheduled'::text) |
| | 72 | -> Bitmap Index Scan on idx_rides_status_dep |
| | 73 | Index Cond: ((status)::text = 'scheduled'::text) |
| | 74 | -> Aggregate |
| | 75 | -> Bitmap Heap Scan on ratings |
| | 76 | Recheck Cond: (reviewee_user_id = ud.id) |
| | 77 | -> Bitmap Index Scan on idx_ratings_reviewee |
| | 78 | Index Cond: (reviewee_user_id = ud.id) |
| | 79 | Planning Time: 19.415 ms |
| | 80 | Execution Time: 3980.397 ms |
| | 81 | }}} |
| | 82 | |
| | 83 | `idx_rides_status_dep` го заменува целосниот скен на `rides` со `Bitmap Index Scan` — планерот директно ги наоѓа редовите со `status = 'scheduled'`. `idx_ratings_reviewee` го заменува `Seq Scan on ratings` со `Bitmap Index Scan` по `reviewee_user_id` за секој возач поединечно. Вкупното подобрување е ~18x (од 72.5 s на 3.98 s). |
| 155 | | Filter: (driver_id = 12902) |
| 156 | | Rows Removed by Filter: 999934 |
| 157 | | Planning Time: 2.805 ms |
| 158 | | Execution Time: 692.124 ms |
| 159 | | }}} |
| 160 | | |
| 161 | | `idx_vo_driver_active` е искористен преку `Index Only Scan` за брзо наоѓање на активното возило. Агрегацијата на `ratings` сепак бара целосен скен бидејќи погледот ги агрегира оцените за сите возачи — ова е агрегација врз цело множество каде индексот не може да ја елиминира потребата за читање на сите редови. Затоа вкупното подобрување е помало од очекуваното. |
| 162 | | |
| 163 | | === 3. Анализа на поглед 3, манифест на возење === |
| 164 | | |
| 165 | | Прашалникот кој го тестираме: |
| 166 | | {{{ |
| 167 | | SELECT * FROM v_ride_manifest WHERE ride_id = 1000; |
| 168 | | }}} |
| 169 | | |
| 170 | | ==== Време на извршување без индекси: |
| 171 | | |
| 172 | | **644 ms** |
| 173 | | |
| 174 | | {{{ |
| 175 | | Nested Loop Left Join (cost=3.44..64.11 rows=1 width=127) |
| 176 | | (actual time=0.310..0.590 rows=3 loops=1) |
| | 160 | Filter: (driver_id = 1) |
| | 161 | Rows Removed by Filter: 1666556 |
| | 162 | Planning Time: 15.218 ms |
| | 163 | Execution Time: 1379.943 ms |
| | 164 | }}} |
| | 165 | |
| | 166 | `idx_vo_driver_active` е искористен преку `Index Only Scan` за брзо наоѓање на активното возило без пристап до хип. Агрегацијата на `ratings` сепак бара целосен скен бидејќи погледот ги агрегира оцените за сите возачи — ова е агрегација врз цело множество каде индексот не може да ја елиминира потребата за читање на сите редови. `Parallel Seq Scan on rides` исто така останува бидејќи нема индекс по `driver_id` на `rides` (`idx_rides_driver_status` беше отстранет). Подобрувањето е минимално поради доминантната агрегација на `ratings`. |
| | 167 | |
| | 168 | === 3. Анализа на поглед 3, заработка на возач === |
| | 169 | |
| | 170 | Прашалникот кој го тестираме: |
| | 171 | {{{ |
| | 172 | SELECT * FROM v_driver_earnings WHERE driver_id = 1; |
| | 173 | }}} |
| | 174 | |
| | 175 | ==== Време на извршување без индекси: |
| | 176 | |
| | 177 | **33 s 186 ms** |
| | 178 | |
| | 179 | {{{ |
| | 180 | GroupAggregate (cost=1447123.50..1447132.37 rows=169 width=195) |
| | 181 | (actual time=33086.086..33086.368 rows=47.00 loops=1) |
| | 182 | Group Key: ud.name, date_trunc('month', r.departure_time) |
| | 183 | -> Sort |
| | 184 | Sort Key: ud.name, date_trunc('month', r.departure_time) |
| | 185 | Sort Method: quicksort Memory: 43kB |
| | 186 | -> Nested Loop |
| | 187 | -> Index Scan using drivers_pkey on drivers d |
| | 188 | Index Cond: (id = 1) |
| | 189 | -> Hash Join (Hash Cond: b.ride_id = r.id) |
| | 190 | -> HashAggregate (Group Key: b.ride_id) |
| | 191 | Planned Partitions: 256 Batches: 257 |
| | 192 | Disk Usage: 245528kB |
| | 193 | -> Hash Join (Hash Cond: bff.booking_id = b.id) |
| | 194 | -> Seq Scan on booking_final_fare bff |
| | 195 | (12000000 rows — целосен скен) |
| | 196 | -> Seq Scan on bookings b |
| | 197 | Filter: (status = 'completed') |
| | 198 | Rows Removed by Filter: 5757941 |
| | 199 | -> Parallel Seq Scan on rides r |
| | 200 | Filter: (driver_id = 1) AND (status = 'completed') |
| | 201 | Rows Removed by Filter: 1666595 |
| | 202 | Planning Time: 41.731 ms |
| | 203 | Execution Time: 33186.554 ms |
| | 204 | }}} |
| | 205 | |
| | 206 | Постојат две тесни грла: (1) `Seq Scan on bookings` скенира 12 милиони редови за да ги филтрира завршените резервации; (2) `Seq Scan on booking_final_fare` скенира 12 милиони редови за секој JOIN. Планерот мора да ги агрегира сите резервации по возење пред да го примени филтерот по возач — ова е фундаментално ограничување на агрегатниот поглед. Бидејќи погледот врши целосна агрегација, ниеден индекс не може да ја заобиколи потребата за читање на сите редови. Не се додаваат индекси. |
| | 207 | |
| | 208 | ==== Нема индекси — агрегатен поглед: |
| | 209 | |
| | 210 | Времето на извршување останува **~33 s** — индексите не влијаат на перформансите на агрегатни погледи кои мора да ги прочитаат сите редови. |
| | 211 | |
| | 212 | === 4. Анализа на поглед 4, историја на патувања на патник === |
| | 213 | |
| | 214 | Прашалникот кој го тестираме: |
| | 215 | {{{ |
| | 216 | SELECT * FROM v_passenger_trip_history WHERE passenger_user_id = 1; |
| | 217 | }}} |
| | 218 | |
| | 219 | ==== Време на извршување без индекси: |
| | 220 | |
| | 221 | **721 ms** |
| | 222 | |
| | 223 | {{{ |
| | 224 | Nested Loop Left Join (cost=1022.14..184161.50 rows=343 width=558) |
| | 225 | (actual time=2.979..721.148 rows=344.00 loops=1) |
| | 226 | -> Gather (cost=1009.62..183546.43 rows=343 width=110) |
| | 227 | (actual time=2.205..511.797 rows=344.00 loops=1) |
| | 228 | Workers Planned: 2 |
| | 229 | Workers Launched: 2 |
| | 230 | -> Nested Loop |
| | 231 | -> Hash Join |
| | 232 | Hash Cond: (b.passenger_id = p.id) |
| | 233 | -> Parallel Seq Scan on bookings b |
| | 234 | (12000001 rows — целосен скен) |
| | 235 | -> Index Scan using passengers_user_id_key on passengers p |
| | 236 | Index Cond: (user_id = 1) |
| | 237 | -> Index Scan using rides_pkey on rides r |
| | 238 | Index Cond: (id = b.ride_id) |
| | 239 | -> Index Scan using uq_ratings_per_ride on ratings rt |
| | 240 | Index Cond: (reviewer_user_id = 1) AND (ride_id = b.ride_id) |
| | 241 | Planning Time: 6.787 ms |
| | 242 | Execution Time: 721.475 ms |
| | 243 | }}} |
| | 244 | |
| | 245 | Најбавната операција е `Parallel Seq Scan on bookings` — без индекс базата скенира сите 12,000,000 резервации за секое барање на патник. Иако `passengers_user_id_key` веќе постои (уникатен индекс), нема индекс по `bookings.passenger_id`. Ги додаваме следните индекси: |
| | 246 | |
| | 247 | {{{ |
| | 248 | CREATE INDEX idx_bookings_passenger_id |
| | 249 | ON bookings(passenger_id); |
| | 250 | |
| | 251 | CREATE INDEX idx_passengers_user_id |
| | 252 | ON passengers(user_id); |
| | 253 | |
| | 254 | CREATE INDEX idx_ratings_reviewer_ride |
| | 255 | ON ratings(reviewer_user_id, ride_id); |
| | 256 | }}} |
| | 257 | |
| | 258 | ==== Време на извршување со индекси: |
| | 259 | |
| | 260 | **562 ms** (беше 721 ms) |
| | 261 | |
| | 262 | {{{ |
| | 263 | Nested Loop Left Join (cost=22.15..2185.57 rows=343 width=558) |
| | 264 | (actual time=2.315..561.965 rows=344.00 loops=1) |
| | 266 | -> Nested Loop |
| | 267 | -> Index Scan using idx_passengers_user_id on passengers p |
| | 268 | Index Cond: (user_id = 1) |
| | 269 | -> Bitmap Heap Scan on bookings b |
| | 270 | Recheck Cond: (passenger_id = p.id) |
| | 271 | -> Bitmap Index Scan on idx_bookings_passenger_id |
| | 272 | Index Cond: (passenger_id = p.id) |
| | 273 | -> Index Scan using rides_pkey on rides r |
| | 274 | Index Cond: (id = b.ride_id) |
| | 275 | -> Index Scan using idx_ratings_reviewer_ride on ratings rt |
| | 276 | Index Cond: (reviewer_user_id = 1) AND (ride_id = b.ride_id) |
| | 277 | Planning Time: 44.836 ms |
| | 278 | Execution Time: 562.574 ms |
| | 279 | }}} |
| | 280 | |
| | 281 | `idx_bookings_passenger_id` го елиминира целосниот скен на 12 милиони резервации — планерот директно ги наоѓа резервациите на патникот преку `Bitmap Index Scan`. `idx_ratings_reviewer_ride` го решава JOIN за оцените преку еден пристап по индекс наместо скен. |
| | 282 | |
| | 283 | === 5. Анализа на поглед 5, манифест на возење === |
| | 284 | |
| | 285 | Прашалникот кој го тестираме: |
| | 286 | {{{ |
| | 287 | SELECT * FROM v_ride_manifest WHERE ride_id = 1; |
| | 288 | }}} |
| | 289 | |
| | 290 | ==== Време на извршување без индекси: |
| | 291 | |
| | 292 | **2 ms 320 ms** |
| | 293 | |
| | 294 | {{{ |
| | 295 | Nested Loop Left Join (cost=3.45..98.67 rows=2 width=547) |
| | 296 | (actual time=0.719..2.233 rows=3.00 loops=1) |
| | 297 | -> Nested Loop |
| | 298 | -> Nested Loop |
| | 299 | -> Index Scan using rides_pkey on rides r |
| | 300 | Index Cond: (id = 1) |
| | 301 | -> Index Scan using uq_bookings_passenger_ride on bookings b |
| | 302 | Index Cond: (ride_id = 1) |
| | 303 | Filter: (status = ANY ('{confirmed,picked_up,completed}')) |
| | 304 | Rows Removed by Filter: 0 |
| | 305 | -> Index Scan using passengers_pkey on passengers p |
| | 306 | Index Cond: (id = b.passenger_id) |
| | 307 | -> Index Scan using users_pkey on users up |
| | 308 | Index Cond: (id = p.user_id) |
| | 309 | -> Index Scan using route_stops_pkey on route_stops pup_stop |
| | 310 | -> Index Scan using route_stops_pkey on route_stops pdrop_stop |
| | 311 | -> Index Scan using booking_final_fare_booking_id_key |
| | 312 | on booking_final_fare bff |
| | 313 | Index Cond: (booking_id = b.id) |
| | 314 | Planning Time: 5.151 ms |
| | 315 | Execution Time: 2.320 ms |
| | 316 | }}} |
| | 317 | |
| | 318 | Планот на извршување покажува дека планерот веќе користи `Index Scan` по `bookings` преку постоечкиот уникатен индекс `uq_bookings_passenger_ride`. Сите JOIN операции се решаваат преку примарни клучеви. Сепак, го додаваме следниот индекс за да го покриеме `IN` филтерот по `status` без пристап до табелата: |
| | 319 | |
| | 320 | {{{ |
| | 321 | CREATE INDEX idx_bookings_ride_status |
| | 322 | ON bookings(ride_id, status); |
| | 323 | }}} |
| | 324 | |
| | 325 | ==== Време на извршување со индекс: |
| | 326 | |
| | 327 | **2 ms 452 ms** (без значајна промена) |
| | 328 | |
| | 329 | {{{ |
| | 330 | Nested Loop Left Join (cost=3.45..98.67 rows=2 width=547) |
| | 331 | (actual time=0.343..2.142 rows=3.00 loops=1) |
| | 332 | -> Nested Loop |
| | 333 | -> Index Scan using rides_pkey on rides r |
| | 334 | Index Cond: (id = 1) |
| 192 | | Planning Time: 3.xxx ms |
| 193 | | Execution Time: 644.xxx ms |
| 194 | | }}} |
| 195 | | |
| 196 | | Планот на извршување покажува дека планерот веќе користи `Index Scan` по `bookings` преку постоечкиот уникатен индекс `uq_bookings_passenger_ride`. Сите JOIN операции се решаваат преку примарни клучеви. Нема потреба од дополнителни индекси. Сепак, го додаваме следниот индекс за да го покриеме `IN` филтерот по `status` во рамките на индексот без пристап до табелата: |
| 197 | | |
| 198 | | {{{ |
| 199 | | CREATE INDEX IF NOT EXISTS idx_bookings_ride_status |
| 200 | | ON bookings(ride_id, status); |
| 201 | | }}} |
| 202 | | |
| 203 | | ==== Време на извршување со индекс: |
| 204 | | |
| 205 | | **674 ms** (беше 644 ms) |
| 206 | | |
| 207 | | {{{ |
| 208 | | Nested Loop Left Join (cost=3.44..64.11 rows=1 width=127) |
| 209 | | (actual time=0.292..0.562 rows=3 loops=1) |
| 210 | | -> Nested Loop |
| 211 | | -> Index Scan using uq_bookings_passenger_ride on bookings b |
| 212 | | Index Cond: (ride_id = 1000) |
| 213 | | Filter: (status = ANY ('{confirmed,picked_up,completed}')) |
| 214 | | Rows Removed by Filter: 1 |
| 215 | | -> Index Scan using rides_pkey on rides r |
| 216 | | -> Index Scan using routes_pkey on routes ro |
| 217 | | -> Index Scan using locations_pkey on locations orig |
| 218 | | -> Index Scan using locations_pkey on locations dest |
| 219 | | -> Index Scan using passengers_pkey on passengers p |
| 220 | | -> Index Scan using users_pkey on users up |
| 221 | | -> Index Scan using route_stops_pkey on route_stops pup_stop |
| 222 | | -> Index Scan using route_stops_pkey on route_stops pdrop_stop |
| 223 | | -> Index Scan using idx_bff_booking_id on booking_final_fare bff |
| 224 | | Index Cond: (booking_id = b.id) |
| 225 | | Planning Time: 3.373 ms |
| 226 | | Execution Time: 0.749 ms |
| 227 | | }}} |
| 228 | | |
| 229 | | Времето на извршување останува практично исто бидејќи планерот веќе користеше индекси за сите операции. Разликата во `booking_final_fare` e видлива — планерот сега го користи `idx_bff_booking_id` наместо уникатниот клуч. |
| 230 | | |
| 231 | | === 4. Анализа на поглед 4, историја на патувања на патник === |
| 232 | | |
| 233 | | Прашалникот кој го тестираме: |
| 234 | | {{{ |
| 235 | | SELECT * FROM v_passenger_trip_history WHERE passenger_user_id = 500; |
| 236 | | }}} |
| 237 | | |
| 238 | | ==== Време на извршување без индекси: |
| 239 | | |
| 240 | | **10 s 520 ms** |
| 241 | | |
| 242 | | {{{ |
| 243 | | Nested Loop Left Join (cost=1024.73..158574.74 rows=343 width=138) |
| 244 | | (actual time=10500.xxx..10520.xxx rows=343 loops=1) |
| 245 | | -> Nested Loop Left Join |
| 246 | | -> Hash Join |
| 247 | | -> Index Scan using users_pkey on users up |
| 248 | | Index Cond: (id = 500) |
| 249 | | -> Gather |
| 250 | | Workers Planned: 4 |
| 251 | | -> Nested Loop |
| 252 | | -> Parallel Seq Scan on bookings b |
| 253 | | Filter: (passenger_id = p.id) |
| 254 | | Rows Removed by Filter: ~11999657 |
| 255 | | -> Seq Scan on passengers p |
| 256 | | Filter: (user_id = 500) |
| 257 | | -> Index Scan using rides_pkey on rides r |
| 258 | | -> Index Scan using booking_final_fare_booking_id_key |
| 259 | | on booking_final_fare bff |
| 260 | | -> Index Scan using ratings on ratings rt (seq scan fallback) |
| 261 | | Planning Time: 4.xxx ms |
| 262 | | Execution Time: 10520.xxx ms |
| 263 | | }}} |
| 264 | | |
| 265 | | Најбавната операција е `Parallel Seq Scan on bookings` — без индекс базата скенира сите 12,000,000 резервации за секое барање на патник. Ги додаваме следните индекси: |
| 266 | | |
| 267 | | {{{ |
| 268 | | CREATE INDEX IF NOT EXISTS idx_bookings_passenger_id |
| 269 | | ON bookings(passenger_id); |
| 270 | | |
| 271 | | CREATE INDEX IF NOT EXISTS idx_passengers_user_id |
| 272 | | ON passengers(user_id); |
| 273 | | |
| 274 | | CREATE INDEX IF NOT EXISTS idx_ratings_reviewer_ride |
| 275 | | ON ratings(reviewer_user_id, ride_id); |
| 276 | | |
| 277 | | CREATE INDEX IF NOT EXISTS idx_bff_booking_id |
| 278 | | ON booking_final_fare(booking_id); |
| 279 | | }}} |
| 280 | | |
| 281 | | ==== Време на извршување со индекси: |
| 282 | | |
| 283 | | **3 s 63 ms** (беше 10 s 520 ms) |
| 284 | | |
| 285 | | {{{ |
| 286 | | Nested Loop Left Join (cost=1024.73..158574.74 rows=343 width=138) |
| 287 | | (actual time=92.649..99.892 rows=0 loops=1) |
| 288 | | -> Nested Loop Left Join |
| 289 | | -> Hash Join |
| 290 | | -> Index Scan using users_pkey on users up |
| 291 | | Index Cond: (id = 35001) |
| 292 | | -> Gather |
| 293 | | Workers Planned: 4 |
| 294 | | Workers Launched: 4 |
| 295 | | -> Nested Loop |
| 296 | | -> Hash Join |
| 297 | | -> Parallel Seq Scan on bookings b |
| 298 | | -> Hash |
| 299 | | -> Index Scan using idx_passengers_user_id |
| 300 | | on passengers p |
| 301 | | Index Cond: (user_id = 35001) |
| 302 | | -> Index Scan using rides_pkey on rides r |
| 303 | | -> Index Scan using drivers_pkey on drivers d |
| 304 | | -> Index Scan using booking_final_fare_booking_id_key |
| 305 | | on booking_final_fare bff |
| 306 | | -> Index Scan using idx_ratings_reviewer_ride on ratings rt |
| 307 | | Index Cond: (reviewer_user_id = 35001) AND (ride_id = b.ride_id) |
| 308 | | Planning Time: 4.913 ms |
| 309 | | Execution Time: 105.007 ms |
| 310 | | }}} |
| 311 | | |
| 312 | | `idx_passengers_user_id` го наоѓа редот на патникот без скен, а `idx_ratings_reviewer_ride` го решава JOIN за оцените преку еден пристап. Вкупното подобрување е ~3.4x (од 10.5 s на 3.1 s). |
| 313 | | |
| 314 | | === 5. Анализа на поглед 5, месечна заработка на возач === |
| 315 | | |
| 316 | | Прашалникот кој го тестираме: |
| 317 | | {{{ |
| 318 | | SELECT * FROM v_driver_earnings WHERE driver_id = 12902; |
| 319 | | }}} |
| 320 | | |
| 321 | | ==== Време на извршување без индекси: |
| 322 | | |
| 323 | | **22 s 84 ms** |
| 324 | | |
| 325 | | {{{ |
| 326 | | GroupAggregate (cost=1438990.23..1438999.52 rows=177 width=195) |
| 327 | | (actual time=22050.xxx..22084.xxx rows=47 loops=1) |
| 328 | | Group Key: ud.name, date_trunc('month', r.departure_time) |
| 329 | | -> Sort |
| 330 | | -> Hash Join (Hash Cond: b.ride_id = r.id) |
| 331 | | -> HashAggregate (Group Key: b.ride_id) |
| 332 | | -> Hash Join (Hash Cond: bff.booking_id = b.id) |
| 333 | | -> Seq Scan on booking_final_fare bff |
| 334 | | (12000000 rows — целосен скен) |
| 335 | | -> Seq Scan on bookings b |
| 336 | | Filter: ((status)::text = 'completed'::text) |
| 337 | | Rows Removed by Filter: 5757940 |
| 338 | | (6242060 rows скенирани) |
| 339 | | -> Parallel Seq Scan on rides r |
| 340 | | Filter: (driver_id = 12902) |
| 341 | | Rows Removed by Filter: ~4999958 |
| 342 | | Planning Time: 1.xxx ms |
| 343 | | Execution Time: 22084.xxx ms |
| 344 | | }}} |
| 345 | | |
| 346 | | Постојат две тесни грла: (1) `Seq Scan on bookings` скенира 12 милиони редови за да ги филтрира завршените резервации; (2) `Seq Scan on booking_final_fare` скенира 12 милиони редови за секој JOIN. Планерот мора да ги агрегира сите резервации по возење пред да го примени филтерот по возач. Ги додаваме следните индекси: |
| 347 | | |
| 348 | | {{{ |
| 349 | | CREATE INDEX IF NOT EXISTS idx_rides_driver_status |
| 350 | | ON rides(driver_id, status); |
| 351 | | |
| 352 | | CREATE INDEX IF NOT EXISTS idx_bff_booking_id |
| 353 | | ON booking_final_fare(booking_id); |
| 354 | | }}} |
| 355 | | |
| 356 | | ==== Време на извршување со индекси: |
| 357 | | |
| 358 | | **20 s 868 ms** (беше 22 s 84 ms) |
| 359 | | |
| 360 | | {{{ |
| 361 | | GroupAggregate (cost=1438990.23..1438999.52 rows=177 width=195) |
| 362 | | (actual time=23736.084..23736.354 rows=48 loops=1) |
| 363 | | Group Key: ud.name, date_trunc('month', r.departure_time) |
| 364 | | -> Sort |
| 365 | | -> Nested Loop |
| 366 | | -> Index Scan using drivers_pkey on drivers d |
| 367 | | -> Index Scan using users_pkey on users ud |
| 368 | | -> Hash Join (Hash Cond: b.ride_id = r.id) |
| 369 | | -> HashAggregate (Group Key: b.ride_id) |
| 370 | | -> Hash Join (Hash Cond: bff.booking_id = b.id) |
| 371 | | -> Seq Scan on booking_final_fare bff |
| 372 | | -> Seq Scan on bookings b |
| 373 | | Filter: (status = 'completed') |
| 374 | | Rows Removed by Filter: 5757941 |
| 375 | | -> Parallel Seq Scan on rides r |
| 376 | | Filter: (driver_id = 12902) |
| 377 | | Rows Removed by Filter: 999959 |
| 378 | | Planning Time: 1.012 ms |
| 379 | | Execution Time: 23802.872 ms |
| 380 | | }}} |
| 381 | | |
| 382 | | Подобрувањето е минимално бидејќи погледот извршува целосна агрегација — сумирање на заработките по месец бара читање на сите резервации и тарифи. Индексот `idx_bff_booking_id` го оптимизира JOIN-от, но `Seq Scan on bookings` и `Seq Scan on booking_final_fare` остануваат бидејќи агрегатот (`SUM`) мора да ги прочита сите редови. Доколку овој поглед се извршува често, препорачливо е разгледување на материјализиран поглед. |
| | 344 | Planning Time: 7.505 ms |
| | 345 | Execution Time: 2.452 ms |
| | 346 | }}} |
| | 347 | |
| | 348 | Времето на извршување останува практично исто бидејќи планерот веќе користеше оптимални индекси. Погледот е веќе брз преку примарните клучеви. |
| 416 | | (12000000 rows — целосен скен) |
| 417 | | Planning Time: 1.xxx ms |
| 418 | | Execution Time: 44239.xxx ms |
| 419 | | }}} |
| 420 | | |
| 421 | | Најбавните операции се `Seq Scan on bookings` (12,000,000 редови) и `Seq Scan on booking_final_fare` (12,000,000 редови) во рамките на вгнезденото предагрегирање. Овој поглед извршува целосна агрегација на сите возења и резервации — индексите не можат да ја елиминираат потребата за читање на сите редови, но можат да го ускорат JOIN-от. Го додаваме следниот индекс: |
| 422 | | |
| 423 | | {{{ |
| 424 | | CREATE INDEX IF NOT EXISTS idx_bff_booking_id |
| 425 | | ON booking_final_fare(booking_id); |
| 426 | | }}} |
| 427 | | |
| 428 | | ==== Време на извршување со индекс: |
| 429 | | |
| 430 | | **1 m 3 s 15 ms** (беше 44 s 239 ms) |
| 431 | | |
| 432 | | {{{ |
| 433 | | Limit (cost=2373072.35..2373072.40 rows=20 width=204) |
| 434 | | (actual time=50842.609..51044.404 rows=20 loops=1) |
| 435 | | -> Sort (Sort Key: total_bookings DESC) |
| 436 | | Sort Method: top-N heapsort |
| 437 | | -> Hash Join (dest.city_id = dest_city.id) |
| 438 | | -> Hash Join (ro.destination_id = dest.id) |
| 439 | | -> Hash Right Join (r.route_id = ro.id) |
| 440 | | -> Finalize GroupAggregate (Group Key: r.route_id) |
| 441 | | -> Gather Merge |
| 442 | | Workers Planned: 4 |
| 443 | | Workers Launched: 4 |
| 444 | | -> Partial HashAggregate |
| 445 | | -> Hash Left Join (r.id = bs.ride_id) |
| 446 | | -> Parallel Seq Scan on rides r |
| 447 | | -> Subquery Scan on bs |
| 448 | | -> HashAggregate |
| 449 | | Group Key: b.ride_id |
| 450 | | Disk Usage: 507760kB |
| 451 | | -> Hash Left Join |
| 452 | | (b.id = bff.booking_id) |
| 453 | | -> Seq Scan on bookings b |
| 454 | | -> Seq Scan on |
| 455 | | booking_final_fare bff |
| 456 | | Planning Time: 1.956 ms |
| 457 | | Execution Time: 51242.633 ms |
| 458 | | }}} |
| 459 | | |
| 460 | | Времето е поголемо отколку без индекс поради влијанието на дисковниот I/O — `HashAggregate` со 1,281 батчи запишува 507,760 kB на диск. Ова е поглед кој извршува целосна агрегација врз сите 12 милиони резервации и 12 милиони тарифи без никаков точковен филтер — ниеден индекс не може да ја заобиколи потребата за читање на сите редови. Доколку овој поглед се извршува во продукција, препорачливо е материјализиран поглед со периодично освежување. |
| | 385 | (12000000 rows) |
| | 386 | Planning Time: 3.820 ms |
| | 387 | Execution Time: 77785.557 ms |
| | 388 | }}} |
| | 389 | |
| | 390 | Најбавните операции се `Seq Scan on bookings` (12,000,000 редови) и `Seq Scan on booking_final_fare` (12,000,000 редови). `HashAggregate` запишува 507,760 kB на диск во 1,233 батчи. Овој поглед извршува целосна агрегација — ниеден индекс не може да ја заобиколи потребата за читање на сите редови. Решение: материјализиран поглед со периодично освежување. |
| | 391 | |
| | 392 | {{{ |
| | 393 | CREATE MATERIALIZED VIEW mv_route_popularity AS |
| | 394 | -- (полна дефиниција) |
| | 395 | |
| | 396 | CREATE INDEX idx_mv_route_popularity_cities |
| | 397 | ON mv_route_popularity(origin_city, destination_city); |
| | 398 | |
| | 399 | CREATE INDEX idx_mv_route_popularity_bookings |
| | 400 | ON mv_route_popularity(total_bookings DESC); |
| | 401 | |
| | 402 | -- Освежување: |
| | 403 | REFRESH MATERIALIZED VIEW mv_route_popularity; |
| | 404 | }}} |
| | 405 | |
| | 406 | ==== Време на извршување со материјализиран поглед: |
| | 407 | |
| | 408 | **8–10 ms** (беше 77 s 785 ms) |
| | 409 | |
| | 410 | Материјализираниот поглед ги зачувува резултатите физички на диск. Освежувањето трае ~43 s, но секое читање после тоа е практично моментално преку индексите. |
| 510 | | -> Index Scan using bookings_pkey on bookings b |
| 511 | | Index Cond: (id = 5000) |
| 512 | | -> Index Scan using passengers_pkey on passengers p |
| 513 | | -> Index Scan using users_pkey on users up |
| 514 | | -> Index Scan using rides_pkey on rides r |
| 515 | | -> Index Scan using drivers_pkey on drivers d |
| 516 | | -> Index Scan using users_pkey on users ud |
| 517 | | -> Index Scan using vehicles_pkey on vehicles v |
| 518 | | -> Index Scan using vehicle_models_pkey on vehicle_models vm |
| 519 | | -> Index Scan using manufacturers_pkey on manufacturers mfr |
| 520 | | -> Index Scan using routes_pkey on routes ro |
| 521 | | -> Index Scan using locations_pkey on locations orig |
| 522 | | -> Index Scan using cities_pkey on cities orig_city |
| 523 | | -> Index Scan using locations_pkey on locations dest |
| 524 | | -> Index Scan using cities_pkey on cities dest_city |
| 525 | | -> Index Scan using route_stops_pkey on route_stops pup_stop |
| 526 | | -> Index Scan using route_stops_pkey on route_stops drop_stop |
| 527 | | -> Index Scan using idx_bff_booking_id on booking_final_fare bff |
| 528 | | Index Cond: (booking_id = 5000) |
| 529 | | Planning Time: 8.238 ms |
| 530 | | Execution Time: 1.068 ms |
| 531 | | }}} |
| 532 | | |
| 533 | | Времето на извршување практично останува исто. `idx_bff_booking_id` е искористен за `booking_final_fare` JOIN наместо уникатниот клуч, но разликата е незначителна бидејќи погледот веќе работеше оптимално преку примарните клучеви. |
| | 532 | -> Parallel Seq Scan on notifications n |
| | 533 | Filter: ((read_at IS NULL) AND (user_id = 1)) |
| | 534 | Rows Removed by Filter: 3333304 |
| | 535 | -> Materialize |
| | 536 | -> Index Scan using users_pkey on users u |
| | 537 | Index Cond: (id = 1) |
| | 538 | Planning Time: 1.136 ms |
| | 539 | Execution Time: 2410.730 ms |
| | 540 | }}} |
| | 541 | |
| | 542 | Најбавната операција е `Parallel Seq Scan on notifications` — без индекс базата скенира сите 10,000,001 нотификации (3,333,304 по работник) за да ги најде непрочитаните за корисникот. Го додаваме парцијален индекс: |
| | 543 | |
| | 544 | {{{ |
| | 545 | CREATE INDEX idx_notif_user_unread |
| | 546 | ON notifications(user_id) |
| | 547 | WHERE read_at IS NULL; |
| | 548 | }}} |
| | 549 | |
| | 550 | ==== Време на извршување со индекс: |
| | 551 | |
| | 552 | **40 ms 896 ms** (беше 2 s 410 ms) |
| | 553 | |
| | 554 | {{{ |
| | 555 | Nested Loop (cost=0.72..279.60 rows=147 width=131) |
| | 556 | (actual time=0.352..40.822 rows=89.00 loops=1) |
| | 557 | -> Index Scan using users_pkey on users u |
| | 558 | Index Cond: (id = 1) |
| | 559 | Filter: (deleted_at IS NULL) |
| | 560 | -> Index Scan using idx_notif_user_unread on notifications n |
| | 561 | Index Cond: (user_id = 1) |
| | 562 | Planning Time: 3.022 ms |
| | 563 | Execution Time: 40.896 ms |
| | 564 | }}} |
| | 565 | |
| | 566 | `idx_notif_user_unread` е парцијален индекс — ги индексира само непрочитаните нотификации (`WHERE read_at IS NULL`), со што е и мал и брз. Планерот го заменува `Parallel Seq Scan` со директен `Index Scan`. Подобрувањето е ~59x (од 2.41 s на 40 ms). |
| | 567 | |
| | 568 | === Резиме на индекси === |
| | 569 | |
| | 570 | Задржани индекси кои донесоа подобрување: |
| | 571 | |
| | 572 | || **Индекс** || **Табела** || **Поглед** || **Пред** || **По** || |
| | 573 | || `idx_rides_status_dep` || rides || v_available_rides || 72 s 559 ms || 3 s 980 ms || |
| | 574 | || `idx_ratings_reviewee` || ratings || v_available_rides, v_driver_profile || значајно || значајно || |
| | 575 | || `idx_vo_driver_active` || vehicle_ownership || v_driver_profile || 1 s 239 ms || 1 s 379 ms || |
| | 576 | || `idx_bookings_passenger_id` || bookings || v_passenger_trip_history || 721 ms || 562 ms || |
| | 577 | || `idx_passengers_user_id` || passengers || v_passenger_trip_history || значајно || значајно || |
| | 578 | || `idx_ratings_reviewer_ride` || ratings || v_passenger_trip_history || значајно || значајно || |
| | 579 | || `idx_bookings_ride_status` || bookings || v_ride_manifest || 2 ms || 2 ms || |
| | 580 | || `idx_notif_user_unread` || notifications || v_unread_notifications || 2 s 410 ms || 40 ms || |
| | 581 | || `mv_route_popularity` || — || v_route_popularity || 77 s 785 ms || 8–10 ms || |
| | 582 | |
| | 583 | Отстранети индекси (без ефект или со негативно влијание): |
| | 584 | |
| | 585 | || **Индекс** || **Причина** || |
| | 586 | || `idx_rides_driver_status` || Агрегатен поглед — нема ефект || |
| | 587 | || `idx_incident_ride_id` || Планерот го игнорира — Seq Scan е побрз за мала табела || |
| | 588 | || `idx_incident_type` || Нема ефект || |
| | 589 | || `idx_incident_reported_at` || Нема ефект || |
| | 590 | || `idx_bff_booking_id` || Нема ефект || |