Анализата е направена со EXPLAIN (ANALYZE, BUFFERS), при што се споредува планот за извршување пред и по додавање на дополнителните индекси. Во почетната состојба базата ги содржи само индексите кои PostgreSQL автоматски ги креира за PRIMARY KEY и UNIQUE ограничувања. Овие индекси не се третираат како дополнителна оптимизација, туку како дел од основната релациска структура на базата.
Во моментот на тестирањето, базата ја има следната количина на податоци:
| Табела | Број на редови |
|---|---|
user | 3 |
transaction | 12 |
transaction_account | 5 |
transaction_breakdown | 11 |
tag | 11 |
tag_assigned_to_transaction | 16 |
Поради малата количина на податоци, очекувано е PostgreSQL во повеќе случаи да избере Seq Scan, бидејќи целосното читање на мала табела е поевтино од пристап преку индекс. Затоа, во анализата не се гледа само времето на извршување, туку и дали индексот навистина се користи во execution plan-от.
Предложени дополнителни индекси
Следните индекси се предложени затоа што колоните често се користат во JOIN и WHERE услови во аналитичките извештаи:
CREATE INDEX IF NOT EXISTS idx_transaction_date ON "transaction"(date); CREATE INDEX IF NOT EXISTS idx_transaction_account_user_id ON transaction_account(user_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_transaction_id ON transaction_breakdown(transaction_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_account_id ON transaction_breakdown(transaction_account_id); CREATE INDEX IF NOT EXISTS idx_tag_assigned_transaction_id ON tag_assigned_to_transaction(transaction_id); CREATE INDEX IF NOT EXISTS idx_tag_assigned_tag_id ON tag_assigned_to_transaction(tag_id);
Индексот idx_transaction_date е наменет за извештаи кои филтрираат трансакции според временски период. Индексот idx_transaction_account_user_id е наменет за извештаи кои ги ограничуваат резултатите на конкретен корисник. Индексите на transaction_breakdown и tag_assigned_to_transaction се наменети за побрзо поврзување на трансакциите со сметки и тагови.
Сценарио 1: Трендови на трошење според тагови за корисник
Цел:
Овој извештај ја прикажува потрошувачката по тагови за конкретен корисник во последните шест месеци. Прашалникот е комплексен затоа што ги поврзува табелите tag_assigned_to_transaction, transaction, transaction_breakdown, transaction_account и tag, а дополнително врши филтрирање по корисник и датум, групирање по таг и месец, и сумирање на потрошената сума.
Анализиран SQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
tg.tag_name,
DATE_TRUNC('month', t.date) AS month,
SUM(tb.spent_amount) AS total_spent
FROM tag_assigned_to_transaction tat
JOIN "transaction" t
ON tat.transaction_id = t.transaction_id
JOIN transaction_breakdown tb
ON t.transaction_id = tb.transaction_id
JOIN transaction_account ta
ON tb.transaction_account_id = ta.transaction_account_id
JOIN tag tg
ON tat.tag_id = tg.tag_id
WHERE
ta.user_id = 1
AND t.date >= DATE_TRUNC('month', CURRENT_DATE - INTERVAL '6 MONTHS')
GROUP BY
tg.tag_name, month
ORDER BY
tg.tag_name, month;
Релевантни индекси за ова сценарио:
CREATE INDEX IF NOT EXISTS idx_transaction_date ON "transaction"(date); CREATE INDEX IF NOT EXISTS idx_transaction_account_user_id ON transaction_account(user_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_transaction_id ON transaction_breakdown(transaction_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_account_id ON transaction_breakdown(transaction_account_id); CREATE INDEX IF NOT EXISTS idx_tag_assigned_transaction_id ON tag_assigned_to_transaction(transaction_id); CREATE INDEX IF NOT EXISTS idx_tag_assigned_tag_id ON tag_assigned_to_transaction(tag_id);
План пред додавање на индексите:
GroupAggregate (cost=6.54..6.77 rows=9 width=53) (actual time=0.176..0.189 rows=7 loops=1)
Group Key: tg.tag_name, (date_trunc('month'::text, t.date))
Buffers: shared hit=5
-> Sort (cost=6.54..6.57 rows=9 width=25) (actual time=0.162..0.167 rows=16 loops=1)
Sort Key: tg.tag_name, (date_trunc('month'::text, t.date))
Sort Method: quicksort Memory: 25kB
-> Hash Join (cost=4.99..6.40 rows=9 width=25) (actual time=0.119..0.142 rows=16 loops=1)
Hash Cond: (tat.tag_id = tg.tag_id)
-> Hash Join (cost=3.74..5.10 rows=9 width=16) (actual time=0.086..0.099 rows=16 loops=1)
Hash Cond: (tat.transaction_id = t.transaction_id)
-> Hash Join (cost=2.35..3.67 rows=10 width=16) (actual time=0.057..0.066 rows=16 loops=1)
Hash Cond: (tat.transaction_id = tb.transaction_id)
-> Seq Scan on tag_assigned_to_transaction tat
-> Hash
-> Hash Join
Hash Cond: (tb.transaction_account_id = ta.transaction_account_id)
-> Seq Scan on transaction_breakdown tb
-> Hash
-> Seq Scan on transaction_account ta
Filter: (user_id = 1)
Rows Removed by Filter: 2
-> Hash
-> Seq Scan on transaction t
Filter: (date >= date_trunc('month'::text, (CURRENT_DATE - '6 mons'::interval)))
-> Hash
-> Seq Scan on tag tg
Planning Time: 0.733 ms
Execution Time: 0.296 ms
План по додавање на индексите:
GroupAggregate (cost=6.54..6.77 rows=9 width=53) (actual time=0.154..0.166 rows=7 loops=1)
Group Key: tg.tag_name, (date_trunc('month'::text, t.date))
Buffers: shared hit=5
-> Sort (cost=6.54..6.57 rows=9 width=25) (actual time=0.142..0.146 rows=16 loops=1)
Sort Key: tg.tag_name, (date_trunc('month'::text, t.date))
Sort Method: quicksort Memory: 25kB
-> Hash Join (cost=4.99..6.40 rows=9 width=25) (actual time=0.100..0.122 rows=16 loops=1)
Hash Cond: (tat.tag_id = tg.tag_id)
-> Hash Join (cost=3.74..5.10 rows=9 width=16) (actual time=0.068..0.081 rows=16 loops=1)
Hash Cond: (tat.transaction_id = t.transaction_id)
-> Hash Join (cost=2.35..3.67 rows=10 width=16) (actual time=0.040..0.049 rows=16 loops=1)
Hash Cond: (tat.transaction_id = tb.transaction_id)
-> Seq Scan on tag_assigned_to_transaction tat
-> Hash
-> Hash Join
Hash Cond: (tb.transaction_account_id = ta.transaction_account_id)
-> Seq Scan on transaction_breakdown tb
-> Hash
-> Seq Scan on transaction_account ta
Filter: (user_id = 1)
Rows Removed by Filter: 2
-> Hash
-> Seq Scan on transaction t
Filter: (date >= date_trunc('month'::text, (CURRENT_DATE - '6 mons'::interval)))
-> Hash
-> Seq Scan on tag tg
Planning Time: 0.921 ms
Execution Time: 0.242 ms
Споредба:
| Метрика | Пред индекси | По индекси |
|---|---|---|
| Planning Time | 0.733 ms | 0.921 ms |
| Execution Time | 0.296 ms | 0.242 ms |
| Тип на скенирање | Seq Scan | Seq Scan
|
| Дали новите индекси се користат | Не | Не |
Заклучок:
Времето на извршување се намали од 0.296 ms на 0.242 ms, што претставува намалување од приближно 18.2%. Сепак, execution plan-от и по додавање на индексите користи Seq Scan над сите релевантни табели, а не Index Scan или Bitmap Index Scan. Затоа подобрувањето не може да се припише директно на индексите. Најверојатната причина е малата количина на податоци и фактот што сите блокови се веќе во shared buffers. Индексите остануваат логични за идно зголемување на бројот на трансакции, тагови и breakdown записи.
Сценарио 2: Сметки со највисоко вкупно трошење во изминатата година
Цел:
Овој извештај ги прикажува сметките со најголема вкупна потрошувачка во последната година. Прашалникот користи JOIN помеѓу transaction_breakdown, transaction и transaction_account, филтрирање по датум, SUM агрегација, GROUP BY, ORDER BY и LIMIT.
Анализиран SQL:
EXPLAIN (ANALYZE, BUFFERS) SELECT ta.account_name, COALESCE(SUM(tb.spent_amount), 0) AS total_spent FROM transaction_breakdown tb JOIN "transaction" t ON tb.transaction_id = t.transaction_id JOIN transaction_account ta ON tb.transaction_account_id = ta.transaction_account_id WHERE t.date >= NOW() - INTERVAL '1 YEAR' GROUP BY ta.account_name ORDER BY total_spent DESC LIMIT 10;
Релевантни индекси за ова сценарио:
CREATE INDEX IF NOT EXISTS idx_transaction_date ON "transaction"(date); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_transaction_id ON transaction_breakdown(transaction_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_account_id ON transaction_breakdown(transaction_account_id);
План пред додавање на индексите:
Limit (cost=3.85..3.86 rows=5 width=40) (actual time=0.110..0.114 rows=3 loops=1) Buffers: shared hit=3 -> Sort (cost=3.85..3.86 rows=5 width=40) (actual time=0.108..0.111 rows=3 loops=1) Sort Key: (COALESCE(sum(tb.spent_amount), '0'::numeric)) DESC Sort Method: quicksort Memory: 25kB -> HashAggregate (cost=3.73..3.79 rows=5 width=40) (actual time=0.097..0.101 rows=3 loops=1) Group Key: ta.account_name -> Hash Join (cost=2.47..3.67 rows=11 width=12) (actual time=0.059..0.069 rows=11 loops=1) Hash Cond: (tb.transaction_account_id = ta.transaction_account_id) -> Hash Join (cost=1.36..2.51 rows=11 width=8) (actual time=0.032..0.038 rows=11 loops=1) Hash Cond: (tb.transaction_id = t.transaction_id) -> Seq Scan on transaction_breakdown tb -> Hash -> Seq Scan on transaction t Filter: (date >= (now() - '1 year'::interval)) -> Hash -> Seq Scan on transaction_account ta Planning Time: 0.403 ms Execution Time: 0.191 ms
План по додавање на индексите:
Limit (cost=3.85..3.86 rows=5 width=40) (actual time=0.100..0.104 rows=3 loops=1) Buffers: shared hit=3 -> Sort (cost=3.85..3.86 rows=5 width=40) (actual time=0.098..0.101 rows=3 loops=1) Sort Key: (COALESCE(sum(tb.spent_amount), '0'::numeric)) DESC Sort Method: quicksort Memory: 25kB -> HashAggregate (cost=3.73..3.79 rows=5 width=40) (actual time=0.086..0.090 rows=3 loops=1) Group Key: ta.account_name -> Hash Join (cost=2.47..3.67 rows=11 width=12) (actual time=0.060..0.070 rows=11 loops=1) Hash Cond: (tb.transaction_account_id = ta.transaction_account_id) -> Hash Join (cost=1.36..2.51 rows=11 width=8) (actual time=0.030..0.036 rows=11 loops=1) Hash Cond: (tb.transaction_id = t.transaction_id) -> Seq Scan on transaction_breakdown tb -> Hash -> Seq Scan on transaction t Filter: (date >= (now() - '1 year'::interval)) -> Hash -> Seq Scan on transaction_account ta Planning Time: 0.451 ms Execution Time: 0.166 ms
Споредба:
| Метрика | Пред индекси | По индекси |
|---|---|---|
| Planning Time | 0.403 ms | 0.451 ms |
| Execution Time | 0.191 ms | 0.166 ms |
| Тип на скенирање | Seq Scan | Seq Scan
|
| Дали новите индекси се користат | Не | Не |
Заклучок:
Времето на извршување се намали од 0.191 ms на 0.166 ms, што претставува намалување од приближно 13.1%. Сепак, планот за извршување не користи ниту еден од новите индекси. PostgreSQL продолжува со Seq Scan на transaction, transaction_breakdown и transaction_account, затоа што табелите имаат многу мал број редови и читањето на целата табела е поевтино од пристап преку индекс. Поради тоа, малото подобрување не се смета како директен ефект од индексите. Индексот на transaction.date сепак е оправдан за идно зголемување на бројот на трансакции, бидејќи извештајот филтрира според временски период.
Сценарио 3: Годишни трендови на трансакции за корисник
Цел:
Овој извештај ја прикажува потрошувачката по квартали за конкретен корисник во тековната година. Прашалникот користи JOIN помеѓу transaction_breakdown, transaction и transaction_account, филтрирање по корисник и временски период, SUM агрегации со CASE WHEN, GROUP BY и ORDER BY.
Во анализата условот за година е напишан како временски опсег, наместо само со EXTRACT(YEAR FROM t.date), за B-Tree индексот на date да може поефикасно да се користи кога табелата ќе порасне.
Анализиран SQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
ta.account_name,
SUM(CASE WHEN EXTRACT(QUARTER FROM t.date) = 1 THEN tb.spent_amount ELSE 0 END) AS q1_spent,
SUM(CASE WHEN EXTRACT(QUARTER FROM t.date) = 2 THEN tb.spent_amount ELSE 0 END) AS q2_spent,
SUM(CASE WHEN EXTRACT(QUARTER FROM t.date) = 3 THEN tb.spent_amount ELSE 0 END) AS q3_spent,
SUM(CASE WHEN EXTRACT(QUARTER FROM t.date) = 4 THEN tb.spent_amount ELSE 0 END) AS q4_spent
FROM transaction_breakdown tb
JOIN "transaction" t
ON tb.transaction_id = t.transaction_id
JOIN transaction_account ta
ON tb.transaction_account_id = ta.transaction_account_id
WHERE
ta.user_id = 1
AND t.date >= DATE_TRUNC('year', CURRENT_DATE)
AND t.date < DATE_TRUNC('year', CURRENT_DATE) + INTERVAL '1 YEAR'
GROUP BY
ta.account_name
ORDER BY
ta.account_name;
Релевантни индекси за ова сценарио:
CREATE INDEX IF NOT EXISTS idx_transaction_date ON "transaction"(date); CREATE INDEX IF NOT EXISTS idx_transaction_account_user_id ON transaction_account(user_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_transaction_id ON transaction_breakdown(transaction_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_account_id ON transaction_breakdown(transaction_account_id);
План пред додавање на индексите:
Sort (cost=4.16..4.17 rows=3 width=136) (actual time=0.141..0.144 rows=3 loops=1)
Sort Key: ta.account_name
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=3
-> HashAggregate (cost=4.08..4.14 rows=3 width=136) (actual time=0.126..0.131 rows=3 loops=1)
Group Key: ta.account_name
-> Hash Join (cost=2.35..3.85 rows=7 width=20) (actual time=0.070..0.089 rows=11 loops=1)
Hash Cond: (t.transaction_id = tb.transaction_id)
-> Seq Scan on transaction t
Filter: ((date >= date_trunc('year'::text, (CURRENT_DATE)::timestamp with time zone))
AND (date < (date_trunc('year'::text, (CURRENT_DATE)::timestamp with time zone) + '1 year'::interval)))
-> Hash
-> Hash Join
Hash Cond: (tb.transaction_account_id = ta.transaction_account_id)
-> Seq Scan on transaction_breakdown tb
-> Hash
-> Seq Scan on transaction_account ta
Filter: (user_id = 1)
Rows Removed by Filter: 2
Planning Time: 0.402 ms
Execution Time: 0.242 ms
План по додавање на индексите:
Sort (cost=4.16..4.17 rows=3 width=136) (actual time=0.131..0.135 rows=3 loops=1)
Sort Key: ta.account_name
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=3
-> HashAggregate (cost=4.08..4.14 rows=3 width=136) (actual time=0.117..0.122 rows=3 loops=1)
Group Key: ta.account_name
-> Hash Join (cost=2.35..3.85 rows=7 width=20) (actual time=0.063..0.082 rows=11 loops=1)
Hash Cond: (t.transaction_id = tb.transaction_id)
-> Seq Scan on transaction t
Filter: ((date >= date_trunc('year'::text, (CURRENT_DATE)::timestamp with time zone))
AND (date < (date_trunc('year'::text, (CURRENT_DATE)::timestamp with time zone) + '1 year'::interval)))
-> Hash
-> Hash Join
Hash Cond: (tb.transaction_account_id = ta.transaction_account_id)
-> Seq Scan on transaction_breakdown tb
-> Hash
-> Seq Scan on transaction_account ta
Filter: (user_id = 1)
Rows Removed by Filter: 2
Planning Time: 0.500 ms
Execution Time: 0.212 ms
Споредба:
| Метрика | Пред индекси | По индекси |
|---|---|---|
| Planning Time | 0.402 ms | 0.500 ms |
| Execution Time | 0.242 ms | 0.212 ms |
| Тип на скенирање | Seq Scan | Seq Scan
|
| Дали новите индекси се користат | Не | Не |
Заклучок:
Времето на извршување се намали од 0.242 ms на 0.212 ms, што претставува намалување од приближно 12.4%. Сепак, планот останува ист во однос на пристапот до табелите: PostgreSQL користи Seq Scan, а не новите индекси. Причината е малата количина на податоци: transaction има 12 редови, transaction_breakdown има 11 редови, а transaction_account има 5 редови. За ваква големина, секвенцијалното читање е поевтино од пристап преку индекс. Индексите сепак се соодветни за идно зголемување на базата, особено затоа што извештајот филтрира по user_id и по временски период.
Финален заклучок од анализата
Во сите три сценарија, PostgreSQL продолжи да користи Seq Scan и по додавање на предложените индекси. Тоа значи дека индексите не беа реално искористени во моменталниот execution plan.
Ова однесување е очекувано поради малата количина на податоци во тест базата. Во моментот на тестирањето, најголемите релевантни табели имаат само 12 трансакции, 11 breakdown записи и 16 tag-assignment записи. За вакви табели, PostgreSQL правилно проценува дека целосно скенирање на табелата е поевтино од користење индекс.
Иако execution time се намали во сите три сценарија, тоа не може директно да се припише на индексите, бидејќи execution plan-от не покажува Index Scan, Bitmap Index Scan или Index Only Scan. Малите разлики во времето најверојатно се резултат на кеширање, повторно користење на податоци во shared buffers и нормална варијација при извршување.
Сепак, предложените индекси остануваат оправдани како подготовка за поголема количина на податоци. Со раст на бројот на корисници, трансакции, сметки, тагови и breakdown записи, индексите на transaction.date, transaction_account.user_id, transaction_breakdown.transaction_id, transaction_breakdown.transaction_account_id, tag_assigned_to_transaction.transaction_id и tag_assigned_to_transaction.tag_id ќе овозможат поефикасно филтрирање и поврзување на податоците во аналитичките извештаи.
