wiki:QueryOptimization

Version 2 (modified by 231138, 6 days ago) ( diff )

--

Напредни бази на податоци

Фаза 4 — Индекси и оптимизација на прашалници

Проект: DriveNet

Александар Милошевски 231138 Исидора Кузмановска 231052 Андон Михајлов 231016

Во оваа фаза ги анализираме погледите дефинирани во Фаза 2 преку прашалници базирани на реални сценарија кои ќе бидат присутни во нашата апликација и истите ги оптимизираме со помош на индекси.

1. Анализа на поглед 1, достапни возења по град на поаѓање и пристигнување

Прашалникот кој го тестираме:

SELECT * FROM v_available_rides
WHERE origin_city = 'Skopje' AND destination_city = 'Bitola';

Време на извршување без индекси:

72 s 559 ms

Nested Loop Left Join  (cost=38175.80..423608.72 rows=9 width=643)
                       (actual time=178.534..72559.294 rows=547.00 loops=1)
  ->  Gather  (cost=1023.28..89235.80 rows=9 width=767)
              (actual time=1.669..2.593 rows=547.00 loops=3)
        Workers Planned: 2
        Workers Launched: 2
        ->  Nested Loop
              ->  Parallel Seq Scan on rides r
                    Filter: ((status)::text = 'scheduled'::text)
                    Rows Removed by Filter: 1583512
              ->  Hash Join
                    Hash Cond: (r.route_id = ro.id)
                    ->  Index Scan using cities_name_country_key on cities orig_city
                          Index Cond: ((name)::text = 'Skopje'::text)
                    ->  Index Scan using cities_pkey on cities dest_city
                          Filter: ((name)::text = 'Bitola'::text)
                          Rows Removed by Filter: 1
  ->  Aggregate
        ->  Seq Scan on ratings
              Filter: (reviewee_user_id = ud.id)
              Rows Removed by Filter: 1871226
Planning Time: 2.672 ms
Execution Time: 72559.907 ms

Постојат две тесни грла: (1) Parallel Seq Scan on rides скенира сите 5 милиони редови за да ги филтрира само оние со status = 'scheduled'; (2) Seq Scan on ratings се извршува 547 пати (по еднаш за секој возач во резултатот) и секојпат скенира сите 1,871,226 редови за да ги филтрира оцените по reviewee_user_id. Ова е резултат на LEFT JOIN LATERAL — агрегацијата се врши по возач, но без индекс секој пат се скенира целата табела. Ги додаваме следните индекси:

CREATE INDEX idx_rides_status_dep
    ON rides(status, departure_time);

CREATE INDEX idx_ratings_reviewee
    ON ratings(reviewee_user_id);

Време на извршување со индекси:

3 s 980 ms (беше 72 s 559 ms)

Nested Loop Left Join  (cost=4373.93..71603.67 rows=9 width=643)
                       (actual time=628.137..3979.211 rows=547.00 loops=1)
  ->  Gather  (cost=3902.09..67356.78 rows=9 width=767)
              (actual time=122.653..127.176 rows=547.00 loops=1)
        Workers Planned: 2
        Workers Launched: 2
        ->  Parallel Bitmap Heap Scan on rides r
              Recheck Cond: ((status)::text = 'scheduled'::text)
              ->  Bitmap Index Scan on idx_rides_status_dep
                    Index Cond: ((status)::text = 'scheduled'::text)
  ->  Aggregate
        ->  Bitmap Heap Scan on ratings
              Recheck Cond: (reviewee_user_id = ud.id)
              ->  Bitmap Index Scan on idx_ratings_reviewee
                    Index Cond: (reviewee_user_id = ud.id)
Planning Time: 19.415 ms
Execution Time: 3980.397 ms

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).

2. Анализа на поглед 2, профил на возач

Прашалникот кој го тестираме:

SELECT * FROM v_driver_profile WHERE driver_id = 1;

Време на извршување без индекси:

1 s 239 ms

Nested Loop Left Join  (cost=33434.20..122712.76 rows=1 width=207)
                       (actual time=1232.657..1239.251 rows=1.00 loops=1)
  ->  Hash Right Join
        Hash Cond: (ratings.reviewee_user_id = ud.id)
        ->  Finalize HashAggregate
              Group Key: ratings.reviewee_user_id
              ->  Gather
                    Workers Planned: 2
                    ->  Partial HashAggregate
                          ->  Parallel Seq Scan on ratings
                                (623783 rows — агрегација за сите возачи)
        ->  Nested Loop
              ->  Index Scan using drivers_pkey on drivers d
                    Index Cond: (id = 1)
  ->  Finalize GroupAggregate
        ->  Gather
              ->  Parallel Seq Scan on rides
                    Filter: (driver_id = 1)
                    Rows Removed by Filter: 1666556
  ->  Seq Scan on vehicle_ownership vo
        Filter: (is_active AND (driver_id = 1))
        Rows Removed by Filter: 17998
Planning Time: 3.165 ms
Execution Time: 1239.537 ms

Постојат три тесни грла: (1) Parallel Seq Scan on ratings скенира 623,783 редови за агрегација по reviewee_user_id за сите возачи; (2) Parallel Seq Scan on rides скенира 5,000,000 редови со Filter: driver_id = 1; (3) Seq Scan on vehicle_ownership скенира 18,000 редови за да го најде активното возило. Ги додаваме следните индекси:

CREATE INDEX idx_drivers_user_id
    ON drivers(user_id);

CREATE INDEX idx_ratings_reviewee
    ON ratings(reviewee_user_id);

CREATE INDEX idx_vo_driver_active
    ON vehicle_ownership(driver_id, is_active, vehicle_id);

Време на извршување со индекси:

1 s 379 ms (беше 1 s 239 ms)

Nested Loop Left Join  (cost=33438.98..121697.48 rows=1 width=207)
                       (actual time=1366.020..1378.263 rows=1.00 loops=1)
  ->  Hash Right Join
        Hash Cond: (ratings.reviewee_user_id = ud.id)
        ->  Finalize HashAggregate
              Group Key: ratings.reviewee_user_id
              ->  Gather
                    Workers Planned: 2
                    ->  Partial HashAggregate
                          ->  Parallel Seq Scan on ratings
                                (623783 rows loops=3)
        ->  Index Only Scan using idx_vo_driver_active
              on vehicle_ownership vo
              Index Cond: (driver_id = 1) AND (is_active = true)
              Heap Fetches: 0
  ->  Finalize GroupAggregate
        ->  Gather
              Workers Planned: 2
              ->  Parallel Seq Scan on rides
                    Filter: (driver_id = 1)
                    Rows Removed by Filter: 1666556
Planning Time: 15.218 ms
Execution Time: 1379.943 ms

idx_vo_driver_active е искористен преку Index Only Scan за брзо наоѓање на активното возило без пристап до хип. Агрегацијата на ratings сепак бара целосен скен бидејќи погледот ги агрегира оцените за сите возачи — ова е агрегација врз цело множество каде индексот не може да ја елиминира потребата за читање на сите редови. Parallel Seq Scan on rides исто така останува бидејќи нема индекс по driver_id на rides (idx_rides_driver_status беше отстранет). Подобрувањето е минимално поради доминантната агрегација на ratings.

3. Анализа на поглед 3, заработка на возач

Прашалникот кој го тестираме:

SELECT * FROM v_driver_earnings WHERE driver_id = 1;

Време на извршување без индекси:

33 s 186 ms

GroupAggregate  (cost=1447123.50..1447132.37 rows=169 width=195)
                (actual time=33086.086..33086.368 rows=47.00 loops=1)
  Group Key: ud.name, date_trunc('month', r.departure_time)
  ->  Sort
        Sort Key: ud.name, date_trunc('month', r.departure_time)
        Sort Method: quicksort  Memory: 43kB
        ->  Nested Loop
              ->  Index Scan using drivers_pkey on drivers d
                    Index Cond: (id = 1)
              ->  Hash Join  (Hash Cond: b.ride_id = r.id)
                    ->  HashAggregate  (Group Key: b.ride_id)
                          Planned Partitions: 256  Batches: 257
                          Disk Usage: 245528kB
                          ->  Hash Join  (Hash Cond: bff.booking_id = b.id)
                                ->  Seq Scan on booking_final_fare bff
                                      (12000000 rows — целосен скен)
                                ->  Seq Scan on bookings b
                                      Filter: (status = 'completed')
                                      Rows Removed by Filter: 5757941
                    ->  Parallel Seq Scan on rides r
                          Filter: (driver_id = 1) AND (status = 'completed')
                          Rows Removed by Filter: 1666595
Planning Time: 41.731 ms
Execution Time: 33186.554 ms

Постојат две тесни грла: (1) Seq Scan on bookings скенира 12 милиони редови за да ги филтрира завршените резервации; (2) Seq Scan on booking_final_fare скенира 12 милиони редови за секој JOIN. Планерот мора да ги агрегира сите резервации по возење пред да го примени филтерот по возач — ова е фундаментално ограничување на агрегатниот поглед. Бидејќи погледот врши целосна агрегација, ниеден индекс не може да ја заобиколи потребата за читање на сите редови. Не се додаваат индекси.

Нема индекси — агрегатен поглед:

Времето на извршување останува ~33 s — индексите не влијаат на перформансите на агрегатни погледи кои мора да ги прочитаат сите редови.

4. Анализа на поглед 4, историја на патувања на патник

Прашалникот кој го тестираме:

SELECT * FROM v_passenger_trip_history WHERE passenger_user_id = 1;

Време на извршување без индекси:

721 ms

Nested Loop Left Join  (cost=1022.14..184161.50 rows=343 width=558)
                       (actual time=2.979..721.148 rows=344.00 loops=1)
  ->  Gather  (cost=1009.62..183546.43 rows=343 width=110)
              (actual time=2.205..511.797 rows=344.00 loops=1)
        Workers Planned: 2
        Workers Launched: 2
        ->  Nested Loop
              ->  Hash Join
                    Hash Cond: (b.passenger_id = p.id)
                    ->  Parallel Seq Scan on bookings b
                          (12000001 rows — целосен скен)
                    ->  Index Scan using passengers_user_id_key on passengers p
                          Index Cond: (user_id = 1)
              ->  Index Scan using rides_pkey on rides r
                    Index Cond: (id = b.ride_id)
  ->  Index Scan using uq_ratings_per_ride on ratings rt
        Index Cond: (reviewer_user_id = 1) AND (ride_id = b.ride_id)
Planning Time: 6.787 ms
Execution Time: 721.475 ms

Најбавната операција е Parallel Seq Scan on bookings — без индекс базата скенира сите 12,000,000 резервации за секое барање на патник. Иако passengers_user_id_key веќе постои (уникатен индекс), нема индекс по bookings.passenger_id. Ги додаваме следните индекси:

CREATE INDEX idx_bookings_passenger_id
    ON bookings(passenger_id);

CREATE INDEX idx_passengers_user_id
    ON passengers(user_id);

CREATE INDEX idx_ratings_reviewer_ride
    ON ratings(reviewer_user_id, ride_id);

Време на извршување со индекси:

562 ms (беше 721 ms)

Nested Loop Left Join  (cost=22.15..2185.57 rows=343 width=558)
                       (actual time=2.315..561.965 rows=344.00 loops=1)
  ->  Nested Loop
        ->  Nested Loop
              ->  Index Scan using idx_passengers_user_id on passengers p
                    Index Cond: (user_id = 1)
              ->  Bitmap Heap Scan on bookings b
                    Recheck Cond: (passenger_id = p.id)
                    ->  Bitmap Index Scan on idx_bookings_passenger_id
                          Index Cond: (passenger_id = p.id)
              ->  Index Scan using rides_pkey on rides r
                    Index Cond: (id = b.ride_id)
  ->  Index Scan using idx_ratings_reviewer_ride on ratings rt
        Index Cond: (reviewer_user_id = 1) AND (ride_id = b.ride_id)
Planning Time: 44.836 ms
Execution Time: 562.574 ms

idx_bookings_passenger_id го елиминира целосниот скен на 12 милиони резервации — планерот директно ги наоѓа резервациите на патникот преку Bitmap Index Scan. idx_ratings_reviewer_ride го решава JOIN за оцените преку еден пристап по индекс наместо скен.

5. Анализа на поглед 5, манифест на возење

Прашалникот кој го тестираме:

SELECT * FROM v_ride_manifest WHERE ride_id = 1;

Време на извршување без индекси:

2 ms 320 ms

Nested Loop Left Join  (cost=3.45..98.67 rows=2 width=547)
                       (actual time=0.719..2.233 rows=3.00 loops=1)
  ->  Nested Loop
        ->  Nested Loop
              ->  Index Scan using rides_pkey on rides r
                    Index Cond: (id = 1)
              ->  Index Scan using uq_bookings_passenger_ride on bookings b
                    Index Cond: (ride_id = 1)
                    Filter: (status = ANY ('{confirmed,picked_up,completed}'))
                    Rows Removed by Filter: 0
              ->  Index Scan using passengers_pkey on passengers p
                    Index Cond: (id = b.passenger_id)
              ->  Index Scan using users_pkey on users up
                    Index Cond: (id = p.user_id)
              ->  Index Scan using route_stops_pkey on route_stops pup_stop
              ->  Index Scan using route_stops_pkey on route_stops pdrop_stop
  ->  Index Scan using booking_final_fare_booking_id_key
        on booking_final_fare bff
        Index Cond: (booking_id = b.id)
Planning Time: 5.151 ms
Execution Time: 2.320 ms

Планот на извршување покажува дека планерот веќе користи Index Scan по bookings преку постоечкиот уникатен индекс uq_bookings_passenger_ride. Сите JOIN операции се решаваат преку примарни клучеви. Сепак, го додаваме следниот индекс за да го покриеме IN филтерот по status без пристап до табелата:

CREATE INDEX idx_bookings_ride_status
    ON bookings(ride_id, status);

Време на извршување со индекс:

2 ms 452 ms (без значајна промена)

Nested Loop Left Join  (cost=3.45..98.67 rows=2 width=547)
                       (actual time=0.343..2.142 rows=3.00 loops=1)
  ->  Nested Loop
        ->  Index Scan using rides_pkey on rides r
              Index Cond: (id = 1)
        ->  Index Scan using uq_bookings_passenger_ride on bookings b
              Index Cond: (ride_id = 1)
              Filter: (status = ANY ('{confirmed,picked_up,completed}'))
        ->  Index Scan using passengers_pkey on passengers p
        ->  Index Scan using users_pkey on users up
        ->  Index Scan using route_stops_pkey on route_stops pup_stop
        ->  Index Scan using route_stops_pkey on route_stops pdrop_stop
  ->  Index Scan using booking_final_fare_booking_id_key
        on booking_final_fare bff
Planning Time: 7.505 ms
Execution Time: 2.452 ms

Времето на извршување останува практично исто бидејќи планерот веќе користеше оптимални индекси. Погледот е веќе брз преку примарните клучеви.

6. Анализа на поглед 6, популарност на рути

Прашалникот кој го тестираме:

SELECT * FROM v_route_popularity ORDER BY total_bookings DESC LIMIT 10;

Време на извршување без индекси:

77 s 785 ms

Limit  (cost=2377094.80..2377094.83 rows=10 width=624)
       (actual time=77411.184..77595.364 rows=10.00 loops=1)
  ->  Sort  (Sort Key: total_bookings DESC)
        Sort Method: top-N heapsort
        ->  Hash Join
              ->  Finalize GroupAggregate  (Group Key: r.route_id)
                    ->  Gather Merge
                          Workers Planned: 2
                          Workers Launched: 2
                          ->  Partial HashAggregate
                                ->  Hash Left Join  (r.id = bs.ride_id)
                                      ->  Parallel Seq Scan on rides r
                                      ->  Subquery Scan on bs
                                            ->  HashAggregate
                                                  Group Key: b.ride_id
                                                  Batches: 1233
                                                  Disk Usage: 507760kB
                                                  ->  Hash Left Join
                                                        (b.id = bff.booking_id)
                                                        ->  Seq Scan on bookings b
                                                              (12000001 rows)
                                                        ->  Seq Scan on
                                                            booking_final_fare bff
                                                              (12000000 rows)
Planning Time: 3.820 ms
Execution Time: 77785.557 ms

Најбавните операции се Seq Scan on bookings (12,000,000 редови) и Seq Scan on booking_final_fare (12,000,000 редови). HashAggregate запишува 507,760 kB на диск во 1,233 батчи. Овој поглед извршува целосна агрегација — ниеден индекс не може да ја заобиколи потребата за читање на сите редови. Решение: материјализиран поглед со периодично освежување.

CREATE MATERIALIZED VIEW mv_route_popularity AS
    -- (полна дефиниција)

CREATE INDEX idx_mv_route_popularity_cities
    ON mv_route_popularity(origin_city, destination_city);

CREATE INDEX idx_mv_route_popularity_bookings
    ON mv_route_popularity(total_bookings DESC);

-- Освежување:
REFRESH MATERIALIZED VIEW mv_route_popularity;

Време на извршување со материјализиран поглед:

8–10 ms (беше 77 s 785 ms)

Материјализираниот поглед ги зачувува резултатите физички на диск. Освежувањето трае ~43 s, но секое читање после тоа е практично моментално преку индексите.

7. Анализа на поглед 7, детали за резервација

Прашалникот кој го тестираме:

SELECT * FROM v_booking_details WHERE booking_id = 1;

Време на извршување без индекси:

2 ms 116 ms

Nested Loop Left Join  (cost=4.60..54.37 rows=1 width=669)
                       (actual time=1.653..1.667 rows=1.00 loops=1)
  ->  Nested Loop
        ->  Index Scan using bookings_pkey on bookings b
              Index Cond: (id = 1)
        ->  Index Scan using passengers_pkey on passengers p
        ->  Index Scan using users_pkey on users up
        ->  Index Scan using rides_pkey on rides r
        ->  Index Scan using drivers_pkey on drivers d
        ->  Index Scan using users_pkey on users ud
        ->  Index Scan using vehicles_pkey on vehicles v
        ->  Index Scan using vehicle_models_pkey on vehicle_models vm
        ->  Index Scan using manufacturers_pkey on manufacturers mfr
        ->  Index Scan using routes_pkey on routes ro
        ->  Index Scan using locations_pkey on locations orig
        ->  Index Scan using cities_pkey on cities orig_city
        ->  Index Scan using locations_pkey on locations dest
        ->  Index Scan using cities_pkey on cities dest_city
        ->  Index Scan using route_stops_pkey on route_stops pup_stop
        ->  Index Scan using route_stops_pkey on route_stops drop_stop
  ->  Index Scan using booking_final_fare_booking_id_key
        on booking_final_fare bff
        Index Cond: (booking_id = 1)
Planning Time: 37.417 ms
Execution Time: 2.116 ms

Планот на извршување покажува дека планерот веќе користи Index Scan по примарни клучеви за сите JOIN операции. Нема потреба од дополнителни индекси. Нема потреба да се преуредува прашалникот.

Нема индекси — веќе оптимален:

Времето на извршување останува ~2 ms — погледот работи оптимално преку постоечките примарни клучеви.

8. Анализа на поглед 8, резиме на инциденти

Прашалникот кој го тестираме:

SELECT * FROM v_incident_summary WHERE ride_id = 216;

Време на извршување без индекси:

17 ms 335 ms

Nested Loop  (cost=1.59..1266.66 rows=1 width=594)
             (actual time=3.666..17.240 rows=1.00 loops=1)
  ->  Nested Loop
        ->  Nested Loop
              ->  Seq Scan on incident_reports ir
                    Filter: (ride_id = 216)
                    Rows Removed by Filter: 49995
              ->  Index Scan using users_pkey on users u_rep
                    Index Cond: (id = ir.reporter_id)
              ->  Index Scan using rides_pkey on rides r
                    Index Cond: (id = 216)
              ->  Index Scan using drivers_pkey on drivers d
              ->  Index Scan using users_pkey on users u_drv
          ->  Seq Scan on routes ro
          ->  Seq Scan on locations orig
          ->  Index Scan using cities_pkey on cities orig_city
          ->  Seq Scan on locations dest
          ->  Index Scan using cities_pkey on cities dest_city
Planning Time: 1.169 ms
Execution Time: 17.335 ms

Seq Scan on incident_reports скенира 49,996 редови со Filter: ride_id = 216. Иако беше тестиран idx_incident_ride_id, планерот не го користеше бидејќи табелата е релативно мала и Seq Scan е побрз за мали табели. Индексот беше отстранет.

Време на извршување со индекс:

46 ms 095 ms (без подобрување — индексот отстранет)

Nested Loop  (cost=1.59..1266.66 rows=1 width=594)
             (actual time=32.141..45.969 rows=1.00 loops=1)
  ->  Seq Scan on incident_reports ir
        Filter: (ride_id = 216)
        Rows Removed by Filter: 49995
  ->  Index Scan using users_pkey on users u_rep
        Index Cond: (id = ir.reporter_id)
  ->  Index Scan using rides_pkey on rides r
        Index Cond: (id = 216)
  ->  Index Scan using drivers_pkey on drivers d
  ->  Index Scan using users_pkey on users u_drv
Planning Time: 4.874 ms
Execution Time: 46.095 ms

Планерот продолжува да користи Seq Scan on incident_reports дури и со индекс — за 49,996 редови Seq Scan е поефикасен. Индексите idx_incident_ride_id, idx_incident_type и idx_incident_reported_at беа отстранети.

9. Анализа на поглед 9, непрочитани нотификации

Прашалникот кој го тестираме:

SELECT * FROM v_unread_notifications WHERE user_id = 1;

Време на извршување без индекси:

2 s 410 ms

Gather  (cost=1000.29..199683.87 rows=147 width=131)
        (actual time=1212.278..2410.665 rows=89.00 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Nested Loop
        ->  Parallel Seq Scan on notifications n
              Filter: ((read_at IS NULL) AND (user_id = 1))
              Rows Removed by Filter: 3333304
        ->  Materialize
              ->  Index Scan using users_pkey on users u
                    Index Cond: (id = 1)
Planning Time: 1.136 ms
Execution Time: 2410.730 ms

Најбавната операција е Parallel Seq Scan on notifications — без индекс базата скенира сите 10,000,001 нотификации (3,333,304 по работник) за да ги најде непрочитаните за корисникот. Го додаваме парцијален индекс:

CREATE INDEX idx_notif_user_unread
    ON notifications(user_id)
    WHERE read_at IS NULL;

Време на извршување со индекс:

40 ms 896 ms (беше 2 s 410 ms)

Nested Loop  (cost=0.72..279.60 rows=147 width=131)
             (actual time=0.352..40.822 rows=89.00 loops=1)
  ->  Index Scan using users_pkey on users u
        Index Cond: (id = 1)
        Filter: (deleted_at IS NULL)
  ->  Index Scan using idx_notif_user_unread on notifications n
        Index Cond: (user_id = 1)
Planning Time: 3.022 ms
Execution Time: 40.896 ms

idx_notif_user_unread е парцијален индекс — ги индексира само непрочитаните нотификации (WHERE read_at IS NULL), со што е и мал и брз. Планерот го заменува Parallel Seq Scan со директен Index Scan. Подобрувањето е ~59x (од 2.41 s на 40 ms).

Резиме на индекси

Задржани индекси кои донесоа подобрување:

Индекс Табела Поглед Пред По
idx_rides_status_dep rides v_available_rides 72 s 559 ms 3 s 980 ms
idx_ratings_reviewee ratings v_available_rides, v_driver_profile значајно значајно
idx_vo_driver_active vehicle_ownership v_driver_profile 1 s 239 ms 1 s 379 ms
idx_bookings_passenger_id bookings v_passenger_trip_history 721 ms 562 ms
idx_passengers_user_id passengers v_passenger_trip_history значајно значајно
idx_ratings_reviewer_ride ratings v_passenger_trip_history значајно значајно
idx_bookings_ride_status bookings v_ride_manifest 2 ms 2 ms
idx_notif_user_unread notifications v_unread_notifications 2 s 410 ms 40 ms
mv_route_popularity v_route_popularity 77 s 785 ms 8–10 ms

Отстранети индекси (без ефект или со негативно влијание):

Индекс Причина
idx_rides_driver_status Агрегатен поглед — нема ефект
idx_incident_ride_id Планерот го игнорира — Seq Scan е побрз за мала табела
idx_incident_type Нема ефект
idx_incident_reported_at Нема ефект
idx_bff_booking_id Нема ефект
Note: See TracWiki for help on using the wiki.