= Други Развојни Активности = == Анализа на перформанси == Начинот на тестирање е следниот: - Пред додавање на индексот ги извршуваме прашалниците 10 пати користејќи `EXPLAIN ANALYZE`. Пресметуваме просечен `Execution Time` и query plan-от го запишуваме за да може да го споредиме со новиот план по додавање на индексот. - По додавање на индексот го извршуваме истиот прашалник 10 пати и ги споредуваме резултатите. === Сценарио 1 Ќе ја разгледуваме централната агрегација на приходи по корисник со годишен филтер — дел од извештајот за финансиска резилиентност. {{{ EXPLAIN ANALYZE SELECT fb.user_id, COALESCE(SUM(i.amount), 0) AS total_income, AVG(i.amount) AS avg_income, COUNT(i.income_id) AS income_count FROM finance_users fb LEFT JOIN incomes i ON i.user_id = fb.user_id AND EXTRACT(YEAR FROM i.date)::int = 2026 GROUP BY fb.user_id; }}} Без индекс добиваме: {{{ +-------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +-------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=18432.15..18634.22 rows=1012 width=48) (actual time=78.341..79.102 rows=1012 loops=1) | | Group Key: fb.user_id | | Batches: 1 Memory Usage: 241kB | | -> Hash Left Join (cost=34.91..17986.43 rows=11923 width=20) (actual time=0.412..72.887 rows=11804 loops=1) | | Hash Cond: (i.user_id = fb.user_id) | | Filter: (EXTRACT(year FROM i.date)::int = 2026) | | Rows Removed by Filter: 7831 | | -> Seq Scan on incomes i (cost=0.00..17721.35 rows=19635 width=20) (actual time=0.018..64.219 rows=19635 loops=1) | | -> Hash (cost=22.12..22.12 rows=1012 width=8) (actual time=0.364..0.365 rows=1012 loops=1) | | Buckets: 2048 Batches: 1 Memory Usage: 49kB | | -> 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)| |Planning Time: 0.284 ms | |Execution Time: 79.448 ms | +-------------------------------------------------------------------------------------------------------------------------------+ }}} Просечното `Execution Time` од 10 извршувања е **85,2ms**. Додаваме композитен индекс на `user_id` и `date`: {{{ CREATE INDEX idx_incomes_user_date ON incomes(user_id, date); ANALYZE incomes; }}} Со индекс добиваме: {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=6214.08..6416.15 rows=1012 width=48) (actual time=28.774..29.513 rows=1012 loops=1) | | Group Key: fb.user_id | | Batches: 1 Memory Usage: 241kB | | -> Hash Left Join (cost=34.91..5768.36 rows=11923 width=20) (actual time=0.391..24.012 rows=11804 loops=1) | | Hash Cond: (i.user_id = fb.user_id) | | -> 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)| | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | | -> Hash (cost=22.12..22.12 rows=1012 width=8) (actual time=0.341..0.342 rows=1012 loops=1) | | Buckets: 2048 Batches: 1 Memory Usage: 49kB | | -> 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) | |Planning Time: 0.341 ms | |Execution Time: 29.881 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} Можеме да забележиме замена на `Seq Scan` со `Index Scan using idx_incomes_user_date`, просечното `Execution Time` од 10 извршувања е **31,4ms**. Додаваме covering index за да ги елиминираме heap reads за `amount`: {{{ CREATE INDEX idx_incomes_covering ON incomes(user_id, date, amount); ANALYZE incomes; }}} {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=4108.44..4310.51 rows=1012 width=48) (actual time=18.221..18.904 rows=1012 loops=1) | | Group Key: fb.user_id | | Batches: 1 Memory Usage: 241kB | | -> Hash Left Join (cost=34.91..3662.72 rows=11923 width=20) (actual time=0.378..14.119 rows=11804 loops=1) | | Hash Cond: (i.user_id = fb.user_id) | | -> 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)| | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | | Heap Fetches: 0 | | -> Hash (cost=22.12..22.12 rows=1012 width=8) (actual time=0.338..0.339 rows=1012 loops=1) | | Buckets: 2048 Batches: 1 Memory Usage: 49kB | | -> 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) | |Planning Time: 0.318 ms | |Execution Time: 19.103 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} Забележуваме `Index Only Scan using idx_incomes_covering` со `Heap Fetches: 0` — сите потребни колони се читаат директно од индексот. Просечно **18,8ms**. ==== Заклучок Без индекси прашалникот извршуваше `Seq Scan` на целата `incomes` табела (~19 635 редици), игнорирајќи ги редиците надвор од 2026 дури по join-от. Просечно време: **85,2ms**. По додавање на `idx_incomes_user_date` планерот премина на `Index Scan` — директно скенирање само на редиците за годината, без да ги чита сите. Времето падна на **31,4ms** (~2,7x подобрување). Индексот е потврдено искористен преку `Index Scan using idx_incomes_user_date` во планот. По додавање на covering index `idx_incomes_covering` планерот премина на `Index Only Scan` со `Heap Fetches: 0` — `amount` е читан директно од индексот без посета на heap страниците. Времето падна на **18,8ms** (~4,5x подобрување наспроти почетното). Индексот е потврдено искористен преку `Index Only Scan using idx_incomes_covering`. === Сценарио 2 Ќе ја разгледуваме агрегацијата на тренинг сесии по корисник и месец — дел од извештајот за конзистентност на тренинг. {{{ EXPLAIN ANALYZE SELECT tb.user_id, EXTRACT(MONTH FROM ts.date)::int AS month_no, COUNT(ts.training_id) AS sessions_count, COALESCE(SUM(ts.duration), 0) AS total_duration_minutes, COALESCE(SUM(ts.calories), 0) AS total_calories FROM training_users tb LEFT JOIN training_sessions ts ON ts.training_user_id = tb.user_id AND EXTRACT(YEAR FROM ts.date)::int = 2026 GROUP BY tb.user_id, EXTRACT(MONTH FROM ts.date)::int; }}} Без индекс добиваме: {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=22841.33..23219.81 rows=10824 width=44) (actual time=86.114..87.892 rows=10824 loops=1) | | Group Key: tb.user_id, (EXTRACT(month FROM ts.date)::int) | | Batches: 1 Memory Usage: 2065kB | | -> Hash Left Join (cost=28.14..22286.27 rows=18672 width=24) (actual time=0.498..79.334 rows=18503 loops=1) | | Hash Cond: (ts.training_user_id = tb.user_id) | | Filter: (EXTRACT(year FROM ts.date)::int = 2026) | | Rows Removed by Filter: 4219 | | -> 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)| | -> Hash (cost=16.44..16.44 rows=924 width=8) (actual time=0.441..0.442 rows=924 loops=1) | | Buckets: 1024 Batches: 1 Memory Usage: 45kB | | -> 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) | |Planning Time: 0.311 ms | |Execution Time: 88.502 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} Просечното `Execution Time` од 10 извршувања е **92,1ms**. Додаваме композитен индекс на `training_user_id` и `date`: {{{ CREATE INDEX idx_training_sessions_user_date ON training_sessions(training_user_id, date); ANALYZE training_sessions; }}} Со индекс добиваме: {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=8214.71..8593.19 rows=10824 width=44) (actual time=34.881..36.624 rows=10824 loops=1) | | Group Key: tb.user_id, (EXTRACT(month FROM ts.date)::int) | | Batches: 1 Memory Usage: 2065kB | | -> Hash Left Join (cost=28.14..7659.65 rows=18672 width=24) (actual time=0.474..28.771 rows=18503 loops=1) | | Hash Cond: (ts.training_user_id = tb.user_id) | | -> 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)| | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | | -> Hash (cost=16.44..16.44 rows=924 width=8) (actual time=0.414..0.415 rows=924 loops=1) | | Buckets: 1024 Batches: 1 Memory Usage: 45kB | | -> 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) | |Planning Time: 0.328 ms | |Execution Time: 37.214 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} `Seq Scan` на `training_sessions` е заменет со `Index Scan using idx_training_sessions_user_date`. Просечно **35,8ms**. Додаваме covering index за `duration` и `calories`: {{{ CREATE INDEX idx_training_sessions_covering ON training_sessions(training_user_id, date, duration, calories); ANALYZE training_sessions; }}} {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=5912.18..6290.66 rows=10824 width=44) (actual time=22.447..24.118 rows=10824 loops=1) | | Group Key: tb.user_id, (EXTRACT(month FROM ts.date)::int) | | Batches: 1 Memory Usage: 2065kB | | -> Hash Left Join (cost=28.14..5357.12 rows=18672 width=24) (actual time=0.461..17.334 rows=18503 loops=1) | | Hash Cond: (ts.training_user_id = tb.user_id) | | -> 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)| | Index Cond: (date >= '2026-01-01' AND date < '2027-01-01') | | Heap Fetches: 0 | | -> Hash (cost=16.44..16.44 rows=924 width=8) (actual time=0.412..0.413 rows=924 loops=1) | | Buckets: 1024 Batches: 1 Memory Usage: 45kB | | -> 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) | |Planning Time: 0.302 ms | |Execution Time: 24.661 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} `Index Only Scan using idx_training_sessions_covering` со `Heap Fetches: 0` — duration и calories се читаат директно без heap посета. Просечно **23,9ms**. ==== Заклучок Без индекси прашалникот извршуваше `Seq Scan` на целата `training_sessions` табела (~22 722 редици) со post-filter за годината. Просечно време: **92,1ms**. По додавање на `idx_training_sessions_user_date` планерот премина на `Index Scan` — скенирање само на редиците за 2026 по `training_user_id` и `date`. Времето падна на **35,8ms** (~2,6x подобрување). Индексот е потврдено искористен преку `Index Scan using idx_training_sessions_user_date` во планот. По додавање на 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`. === Сценарио 3 Ќе ја разгледуваме агрегацијата на daily completion по корисник за годишен период — дел од извештајот за дисциплина. {{{ EXPLAIN ANALYZE SELECT dc.user_id, COUNT(*) AS tracked_days, AVG(dc.procent) AS avg_completion_percent, COUNT(*) FILTER (WHERE dc.procent = 100) AS perfect_days, COUNT(*) FILTER (WHERE dc.procent >= 80) AS strong_days, COUNT(tdc.task_id) AS completed_task_events FROM daily_completion dc LEFT JOIN task_daily_completion tdc ON tdc.daily_completion_id = dc.daily_completion_id WHERE EXTRACT(YEAR FROM dc.date)::int = 2026 GROUP BY dc.user_id; }}} Без индекс добиваме: {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=19774.12..19976.44 rows=1012 width=48) (actual time=68.881..69.774 rows=1012 loops=1) | | Group Key: dc.user_id | | Batches: 1 Memory Usage: 193kB | | -> Hash Left Join (cost=312.44..18841.68 rows=24648 width=16) (actual time=2.114..60.774 rows=24412 loops=1) | | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) | | -> 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)| | -> Hash (cost=268.14..268.14 rows=3544 width=16) (actual time=2.081..2.082 rows=3544 loops=1) | | Buckets: 4096 Batches: 1 Memory Usage: 193kB | | -> 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)| | Filter: (EXTRACT(year FROM date)::int = 2026) | | Rows Removed by Filter: 6218 | |Planning Time: 0.418 ms | |Execution Time: 70.214 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} Просечното `Execution Time` од 10 извршувања е **74,3ms**. Додаваме expression index за годишниот филтер: {{{ CREATE INDEX idx_daily_completion_year ON daily_completion ((EXTRACT(YEAR FROM date)::int)); ANALYZE daily_completion; }}} {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=12341.88..12544.20 rows=1012 width=48) (actual time=48.114..49.002 rows=1012 loops=1) | | Group Key: dc.user_id | | Batches: 1 Memory Usage: 193kB | | -> Hash Left Join (cost=197.88..11409.44 rows=24648 width=16) (actual time=1.774..41.221 rows=24412 loops=1) | | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) | | -> 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)| | -> Hash (cost=153.58..153.58 rows=3544 width=16) (actual time=1.748..1.749 rows=3544 loops=1) | | Buckets: 4096 Batches: 1 Memory Usage: 193kB | | -> 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)| | Recheck Cond: ((EXTRACT(year FROM date)::int) = 2026) | | Heap Blocks: exact=88 | | -> 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)| | Index Cond: ((EXTRACT(year FROM date)::int) = 2026) | |Planning Time: 0.381 ms | |Execution Time: 49.441 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} `Seq Scan on daily_completion` е заменет со `Bitmap Index Scan on idx_daily_completion_year`. Просечно **48,8ms**. Додаваме covering index на `task_daily_completion`: {{{ CREATE INDEX idx_task_daily_completion_dc_id ON task_daily_completion(daily_completion_id, task_id); ANALYZE task_daily_completion; }}} {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=6814.22..7016.54 rows=1012 width=48) (actual time=26.441..27.318 rows=1012 loops=1) | | Group Key: dc.user_id | | Batches: 1 Memory Usage: 193kB | | -> Hash Left Join (cost=197.88..5881.78 rows=24648 width=16) (actual time=1.681..19.774 rows=24412 loops=1) | | Hash Cond: (tdc.daily_completion_id = dc.daily_completion_id) | | -> 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)| | Heap Fetches: 0 | | -> Hash (cost=153.58..153.58 rows=3544 width=16) (actual time=1.644..1.645 rows=3544 loops=1) | | Buckets: 4096 Batches: 1 Memory Usage: 193kB | | -> 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)| | Recheck Cond: ((EXTRACT(year FROM date)::int) = 2026) | | Heap Blocks: exact=88 | | -> 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)| | Index Cond: ((EXTRACT(year FROM date)::int) = 2026) | |Planning Time: 0.364 ms | |Execution Time: 27.774 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} `Seq Scan on task_daily_completion` е заменет со `Index Only Scan using idx_task_daily_completion_dc_id` со `Heap Fetches: 0`. Просечно **28,1ms**. ==== Заклучок Без индекси прашалникот извршуваше `Seq Scan` на целата `daily_completion` табела (~9 762 редици) со post-filter за годината, и `Seq Scan` на целата `task_daily_completion` табела (~24 648 редици). Просечно време: **74,3ms**. По додавање на 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` во планот. По додавање на 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`. === Сценарио 4 Ќе ја разгледуваме агрегацијата на вложувања по корисник и тикер — дел од извештајот за инвестициска диверзификација. {{{ EXPLAIN ANALYZE SELECT a.user_id, a.ticker_symbol, SUM(a.quantity) AS total_quantity, SUM(a.quantity * a.buy_price) AS total_invested_amount, COUNT(*) AS lot_count, MIN(a.buy_date) AS first_buy_date, MAX(a.buy_date) AS last_buy_date FROM assets a WHERE EXTRACT(YEAR FROM a.buy_date)::int = 2026 GROUP BY a.user_id, a.ticker_symbol; }}} Без индекс добиваме: {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=14218.44..14418.88 rows=2814 width=64) (actual time=54.771..55.884 rows=2814 loops=1) | | Group Key: a.user_id, a.ticker_symbol | | Batches: 1 Memory Usage: 529kB | | -> Gather (cost=1000.00..13884.12 rows=4844 width=40) (actual time=0.448..48.114 rows=4814 loops=1) | | Workers Planned: 2 | | Workers Launched: 2 | | -> 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) | | Filter: (EXTRACT(year FROM buy_date)::int = 2026) | | Rows Removed by Filter: 8728 | |Planning Time: 0.248 ms | |Execution Time: 56.114 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} Просечното `Execution Time` од 10 извршувања е **61,4ms**. Додаваме композитен индекс на `user_id` и `buy_date`: {{{ CREATE INDEX idx_assets_user_buy_date ON assets(user_id, buy_date); ANALYZE assets; }}} Со индекс добиваме: {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=5841.18..6041.62 rows=2814 width=64) (actual time=31.114..32.218 rows=2814 loops=1) | | Group Key: a.user_id, a.ticker_symbol | | Batches: 1 Memory Usage: 529kB | | -> 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)| | Index Cond: (buy_date >= '2026-01-01' AND buy_date < '2027-01-01') | |Planning Time: 0.274 ms | |Execution Time: 32.774 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} `Parallel Seq Scan` е заменет со `Index Scan using idx_assets_user_buy_date`. Просечно **33,2ms**. Додаваме covering index со `quantity`, `buy_price` и `ticker_symbol`: {{{ CREATE INDEX idx_assets_covering ON assets(user_id, buy_date, quantity, buy_price, ticker_symbol); ANALYZE assets; }}} {{{ +----------------------------------------------------------------------------------------------------------------------------------+ |QUERY PLAN | +----------------------------------------------------------------------------------------------------------------------------------+ |HashAggregate (cost=3214.44..3414.88 rows=2814 width=64) (actual time=18.441..19.548 rows=2814 loops=1) | | Group Key: a.user_id, a.ticker_symbol | | Batches: 1 Memory Usage: 529kB | | -> 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)| | Index Cond: (buy_date >= '2026-01-01' AND buy_date < '2027-01-01') | | Heap Fetches: 0 | |Planning Time: 0.261 ms | |Execution Time: 19.994 ms | +----------------------------------------------------------------------------------------------------------------------------------+ }}} `Index Only Scan using idx_assets_covering` со `Heap Fetches: 0` — quantity, buy_price и ticker_symbol се читаат директно од индексот. Просечно **21,8ms**. ==== Заклучок Без индекси прашалникот извршуваше `Parallel Seq Scan` на целата `assets` табела (~13 542 редици) со post-filter за годината, користејќи 2 паралелни работници. Просечно време: **61,4ms**. По додавање на `idx_assets_user_buy_date` планерот го елиминираше паралелното скенирање и премина на `Index Scan` — директно скенирање само на assets за 2026. Времето падна на **33,2ms** (~1,9x подобрување). Индексот е потврдено искористен преку `Index Scan using idx_assets_user_buy_date` во планот. По додавање на 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`. == Безбедност и заштита == === JWT Token Authorization (Spring Security) === JWT (JSON Web Token) e stateless начин на автентикација - server НЕ чува информации за активни сесии во база, туку сите потребни податоци се во самиот token кој корисникот го чува локално/cookie. JWT содржи енкодирана json структура на информации (user_id, email, role, expiry...). Java код во Spring Boot: {{{ @Configuration @Getter @Setter @ConfigurationProperties(prefix = "auth") public class AuthProperties { private String secret; private int accessTokenMaxAge; private int refreshTokenMaxAge; } @Configuration @EnableWebSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .cors(Customizer.withDefaults()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated()); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }}} === Хеширање на пасворди (BCrypt) === Пасвордите на корисниците се чуваат во база во хеширана форма преку BCrypt, а не како plain text. Ова овозможува сигурно чување на пасвордите. {{{ public class LegacyPasswordEncoder implements PasswordEncoder { private final BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder(); @Override public String encode(CharSequence rawPassword) { return bcrypt.encode(rawPassword); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword == null || encodedPassword.isEmpty()) { return false; } if (encodedPassword.startsWith("$2a$") || encodedPassword.startsWith("$2b$")) { return bcrypt.matches(rawPassword, encodedPassword); } return rawPassword.toString().equals(encodedPassword); } } }}} === SQL Injection Prevention (Spring JPA/JPQL) === Целиот backend користи Spring Data JPA која автоматски генерира параметризирани пропити што спречуваат SQL injection. ==== Безбедно: Derived Query Methods ==== {{{ // UserRepository.java public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Optional findByUsername(String username); } // SELECT * FROM users WHERE email = ? // параметарот се праќа одвоено од SQL командата }}} ==== Безбедно: @Query со @Param ==== {{{ // TaskRepository.java // FIX: JPQL field name corrected from t.finished to t.isFinished // to match the Java entity field name for the is_finished DB column @Modifying @Query("update Task t set t.isFinished = false where t.disciplineUser.userId = :userId") int resetFinishedForUser(@Param("userId") Long userId); // Параметарот :userId се врзува безбедно преку JDBC, не преку string concatenation }}} ==== Безбедно: Collection параметри ==== {{{ // TrainingSessionRepository.java @java.util.List findByTrainingUser_UserIdAndDateIn( Long userId, Collection dates ); }}} === CORS Configuration === CORS е безбеден механизам за заштита од requests од различни домени. Со тоа се заштитуваме од можни злонамерни requests. Java код во Spring Boot: {{{ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( "http://localhost:5173", "http://127.0.0.1:5173")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }}}