Напредни бази на податоци
Фаза 4 — Индекси и оптимизација на прашалници
Проект: DriveNet
Александар Милошевски 231138 Исидора Кузмановска 231052 Андон Михајлов 231016
Во оваа фаза ги анализираме погледите дефинирани во Фаза 2 преку прашалници базирани на реални сценарија кои ќе бидат присутни во нашата апликација и истите ги оптимизираме со помош на индекси.
1. Анализа на поглед 1, достапни возења по град на поаѓање
Прашалникот кој го тестираме:
SELECT * FROM v_available_rides WHERE origin_city = 'Скопје' LIMIT 50;
Време на извршување без индекси:
2 s 92 ms
Limit (cost=473.63..24708.84 rows=50 width=223)
(actual time=2040.xxx..2092.xxx rows=0 loops=1)
-> Hash Join
Hash Cond: (ud.id = vdp.driver_user_id)
-> Seq Scan on rides r
Filter: ((status)::text = 'scheduled'::text)
Rows Removed by Filter: 4750535
-> Hash
-> Finalize HashAggregate
Group Key: ratings.reviewee_user_id
-> Gather
Workers Planned: 3
-> Partial HashAggregate
-> Parallel Seq Scan on ratings
(603662 rows — full scan за сите 15000 возачи)
Planning Time: 5.xxx ms
Execution Time: 2092.xxx ms
Постојат два тесни грла: (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 кој ги агрегира оцените само за возачот на тековниот ред, а потоа се додаваат следните индекси:
CREATE INDEX IF NOT EXISTS idx_rides_status_dep
ON rides(status, departure_time);
CREATE INDEX IF NOT EXISTS idx_ratings_reviewee
ON ratings(reviewee_user_id);
Време на извршување со индекси:
1 s 821 ms (беше 2 s 92 ms)
Limit (cost=473.63..24708.84 rows=50 width=223)
(actual time=1315.180..1315.194 rows=0 loops=1)
-> Nested Loop Left Join
-> Nested Loop
-> Seq Scan on rides r
Filter: ((status)::text = 'scheduled'::text)
Rows Removed by Filter: 4750535
-> Index Scan using routes_pkey on routes ro
-> Index Scan using locations_pkey on locations orig
-> Materialize
-> Seq Scan on cities orig_city
Filter: ((name)::text = 'Скопје'::text)
Rows Removed by Filter: 20
-> 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: 5.712 ms
Execution Time: 1315.531 ms
Со LEFT JOIN LATERAL и idx_ratings_reviewee агрегацијата на оцените се врши само за возачот на тековниот ред наместо за сите 15,000 возачи. Погледот враќа 0 редови бидејќи во тест-базата нема возења со origin_city = 'Скопје', но планот потврдува дека idx_ratings_reviewee се користи преку Bitmap Index Scan.
2. Анализа на поглед 2, профил на возач
Прашалникот кој го тестираме:
SELECT * FROM v_driver_profile WHERE driver_id = 12902;
Време на извршување без индекси:
1 s 790 ms
Nested Loop Left Join (cost=31329.20..109115.72 rows=1 width=207)
(actual time=1760.xxx..1790.xxx rows=1 loops=1)
-> Hash Right Join
-> Finalize HashAggregate
Group Key: ratings.reviewee_user_id
-> Gather
Workers Planned: 3
-> Partial HashAggregate
-> Parallel Seq Scan on ratings
(603662 rows — агрегација за сите возачи)
-> Hash
-> Seq Scan on vehicle_ownership vo
Filter: ((driver_id = 12902) AND (is_active = true))
-> Finalize GroupAggregate
-> Gather
-> Parallel Seq Scan on rides
Filter: (driver_id = 12902)
Rows Removed by Filter: 4999917
Planning Time: 2.xxx ms
Execution Time: 1790.xxx ms
Постојат две тесни грла: (1) Parallel Seq Scan on ratings скенира 603,662 редови за да ги агрегира оцените по reviewee_user_id за сите возачи; (2) Parallel Seq Scan on rides скенира 5,000,000 редови со Filter: driver_id = 12902. Ги додаваме следните индекси:
CREATE INDEX IF NOT EXISTS idx_rides_driver_status
ON rides(driver_id, status);
CREATE INDEX IF NOT EXISTS idx_ratings_reviewee
ON ratings(reviewee_user_id);
CREATE INDEX IF NOT EXISTS idx_vo_driver_active
ON vehicle_ownership(driver_id, is_active, vehicle_id);
Време на извршување со индекси:
2 s 198 ms (беше 1 s 790 ms)
Nested Loop Left Join (cost=31329.20..109115.72 rows=1 width=207)
(actual time=674.024..685.211 rows=1 loops=1)
-> Nested Loop Left Join
-> Hash Right Join
-> Finalize HashAggregate
Group Key: ratings.reviewee_user_id
-> Gather
Workers Planned: 3
-> Partial HashAggregate
-> Parallel Seq Scan on ratings
(467838 rows loops=4)
-> Index Only Scan using idx_vo_driver_active
on vehicle_ownership vo
Index Cond: (driver_id = 12902) AND (is_active = true)
Heap Fetches: 1
-> Index Scan using drivers_pkey on drivers d
-> Index Scan using users_pkey on users ud
-> Index Scan using vehicles_pkey on vehicles v
-> Finalize GroupAggregate
-> Gather
Workers Planned: 4
-> Parallel Seq Scan on rides
Filter: (driver_id = 12902)
Rows Removed by Filter: 999934
Planning Time: 2.805 ms
Execution Time: 692.124 ms
idx_vo_driver_active е искористен преку Index Only Scan за брзо наоѓање на активното возило. Агрегацијата на ratings сепак бара целосен скен бидејќи погледот ги агрегира оцените за сите возачи — ова е агрегација врз цело множество каде индексот не може да ја елиминира потребата за читање на сите редови. Затоа вкупното подобрување е помало од очекуваното.
3. Анализа на поглед 3, манифест на возење
Прашалникот кој го тестираме:
SELECT * FROM v_ride_manifest WHERE ride_id = 1000;
Време на извршување без индекси:
644 ms
Nested Loop Left Join (cost=3.44..64.11 rows=1 width=127)
(actual time=0.310..0.590 rows=3 loops=1)
-> Nested Loop
-> Index Scan using uq_bookings_passenger_ride on bookings b
Index Cond: (ride_id = 1000)
Filter: (status = ANY ('{confirmed,picked_up,completed}'))
Rows Removed by Filter: 1
-> Index Scan using rides_pkey on rides r
-> Index Scan using routes_pkey on routes ro
-> Index Scan using locations_pkey on locations orig
-> Index Scan using locations_pkey on locations dest
-> 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: 3.xxx ms
Execution Time: 644.xxx ms
Планот на извршување покажува дека планерот веќе користи Index Scan по bookings преку постоечкиот уникатен индекс uq_bookings_passenger_ride. Сите JOIN операции се решаваат преку примарни клучеви. Нема потреба од дополнителни индекси. Сепак, го додаваме следниот индекс за да го покриеме IN филтерот по status во рамките на индексот без пристап до табелата:
CREATE INDEX IF NOT EXISTS idx_bookings_ride_status
ON bookings(ride_id, status);
Време на извршување со индекс:
674 ms (беше 644 ms)
Nested Loop Left Join (cost=3.44..64.11 rows=1 width=127)
(actual time=0.292..0.562 rows=3 loops=1)
-> Nested Loop
-> Index Scan using uq_bookings_passenger_ride on bookings b
Index Cond: (ride_id = 1000)
Filter: (status = ANY ('{confirmed,picked_up,completed}'))
Rows Removed by Filter: 1
-> Index Scan using rides_pkey on rides r
-> Index Scan using routes_pkey on routes ro
-> Index Scan using locations_pkey on locations orig
-> Index Scan using locations_pkey on locations dest
-> 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 idx_bff_booking_id on booking_final_fare bff
Index Cond: (booking_id = b.id)
Planning Time: 3.373 ms
Execution Time: 0.749 ms
Времето на извршување останува практично исто бидејќи планерот веќе користеше индекси за сите операции. Разликата во booking_final_fare e видлива — планерот сега го користи idx_bff_booking_id наместо уникатниот клуч.
4. Анализа на поглед 4, историја на патувања на патник
Прашалникот кој го тестираме:
SELECT * FROM v_passenger_trip_history WHERE passenger_user_id = 500;
Време на извршување без индекси:
10 s 520 ms
Nested Loop Left Join (cost=1024.73..158574.74 rows=343 width=138)
(actual time=10500.xxx..10520.xxx rows=343 loops=1)
-> Nested Loop Left Join
-> Hash Join
-> Index Scan using users_pkey on users up
Index Cond: (id = 500)
-> Gather
Workers Planned: 4
-> Nested Loop
-> Parallel Seq Scan on bookings b
Filter: (passenger_id = p.id)
Rows Removed by Filter: ~11999657
-> Seq Scan on passengers p
Filter: (user_id = 500)
-> Index Scan using rides_pkey on rides r
-> Index Scan using booking_final_fare_booking_id_key
on booking_final_fare bff
-> Index Scan using ratings on ratings rt (seq scan fallback)
Planning Time: 4.xxx ms
Execution Time: 10520.xxx ms
Најбавната операција е Parallel Seq Scan on bookings — без индекс базата скенира сите 12,000,000 резервации за секое барање на патник. Ги додаваме следните индекси:
CREATE INDEX IF NOT EXISTS idx_bookings_passenger_id
ON bookings(passenger_id);
CREATE INDEX IF NOT EXISTS idx_passengers_user_id
ON passengers(user_id);
CREATE INDEX IF NOT EXISTS idx_ratings_reviewer_ride
ON ratings(reviewer_user_id, ride_id);
CREATE INDEX IF NOT EXISTS idx_bff_booking_id
ON booking_final_fare(booking_id);
Време на извршување со индекси:
3 s 63 ms (беше 10 s 520 ms)
Nested Loop Left Join (cost=1024.73..158574.74 rows=343 width=138)
(actual time=92.649..99.892 rows=0 loops=1)
-> Nested Loop Left Join
-> Hash Join
-> Index Scan using users_pkey on users up
Index Cond: (id = 35001)
-> Gather
Workers Planned: 4
Workers Launched: 4
-> Nested Loop
-> Hash Join
-> Parallel Seq Scan on bookings b
-> Hash
-> Index Scan using idx_passengers_user_id
on passengers p
Index Cond: (user_id = 35001)
-> Index Scan using rides_pkey on rides r
-> Index Scan using drivers_pkey on drivers d
-> Index Scan using booking_final_fare_booking_id_key
on booking_final_fare bff
-> Index Scan using idx_ratings_reviewer_ride on ratings rt
Index Cond: (reviewer_user_id = 35001) AND (ride_id = b.ride_id)
Planning Time: 4.913 ms
Execution Time: 105.007 ms
idx_passengers_user_id го наоѓа редот на патникот без скен, а idx_ratings_reviewer_ride го решава JOIN за оцените преку еден пристап. Вкупното подобрување е ~3.4x (од 10.5 s на 3.1 s).
5. Анализа на поглед 5, месечна заработка на возач
Прашалникот кој го тестираме:
SELECT * FROM v_driver_earnings WHERE driver_id = 12902;
Време на извршување без индекси:
22 s 84 ms
GroupAggregate (cost=1438990.23..1438999.52 rows=177 width=195)
(actual time=22050.xxx..22084.xxx rows=47 loops=1)
Group Key: ud.name, date_trunc('month', r.departure_time)
-> Sort
-> Hash Join (Hash Cond: b.ride_id = r.id)
-> HashAggregate (Group Key: b.ride_id)
-> 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)::text = 'completed'::text)
Rows Removed by Filter: 5757940
(6242060 rows скенирани)
-> Parallel Seq Scan on rides r
Filter: (driver_id = 12902)
Rows Removed by Filter: ~4999958
Planning Time: 1.xxx ms
Execution Time: 22084.xxx ms
Постојат две тесни грла: (1) Seq Scan on bookings скенира 12 милиони редови за да ги филтрира завршените резервации; (2) Seq Scan on booking_final_fare скенира 12 милиони редови за секој JOIN. Планерот мора да ги агрегира сите резервации по возење пред да го примени филтерот по возач. Ги додаваме следните индекси:
CREATE INDEX IF NOT EXISTS idx_rides_driver_status
ON rides(driver_id, status);
CREATE INDEX IF NOT EXISTS idx_bff_booking_id
ON booking_final_fare(booking_id);
Време на извршување со индекси:
20 s 868 ms (беше 22 s 84 ms)
GroupAggregate (cost=1438990.23..1438999.52 rows=177 width=195)
(actual time=23736.084..23736.354 rows=48 loops=1)
Group Key: ud.name, date_trunc('month', r.departure_time)
-> Sort
-> Nested Loop
-> Index Scan using drivers_pkey on drivers d
-> Index Scan using users_pkey on users ud
-> Hash Join (Hash Cond: b.ride_id = r.id)
-> HashAggregate (Group Key: b.ride_id)
-> Hash Join (Hash Cond: bff.booking_id = b.id)
-> Seq Scan on booking_final_fare bff
-> Seq Scan on bookings b
Filter: (status = 'completed')
Rows Removed by Filter: 5757941
-> Parallel Seq Scan on rides r
Filter: (driver_id = 12902)
Rows Removed by Filter: 999959
Planning Time: 1.012 ms
Execution Time: 23802.872 ms
Подобрувањето е минимално бидејќи погледот извршува целосна агрегација — сумирање на заработките по месец бара читање на сите резервации и тарифи. Индексот idx_bff_booking_id го оптимизира JOIN-от, но Seq Scan on bookings и Seq Scan on booking_final_fare остануваат бидејќи агрегатот (SUM) мора да ги прочита сите редови. Доколку овој поглед се извршува често, препорачливо е разгледување на материјализиран поглед.
6. Анализа на поглед 6, популарност на рути
Прашалникот кој го тестираме:
SELECT * FROM v_route_popularity ORDER BY total_bookings DESC LIMIT 20;
Време на извршување без индекси:
44 s 239 ms
Limit (cost=2373072.35..2373072.40 rows=20 width=204)
(actual time=44200.xxx..44239.xxx rows=20 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: 4
-> 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)
-> 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: 1.xxx ms
Execution Time: 44239.xxx ms
Најбавните операции се Seq Scan on bookings (12,000,000 редови) и Seq Scan on booking_final_fare (12,000,000 редови) во рамките на вгнезденото предагрегирање. Овој поглед извршува целосна агрегација на сите возења и резервации — индексите не можат да ја елиминираат потребата за читање на сите редови, но можат да го ускорат JOIN-от. Го додаваме следниот индекс:
CREATE INDEX IF NOT EXISTS idx_bff_booking_id
ON booking_final_fare(booking_id);
Време на извршување со индекс:
1 m 3 s 15 ms (беше 44 s 239 ms)
Limit (cost=2373072.35..2373072.40 rows=20 width=204)
(actual time=50842.609..51044.404 rows=20 loops=1)
-> Sort (Sort Key: total_bookings DESC)
Sort Method: top-N heapsort
-> Hash Join (dest.city_id = dest_city.id)
-> Hash Join (ro.destination_id = dest.id)
-> Hash Right Join (r.route_id = ro.id)
-> Finalize GroupAggregate (Group Key: r.route_id)
-> Gather Merge
Workers Planned: 4
Workers Launched: 4
-> 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
Disk Usage: 507760kB
-> Hash Left Join
(b.id = bff.booking_id)
-> Seq Scan on bookings b
-> Seq Scan on
booking_final_fare bff
Planning Time: 1.956 ms
Execution Time: 51242.633 ms
Времето е поголемо отколку без индекс поради влијанието на дисковниот I/O — HashAggregate со 1,281 батчи запишува 507,760 kB на диск. Ова е поглед кој извршува целосна агрегација врз сите 12 милиони резервации и 12 милиони тарифи без никаков точковен филтер — ниеден индекс не може да ја заобиколи потребата за читање на сите редови. Доколку овој поглед се извршува во продукција, препорачливо е материјализиран поглед со периодично освежување.
7. Анализа на поглед 7, детали за резервација
Прашалникот кој го тестираме:
SELECT * FROM v_booking_details WHERE booking_id = 5000;
Време на извршување без индекси:
427 ms
Nested Loop Left Join (cost=4.59..53.91 rows=1 width=249)
(actual time=0.650..0.680 rows=1 loops=1)
-> Nested Loop
-> Index Scan using bookings_pkey on bookings b
Index Cond: (id = 5000)
-> 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
Planning Time: 8.xxx ms
Execution Time: 427.xxx ms
Планот на извршување покажува дека планерот веќе користи Index Scan по примарни клучеви за сите JOIN операции. Нема потреба од дополнителни индекси. Нема потреба да се преуредува прашалникот.
Време на извршување со индекс:
659 ms (беше 427 ms)
Nested Loop Left Join (cost=4.59..53.91 rows=1 width=249)
(actual time=0.687..0.701 rows=1 loops=1)
-> Nested Loop
-> Index Scan using bookings_pkey on bookings b
Index Cond: (id = 5000)
-> 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 idx_bff_booking_id on booking_final_fare bff
Index Cond: (booking_id = 5000)
Planning Time: 8.238 ms
Execution Time: 1.068 ms
Времето на извршување практично останува исто. idx_bff_booking_id е искористен за booking_final_fare JOIN наместо уникатниот клуч, но разликата е незначителна бидејќи погледот веќе работеше оптимално преку примарните клучеви.
