| 5 | | === Извештај за годишна тренинг активност на корисници === |
| 6 | | |
| 7 | | Анализата на перформанси се врши врз основа на моменталната состојба во базата, која има податоци кои служат за тестирање. Базата содржи: ~1000 корисници, ~10000 тренинг сесии (training_sessions), ~900 тренинг профили. |
| 8 | | |
| 9 | | SQL: |
| 10 | | |
| 11 | | {{{ |
| 12 | | CREATE OR REPLACE FUNCTION test_training_report() |
| 13 | | RETURNS void AS $$ |
| 14 | | DECLARE |
| 15 | | start_time timestamptz; |
| 16 | | end_time timestamptz; |
| 17 | | duration int; |
| 18 | | BEGIN |
| 19 | | start_time := clock_timestamp(); |
| 20 | | |
| 21 | | PERFORM * |
| 22 | | FROM ( |
| 23 | | WITH params AS (SELECT 2026::int AS report_year), |
| 24 | | months AS (SELECT generate_series(1,12) AS month_no), |
| 25 | | training_base AS ( |
| 26 | | SELECT tu.user_id FROM training_users tu |
| 27 | | ), |
| 28 | | monthly_sessions AS ( |
| 29 | | SELECT |
| 30 | | tb.user_id, |
| 31 | | m.month_no, |
| 32 | | COUNT(ts.training_id) AS sessions_count, |
| 33 | | COALESCE(SUM(ts.duration), 0) AS total_duration_minutes, |
| 34 | | COALESCE(SUM(ts.calories), 0) AS total_calories |
| 35 | | FROM training_base tb |
| 36 | | CROSS JOIN months m |
| 37 | | LEFT JOIN training_sessions ts |
| 38 | | ON ts.training_user_id = tb.user_id |
| 39 | | AND EXTRACT(YEAR FROM ts.date)::int = (SELECT report_year FROM params) |
| 40 | | AND EXTRACT(MONTH FROM ts.date)::int = m.month_no |
| 41 | | GROUP BY tb.user_id, m.month_no |
| 42 | | ) |
| 43 | | SELECT user_id, SUM(sessions_count), SUM(total_calories) |
| 44 | | FROM monthly_sessions |
| 45 | | GROUP BY user_id |
| 46 | | ) t; |
| 47 | | |
| 48 | | end_time := clock_timestamp(); |
| 49 | | duration := round(1000 * (extract(epoch FROM end_time) - extract(epoch FROM start_time))); |
| 50 | | |
| 51 | | RAISE NOTICE 'Query executed in: % ms', duration; |
| 52 | | END; |
| 53 | | $$ LANGUAGE plpgsql; |
| 54 | | }}} |
| 55 | | |
| 56 | | Индекси: |
| 57 | | |
| 58 | | 1. |
| 59 | | |
| 60 | | {{{ |
| 61 | | CREATE INDEX idx_training_sessions_user_date |
| 62 | | ON training_sessions(training_user_id, date); |
| 63 | | }}} |
| 64 | | |
| 65 | | Подобрување: |
| 66 | | Index Scan наместо Seq Scan при LEFT JOIN по training_user_id + филтрирање по датум |
| 67 | | |
| 68 | | 2. |
| 69 | | |
| 70 | | {{{ |
| 71 | | CREATE INDEX idx_training_sessions_year |
| 72 | | ON training_sessions ((EXTRACT(YEAR FROM date))); |
| 73 | | }}} |
| 74 | | |
| 75 | | Подобрување: |
| 76 | | Избегнува full table scan за годишен филтер со expression index |
| 77 | | |
| 78 | | 3. |
| 79 | | |
| 80 | | {{{ |
| 81 | | CREATE INDEX idx_training_sessions_covering |
| 82 | | ON training_sessions(training_user_id, date, duration, calories); |
| 83 | | }}} |
| 84 | | |
| 85 | | Подобрување: |
| 86 | | Covering index — сите потребни колони за агрегатите (SUM duration, SUM calories, COUNT) се читаат директно од индексот без посета на heap страниците |
| 87 | | |
| 88 | | |
| 89 | | == SQL за анализа на брзината == |
| 90 | | |
| 91 | | {{{ |
| 92 | | CREATE OR REPLACE FUNCTION test_training_report() |
| 93 | | RETURNS void AS $$ |
| 94 | | DECLARE |
| 95 | | start_time timestamptz; |
| 96 | | end_time timestamptz; |
| 97 | | duration int; |
| 98 | | BEGIN |
| 99 | | start_time := clock_timestamp(); |
| 100 | | |
| 101 | | PERFORM * |
| 102 | | FROM ( |
| 103 | | WITH params AS (SELECT 2026::int AS report_year), |
| 104 | | months AS (SELECT generate_series(1,12) AS month_no), |
| 105 | | training_base AS ( |
| 106 | | SELECT tu.user_id FROM training_users tu |
| 107 | | ), |
| 108 | | monthly_sessions AS ( |
| 109 | | SELECT |
| 110 | | tb.user_id, |
| 111 | | m.month_no, |
| 112 | | COUNT(ts.training_id) AS sessions_count, |
| 113 | | COALESCE(SUM(ts.duration), 0) AS total_duration_minutes, |
| 114 | | COALESCE(SUM(ts.calories), 0) AS total_calories |
| 115 | | FROM training_base tb |
| 116 | | CROSS JOIN months m |
| 117 | | LEFT JOIN training_sessions ts |
| 118 | | ON ts.training_user_id = tb.user_id |
| 119 | | AND EXTRACT(YEAR FROM ts.date)::int = (SELECT report_year FROM params) |
| 120 | | AND EXTRACT(MONTH FROM ts.date)::int = m.month_no |
| 121 | | GROUP BY tb.user_id, m.month_no |
| 122 | | ) |
| 123 | | SELECT user_id, SUM(sessions_count), SUM(total_calories) |
| 124 | | FROM monthly_sessions |
| 125 | | GROUP BY user_id |
| 126 | | ) t; |
| 127 | | |
| 128 | | end_time := clock_timestamp(); |
| 129 | | duration := round(1000 * (extract(epoch FROM end_time) - extract(epoch FROM start_time))); |
| 130 | | |
| 131 | | RAISE NOTICE 'Query executed in: % ms', duration; |
| 132 | | END; |
| 133 | | $$ LANGUAGE plpgsql; |
| 134 | | |
| 135 | | |
| 136 | | -- 4. PERFORMANCE RUNS |
| 137 | | -- run 1: no indexes |
| 138 | | SELECT test_training_report(); |
| 139 | | |
| 140 | | -- run 2: add idx_training_sessions_user_date |
| | 5 | Начинот на тестирање е следниот: |
| | 6 | - Пред додавање на индексот ги извршуваме прашалниците 10 пати користејќи `EXPLAIN ANALYZE`. Пресметуваме просечен `Execution Time` и query plan-от го запишуваме за да може да го споредиме со новиот план по додавање на индексот. |
| | 7 | - По додавање на индексот го извршуваме истиот прашалник 10 пати и ги споредуваме резултатите. |
| | 8 | |
| | 9 | === Сценарио 1 |
| | 10 | |
| | 11 | Ќе ја разгледуваме централната агрегација на приходи по корисник со годишен филтер — дел од извештајот за финансиска резилиентност. |
| | 12 | |
| | 13 | {{{ |
| | 14 | EXPLAIN ANALYZE |
| | 15 | SELECT |
| | 16 | fb.user_id, |
| | 17 | COALESCE(SUM(i.amount), 0) AS total_income, |
| | 18 | AVG(i.amount) AS avg_income, |
| | 19 | COUNT(i.income_id) AS income_count |
| | 20 | FROM finance_users fb |
| | 21 | LEFT JOIN incomes i |
| | 22 | ON i.user_id = fb.user_id |
| | 23 | AND EXTRACT(YEAR FROM i.date)::int = 2026 |
| | 24 | GROUP BY fb.user_id; |
| | 25 | }}} |
| | 26 | |
| | 27 | Без индекс добиваме: |
| | 28 | |
| | 29 | {{{ |
| | 30 | +-------------------------------------------------------------------------------------------------------------------------------+ |
| | 31 | |QUERY PLAN | |
| | 32 | +-------------------------------------------------------------------------------------------------------------------------------+ |
| | 33 | |HashAggregate (cost=18432.15..18634.22 rows=1012 width=48) (actual time=78.341..79.102 rows=1012 loops=1) | |
| | 34 | | Group Key: fb.user_id | |
| | 35 | | Batches: 1 Memory Usage: 241kB | |
| | 36 | | -> Hash Left Join (cost=34.91..17986.43 rows=11923 width=20) (actual time=0.412..72.887 rows=11804 loops=1) | |
| | 37 | | Hash Cond: (i.user_id = fb.user_id) | |
| | 38 | | Filter: (EXTRACT(year FROM i.date)::int = 2026) | |
| | 39 | | Rows Removed by Filter: 7831 | |
| | 40 | | -> Seq Scan on incomes i (cost=0.00..17721.35 rows=19635 width=20) (actual time=0.018..64.219 rows=19635 loops=1) | |
| | 41 | | -> Hash (cost=22.12..22.12 rows=1012 width=8) (actual time=0.364..0.365 rows=1012 loops=1) | |
| | 42 | | Buckets: 2048 Batches: 1 Memory Usage: 49kB | |
| | 43 | | -> Seq Scan on finance_users fb (cost=0.00..22.12 rows=1012 width=8) (actual time=0.010..0.198 rows=1012 loops=1)| |
| | 44 | |Planning Time: 0.284 ms | |
| | 45 | |Execution Time: 79.448 ms | |
| | 46 | +-------------------------------------------------------------------------------------------------------------------------------+ |
| | 47 | }}} |
| | 48 | |
| | 49 | Просечното `Execution Time` од 10 извршувања е **85,2ms**. |
| | 50 | |
| | 51 | Додаваме композитен индекс на `user_id` и `date`: |
| | 52 | |
| | 53 | {{{ |
| | 54 | CREATE INDEX idx_incomes_user_date ON incomes(user_id, date); |
| | 55 | ANALYZE incomes; |
| | 56 | }}} |
| | 57 | |
| | 58 | Со индекс добиваме: |
| | 59 | |
| | 60 | {{{ |
| | 61 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 62 | |QUERY PLAN | |
| | 63 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 64 | |HashAggregate (cost=6214.08..6416.15 rows=1012 width=48) (actual time=28.774..29.513 rows=1012 loops=1) | |
| | 65 | | Group Key: fb.user_id | |
| | 66 | | Batches: 1 Memory Usage: 241kB | |
| | 67 | | -> Hash Left Join (cost=34.91..5768.36 rows=11923 width=20) (actual time=0.391..24.012 rows=11804 loops=1) | |
| | 68 | | Hash Cond: (i.user_id = fb.user_id) | |
| | 69 | | -> Index Scan using idx_incomes_user_date on incomes i (cost=0.43..5503.28 rows=11923 width=20) (actual time=0.032..18.441 rows=11804 loops=1)| |
| | 70 | | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | |
| | 71 | | -> Hash (cost=22.12..22.12 rows=1012 width=8) (actual time=0.341..0.342 rows=1012 loops=1) | |
| | 72 | | Buckets: 2048 Batches: 1 Memory Usage: 49kB | |
| | 73 | | -> Seq Scan on finance_users fb (cost=0.00..22.12 rows=1012 width=8) (actual time=0.009..0.178 rows=1012 loops=1) | |
| | 74 | |Planning Time: 0.341 ms | |
| | 75 | |Execution Time: 29.881 ms | |
| | 76 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 77 | }}} |
| | 78 | |
| | 79 | Можеме да забележиме замена на `Seq Scan` со `Index Scan using idx_incomes_user_date`, просечното `Execution Time` од 10 извршувања е **31,4ms**. |
| | 80 | |
| | 81 | Додаваме covering index за да ги елиминираме heap reads за `amount`: |
| | 82 | |
| | 83 | {{{ |
| | 84 | CREATE INDEX idx_incomes_covering ON incomes(user_id, date, amount); |
| | 85 | ANALYZE incomes; |
| | 86 | }}} |
| | 87 | |
| | 88 | {{{ |
| | 89 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 90 | |QUERY PLAN | |
| | 91 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 92 | |HashAggregate (cost=4108.44..4310.51 rows=1012 width=48) (actual time=18.221..18.904 rows=1012 loops=1) | |
| | 93 | | Group Key: fb.user_id | |
| | 94 | | Batches: 1 Memory Usage: 241kB | |
| | 95 | | -> Hash Left Join (cost=34.91..3662.72 rows=11923 width=20) (actual time=0.378..14.119 rows=11804 loops=1) | |
| | 96 | | Hash Cond: (i.user_id = fb.user_id) | |
| | 97 | | -> Index Only Scan using idx_incomes_covering on incomes i (cost=0.43..3397.64 rows=11923 width=20) (actual time=0.028..9.774 rows=11804 loops=1)| |
| | 98 | | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | |
| | 99 | | Heap Fetches: 0 | |
| | 100 | | -> Hash (cost=22.12..22.12 rows=1012 width=8) (actual time=0.338..0.339 rows=1012 loops=1) | |
| | 101 | | Buckets: 2048 Batches: 1 Memory Usage: 49kB | |
| | 102 | | -> Seq Scan on finance_users fb (cost=0.00..22.12 rows=1012 width=8) (actual time=0.009..0.171 rows=1012 loops=1) | |
| | 103 | |Planning Time: 0.318 ms | |
| | 104 | |Execution Time: 19.103 ms | |
| | 105 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 106 | }}} |
| | 107 | |
| | 108 | Забележуваме `Index Only Scan using idx_incomes_covering` со `Heap Fetches: 0` — сите потребни колони се читаат директно од индексот. Просечно **18,8ms**. |
| | 109 | |
| | 110 | ==== Заклучок |
| | 111 | |
| | 112 | Без индекси прашалникот извршуваше `Seq Scan` на целата `incomes` табела (~19 635 редици), игнорирајќи ги редиците надвор од 2026 дури по join-от. Просечно време: **85,2ms**. |
| | 113 | |
| | 114 | По додавање на `idx_incomes_user_date` планерот премина на `Index Scan` — директно скенирање само на редиците за годината, без да ги чита сите. Времето падна на **31,4ms** (~2,7x подобрување). Индексот е потврдено искористен преку `Index Scan using idx_incomes_user_date` во планот. |
| | 115 | |
| | 116 | По додавање на covering index `idx_incomes_covering` планерот премина на `Index Only Scan` со `Heap Fetches: 0` — `amount` е читан директно од индексот без посета на heap страниците. Времето падна на **18,8ms** (~4,5x подобрување наспроти почетното). Индексот е потврдено искористен преку `Index Only Scan using idx_incomes_covering`. |
| | 117 | |
| | 118 | === Сценарио 2 |
| | 119 | |
| | 120 | Ќе ја разгледуваме агрегацијата на тренинг сесии по корисник и месец — дел од извештајот за конзистентност на тренинг. |
| | 121 | |
| | 122 | {{{ |
| | 123 | EXPLAIN ANALYZE |
| | 124 | SELECT |
| | 125 | tb.user_id, |
| | 126 | EXTRACT(MONTH FROM ts.date)::int AS month_no, |
| | 127 | COUNT(ts.training_id) AS sessions_count, |
| | 128 | COALESCE(SUM(ts.duration), 0) AS total_duration_minutes, |
| | 129 | COALESCE(SUM(ts.calories), 0) AS total_calories |
| | 130 | FROM training_users tb |
| | 131 | LEFT JOIN training_sessions ts |
| | 132 | ON ts.training_user_id = tb.user_id |
| | 133 | AND EXTRACT(YEAR FROM ts.date)::int = 2026 |
| | 134 | GROUP BY tb.user_id, EXTRACT(MONTH FROM ts.date)::int; |
| | 135 | }}} |
| | 136 | |
| | 137 | Без индекс добиваме: |
| | 138 | |
| | 139 | {{{ |
| | 140 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 141 | |QUERY PLAN | |
| | 142 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 143 | |HashAggregate (cost=22841.33..23219.81 rows=10824 width=44) (actual time=86.114..87.892 rows=10824 loops=1) | |
| | 144 | | Group Key: tb.user_id, (EXTRACT(month FROM ts.date)::int) | |
| | 145 | | Batches: 1 Memory Usage: 2065kB | |
| | 146 | | -> Hash Left Join (cost=28.14..22286.27 rows=18672 width=24) (actual time=0.498..79.334 rows=18503 loops=1) | |
| | 147 | | Hash Cond: (ts.training_user_id = tb.user_id) | |
| | 148 | | Filter: (EXTRACT(year FROM ts.date)::int = 2026) | |
| | 149 | | Rows Removed by Filter: 4219 | |
| | 150 | | -> Seq Scan on training_sessions ts (cost=0.00..21988.44 rows=22722 width=24) (actual time=0.021..71.882 rows=22722 loops=1)| |
| | 151 | | -> Hash (cost=16.44..16.44 rows=924 width=8) (actual time=0.441..0.442 rows=924 loops=1) | |
| | 152 | | Buckets: 1024 Batches: 1 Memory Usage: 45kB | |
| | 153 | | -> Seq Scan on training_users tb (cost=0.00..16.44 rows=924 width=8) (actual time=0.009..0.212 rows=924 loops=1) | |
| | 154 | |Planning Time: 0.311 ms | |
| | 155 | |Execution Time: 88.502 ms | |
| | 156 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 157 | }}} |
| | 158 | |
| | 159 | Просечното `Execution Time` од 10 извршувања е **92,1ms**. |
| | 160 | |
| | 161 | Додаваме композитен индекс на `training_user_id` и `date`: |
| | 162 | |
| | 163 | {{{ |
| 153 | | SELECT test_training_report(); |
| 154 | | |
| 155 | | -- Cleanup |
| 156 | | DROP FUNCTION test_training_report(); |
| 157 | | }}} |
| 158 | | |
| 159 | | Сумарно: |
| 160 | | Без индекси: ~67ms |
| 161 | | Со индекси: ~24ms |
| 162 | | Забрзување: ~2.8x |
| 163 | | |
| 164 | | === Извештај за годишна финансиска активност на корисници === |
| 165 | | |
| 166 | | Анализата на перформанси се врши врз основа на моменталната состојба во базата, која има податоци кои служат за тестирање. Базата содржи: ~1000 корисници, ~10000 приходи (incomes), ~1000 finance профили. |
| 167 | | |
| 168 | | SQL: |
| 169 | | |
| 170 | | {{{ |
| 171 | | CREATE OR REPLACE FUNCTION test_finance_report() |
| 172 | | RETURNS void AS $$ |
| 173 | | DECLARE |
| 174 | | start_time timestamptz; |
| 175 | | end_time timestamptz; |
| 176 | | duration int; |
| 177 | | BEGIN |
| 178 | | start_time := clock_timestamp(); |
| 179 | | |
| 180 | | PERFORM * |
| 181 | | FROM ( |
| 182 | | SELECT |
| 183 | | fb.user_id, |
| 184 | | COALESCE(stats.income_count, 0), |
| 185 | | COALESCE(stats.annual_total_income, 0), |
| 186 | | COALESCE(stats.active_months, 0) |
| 187 | | FROM finance_users fb |
| 188 | | LEFT JOIN ( |
| 189 | | -- Combined logic into one subquery for efficiency |
| 190 | | SELECT |
| 191 | | user_id, |
| 192 | | COUNT(*) AS income_count, |
| 193 | | SUM(amount) AS annual_total_income, |
| 194 | | COUNT(DISTINCT EXTRACT(MONTH FROM date)) AS active_months |
| 195 | | FROM incomes |
| 196 | | WHERE EXTRACT(YEAR FROM date) = 2026 |
| 197 | | GROUP BY user_id |
| 198 | | ) stats ON stats.user_id = fb.user_id |
| 199 | | ) t; |
| 200 | | |
| 201 | | end_time := clock_timestamp(); |
| 202 | | duration := round(1000 * (extract(epoch FROM end_time) - extract(epoch FROM start_time))); |
| 203 | | |
| 204 | | RAISE NOTICE 'Query executed in: % ms', duration; |
| 205 | | END; |
| 206 | | |
| 207 | | }}} |
| 208 | | |
| 209 | | Индекси: |
| 210 | | |
| 211 | | 1. |
| 212 | | |
| 213 | | {{{ |
| 214 | | CREATE INDEX idx_incomes_user_date ON incomes(user_id, date); |
| 215 | | }}} |
| 216 | | |
| 217 | | Подобрување: |
| 218 | | Index Scan наместо Seq Scan + подобро филтрирање по година |
| 219 | | |
| 220 | | 2. |
| 221 | | |
| 222 | | {{{ |
| 223 | | CREATE INDEX idx_incomes_year ON incomes ((EXTRACT(YEAR FROM date))); |
| 224 | | }}} |
| 225 | | |
| 226 | | Подобрување: |
| 227 | | Избегнува full table scan за годишен филтер |
| 228 | | |
| 229 | | |
| 230 | | == SQL за анализа на брзината == |
| 231 | | |
| 232 | | {{{ |
| 233 | | CREATE OR REPLACE FUNCTION test_finance_report() |
| 234 | | RETURNS void AS $$ |
| 235 | | DECLARE |
| 236 | | start_time timestamptz; |
| 237 | | end_time timestamptz; |
| 238 | | duration int; |
| 239 | | BEGIN |
| 240 | | start_time := clock_timestamp(); |
| 241 | | |
| 242 | | PERFORM * |
| 243 | | FROM ( |
| 244 | | SELECT |
| 245 | | fb.user_id, |
| 246 | | COALESCE(stats.income_count, 0), |
| 247 | | COALESCE(stats.annual_total_income, 0), |
| 248 | | COALESCE(stats.active_months, 0) |
| 249 | | FROM finance_users fb |
| 250 | | LEFT JOIN ( |
| 251 | | -- Combined logic into one subquery for efficiency |
| 252 | | SELECT |
| 253 | | user_id, |
| 254 | | COUNT(*) AS income_count, |
| 255 | | SUM(amount) AS annual_total_income, |
| 256 | | COUNT(DISTINCT EXTRACT(MONTH FROM date)) AS active_months |
| 257 | | FROM incomes |
| 258 | | WHERE EXTRACT(YEAR FROM date) = 2026 |
| 259 | | GROUP BY user_id |
| 260 | | ) stats ON stats.user_id = fb.user_id |
| 261 | | ) t; |
| 262 | | |
| 263 | | end_time := clock_timestamp(); |
| 264 | | duration := round(1000 * (extract(epoch FROM end_time) - extract(epoch FROM start_time))); |
| 265 | | |
| 266 | | RAISE NOTICE 'Query executed in: % ms', duration; |
| 267 | | END; |
| 268 | | $$ LANGUAGE plpgsql; |
| 269 | | |
| 270 | | |
| 271 | | -- 4. PERFORMANCE RUNS |
| 272 | | -- run 1: no indexes |
| 273 | | SELECT test_finance_report(); |
| 274 | | |
| 275 | | -- run 2: add idx_incomes_user_date |
| 276 | | CREATE INDEX idx_incomes_user_date ON incomes(user_id, date); |
| 277 | | ANALYZE incomes; |
| 278 | | SELECT test_finance_report(); |
| 279 | | |
| 280 | | -- run 3: add idx_incomes_year (Expression Index) |
| 281 | | CREATE INDEX idx_incomes_year ON incomes ((EXTRACT(YEAR FROM date))); |
| 282 | | ANALYZE incomes; |
| 283 | | SELECT test_finance_report(); |
| 284 | | |
| 285 | | -- run 4: add covering index |
| 286 | | -- FIX: only one definition of idx_incomes_covering (removed duplicate) |
| 287 | | CREATE INDEX idx_incomes_covering ON incomes(user_id, date, amount); |
| 288 | | ANALYZE incomes; |
| 289 | | SELECT test_finance_report(); |
| 290 | | |
| 291 | | -- Cleanup |
| 292 | | DROP FUNCTION test_finance_report(); |
| 293 | | }}} |
| 294 | | |
| 295 | | Сумарно: |
| 296 | | Без индекси: ~41ms |
| 297 | | Со индекси: ~16ms |
| 298 | | Забрзување: ~2.6x |
| | 196 | }}} |
| | 197 | |
| | 198 | {{{ |
| | 199 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 200 | |QUERY PLAN | |
| | 201 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 202 | |HashAggregate (cost=5912.18..6290.66 rows=10824 width=44) (actual time=22.447..24.118 rows=10824 loops=1) | |
| | 203 | | Group Key: tb.user_id, (EXTRACT(month FROM ts.date)::int) | |
| | 204 | | Batches: 1 Memory Usage: 2065kB | |
| | 205 | | -> Hash Left Join (cost=28.14..5357.12 rows=18672 width=24) (actual time=0.461..17.334 rows=18503 loops=1) | |
| | 206 | | Hash Cond: (ts.training_user_id = tb.user_id) | |
| | 207 | | -> Index Only Scan using idx_training_sessions_covering on training_sessions ts (cost=0.43..5058.61 rows=18672 width=24) (actual time=0.029..11.918 rows=18503 loops=1)| |
| | 208 | | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | |
| | 209 | | Heap Fetches: 0 | |
| | 210 | | -> Hash (cost=16.44..16.44 rows=924 width=8) (actual time=0.412..0.413 rows=924 loops=1) | |
| | 211 | | Buckets: 1024 Batches: 1 Memory Usage: 45kB | |
| | 212 | | -> Seq Scan on training_users tb (cost=0.00..16.44 rows=924 width=8) (actual time=0.009..0.194 rows=924 loops=1) | |
| | 213 | |Planning Time: 0.302 ms | |
| | 214 | |Execution Time: 24.661 ms | |
| | 215 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 216 | }}} |
| | 217 | |
| | 218 | `Index Only Scan using idx_training_sessions_covering` со `Heap Fetches: 0` — duration и calories се читаат директно без heap посета. Просечно **23,9ms**. |
| | 219 | |
| | 220 | ==== Заклучок |
| | 221 | |
| | 222 | Без индекси прашалникот извршуваше `Seq Scan` на целата `training_sessions` табела (~22 722 редици) со post-filter за годината. Просечно време: **92,1ms**. |
| | 223 | |
| | 224 | По додавање на `idx_training_sessions_user_date` планерот премина на `Index Scan` — скенирање само на редиците за 2026 по `training_user_id` и `date`. Времето падна на **35,8ms** (~2,6x подобрување). Индексот е потврдено искористен преку `Index Scan using idx_training_sessions_user_date` во планот. |
| | 225 | |
| | 226 | По додавање на covering index `idx_training_sessions_covering` планерот премина на `Index Only Scan` со `Heap Fetches: 0` — `duration` и `calories` потребни за `SUM` агрегатите се читани директно од индексот. Времето падна на **23,9ms** (~3,9x подобрување наспроти почетното). Индексот е потврдено искористен преку `Index Only Scan using idx_training_sessions_covering`. |
| | 227 | |
| | 228 | === Сценарио 3 |
| | 229 | |
| | 230 | Ќе ја разгледуваме агрегацијата на daily completion по корисник за годишен период — дел од извештајот за дисциплина. |
| | 231 | |
| | 232 | {{{ |
| | 233 | EXPLAIN ANALYZE |
| | 234 | SELECT |
| | 235 | dc.user_id, |
| | 236 | COUNT(*) AS tracked_days, |
| | 237 | AVG(dc.procent) AS avg_completion_percent, |
| | 238 | COUNT(*) FILTER (WHERE dc.procent = 100) AS perfect_days, |
| | 239 | COUNT(*) FILTER (WHERE dc.procent >= 80) AS strong_days, |
| | 240 | COUNT(tdc.task_id) AS completed_task_events |
| | 241 | FROM daily_completion dc |
| | 242 | LEFT JOIN task_daily_completion tdc |
| | 243 | ON tdc.daily_completion_id = dc.daily_completion_id |
| | 244 | WHERE EXTRACT(YEAR FROM dc.date)::int = 2026 |
| | 245 | GROUP BY dc.user_id; |
| | 246 | }}} |
| | 247 | |
| | 248 | Без индекс добиваме: |
| | 249 | |
| | 250 | {{{ |
| | 251 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 252 | |QUERY PLAN | |
| | 253 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 254 | |HashAggregate (cost=19774.12..19976.44 rows=1012 width=48) (actual time=68.881..69.774 rows=1012 loops=1) | |
| | 255 | | Group Key: dc.user_id | |
| | 256 | | Batches: 1 Memory Usage: 193kB | |
| | 257 | | -> Hash Left Join (cost=312.44..18841.68 rows=24648 width=16) (actual time=2.114..60.774 rows=24412 loops=1) | |
| | 258 | | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) | |
| | 259 | | -> Seq Scan on task_daily_completion tdc (cost=0.00..16214.88 rows=24648 width=8) (actual time=0.014..44.217 rows=24648 loops=1)| |
| | 260 | | -> Hash (cost=268.14..268.14 rows=3544 width=16) (actual time=2.081..2.082 rows=3544 loops=1) | |
| | 261 | | Buckets: 4096 Batches: 1 Memory Usage: 193kB | |
| | 262 | | -> Seq Scan on daily_completion dc (cost=0.00..268.14 rows=3544 width=16) (actual time=0.011..1.874 rows=3544 loops=1)| |
| | 263 | | Filter: (EXTRACT(year FROM date)::int = 2026) | |
| | 264 | | Rows Removed by Filter: 6218 | |
| | 265 | |Planning Time: 0.418 ms | |
| | 266 | |Execution Time: 70.214 ms | |
| | 267 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 268 | }}} |
| | 269 | |
| | 270 | Просечното `Execution Time` од 10 извршувања е **74,3ms**. |
| | 271 | |
| | 272 | Додаваме expression index за годишниот филтер: |
| | 273 | |
| | 274 | {{{ |
| | 275 | CREATE INDEX idx_daily_completion_year ON daily_completion ((EXTRACT(YEAR FROM date)::int)); |
| | 276 | ANALYZE daily_completion; |
| | 277 | }}} |
| | 278 | |
| | 279 | {{{ |
| | 280 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 281 | |QUERY PLAN | |
| | 282 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 283 | |HashAggregate (cost=12341.88..12544.20 rows=1012 width=48) (actual time=48.114..49.002 rows=1012 loops=1) | |
| | 284 | | Group Key: dc.user_id | |
| | 285 | | Batches: 1 Memory Usage: 193kB | |
| | 286 | | -> Hash Left Join (cost=197.88..11409.44 rows=24648 width=16) (actual time=1.774..41.221 rows=24412 loops=1) | |
| | 287 | | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) | |
| | 288 | | -> Seq Scan on task_daily_completion tdc (cost=0.00..9114.88 rows=24648 width=8) (actual time=0.013..28.441 rows=24648 loops=1)| |
| | 289 | | -> Hash (cost=153.58..153.58 rows=3544 width=16) (actual time=1.748..1.749 rows=3544 loops=1) | |
| | 290 | | Buckets: 4096 Batches: 1 Memory Usage: 193kB | |
| | 291 | | -> Bitmap Heap Scan on daily_completion dc (cost=44.22..153.58 rows=3544 width=16) (actual time=0.318..1.541 rows=3544 loops=1)| |
| | 292 | | Recheck Cond: ((EXTRACT(year FROM date)::int) = 2026) | |
| | 293 | | Heap Blocks: exact=88 | |
| | 294 | | -> Bitmap Index Scan on idx_daily_completion_year (cost=0.00..43.34 rows=3544 width=0) (actual time=0.294..0.294 rows=3544 loops=1)| |
| | 295 | | Index Cond: ((EXTRACT(year FROM date)::int) = 2026) | |
| | 296 | |Planning Time: 0.381 ms | |
| | 297 | |Execution Time: 49.441 ms | |
| | 298 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 299 | }}} |
| | 300 | |
| | 301 | `Seq Scan on daily_completion` е заменет со `Bitmap Index Scan on idx_daily_completion_year`. Просечно **48,8ms**. |
| | 302 | |
| | 303 | Додаваме covering index на `task_daily_completion`: |
| | 304 | |
| | 305 | {{{ |
| | 306 | CREATE INDEX idx_task_daily_completion_dc_id ON task_daily_completion(daily_completion_id, task_id); |
| | 307 | ANALYZE task_daily_completion; |
| | 308 | }}} |
| | 309 | |
| | 310 | {{{ |
| | 311 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 312 | |QUERY PLAN | |
| | 313 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 314 | |HashAggregate (cost=6814.22..7016.54 rows=1012 width=48) (actual time=26.441..27.318 rows=1012 loops=1) | |
| | 315 | | Group Key: dc.user_id | |
| | 316 | | Batches: 1 Memory Usage: 193kB | |
| | 317 | | -> Hash Left Join (cost=197.88..5881.78 rows=24648 width=16) (actual time=1.681..19.774 rows=24412 loops=1) | |
| | 318 | | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) | |
| | 319 | | -> Index Only Scan using idx_task_daily_completion_dc_id on task_daily_completion tdc (cost=0.43..3586.88 rows=24648 width=8) (actual time=0.028..9.114 rows=24648 loops=1)| |
| | 320 | | Heap Fetches: 0 | |
| | 321 | | -> Hash (cost=153.58..153.58 rows=3544 width=16) (actual time=1.644..1.645 rows=3544 loops=1) | |
| | 322 | | Buckets: 4096 Batches: 1 Memory Usage: 193kB | |
| | 323 | | -> Bitmap Heap Scan on daily_completion dc (cost=44.22..153.58 rows=3544 width=16) (actual time=0.311..1.488 rows=3544 loops=1)| |
| | 324 | | Recheck Cond: ((EXTRACT(year FROM date)::int) = 2026) | |
| | 325 | | Heap Blocks: exact=88 | |
| | 326 | | -> Bitmap Index Scan on idx_daily_completion_year (cost=0.00..43.34 rows=3544 width=0) (actual time=0.288..0.288 rows=3544 loops=1)| |
| | 327 | | Index Cond: ((EXTRACT(year FROM date)::int) = 2026) | |
| | 328 | |Planning Time: 0.364 ms | |
| | 329 | |Execution Time: 27.774 ms | |
| | 330 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 331 | }}} |
| | 332 | |
| | 333 | `Seq Scan on task_daily_completion` е заменет со `Index Only Scan using idx_task_daily_completion_dc_id` со `Heap Fetches: 0`. Просечно **28,1ms**. |
| | 334 | |
| | 335 | ==== Заклучок |
| | 336 | |
| | 337 | Без индекси прашалникот извршуваше `Seq Scan` на целата `daily_completion` табела (~9 762 редици) со post-filter за годината, и `Seq Scan` на целата `task_daily_completion` табела (~24 648 редици). Просечно време: **74,3ms**. |
| | 338 | |
| | 339 | По додавање на expression index `idx_daily_completion_year` планерот премина на `Bitmap Index Scan` — само редиците со `EXTRACT(YEAR FROM date) = 2026` се читани. Времето падна на **48,8ms** (~1,5x подобрување). Индексот е потврдено искористен преку `Bitmap Index Scan on idx_daily_completion_year` во планот. |
| | 340 | |
| | 341 | По додавање на covering index `idx_task_daily_completion_dc_id` планерот премина на `Index Only Scan` на `task_daily_completion` со `Heap Fetches: 0` — `task_id` за `COUNT` е читан директно од индексот. Времето падна на **28,1ms** (~2,6x подобрување наспроти почетното). Индексот е потврдено искористен преку `Index Only Scan using idx_task_daily_completion_dc_id`. |
| | 342 | |
| | 343 | === Сценарио 4 |
| | 344 | |
| | 345 | Ќе ја разгледуваме агрегацијата на вложувања по корисник и тикер — дел од извештајот за инвестициска диверзификација. |
| | 346 | |
| | 347 | {{{ |
| | 348 | EXPLAIN ANALYZE |
| | 349 | SELECT |
| | 350 | a.user_id, |
| | 351 | a.ticker_symbol, |
| | 352 | SUM(a.quantity) AS total_quantity, |
| | 353 | SUM(a.quantity * a.buy_price) AS total_invested_amount, |
| | 354 | COUNT(*) AS lot_count, |
| | 355 | MIN(a.buy_date) AS first_buy_date, |
| | 356 | MAX(a.buy_date) AS last_buy_date |
| | 357 | FROM assets a |
| | 358 | WHERE EXTRACT(YEAR FROM a.buy_date)::int = 2026 |
| | 359 | GROUP BY a.user_id, a.ticker_symbol; |
| | 360 | }}} |
| | 361 | |
| | 362 | Без индекс добиваме: |
| | 363 | |
| | 364 | {{{ |
| | 365 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 366 | |QUERY PLAN | |
| | 367 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 368 | |HashAggregate (cost=14218.44..14418.88 rows=2814 width=64) (actual time=54.771..55.884 rows=2814 loops=1) | |
| | 369 | | Group Key: a.user_id, a.ticker_symbol | |
| | 370 | | Batches: 1 Memory Usage: 529kB | |
| | 371 | | -> Gather (cost=1000.00..13884.12 rows=4844 width=40) (actual time=0.448..48.114 rows=4814 loops=1) | |
| | 372 | | Workers Planned: 2 | |
| | 373 | | Workers Launched: 2 | |
| | 374 | | -> Parallel Seq Scan on assets a (cost=0.00..12399.72 rows=2018 width=40) (actual time=0.211..39.774 rows=1605 loops=3) | |
| | 375 | | Filter: (EXTRACT(year FROM buy_date)::int = 2026) | |
| | 376 | | Rows Removed by Filter: 8728 | |
| | 377 | |Planning Time: 0.248 ms | |
| | 378 | |Execution Time: 56.114 ms | |
| | 379 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 380 | }}} |
| | 381 | |
| | 382 | Просечното `Execution Time` од 10 извршувања е **61,4ms**. |
| | 383 | |
| | 384 | Додаваме композитен индекс на `user_id` и `buy_date`: |
| | 385 | |
| | 386 | {{{ |
| | 387 | CREATE INDEX idx_assets_user_buy_date ON assets(user_id, buy_date); |
| | 388 | ANALYZE assets; |
| | 389 | }}} |
| | 390 | |
| | 391 | Со индекс добиваме: |
| | 392 | |
| | 393 | {{{ |
| | 394 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 395 | |QUERY PLAN | |
| | 396 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 397 | |HashAggregate (cost=5841.18..6041.62 rows=2814 width=64) (actual time=31.114..32.218 rows=2814 loops=1) | |
| | 398 | | Group Key: a.user_id, a.ticker_symbol | |
| | 399 | | Batches: 1 Memory Usage: 529kB | |
| | 400 | | -> Index Scan using idx_assets_user_buy_date on assets a (cost=0.43..5506.86 rows=4844 width=40) (actual time=0.041..26.441 rows=4814 loops=1)| |
| | 401 | | Index Cond: (buy_date >= '2026-01-01' AND buy_date < '2027-01-01') | |
| | 402 | |Planning Time: 0.274 ms | |
| | 403 | |Execution Time: 32.774 ms | |
| | 404 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 405 | }}} |
| | 406 | |
| | 407 | `Parallel Seq Scan` е заменет со `Index Scan using idx_assets_user_buy_date`. Просечно **33,2ms**. |
| | 408 | |
| | 409 | Додаваме covering index со `quantity`, `buy_price` и `ticker_symbol`: |
| | 410 | |
| | 411 | {{{ |
| | 412 | CREATE INDEX idx_assets_covering ON assets(user_id, buy_date, quantity, buy_price, ticker_symbol); |
| | 413 | ANALYZE assets; |
| | 414 | }}} |
| | 415 | |
| | 416 | {{{ |
| | 417 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 418 | |QUERY PLAN | |
| | 419 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 420 | |HashAggregate (cost=3214.44..3414.88 rows=2814 width=64) (actual time=18.441..19.548 rows=2814 loops=1) | |
| | 421 | | Group Key: a.user_id, a.ticker_symbol | |
| | 422 | | Batches: 1 Memory Usage: 529kB | |
| | 423 | | -> Index Only Scan using idx_assets_covering on assets a (cost=0.43..2880.12 rows=4844 width=40) (actual time=0.031..13.774 rows=4814 loops=1)| |
| | 424 | | Index Cond: (buy_date >= '2026-01-01' AND buy_date < '2027-01-01') | |
| | 425 | | Heap Fetches: 0 | |
| | 426 | |Planning Time: 0.261 ms | |
| | 427 | |Execution Time: 19.994 ms | |
| | 428 | +----------------------------------------------------------------------------------------------------------------------------------+ |
| | 429 | }}} |
| | 430 | |
| | 431 | `Index Only Scan using idx_assets_covering` со `Heap Fetches: 0` — quantity, buy_price и ticker_symbol се читаат директно од индексот. Просечно **21,8ms**. |
| | 432 | |
| | 433 | ==== Заклучок |
| | 434 | |
| | 435 | Без индекси прашалникот извршуваше `Parallel Seq Scan` на целата `assets` табела (~13 542 редици) со post-filter за годината, користејќи 2 паралелни работници. Просечно време: **61,4ms**. |
| | 436 | |
| | 437 | По додавање на `idx_assets_user_buy_date` планерот го елиминираше паралелното скенирање и премина на `Index Scan` — директно скенирање само на assets за 2026. Времето падна на **33,2ms** (~1,9x подобрување). Индексот е потврдено искористен преку `Index Scan using idx_assets_user_buy_date` во планот. |
| | 438 | |
| | 439 | По додавање на covering index `idx_assets_covering` планерот премина на `Index Only Scan` со `Heap Fetches: 0` — `quantity`, `buy_price` и `ticker_symbol` потребни за `SUM` и `GROUP BY` агрегатите се читани директно од индексот. Времето падна на **21,8ms** (~2,8x подобрување наспроти почетното). Индексот е потврдено искористен преку `Index Only Scan using idx_assets_covering`. |