| Version 11 (modified by , 22 hours ago) ( diff ) |
|---|
Оптимизација на прашалници и погледи
Во оваа фаза ќе ги анализираме погледите дефинирани во претходните фази преку прашалници базирани на реални сценарија кои ќе бидат присутни во апликацијата Rezervo. Целта е да се провери времето на извршување на прашалниците пред и после додавање на соодветни индекси, како и да се објасни зошто тие индекси ги подобруваат перформансите.
Rezervo е систем за резервации кај занаетчии и даватели на услуги, каде што корисниците можат да пребаруваат достапни термини, да закажуваат, менуваат или откажуваат резервации, како и да оставаат рецензии за услугите.
За мерење на времињата се користи EXPLAIN ANALYZE. Кај секој поглед прво се мери времето без индекс, потоа се креира индексот и повторно се мери времето со индекс. Како резултат во документацијата се зема вредноста Execution Time.
1. Анализа на поглед customer_appointments_view, преглед на резервации за конкретен корисник
Овој поглед се користи за прикажување на резервациите на конкретен корисник. Во апликацијата ова би се користело на профилот на корисникот, каде што тој може да ги види своите закажани термини, услугата, бизнисот, вработениот и времето на терминот.
Прашалникот кој го тестираме е:
SELECT * FROM customer_appointments_view WHERE customer_id = 100;
За оптимизација го тестираме истиот прашалник пред и после додавање на индексот.
-- 1. Мерење без индекс EXPLAIN ANALYZE SELECT * FROM customer_appointments_view WHERE customer_id = 100; -- 2. Креирање на индекс CREATE INDEX IF NOT EXISTS idx_appointment_customer_not_cancelled ON appointment(customer_id) WHERE status <> 'cancelled';
Време на извршување без индекс:
7.802 ms
Дел од добиениот EXPLAIN ANALYZE output пред додавање на индексот:
Nested Loop (cost=88.12..168.91 rows=5 width=151) (actual time=3.988..7.635 rows=6 loops=1) -> Hash Join (cost=87.68..126.58 rows=5 width=97) (actual time=3.625..4.537 rows=6 loops=1) Hash Cond: (a.service_id = s.service_id) -> Index Scan using idx_appointment_customer_profile on appointment a Index Cond: (customer_id = 100) Filter: ((status)::text <> 'cancelled'::text) Rows Removed by Filter: 1 Planning Time: 8.862 ms Execution Time: 7.802 ms
Во овој план се гледа дека за табелата appointment се користи индекс според customer_id, но сепак дополнително се применува филтерот:
Filter: ((status)::text <> 'cancelled'::text)
Ова значи дека базата ги наоѓа резервациите според customer_id, но потоа мора дополнително да ги филтрира оние кои имаат статус различен од cancelled.
За да го оптимизираме ова, креираме парцијален индекс:
CREATE INDEX IF NOT EXISTS idx_appointment_customer_not_cancelled ON appointment(customer_id) WHERE status <> 'cancelled';
Овој индекс ги содржи само резервациите кои не се откажани. Бидејќи погледот customer_appointments_view ги користи токму тие резервации, индексот е помал и поефикасен.
Време на извршување со индекс:
0.803 ms
Дел од добиениот EXPLAIN ANALYZE output после додавање на индексот:
Nested Loop (cost=87.54..168.32 rows=5 width=151) (actual time=0.576..0.714 rows=6 loops=1) -> Index Scan using idx_appointment_customer_not_cancelled on appointment a Index Cond: (customer_id = 100) Planning Time: 3.150 ms Execution Time: 0.803 ms
После додавање на индексот се гледа дека планерот го користи новиот индекс:
Index Scan using idx_appointment_customer_not_cancelled on appointment a Index Cond: (customer_id = 100)
Овој пат нема дополнителен филтер за status <> 'cancelled', бидејќи самиот индекс веќе ги содржи само редиците кои не се откажани.
Споредба на времиња
| Поглед | Пред индекс | После индекс | Подобрување |
|---|---|---|---|
| customer_appointments_view | 7.802 ms | 0.803 ms | околу 9.7 пати побрзо |
Со додавање на парцијалниот индекс idx_appointment_customer_not_cancelled, времето на извршување се намали од 7.802 ms на 0.803 ms. Ова покажува дека индексот е соодветен за овој поглед, бидејќи прашалникот најчесто пребарува резервации за конкретен корисник и ги исклучува откажаните резервации.
2. Анализа на поглед available_slots, пребарување на слободни термини за даден бизнис и датум
Овој поглед се користи кога корисник пребарува достапни термини кај одреден бизнис за конкретен ден. Ова е едно од најважните сценарија во апликацијата, бидејќи процесот на резервација започнува со избор на слободен термин.
Прашалникот кој го тестираме е:
SELECT * FROM available_slots WHERE business_id = 1 AND date = CURRENT_DATE;
За оптимизација го тестираме истиот прашалник пред и после додавање на индексот.
-- 1. Мерење без индекс EXPLAIN ANALYZE SELECT * FROM available_slots WHERE business_id = 1 AND date = CURRENT_DATE; -- 2. Креирање на индекс CREATE INDEX IF NOT EXISTS idx_time_slot_available_business_date ON time_slot(business_id, date) WHERE is_available = TRUE;
Време на извршување без индекс:
7157.628 ms
Дел од добиениот EXPLAIN ANALYZE output пред додавање на индексот:
Nested Loop (cost=1001.27..479269.43 rows=21 width=100) (actual time=7156.545..7157.282 rows=48 loops=1) -> Gather Workers Planned: 2 Workers Launched: 2 -> Parallel Seq Scan on time_slot ts Filter: (is_available AND (business_id = 1) AND (date = CURRENT_DATE)) Rows Removed by Filter: 6666651 Planning Time: 4.741 ms Execution Time: 7157.628 ms
Во овој план се гледа дека најбавниот дел е скенирањето на табелата time_slot:
Parallel Seq Scan on time_slot ts Filter: (is_available AND (business_id = 1) AND (date = CURRENT_DATE)) Rows Removed by Filter: 6666651
Ова значи дека базата мора да помине низ многу голем број редици во time_slot за да ги најде термините кои се достапни, се за конкретниот бизнис и се за тековниот датум. Поради тоа времето на извршување е високо.
За да го оптимизираме ова, креираме парцијален индекс:
CREATE INDEX IF NOT EXISTS idx_time_slot_available_business_date ON time_slot(business_id, date) WHERE is_available = TRUE;
Овој индекс ги содржи само достапните термини, односно редиците каде што is_available = TRUE. Бидејќи прашалникот секогаш бара достапни термини за конкретен business_id и date, индексот е многу погоден за овој поглед.
Време на извршување со индекс:
0.580 ms
Дел од добиениот EXPLAIN ANALYZE output после додавање на индексот:
Nested Loop (cost=69.25..256.19 rows=35 width=100) (actual time=0.364..0.485 rows=48 loops=1) -> Index Scan using idx_time_slot_available_business_date on time_slot ts Index Cond: ((business_id = 1) AND (date = CURRENT_DATE)) Planning Time: 2.741 ms Execution Time: 0.580 ms
После додавање на индексот, планерот го користи новиот индекс:
Index Scan using idx_time_slot_available_business_date on time_slot ts Index Cond: ((business_id = 1) AND (date = CURRENT_DATE))
Ова значи дека наместо да ја скенира целата табела time_slot, базата директно ги наоѓа достапните термини за бараниот бизнис и датум. Со тоа се избегнува секвенцијалното скенирање на милиони редици.
Споредба на времиња
| Поглед | Пред индекс | После индекс | Подобрување |
|---|---|---|---|
| available_slots | 7157.628 ms | 0.580 ms | над 12000 пати побрзо |
Со додавање на парцијалниот индекс idx_time_slot_available_business_date, времето на извршување се намали од 7157.628 ms на 0.580 ms. Ова е огромно подобрување, бидејќи прашалникот повеќе не прави Parallel Seq Scan врз табелата time_slot, туку користи директен Index Scan преку новиот индекс.
3. Анализа на поглед review_details, детални рецензии според оценка
Овој поглед се користи за прикажување на детални информации за рецензии. Во апликацијата ова може да се користи кога корисникот сака да ги филтрира рецензиите според оценка, на пример да ги види сите рецензии со оценка 5.
Прашалникот кој го тестираме е:
SELECT * FROM review_details WHERE rating = 5;
За оптимизација го тестираме истиот прашалник пред и после додавање на индексот.
-- 1. Мерење без индекс EXPLAIN ANALYZE SELECT * FROM review_details WHERE rating = 5; -- 2. Креирање на индекс CREATE INDEX IF NOT EXISTS idx_review_rating_details ON review(rating);
Време на извршување без индекс:
360.970 ms
Дел од добиениот EXPLAIN ANALYZE output пред додавање на индексот:
Hash Join (cost=39.52..111924.47 rows=1006495 width=35) (actual time=0.710..344.505 rows=1000001 loops=1) -> Seq Scan on review r (cost=0.00..109231.71 rows=1006495 width=27) (actual time=0.169..259.457 rows=1000001 loops=1) Filter: (rating = 5) Rows Removed by Filter: 4000000 Planning Time: 2.849 ms Execution Time: 360.970 ms
Во овој план се гледа дека табелата review се скенира секвенцијално:
Seq Scan on review r Filter: (rating = 5) Rows Removed by Filter: 4000000
Ова значи дека базата ја чита целата табела review, потоа ги задржува само рецензиите со rating = 5, а ги отфрла останатите. Во конкретниот случај се отстранети 4 000 000 редици, што покажува дека без индекс се чита многу поголем број податоци од потребното.
За да го оптимизираме ова, креираме индекс на колоната rating:
CREATE INDEX IF NOT EXISTS idx_review_rating_details ON review(rating);
Овој индекс му овозможува на планерот директно да ги најде редиците со конкретна оценка, наместо да ја скенира целата табела.
Време на извршување со индекс:
239.802 ms
Дел од добиениот EXPLAIN ANALYZE output после додавање на индексот:
Hash Join (cost=11246.93..73210.68 rows=1006319 width=35) (actual time=40.929..223.700 rows=1000001 loops=1) -> Bitmap Heap Scan on review r Recheck Cond: (rating = 5) -> Bitmap Index Scan on idx_review_rating_details Index Cond: (rating = 5) Planning Time: 0.593 ms Execution Time: 239.802 ms
После додавање на индексот се гледа дека планерот веќе не прави Seq Scan врз review, туку користи:
Bitmap Index Scan on idx_review_rating_details Index Cond: (rating = 5)
и потоа:
Bitmap Heap Scan on review r Recheck Cond: (rating = 5)
Ова значи дека базата прво преку индексот ги наоѓа позициите на редиците кои имаат rating = 5, а потоа ги чита тие редици од табелата.
Подобрувањето кај овој поглед е помало во споредба со available_slots, бидејќи rating = 5 се појавува кај многу голем број редици. Во конкретниот резултат има 1 000 001 рецензија со оценка 5. Кога условот враќа голем дел од табелата, индексот помага, но сепак мора да се прочитаат многу редици.
Споредба на времиња
| Поглед | Пред индекс | После индекс | Подобрување |
|---|---|---|---|
| review_details | 360.970 ms | 239.802 ms | околу 1.5 пати побрзо |
Со додавање на индексот idx_review_rating_details, времето на извршување се намали од 360.970 ms на 239.802 ms. Индексот е корисен затоа што го заменува секвенцијалното скенирање на целата табела со Bitmap Index Scan, но подобрувањето не е екстремно бидејќи условот rating = 5 враќа голем број редици.
4. Анализа на поглед customer_profile_view, профил на корисник со негови активности
Овој поглед се користи за прикажување на профил на конкретен корисник. Во апликацијата ова би се користело кога корисникот го отвора својот профил и сака да ги види своите информации, резервации и оставени рецензии.
Прашалникот кој го тестираме е:
SELECT * FROM customer_profile_view WHERE customer_id = 100;
За оптимизација го тестираме истиот прашалник пред и после додавање на индексот.
-- 1. Мерење без индекс EXPLAIN ANALYZE SELECT * FROM customer_profile_view WHERE customer_id = 100; -- 2. Креирање на индекс CREATE INDEX IF NOT EXISTS idx_review_customer_profile ON review(customer_id, rating);
Време на извршување без индекс:
546.242 ms
Дел од добиениот EXPLAIN ANALYZE output пред додавање на индексот:
GroupAggregate (cost=147561.37..147562.12 rows=25 width=117) (actual time=545.746..546.153 rows=1 loops=1) -> Parallel Seq Scan on appointment a Filter: (customer_id = 100) Rows Removed by Filter: 1666665 -> Parallel Seq Scan on review r Filter: (customer_id = 100) Rows Removed by Filter: 1666665 Planning Time: 7.447 ms Execution Time: 546.242 ms
Во овој план се гледа дека најголемиот проблем е што табелите appointment и review се скенираат секвенцијално. За appointment се користи:
Parallel Seq Scan on appointment a Filter: (customer_id = 100) Rows Removed by Filter: 1666665
а за review се користи:
Parallel Seq Scan on review r Filter: (customer_id = 100) Rows Removed by Filter: 1666665
Ова значи дека базата чита многу голем број редици за да ги најде записите кои припаѓаат на корисникот со customer_id = 100.
За оптимизација креираме индекс на табелата review:
CREATE INDEX IF NOT EXISTS idx_review_customer_profile ON review(customer_id, rating);
Овој индекс е корисен бидејќи погледот customer_profile_view ги користи рецензиите на конкретен корисник, а дополнително може да ја користи и колоната rating при пресметки или приказ на информации поврзани со оценките.
Време на извршување со индекс:
123.200 ms
Дел од добиениот EXPLAIN ANALYZE output после додавање на индексот:
GroupAggregate (cost=73811.09..73811.84 rows=25 width=117) (actual time=122.594..123.137 rows=1 loops=1) -> Parallel Seq Scan on appointment a Filter: (customer_id = 100) Rows Removed by Filter: 1666665 -> Index Scan using idx_review_customer_profile on review r Index Cond: (customer_id = 100) Planning Time: 6.138 ms Execution Time: 123.200 ms
После додавање на индексот се гледа дека табелата review повеќе не се скенира секвенцијално, туку се користи новиот индекс:
Index Scan using idx_review_customer_profile on review r Index Cond: (customer_id = 100)
Ова значи дека базата директно ги наоѓа рецензиите за конкретниот корисник, наместо да ја чита целата табела review.
Сепак, во планот и понатаму се гледа дека табелата appointment се скенира секвенцијално:
Parallel Seq Scan on appointment a Filter: (customer_id = 100) Rows Removed by Filter: 1666665
Затоа времето се подобрува значително, но не паѓа под 1 ms како кај некои други погледи.
Споредба на времиња
| Поглед | Пред индекс | После индекс | Подобрување |
|---|---|---|---|
| customer_profile_view | 546.242 ms | 123.200 ms | околу 4.4 пати побрзо |
Со додавање на индексот idx_review_customer_profile, времето на извршување се намали од 546.242 ms на 123.200 ms. Индексот е корисен бидејќи го заменува секвенцијалното скенирање на табелата review со директно индексно пребарување според customer_id.
Иако appointment и понатаму се скенира секвенцијално, оваа оптимизација значително го намалува вкупното време на извршување на погледот.
5. Анализа на поглед reschedule_overview, преглед на барања за промена на термин
Овој поглед се користи за прикажување на барања за презакажување на термини. Во апликацијата ова би се користело од страна на бизнисот или вработениот за да ги види барањата кои се во статус pending, односно барањата кои сè уште чекаат одобрување или одбивање.
Прашалникот кој го тестираме е:
SELECT * FROM reschedule_overview WHERE status = 'pending';
За оптимизација го тестираме истиот прашалник пред и после додавање на индексите.
-- 1. Мерење без индекси EXPLAIN ANALYZE SELECT * FROM reschedule_overview WHERE status = 'pending'; -- 2. Креирање на индексите CREATE INDEX IF NOT EXISTS idx_reschedule_request_status ON reschedule_request(status); CREATE INDEX IF NOT EXISTS idx_reschedule_request_old_slot_id ON reschedule_request(old_slot_id); CREATE INDEX IF NOT EXISTS idx_reschedule_request_new_slot_id ON reschedule_request(new_slot_id);
Време на извршување без индекси:
5.051 ms
Дел од добиениот EXPLAIN ANALYZE output пред додавање на индексите:
Merge Join (cost=124.66..141.39 rows=667 width=64) (actual time=4.200..5.002 rows=667 loops=1) -> Seq Scan on reschedule_request rr Filter: ((status)::text = 'pending'::text) Rows Removed by Filter: 1335 Planning Time: 1.921 ms Execution Time: 5.051 ms
Во овој план се гледа дека табелата reschedule_request се скенира секвенцијално:
Seq Scan on reschedule_request rr Filter: ((status)::text = 'pending'::text) Rows Removed by Filter: 1335
Ова значи дека базата ја чита табелата reschedule_request, па потоа ги задржува само барањата со статус pending.
За оптимизација ги креираме следните индекси:
CREATE INDEX IF NOT EXISTS idx_reschedule_request_status ON reschedule_request(status); CREATE INDEX IF NOT EXISTS idx_reschedule_request_old_slot_id ON reschedule_request(old_slot_id); CREATE INDEX IF NOT EXISTS idx_reschedule_request_new_slot_id ON reschedule_request(new_slot_id);
Индексот idx_reschedule_request_status е корисен за филтрирање според статус, додека индексите idx_reschedule_request_old_slot_id и idx_reschedule_request_new_slot_id можат да помогнат при спојувањата со табелата time_slot, бидејќи погледот ги поврзува старите и новите термини преку овие колони.
Време на извршување со индекси:
3.988 ms
Дел од добиениот EXPLAIN ANALYZE output после додавање на индексите:
Merge Join (cost=121.42..138.15 rows=667 width=64) (actual time=3.341..3.903 rows=667 loops=1) -> Bitmap Heap Scan on reschedule_request rr Recheck Cond: ((status)::text = 'pending'::text) -> Bitmap Index Scan on idx_reschedule_request_status Index Cond: ((status)::text = 'pending'::text) Planning Time: 0.904 ms Execution Time: 3.988 ms
После додавање на индексите се гледа дека планерот повеќе не користи Seq Scan за reschedule_request, туку користи:
Bitmap Index Scan on idx_reschedule_request_status Index Cond: ((status)::text = 'pending'::text)
Ова значи дека базата преку индексот прво ги наоѓа барањата кои имаат статус pending, а потоа ги чита само тие редици од табелата.
Во овој конкретен тест подобрувањето не е многу големо затоа што табелата reschedule_request има релативно мал број редици. Сепак, индексот е корисен затоа што во реална апликација бројот на барања може да расте, а пребарувањето по статус би било често сценарио.
Споредба на времиња
| Поглед | Пред индекси | После индекси | Подобрување |
|---|---|---|---|
| reschedule_overview | 5.051 ms | 3.988 ms | околу 1.27 пати побрзо |
Со додавање на индексите, времето на извршување се намали од 5.051 ms на 3.988 ms. Најважниот индекс во овој случај е idx_reschedule_request_status, бидејќи директно се користи за условот status = 'pending'.
Индексите на old_slot_id и new_slot_id се потенцијално корисни за спојувања со табелата time_slot, иако во конкретниот план најмногу се гледа користењето на индексот за status.
6. Заклучок
Со додавање на соодветни индекси успеавме да ги подобриме перформансите на тестираните прашалници во системот Rezervo.
| Поглед | Пред индекс | После индекс | Подобрување |
|---|---|---|---|
| customer_appointments_view | 7.802 ms | 0.803 ms | околу 9.7 пати побрзо |
| available_slots | 7157.628 ms | 0.580 ms | над 12000 пати побрзо |
| review_details | 360.970 ms | 239.802 ms | околу 1.5 пати побрзо |
| customer_profile_view | 546.242 ms | 123.200 ms | околу 4.4 пати побрзо |
| reschedule_overview | 5.051 ms | 3.988 ms | околу 1.27 пати побрзо |
Најголемо подобрување има кај available_slots, бидејќи пред додавање на индексот базата правеше Parallel Seq Scan врз табелата time_slot и отстрануваше над 6 милиони редици преку филтер. После додавање на парцијалниот индекс, базата директно ги наоѓа достапните термини за конкретен бизнис и датум.
Кај customer_appointments_view подобрувањето е исто така значајно, бидејќи парцијалниот индекс ги содржи само резервациите кои не се откажани. Со тоа се избегнува дополнително филтрирање според статус.
Кај customer_profile_view подобрувањето е значително, бидејќи индексот idx_review_customer_profile го заменува секвенцијалното скенирање на табелата review со директно индексно пребарување според customer_id. Иако табелата appointment и понатаму се скенира секвенцијално, вкупното време се намалува од 546.242 ms на 123.200 ms.
Кај review_details подобрувањето е помало, бидејќи условот rating = 5 враќа многу голем број редици. Индексот го заменува целосното секвенцијално скенирање со Bitmap Index Scan, но бидејќи сепак мора да се прочитаат околу 1 милион рецензии, разликата не е толку голема.
Кај reschedule_overview подобрувањето е најмало, бидејќи табелата reschedule_request во тестот има релативно мал број редици. Индексот idx_reschedule_request_status се користи во планот, но разликата меѓу времето пред и после индексот е мала.
7. Одлука кои индекси се задржуваат, а кои се бришат
Индексите го подобруваат читањето на податоците, но имаат и цена. При секој INSERT, UPDATE или DELETE, базата мора да ги ажурира и индексите. Затоа не е секогаш оправдано да се задржи индекс кој дава многу мало подобрување.
Според добиените резултати, ги задржуваме индексите кои имаат значително намалување на времето и се користат во чести сценарија во апликацијата:
-- Индекси кои ги задржуваме idx_appointment_customer_not_cancelled idx_time_slot_available_business_date idx_review_customer_profile
Овие индекси се оправдани затоа што носат значајно подобрување:
| Индекс | Поглед | Причина за задржување |
|---|---|---|
| idx_appointment_customer_not_cancelled | customer_appointments_view | времето се намалува од 7.802 ms на 0.803 ms |
| idx_time_slot_available_business_date | available_slots | времето се намалува од 7157.628 ms на 0.580 ms |
| idx_review_customer_profile | customer_profile_view | времето се намалува од 546.242 ms на 123.200 ms |
Од друга страна, индексите кај кои подобрувањето е мало ги бришеме, освен ако не се користат во други прашалници надвор од оваа анализа.
-- Индекси кои ги бришеме DROP INDEX IF EXISTS idx_review_rating_details; DROP INDEX IF EXISTS idx_reschedule_request_status; DROP INDEX IF EXISTS idx_reschedule_request_old_slot_id; DROP INDEX IF EXISTS idx_reschedule_request_new_slot_id;
Индексот idx_review_rating_details го намалува времето од 360.970 ms на 239.802 ms, што е подобрување, но не е доволно големо во споредба со бројот на редици кои сепак мора да се прочитаат. Бидејќи rating = 5 враќа околу 1 милион редици, индексот има ограничен ефект.
Индексот idx_reschedule_request_status го намалува времето од 5.051 ms на 3.988 ms, што е многу мала разлика. Дополнително, индексите idx_reschedule_request_old_slot_id и idx_reschedule_request_new_slot_id не се директно искористени во прикажаниот план на извршување. Затоа тие индекси ги бришеме за да не создаваат непотребен трошок при внесување и ажурирање на податоци.
Заклучуваме дека најоправдани индекси за задржување во оваа фаза се парцијалните индекси за customer_appointments_view и available_slots, како и индексот за customer_profile_view, бидејќи таму добивката е најголема и сценаријата се чести во реалната употреба на системот.
