wiki:otherdevelopment

Version 11 (modified by 233062, 6 days ago) ( diff )

--

Други Развојни Активности

Анализа на перформанси

Начинот на тестирање е следниот:

  • Пред додавање на индексот ги извршуваме прашалниците 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: 0amount е читан директно од индексот без посета на 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: 0duration и 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: 0task_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: 0quantity, 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<User, Long> {
    Optional<User> findByEmail(String email);
    Optional<User> 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<TrainingSession> findByTrainingUser_UserIdAndDateIn(
    Long userId,
    Collection<LocalDate> 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;
    }
Note: See TracWiki for help on using the wiki.