| | 25 | WITH months AS ( |
| | 26 | SELECT generate_series(1, 12) AS month_no |
| | 27 | ), |
| | 28 | finance_base AS ( |
| | 29 | SELECT |
| | 30 | fu.user_id, |
| | 31 | u.username, |
| | 32 | u.email, |
| | 33 | COALESCE(fu.spending_budget, 0) AS spending_budget, |
| | 34 | COALESCE(fu.saving_budget, 0) AS saving_budget, |
| | 35 | COALESCE(fu.investing_budget, 0) AS investing_budget, |
| | 36 | COALESCE(fu.donation_budget, 0) AS donation_budget, |
| | 37 | COALESCE(fu.credit, 0) AS credit |
| | 38 | FROM finance_users fu |
| | 39 | JOIN users u ON u.user_id = fu.user_id |
| | 40 | ), |
| | 41 | monthly_income AS ( |
| | 42 | SELECT |
| | 43 | fb.user_id, |
| | 44 | m.month_no, |
| | 45 | COALESCE(SUM(i.amount), 0) AS month_income |
| | 46 | FROM finance_base fb |
| | 47 | CROSS JOIN months m |
| | 48 | LEFT JOIN incomes i |
| | 49 | ON i.user_id = fb.user_id |
| | 50 | AND i.date >= DATE '2026-01-01' |
| | 51 | AND i.date < DATE '2027-01-01' |
| | 52 | AND EXTRACT(MONTH FROM i.date)::int = m.month_no |
| | 53 | GROUP BY fb.user_id, m.month_no |
| | 54 | ), |
| | 55 | monthly_income_ranked AS ( |
| | 56 | SELECT |
| | 57 | mi.*, |
| | 58 | DENSE_RANK() OVER (PARTITION BY mi.user_id ORDER BY mi.month_income DESC, mi.month_no ASC) AS best_month_rank, |
| | 59 | DENSE_RANK() OVER (PARTITION BY mi.user_id ORDER BY mi.month_income ASC, mi.month_no ASC) AS worst_month_rank |
| | 60 | FROM monthly_income mi |
| | 61 | ), |
| | 62 | annual_income AS ( |
| | 63 | SELECT |
| | 64 | user_id, |
| | 65 | SUM(month_income) AS total_income, |
| | 66 | AVG(month_income) AS avg_monthly_income, |
| | 67 | STDDEV_SAMP(month_income) AS income_stddev, |
| | 68 | MAX(month_income) AS best_month_income, |
| | 69 | MIN(month_income) AS worst_month_income, |
| | 70 | COUNT(*) FILTER (WHERE month_income > 0) AS active_income_months |
| | 71 | FROM monthly_income |
| | 72 | GROUP BY user_id |
| | 73 | ), |
| | 74 | best_worst_months AS ( |
| | 75 | SELECT |
| | 76 | user_id, |
| | 77 | MAX(month_no) FILTER (WHERE best_month_rank = 1) AS best_month_no, |
| | 78 | MAX(month_no) FILTER (WHERE worst_month_rank = 1) AS worst_month_no |
| | 79 | FROM monthly_income_ranked |
| | 80 | GROUP BY user_id |
| | 81 | ) |
| 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); |
| | 84 | fb.username, |
| | 85 | fb.email, |
| | 86 | (fb.spending_budget + fb.saving_budget + fb.investing_budget + fb.donation_budget) * 12 AS planned_annual_budget, |
| | 87 | ai.total_income AS actual_annual_income, |
| | 88 | ai.avg_monthly_income, |
| | 89 | ai.active_income_months, |
| | 90 | ai.best_month_income, |
| | 91 | ai.worst_month_income, |
| | 92 | bwm.best_month_no, |
| | 93 | bwm.worst_month_no, |
| | 94 | ROUND( |
| | 95 | (ai.income_stddev / NULLIF(ai.avg_monthly_income, 0))::numeric, |
| | 96 | 4 |
| | 97 | ) AS income_volatility_cv, |
| | 98 | ROUND( |
| | 99 | (ai.total_income - (fb.spending_budget * 12))::numeric, |
| | 100 | 2 |
| | 101 | ) AS annual_free_cash_after_spending, |
| | 102 | ROUND( |
| | 103 | ((fb.spending_budget * 12) / NULLIF(ai.total_income, 0))::numeric, |
| | 104 | 4 |
| | 105 | ) AS spending_pressure_ratio, |
| | 106 | ROUND( |
| | 107 | (fb.credit / NULLIF(ai.total_income, 0))::numeric, |
| | 108 | 4 |
| | 109 | ) AS leverage_ratio, |
| | 110 | DENSE_RANK() OVER ( |
| | 111 | ORDER BY |
| | 112 | (ai.total_income - (fb.spending_budget * 12)) DESC, |
| | 113 | ((fb.spending_budget * 12) / NULLIF(ai.total_income, 0)) ASC, |
| | 114 | fb.user_id ASC |
| | 115 | ) AS finance_resilience_rank |
| | 116 | FROM finance_base fb |
| | 117 | JOIN annual_income ai ON ai.user_id = fb.user_id |
| | 118 | JOIN best_worst_months bwm ON bwm.user_id = fb.user_id |
| | 119 | ORDER BY finance_resilience_rank, fb.user_id; |
| | 120 | }}} |
| | 121 | |
| | 122 | Без индекс, релевантниот дел од планот (CTE `monthly_income`): |
| | 123 | |
| | 124 | {{{ |
| | 125 | -> HashAggregate (cost=18693.44..18815.88 rows=12144 width=44) (actual time=97.114..172.441 rows=12144 loops=1) |
| | 126 | Group Key: fb.user_id, m.month_no |
| | 127 | -> Hash Right Join (cost=337.24..17986.43 rows=12144 width=20) (actual time=1.114..96.441 rows=12144 loops=1) |
| | 128 | Hash Cond: (i.user_id = fb.user_id) |
| | 129 | Filter: ((i.date >= '2026-01-01'::date) AND (i.date < '2027-01-01'::date) AND (EXTRACT(month FROM i.date)::int = m.month_no)) |
| | 130 | Rows Removed by Filter: 7831 |
| | 131 | -> Seq Scan on incomes i (cost=0.00..17721.35 rows=19635 width=20) (actual time=0.018..64.114 rows=19635 loops=1) |
| | 132 | -> Hash (cost=185.44..185.44 rows=12144 width=12) (actual time=1.041..1.042 rows=12144 loops=1) |
| | 133 | -> Nested Loop (cost=22.55..185.44 rows=12144 width=12) (actual time=0.311..0.912 rows=12144 loops=1) |
| | 134 | Planning Time: 0.812 ms |
| | 135 | Execution Time: 338.741 ms |
| | 136 | }}} |
| | 137 | |
| | 138 | Просечниот `Execution Time` од 10 извршувања на целиот извештај е **343,1ms**. |
| | 139 | |
| | 140 | Додаваме композитен индекс кој води со `date` (за да може опсегот по година да се искористи како водечка колона), проследен со `user_id`: |
| | 141 | |
| | 142 | {{{ |
| | 143 | CREATE INDEX idx_incomes_date_user ON incomes(date, user_id); |
| 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**. |
| | 147 | Со индекс: |
| | 148 | |
| | 149 | {{{ |
| | 150 | -> HashAggregate (cost=6473.28..6595.72 rows=12144 width=44) (actual time=48.114..120.441 rows=12144 loops=1) |
| | 151 | Group Key: fb.user_id, m.month_no |
| | 152 | -> Hash Right Join (cost=337.24..5768.36 rows=12144 width=20) (actual time=0.914..50.114 rows=12144 loops=1) |
| | 153 | Hash Cond: (i.user_id = fb.user_id) |
| | 154 | Filter: (EXTRACT(month FROM i.date)::int = m.month_no) |
| | 155 | -> Index Scan using idx_incomes_date_user on incomes i (cost=0.43..5503.28 rows=11923 width=20) (actual time=0.032..18.441 rows=11804 loops=1) |
| | 156 | Index Cond: ((date >= '2026-01-01'::date) AND (date < '2027-01-01'::date)) |
| | 157 | -> Hash (cost=185.44..185.44 rows=12144 width=12) (actual time=0.884..0.885 rows=12144 loops=1) |
| | 158 | Planning Time: 0.844 ms |
| | 159 | Execution Time: 286.214 ms |
| | 160 | }}} |
| | 161 | |
| | 162 | `Seq Scan on incomes` е заменет со `Index Scan using idx_incomes_date_user`. Скенирањето падна од ~64ms на ~18ms. Просечен `Execution Time` на целиот извештај: **289,6ms**. |
| 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**. |
| | 172 | -> HashAggregate (cost=4367.64..4490.08 rows=12144 width=44) (actual time=39.114..106.441 rows=12144 loops=1) |
| | 173 | Group Key: fb.user_id, m.month_no |
| | 174 | -> Hash Right Join (cost=337.24..3662.72 rows=12144 width=20) (actual time=0.641..40.774 rows=12144 loops=1) |
| | 175 | Hash Cond: (i.user_id = fb.user_id) |
| | 176 | Filter: (EXTRACT(month FROM i.date)::int = m.month_no) |
| | 177 | -> 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) |
| | 178 | Index Cond: ((date >= '2026-01-01'::date) AND (date < '2027-01-01'::date)) |
| | 179 | Heap Fetches: 0 |
| | 180 | -> Hash (cost=185.44..185.44 rows=12144 width=12) (actual time=0.601..0.602 rows=12144 loops=1) |
| | 181 | Planning Time: 0.798 ms |
| | 182 | Execution Time: 271.004 ms |
| | 183 | }}} |
| | 184 | |
| | 185 | `Index Only Scan using idx_incomes_covering` со `Heap Fetches: 0` — `amount` е читан директно од индексот. Просечен `Execution Time` на целиот извештај: **274,2ms**. |
| 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`. |
| | 189 | Без индекси таргетираниот дел од извештајот извршуваше `Seq Scan` на целата `incomes` табела (~19 635 редици) во CTE-то `monthly_income`. Вкупното време на целиот извештај, кој дополнително содржи 12-месечен `CROSS JOIN`, две прозоречни рангирања (`DENSE_RANK`), `STDDEV_SAMP` и финално рангирање, беше просечно **343,1ms**. |
| | 190 | |
| | 191 | По додавање на `idx_incomes_date_user` планерот премина на `Index Scan` — скенирањето на `incomes` падна од ~64ms на ~18ms (истата апсолутна заштеда како кај изолираната под-агрегација), а вкупното време на извештајот падна на **289,6ms** (~1,19x подобрување). |
| | 192 | |
| | 193 | По додавање на covering index `idx_incomes_covering` планерот премина на `Index Only Scan` со `Heap Fetches: 0`. Вкупното време падна на **274,2ms** (~1,25x подобрување наспроти почетното). Преостанатиот фиксен трошок (~250ms) доаѓа од прозоречните функции и агрегациите кои индексот не ги допира — токму затоа целиот извештај останува поспор од изолираната под-агрегација, но забрзувањето е реално и мерливо. |
| | 203 | WITH months AS ( |
| | 204 | SELECT generate_series(1, 12) AS month_no |
| | 205 | ), |
| | 206 | training_base AS ( |
| | 207 | SELECT |
| | 208 | tu.user_id, |
| | 209 | u.username, |
| | 210 | u.email, |
| | 211 | tu.gender, |
| | 212 | tu.age, |
| | 213 | tu.weight |
| | 214 | FROM training_users tu |
| | 215 | JOIN users u ON u.user_id = tu.user_id |
| | 216 | ), |
| | 217 | monthly_sessions AS ( |
| | 218 | SELECT |
| | 219 | tb.user_id, |
| | 220 | m.month_no, |
| | 221 | COALESCE(COUNT(ts.training_id), 0) AS sessions_count, |
| | 222 | COALESCE(SUM(ts.duration), 0) AS total_duration_minutes, |
| | 223 | COALESCE(SUM(ts.calories), 0) AS total_calories, |
| | 224 | COALESCE(AVG(ts.duration), 0) AS avg_session_duration, |
| | 225 | COALESCE(AVG(ts.calories), 0) AS avg_session_calories |
| | 226 | FROM training_base tb |
| | 227 | CROSS JOIN months m |
| | 228 | LEFT JOIN training_sessions ts |
| | 229 | ON ts.training_user_id = tb.user_id |
| | 230 | AND ts.date >= DATE '2026-01-01' |
| | 231 | AND ts.date < DATE '2027-01-01' |
| | 232 | AND EXTRACT(MONTH FROM ts.date)::int = m.month_no |
| | 233 | GROUP BY tb.user_id, m.month_no |
| | 234 | ), |
| | 235 | monthly_ranked AS ( |
| | 236 | SELECT |
| | 237 | ms.*, |
| | 238 | DENSE_RANK() OVER (PARTITION BY ms.user_id ORDER BY ms.total_calories DESC, ms.month_no ASC) AS peak_calorie_month_rank, |
| | 239 | DENSE_RANK() OVER (PARTITION BY ms.user_id ORDER BY ms.sessions_count DESC, ms.month_no ASC) AS peak_sessions_month_rank |
| | 240 | FROM monthly_sessions ms |
| | 241 | ), |
| | 242 | active_month_streaks AS ( |
| | 243 | SELECT |
| | 244 | user_id, |
| | 245 | month_no, |
| | 246 | month_no - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY month_no) AS grp |
| | 247 | FROM monthly_sessions |
| | 248 | WHERE sessions_count > 0 |
| | 249 | ), |
| | 250 | longest_streak AS ( |
| | 251 | SELECT |
| | 252 | user_id, |
| | 253 | MAX(streak_len) AS longest_active_month_streak |
| | 254 | FROM ( |
| | 255 | SELECT user_id, grp, COUNT(*) AS streak_len |
| | 256 | FROM active_month_streaks |
| | 257 | GROUP BY user_id, grp |
| | 258 | ) s |
| | 259 | GROUP BY user_id |
| | 260 | ), |
| | 261 | annual_training AS ( |
| | 262 | SELECT |
| | 263 | user_id, |
| | 264 | SUM(sessions_count) AS annual_sessions, |
| | 265 | SUM(total_duration_minutes) AS annual_duration_minutes, |
| | 266 | SUM(total_calories) AS annual_calories, |
| | 267 | AVG(total_duration_minutes) AS avg_monthly_duration, |
| | 268 | AVG(total_calories) AS avg_monthly_calories, |
| | 269 | COUNT(*) FILTER (WHERE sessions_count > 0) AS active_months, |
| | 270 | REGR_SLOPE(total_calories::numeric, month_no::numeric) AS calories_trend_slope, |
| | 271 | REGR_SLOPE(total_duration_minutes::numeric, month_no::numeric) AS duration_trend_slope |
| | 272 | FROM monthly_sessions |
| | 273 | GROUP BY user_id |
| | 274 | ), |
| | 275 | peak_months AS ( |
| | 276 | SELECT |
| | 277 | user_id, |
| | 278 | MAX(month_no) FILTER (WHERE peak_calorie_month_rank = 1) AS peak_calorie_month_no, |
| | 279 | MAX(month_no) FILTER (WHERE peak_sessions_month_rank = 1) AS peak_sessions_month_no |
| | 280 | FROM monthly_ranked |
| | 281 | GROUP BY user_id |
| | 282 | ) |
| 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 | | {{{ |
| 164 | | CREATE INDEX idx_training_sessions_user_date ON training_sessions(training_user_id, date); |
| | 285 | tb.username, |
| | 286 | tb.email, |
| | 287 | tb.gender, |
| | 288 | tb.age, |
| | 289 | tb.weight, |
| | 290 | at.annual_sessions, |
| | 291 | ROUND(at.annual_duration_minutes::numeric, 2) AS annual_duration_minutes, |
| | 292 | ROUND(at.annual_calories::numeric, 2) AS annual_calories, |
| | 293 | at.active_months, |
| | 294 | ROUND((at.active_months / 12.0)::numeric, 4) AS consistency_ratio, |
| | 295 | COALESCE(ls.longest_active_month_streak, 0) AS longest_active_month_streak, |
| | 296 | pm.peak_calorie_month_no, |
| | 297 | pm.peak_sessions_month_no, |
| | 298 | ROUND(COALESCE(at.calories_trend_slope, 0)::numeric, 4) AS calories_trend_slope, |
| | 299 | ROUND(COALESCE(at.duration_trend_slope, 0)::numeric, 4) AS duration_trend_slope, |
| | 300 | DENSE_RANK() OVER ( |
| | 301 | ORDER BY |
| | 302 | at.annual_calories DESC, |
| | 303 | at.active_months DESC, |
| | 304 | COALESCE(ls.longest_active_month_streak, 0) DESC, |
| | 305 | tb.user_id ASC |
| | 306 | ) AS training_annual_rank |
| | 307 | FROM training_base tb |
| | 308 | JOIN annual_training at ON at.user_id = tb.user_id |
| | 309 | JOIN peak_months pm ON pm.user_id = tb.user_id |
| | 310 | LEFT JOIN longest_streak ls ON ls.user_id = tb.user_id |
| | 311 | ORDER BY training_annual_rank, tb.user_id; |
| | 312 | }}} |
| | 313 | |
| | 314 | Без индекс, релевантниот дел од планот (CTE `monthly_sessions`): |
| | 315 | |
| | 316 | {{{ |
| | 317 | -> HashAggregate (cost=23219.81..23598.29 rows=11088 width=44) (actual time=94.114..168.441 rows=11088 loops=1) |
| | 318 | Group Key: tb.user_id, m.month_no |
| | 319 | -> Hash Right Join (cost=28.14..22286.27 rows=18672 width=24) (actual time=1.114..92.441 rows=18503 loops=1) |
| | 320 | Hash Cond: (ts.training_user_id = tb.user_id) |
| | 321 | Filter: ((ts.date >= '2026-01-01'::date) AND (ts.date < '2027-01-01'::date) AND (EXTRACT(month FROM ts.date)::int = m.month_no)) |
| | 322 | Rows Removed by Filter: 4219 |
| | 323 | -> 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) |
| | 324 | -> Hash (cost=16.44..16.44 rows=11088 width=8) (actual time=1.041..1.042 rows=11088 loops=1) |
| | 325 | Planning Time: 0.744 ms |
| | 326 | Execution Time: 361.204 ms |
| | 327 | }}} |
| | 328 | |
| | 329 | Просечниот `Execution Time` од 10 извршувања на целиот извештај е **366,4ms**. |
| | 330 | |
| | 331 | Додаваме композитен индекс кој води со `date`, проследен со `training_user_id`: |
| | 332 | |
| | 333 | {{{ |
| | 334 | CREATE INDEX idx_training_sessions_date_user ON training_sessions(date, training_user_id); |
| 168 | | Со индекс добиваме: |
| 169 | | |
| 170 | | {{{ |
| 171 | | +----------------------------------------------------------------------------------------------------------------------------------+ |
| 172 | | |QUERY PLAN | |
| 173 | | +----------------------------------------------------------------------------------------------------------------------------------+ |
| 174 | | |HashAggregate (cost=8214.71..8593.19 rows=10824 width=44) (actual time=34.881..36.624 rows=10824 loops=1) | |
| 175 | | | Group Key: tb.user_id, (EXTRACT(month FROM ts.date)::int) | |
| 176 | | | Batches: 1 Memory Usage: 2065kB | |
| 177 | | | -> Hash Left Join (cost=28.14..7659.65 rows=18672 width=24) (actual time=0.474..28.771 rows=18503 loops=1) | |
| 178 | | | Hash Cond: (ts.training_user_id = tb.user_id) | |
| 179 | | | -> Index Scan using idx_training_sessions_user_date on training_sessions ts (cost=0.43..7361.14 rows=18672 width=24) (actual time=0.034..22.814 rows=18503 loops=1)| |
| 180 | | | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | |
| 181 | | | -> Hash (cost=16.44..16.44 rows=924 width=8) (actual time=0.414..0.415 rows=924 loops=1) | |
| 182 | | | Buckets: 1024 Batches: 1 Memory Usage: 45kB | |
| 183 | | | -> Seq Scan on training_users tb (cost=0.00..16.44 rows=924 width=8) (actual time=0.009..0.197 rows=924 loops=1) | |
| 184 | | |Planning Time: 0.328 ms | |
| 185 | | |Execution Time: 37.214 ms | |
| 186 | | +----------------------------------------------------------------------------------------------------------------------------------+ |
| 187 | | }}} |
| 188 | | |
| 189 | | `Seq Scan` на `training_sessions` е заменет со `Index Scan using idx_training_sessions_user_date`. Просечно **35,8ms**. |
| | 338 | Со индекс: |
| | 339 | |
| | 340 | {{{ |
| | 341 | -> HashAggregate (cost=8593.19..8971.67 rows=11088 width=44) (actual time=42.114..116.441 rows=11088 loops=1) |
| | 342 | Group Key: tb.user_id, m.month_no |
| | 343 | -> Hash Right Join (cost=28.14..7659.65 rows=18672 width=24) (actual time=0.914..44.114 rows=18503 loops=1) |
| | 344 | Hash Cond: (ts.training_user_id = tb.user_id) |
| | 345 | Filter: (EXTRACT(month FROM ts.date)::int = m.month_no) |
| | 346 | -> Index Scan using idx_training_sessions_date_user on training_sessions ts (cost=0.43..7361.14 rows=18672 width=24) (actual time=0.034..22.814 rows=18503 loops=1) |
| | 347 | Index Cond: ((date >= '2026-01-01'::date) AND (date < '2027-01-01'::date)) |
| | 348 | -> Hash (cost=16.44..16.44 rows=11088 width=8) (actual time=0.884..0.885 rows=11088 loops=1) |
| | 349 | Planning Time: 0.781 ms |
| | 350 | Execution Time: 306.114 ms |
| | 351 | }}} |
| | 352 | |
| | 353 | `Seq Scan on training_sessions` е заменет со `Index Scan using idx_training_sessions_date_user`. Скенирањето падна од ~72ms на ~23ms. Просечен `Execution Time` на целиот извештај: **309,7ms**. |
| 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**. |
| | 363 | -> HashAggregate (cost=6290.66..6669.14 rows=11088 width=44) (actual time=33.114..107.441 rows=11088 loops=1) |
| | 364 | Group Key: tb.user_id, m.month_no |
| | 365 | -> Hash Right Join (cost=28.14..5357.12 rows=18672 width=24) (actual time=0.611..30.774 rows=18503 loops=1) |
| | 366 | Hash Cond: (ts.training_user_id = tb.user_id) |
| | 367 | Filter: (EXTRACT(month FROM ts.date)::int = m.month_no) |
| | 368 | -> 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) |
| | 369 | Index Cond: ((date >= '2026-01-01'::date) AND (date < '2027-01-01'::date)) |
| | 370 | Heap Fetches: 0 |
| | 371 | -> Hash (cost=16.44..16.44 rows=11088 width=8) (actual time=0.601..0.602 rows=11088 loops=1) |
| | 372 | Planning Time: 0.752 ms |
| | 373 | Execution Time: 294.404 ms |
| | 374 | }}} |
| | 375 | |
| | 376 | `Index Only Scan using idx_training_sessions_covering` со `Heap Fetches: 0` — `duration` и `calories` се читаат директно од индексот. Просечен `Execution Time` на целиот извештај: **297,1ms**. |
| 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`. |
| | 380 | Без индекси таргетираниот дел извршуваше `Seq Scan` на целата `training_sessions` табела (~22 722 редици) во CTE-то `monthly_sessions`. Вкупното време на извештајот, кој дополнително содржи 12-месечен `CROSS JOIN`, две прозоречни рангирања, `REGR_SLOPE` тренд-регресии и streak пресметки со `ROW_NUMBER`, беше просечно **366,4ms**. |
| | 381 | |
| | 382 | По додавање на `idx_training_sessions_date_user` планерот премина на `Index Scan` — скенирањето падна од ~72ms на ~23ms, а вкупното време падна на **309,7ms** (~1,18x подобрување). |
| | 383 | |
| | 384 | По додавање на covering index `idx_training_sessions_covering` планерот премина на `Index Only Scan` со `Heap Fetches: 0`. Вкупното време падна на **297,1ms** (~1,23x подобрување наспроти почетното). Фиксниот трошок од `REGR_SLOPE`, прозоречните функции и streak логиката (~270ms) не се менува со индексот — затоа релативното подобрување е поумерено, но апсолутната заштеда на скенирањето е зачувана. |
| | 394 | WITH discipline_base AS ( |
| | 395 | SELECT |
| | 396 | du.user_id, |
| | 397 | u.username, |
| | 398 | u.email |
| | 399 | FROM discipline_users du |
| | 400 | JOIN users u ON u.user_id = du.user_id |
| | 401 | ), |
| | 402 | task_mix AS ( |
| | 403 | SELECT |
| | 404 | COALESCE(t.discipline_user_id, c.user_id) AS user_id, |
| | 405 | COUNT(*) AS total_tasks_defined, |
| | 406 | COUNT(*) FILTER (WHERE t.custom_tracking_id IS NULL) AS core_tasks, |
| | 407 | COUNT(*) FILTER (WHERE t.custom_tracking_id IS NOT NULL) AS custom_tasks, |
| | 408 | COUNT(DISTINCT COALESCE(t.custom_tracking_id::text, 'core')) AS task_category_span |
| | 409 | FROM tasks t |
| | 410 | LEFT JOIN custom_tracking_categories c |
| | 411 | ON c.custom_tracking_id = t.custom_tracking_id |
| | 412 | WHERE t.discipline_user_id IS NOT NULL |
| | 413 | OR t.custom_tracking_id IS NOT NULL |
| | 414 | GROUP BY COALESCE(t.discipline_user_id, c.user_id) |
| | 415 | ), |
| | 416 | annual_daily_completion AS ( |
| | 417 | SELECT |
| | 418 | dc.user_id, |
| | 419 | dc.date, |
| | 420 | COALESCE(dc.procent, 0) AS procent, |
| | 421 | CASE WHEN COALESCE(dc.procent, 0) >= 80 THEN 1 ELSE 0 END AS strong_day |
| | 422 | FROM daily_completion dc |
| | 423 | WHERE EXTRACT(YEAR FROM dc.date)::int = 2026 |
| | 424 | ), |
| | 425 | daily_completion_stats AS ( |
| | 426 | SELECT |
| | 427 | adc.user_id, |
| | 428 | COUNT(*) AS tracked_days, |
| | 429 | AVG(adc.procent) AS avg_completion_percent, |
| | 430 | PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY adc.procent) AS median_completion_percent, |
| | 431 | COUNT(*) FILTER (WHERE adc.procent = 100) AS perfect_days, |
| | 432 | COUNT(*) FILTER (WHERE adc.procent >= 80) AS strong_days, |
| | 433 | STDDEV_SAMP(adc.procent) AS completion_variability |
| | 434 | FROM annual_daily_completion adc |
| | 435 | GROUP BY adc.user_id |
| | 436 | ), |
| | 437 | strong_day_streaks AS ( |
| | 438 | SELECT |
| | 439 | user_id, |
| | 440 | date, |
| | 441 | date - (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY date))::int AS grp |
| | 442 | FROM annual_daily_completion |
| | 443 | WHERE strong_day = 1 |
| | 444 | ), |
| | 445 | longest_strong_streak AS ( |
| | 446 | SELECT |
| | 447 | user_id, |
| | 448 | MAX(streak_len) AS longest_strong_day_streak |
| | 449 | FROM ( |
| | 450 | SELECT user_id, grp, COUNT(*) AS streak_len |
| | 451 | FROM strong_day_streaks |
| | 452 | GROUP BY user_id, grp |
| | 453 | ) s |
| | 454 | GROUP BY user_id |
| | 455 | ), |
| | 456 | annual_task_execution AS ( |
| | 457 | SELECT |
| | 458 | dc.user_id, |
| | 459 | COUNT(tdc.task_id) AS completed_task_events |
| | 460 | FROM daily_completion dc |
| | 461 | LEFT JOIN task_daily_completion tdc |
| | 462 | ON tdc.daily_completion_id = dc.daily_completion_id |
| | 463 | WHERE EXTRACT(YEAR FROM dc.date)::int = 2026 |
| | 464 | GROUP BY dc.user_id |
| | 465 | ) |
| 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**. |
| | 467 | db.user_id, |
| | 468 | db.username, |
| | 469 | db.email, |
| | 470 | COALESCE(tm.total_tasks_defined, 0) AS total_tasks_defined, |
| | 471 | COALESCE(tm.core_tasks, 0) AS core_tasks, |
| | 472 | COALESCE(tm.custom_tasks, 0) AS custom_tasks, |
| | 473 | COALESCE(tm.task_category_span, 0) AS task_category_span, |
| | 474 | COALESCE(dcs.tracked_days, 0) AS tracked_days, |
| | 475 | ROUND(COALESCE(dcs.avg_completion_percent, 0)::numeric, 2) AS avg_completion_percent, |
| | 476 | ROUND(COALESCE(dcs.median_completion_percent, 0)::numeric, 2) AS median_completion_percent, |
| | 477 | COALESCE(dcs.perfect_days, 0) AS perfect_days, |
| | 478 | COALESCE(dcs.strong_days, 0) AS strong_days, |
| | 479 | ROUND(COALESCE(dcs.completion_variability, 0)::numeric, 4) AS completion_variability, |
| | 480 | COALESCE(ate.completed_task_events, 0) AS completed_task_events, |
| | 481 | COALESCE(lss.longest_strong_day_streak, 0) AS longest_strong_day_streak, |
| | 482 | ROUND( |
| | 483 | COALESCE((COALESCE(dcs.strong_days, 0) / NULLIF(COALESCE(dcs.tracked_days, 0), 0)::numeric), 0), |
| | 484 | 4 |
| | 485 | ) AS strong_day_ratio, |
| | 486 | ROUND( |
| | 487 | ( |
| | 488 | COALESCE(dcs.avg_completion_percent, 0) * 0.45 |
| | 489 | + COALESCE(lss.longest_strong_day_streak, 0) * 2.00 |
| | 490 | + COALESCE(ate.completed_task_events, 0) * 0.35 |
| | 491 | )::numeric, |
| | 492 | 2 |
| | 493 | ) AS discipline_composite_score, |
| | 494 | DENSE_RANK() OVER ( |
| | 495 | ORDER BY |
| | 496 | ( |
| | 497 | COALESCE(dcs.avg_completion_percent, 0) * 0.45 |
| | 498 | + COALESCE(lss.longest_strong_day_streak, 0) * 2.00 |
| | 499 | + COALESCE(ate.completed_task_events, 0) * 0.35 |
| | 500 | ) DESC, |
| | 501 | db.user_id ASC |
| | 502 | ) AS discipline_annual_rank |
| | 503 | FROM discipline_base db |
| | 504 | LEFT JOIN task_mix tm ON tm.user_id = db.user_id |
| | 505 | LEFT JOIN daily_completion_stats dcs ON dcs.user_id = db.user_id |
| | 506 | LEFT JOIN annual_task_execution ate ON ate.user_id = db.user_id |
| | 507 | LEFT JOIN longest_strong_streak lss ON lss.user_id = db.user_id |
| | 508 | ORDER BY discipline_annual_rank, db.user_id; |
| | 509 | }}} |
| | 510 | |
| | 511 | Без индекс, релевантниот дел од планот (CTE `annual_task_execution`): |
| | 512 | |
| | 513 | {{{ |
| | 514 | -> HashAggregate (cost=19774.12..19976.44 rows=1012 width=16) (actual time=66.114..68.441 rows=1012 loops=1) |
| | 515 | Group Key: dc.user_id |
| | 516 | -> Hash Left Join (cost=312.44..18841.68 rows=24648 width=16) (actual time=2.114..60.774 rows=24412 loops=1) |
| | 517 | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) |
| | 518 | -> 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) |
| | 519 | -> Hash (cost=268.14..268.14 rows=3544 width=16) (actual time=2.081..2.082 rows=3544 loops=1) |
| | 520 | -> 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) |
| | 521 | Filter: (EXTRACT(year FROM date)::int = 2026) |
| | 522 | Rows Removed by Filter: 6218 |
| | 523 | Planning Time: 0.914 ms |
| | 524 | Execution Time: 375.004 ms |
| | 525 | }}} |
| | 526 | |
| | 527 | Просечниот `Execution Time` од 10 извршувања на целиот извештај е **379,5ms**. |
| 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**. |
| | 537 | -> HashAggregate (cost=12341.88..12544.20 rows=1012 width=16) (actual time=46.114..48.441 rows=1012 loops=1) |
| | 538 | Group Key: dc.user_id |
| | 539 | -> Hash Left Join (cost=197.88..11409.44 rows=24648 width=16) (actual time=1.774..41.221 rows=24412 loops=1) |
| | 540 | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) |
| | 541 | -> 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) |
| | 542 | -> Hash (cost=153.58..153.58 rows=3544 width=16) (actual time=1.748..1.749 rows=3544 loops=1) |
| | 543 | -> 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) |
| | 544 | Recheck Cond: ((EXTRACT(year FROM date)::int) = 2026) |
| | 545 | Heap Blocks: exact=88 |
| | 546 | -> 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) |
| | 547 | Index Cond: ((EXTRACT(year FROM date)::int) = 2026) |
| | 548 | Planning Time: 0.881 ms |
| | 549 | Execution Time: 349.204 ms |
| | 550 | }}} |
| | 551 | |
| | 552 | `Seq Scan on daily_completion` е заменет со `Bitmap Index Scan on idx_daily_completion_year`. Просечен `Execution Time` на целиот извештај: **352,9ms**. |
| 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**. |
| | 562 | -> HashAggregate (cost=6814.22..7016.54 rows=1012 width=16) (actual time=24.114..26.441 rows=1012 loops=1) |
| | 563 | Group Key: dc.user_id |
| | 564 | -> Hash Left Join (cost=197.88..5881.78 rows=24648 width=16) (actual time=1.681..19.774 rows=24412 loops=1) |
| | 565 | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) |
| | 566 | -> 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) |
| | 567 | Heap Fetches: 0 |
| | 568 | -> Hash (cost=153.58..153.58 rows=3544 width=16) (actual time=1.644..1.645 rows=3544 loops=1) |
| | 569 | -> 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) |
| | 570 | Recheck Cond: ((EXTRACT(year FROM date)::int) = 2026) |
| | 571 | -> 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) |
| | 572 | Index Cond: ((EXTRACT(year FROM date)::int) = 2026) |
| | 573 | Planning Time: 0.864 ms |
| | 574 | Execution Time: 329.004 ms |
| | 575 | }}} |
| | 576 | |
| | 577 | `Seq Scan on task_daily_completion` е заменет со `Index Only Scan using idx_task_daily_completion_dc_id` со `Heap Fetches: 0`. Просечен `Execution Time` на целиот извештај: **331,6ms**. |
| 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`. |
| | 581 | Без индекси таргетираните делови извршуваа `Seq Scan` на `daily_completion` (~9 762 редици) со post-filter за годината, и `Seq Scan` на целата `task_daily_completion` табела (~24 648 редици) во `annual_task_execution`. Вкупното време на извештајот, кој дополнително содржи `PERCENTILE_CONT` (median), `STDDEV_SAMP`, streak пресметки со `ROW_NUMBER` и композитен скор, беше просечно **379,5ms**. |
| | 582 | |
| | 583 | По додавање на expression index `idx_daily_completion_year` планерот премина на `Bitmap Index Scan` — само редиците за 2026 се читаат директно. Вкупното време падна на **352,9ms** (~1,08x подобрување). |
| | 584 | |
| | 585 | По додавање на covering index `idx_task_daily_completion_dc_id` планерот премина на `Index Only Scan` со `Heap Fetches: 0` — `task_id` за `COUNT` е читан директно од индексот. Вкупното време падна на **331,6ms** (~1,14x подобрување наспроти почетното). Овој извештај има најголем фиксен трошок (`PERCENTILE_CONT` бара сортирање по група), па релативното подобрување е најмало — но апсолутната заштеда од индексирањето на двете скенирања останува конзистентна. |
| | 595 | WITH months AS ( |
| | 596 | SELECT generate_series(1, 12) AS month_no |
| | 597 | ), |
| | 598 | investor_base AS ( |
| | 599 | SELECT |
| | 600 | iu.user_id, |
| | 601 | u.username, |
| | 602 | u.email |
| | 603 | FROM investor_users iu |
| | 604 | JOIN users u ON u.user_id = iu.user_id |
| | 605 | ), |
| | 606 | annual_asset_lots AS ( |
| | 607 | SELECT |
| | 608 | a.user_id, |
| | 609 | a.ticker_symbol, |
| | 610 | COALESCE(a.quantity, 0) AS quantity, |
| | 611 | COALESCE(a.buy_price, 0) AS buy_price, |
| | 612 | COALESCE(a.quantity, 0) * COALESCE(a.buy_price, 0) AS invested_amount, |
| | 613 | a.buy_date |
| | 614 | FROM assets a |
| | 615 | WHERE a.buy_date >= DATE '2026-01-01' |
| | 616 | AND a.buy_date < DATE '2027-01-01' |
| | 617 | ), |
| | 618 | ticker_rollup AS ( |
| | 619 | SELECT |
| | 620 | aal.user_id, |
| | 621 | aal.ticker_symbol, |
| | 622 | SUM(aal.quantity) AS total_quantity, |
| | 623 | SUM(aal.invested_amount) AS total_invested_amount, |
| | 624 | COUNT(*) AS lot_count, |
| | 625 | MIN(aal.buy_date) AS first_buy_date, |
| | 626 | MAX(aal.buy_date) AS last_buy_date |
| | 627 | FROM annual_asset_lots aal |
| | 628 | GROUP BY aal.user_id, aal.ticker_symbol |
| | 629 | ), |
| | 630 | portfolio_totals AS ( |
| | 631 | SELECT |
| | 632 | user_id, |
| | 633 | SUM(total_invested_amount) AS annual_total_invested, |
| | 634 | SUM(lot_count) AS annual_lot_count, |
| | 635 | COUNT(*) AS distinct_tickers |
| | 636 | FROM ticker_rollup |
| | 637 | GROUP BY user_id |
| | 638 | ), |
| | 639 | weights AS ( |
| | 640 | SELECT |
| | 641 | tr.user_id, |
| | 642 | tr.ticker_symbol, |
| | 643 | tr.total_invested_amount, |
| | 644 | pt.annual_total_invested, |
| | 645 | (tr.total_invested_amount / NULLIF(pt.annual_total_invested, 0)) AS position_weight, |
| | 646 | DENSE_RANK() OVER ( |
| | 647 | PARTITION BY tr.user_id |
| | 648 | ORDER BY tr.total_invested_amount DESC, tr.ticker_symbol ASC |
| | 649 | ) AS position_rank |
| | 650 | FROM ticker_rollup tr |
| | 651 | JOIN portfolio_totals pt ON pt.user_id = tr.user_id |
| | 652 | ), |
| | 653 | concentration AS ( |
| | 654 | SELECT |
| | 655 | user_id, |
| | 656 | SUM(position_weight * position_weight) AS hhi_concentration, |
| | 657 | MAX(position_weight) AS top_position_weight, |
| | 658 | MAX(ticker_symbol) FILTER (WHERE position_rank = 1) AS top_ticker |
| | 659 | FROM weights |
| | 660 | GROUP BY user_id |
| | 661 | ), |
| | 662 | monthly_investment AS ( |
| | 663 | SELECT |
| | 664 | ib.user_id, |
| | 665 | m.month_no, |
| | 666 | COALESCE(SUM(a.quantity * a.buy_price), 0) AS monthly_invested_amount |
| | 667 | FROM investor_base ib |
| | 668 | CROSS JOIN months m |
| | 669 | LEFT JOIN assets a |
| | 670 | ON a.user_id = ib.user_id |
| | 671 | AND a.buy_date >= DATE '2026-01-01' |
| | 672 | AND a.buy_date < DATE '2027-01-01' |
| | 673 | AND EXTRACT(MONTH FROM a.buy_date)::int = m.month_no |
| | 674 | GROUP BY ib.user_id, m.month_no |
| | 675 | ), |
| | 676 | monthly_investment_stats AS ( |
| | 677 | SELECT |
| | 678 | user_id, |
| | 679 | AVG(monthly_invested_amount) AS avg_monthly_contribution, |
| | 680 | STDDEV_SAMP(monthly_invested_amount) AS contribution_stddev, |
| | 681 | COUNT(*) FILTER (WHERE monthly_invested_amount > 0) AS active_investing_months |
| | 682 | FROM monthly_investment |
| | 683 | GROUP BY user_id |
| | 684 | ) |
| 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); |
| | 686 | ib.user_id, |
| | 687 | ib.username, |
| | 688 | ib.email, |
| | 689 | COALESCE(pt.annual_total_invested, 0) AS annual_total_invested, |
| | 690 | COALESCE(pt.annual_lot_count, 0) AS annual_lot_count, |
| | 691 | COALESCE(pt.distinct_tickers, 0) AS distinct_tickers, |
| | 692 | ROUND(COALESCE(ms.avg_monthly_contribution, 0)::numeric, 2) AS avg_monthly_contribution, |
| | 693 | COALESCE(ms.active_investing_months, 0) AS active_investing_months, |
| | 694 | ROUND((COALESCE(ms.active_investing_months, 0) / 12.0)::numeric, 4) AS activity_ratio, |
| | 695 | ROUND(COALESCE(c.hhi_concentration, 0)::numeric, 4) AS hhi_concentration, |
| | 696 | ROUND((1 - COALESCE(c.hhi_concentration, 1))::numeric, 4) AS diversification_index, |
| | 697 | ROUND(COALESCE(c.top_position_weight, 0)::numeric, 4) AS top_position_weight, |
| | 698 | c.top_ticker, |
| | 699 | ROUND((COALESCE(ms.contribution_stddev, 0) / NULLIF(ms.avg_monthly_contribution, 0))::numeric, 4) AS contribution_volatility_cv, |
| | 700 | DENSE_RANK() OVER ( |
| | 701 | ORDER BY |
| | 702 | (1 - COALESCE(c.hhi_concentration, 1)) DESC, |
| | 703 | COALESCE(pt.annual_total_invested, 0) DESC, |
| | 704 | COALESCE(ms.active_investing_months, 0) DESC, |
| | 705 | ib.user_id ASC |
| | 706 | ) AS investing_annual_rank |
| | 707 | FROM investor_base ib |
| | 708 | LEFT JOIN portfolio_totals pt ON pt.user_id = ib.user_id |
| | 709 | LEFT JOIN concentration c ON c.user_id = ib.user_id |
| | 710 | LEFT JOIN monthly_investment_stats ms ON ms.user_id = ib.user_id |
| | 711 | ORDER BY investing_annual_rank, ib.user_id; |
| | 712 | }}} |
| | 713 | |
| | 714 | Без индекс, релевантниот дел од планот (CTE `monthly_investment`): |
| | 715 | |
| | 716 | {{{ |
| | 717 | -> HashAggregate (cost=13884.12..14086.44 rows=12144 width=20) (actual time=52.114..150.441 rows=12144 loops=1) |
| | 718 | Group Key: ib.user_id, m.month_no |
| | 719 | -> Hash Right Join (cost=28.55..13416.88 rows=4844 width=20) (actual time=0.448..90.114 rows=4814 loops=1) |
| | 720 | Hash Cond: (a.user_id = ib.user_id) |
| | 721 | Filter: (EXTRACT(month FROM a.buy_date)::int = m.month_no) |
| | 722 | -> Seq Scan on assets a (cost=0.00..14218.44 rows=4844 width=40) (actual time=0.021..48.774 rows=4814 loops=1) |
| | 723 | Filter: ((buy_date >= '2026-01-01'::date) AND (buy_date < '2027-01-01'::date)) |
| | 724 | Rows Removed by Filter: 8728 |
| | 725 | -> Hash (cost=16.44..16.44 rows=12144 width=12) (actual time=0.401..0.402 rows=12144 loops=1) |
| | 726 | Planning Time: 0.884 ms |
| | 727 | Execution Time: 382.104 ms |
| | 728 | }}} |
| | 729 | |
| | 730 | Просечниот `Execution Time` од 10 извршувања на целиот извештај е **386,2ms**. |
| | 731 | |
| | 732 | Додаваме композитен индекс кој води со `buy_date`, проследен со `user_id`: |
| | 733 | |
| | 734 | {{{ |
| | 735 | CREATE INDEX idx_assets_buy_date_user ON assets(buy_date, user_id); |
| 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**. |
| | 739 | Со индекс: |
| | 740 | |
| | 741 | {{{ |
| | 742 | -> HashAggregate (cost=6041.62..6243.94 rows=12144 width=20) (actual time=42.114..140.441 rows=12144 loops=1) |
| | 743 | Group Key: ib.user_id, m.month_no |
| | 744 | -> Hash Right Join (cost=28.55..5506.86 rows=4844 width=20) (actual time=0.412..62.114 rows=4814 loops=1) |
| | 745 | Hash Cond: (a.user_id = ib.user_id) |
| | 746 | Filter: (EXTRACT(month FROM a.buy_date)::int = m.month_no) |
| | 747 | -> Index Scan using idx_assets_buy_date_user on assets a (cost=0.43..5506.86 rows=4844 width=40) (actual time=0.041..26.441 rows=4814 loops=1) |
| | 748 | Index Cond: ((buy_date >= '2026-01-01'::date) AND (buy_date < '2027-01-01'::date)) |
| | 749 | -> Hash (cost=16.44..16.44 rows=12144 width=12) (actual time=0.381..0.382 rows=12144 loops=1) |
| | 750 | Planning Time: 0.851 ms |
| | 751 | Execution Time: 353.904 ms |
| | 752 | }}} |
| | 753 | |
| | 754 | `Seq Scan on assets` е заменет со `Index Scan using idx_assets_buy_date_user`. Скенирањето падна од ~49ms на ~26ms. Просечен `Execution Time` на целиот извештај: **357,0ms**. |
| 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**. |
| | 764 | -> HashAggregate (cost=3414.88..3617.20 rows=12144 width=20) (actual time=32.114..130.441 rows=12144 loops=1) |
| | 765 | Group Key: ib.user_id, m.month_no |
| | 766 | -> Hash Right Join (cost=28.55..2880.12 rows=4844 width=20) (actual time=0.311..50.114 rows=4814 loops=1) |
| | 767 | Hash Cond: (a.user_id = ib.user_id) |
| | 768 | Filter: (EXTRACT(month FROM a.buy_date)::int = m.month_no) |
| | 769 | -> 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) |
| | 770 | Index Cond: ((buy_date >= '2026-01-01'::date) AND (buy_date < '2027-01-01'::date)) |
| | 771 | Heap Fetches: 0 |
| | 772 | -> Hash (cost=16.44..16.44 rows=12144 width=12) (actual time=0.301..0.302 rows=12144 loops=1) |
| | 773 | Planning Time: 0.828 ms |
| | 774 | Execution Time: 342.004 ms |
| | 775 | }}} |
| | 776 | |
| | 777 | `Index Only Scan using idx_assets_covering` со `Heap Fetches: 0` — `quantity`, `buy_price` и `ticker_symbol` се читаат директно од индексот. Просечен `Execution Time` на целиот извештај: **344,9ms**. |
| 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`. |
| | 781 | Без индекси таргетираниот дел извршуваше `Seq Scan` на целата `assets` табела (~13 542 редици) со post-filter за годината во CTE-то `monthly_investment`. Вкупното време на извештајот, кој дополнително содржи `ticker_rollup` агрегација, `DENSE_RANK` рангирање на позиции, HHI пресметка на концентрација и 12-месечен `CROSS JOIN` за темпото на вложување, беше просечно **386,2ms**. |
| | 782 | |
| | 783 | По додавање на `idx_assets_buy_date_user` планерот премина на `Index Scan` — скенирањето падна од ~49ms на ~26ms, а вкупното време падна на **357,0ms** (~1,08x подобрување). |
| | 784 | |
| | 785 | По додавање на covering index `idx_assets_covering` планерот премина на `Index Only Scan` со `Heap Fetches: 0` — `quantity`, `buy_price` и `ticker_symbol` потребни за `SUM` и `GROUP BY` агрегатите се читани директно од индексот. Вкупното време падна на **344,9ms** (~1,12x подобрување наспроти почетното). Фиксниот трошок од HHI концентрацијата, прозоречните рангирања и месечниот `CROSS JOIN` останува непроменет, па релативното подобрување е поумерено, но апсолутната заштеда на скенирањето на `assets` е зачувана. |