| Version 4 (modified by , 19 hours ago) ( diff ) |
|---|
Останати теми: Безбедност, перформанси и одржување на базата
1. Безбедност на ниво на база
Имплементирани се следните безбедносни механизми директно при комуникацијата со базата, со што се намалува нападниот вектор независно од самиот NextJS сервер:
- Заштита од SQL вбризгување(SQL injection): Клиентската библиотека
postgres.jsкористи параметризирани прашалници преку Tagged Template Literals(на примерsql\...\) по дизајн. Ова спречува директно извршување на малициозен SQL код преку корисничките влезни параметри, бидејќи влезот е секогаш парсиран како податок, а не како команда. - Енкриптирана комуникација(SSL/TLS): Како што е наведено во конфигурацискиот фајл(app/lib/db.ts#L11), конекцијата кон базата строго наметнува SSL сертификат преку параметарот
ssl: 'require'. Ова ја штити транзицијата на чувствителните податоци и лозинки од "man-in-the-middle" напади. - Безбедно поврзување со база: Конекциските стрингови никогаш не се чуваат во изворниот код. Тие се изолирани преку заштитени околински променливи (
POSTGRES_URL).
2. Перформанси и оптимизација
Анализата е направена со EXPLAIN (ANALYZE, BUFFERS), при што се споредува планот за извршување пред и по додавање на дополнителните индекси. Во почетната состојба базата ги содржи само индексите кои PostgreSQL автоматски ги креира за PRIMARY KEY и UNIQUE ограничувања. Овие индекси не се третираат како дополнителна оптимизација, туку како дел од основната релациска структура на базата.
Првично тестирање беше направено со мал број редови во табелите:
Во оваа состојба PostgreSQL користеше Seq Scan и по додавање на индексите. Тоа е очекувано однесување, бидејќи за табели со многу мал број редови целосното читање на табелата е поевтино од пристап преку индекс.
Поради тоа, за пореална анализа беше генериран поголем сет на тест-податоци, при што податоците се логички поврзани преку постоечките релации во базата.
По генерирање на тест-податоците, базата ја има следната состојба:
| Табела | Број на редови |
|---|---|
user | 103 |
transaction | 100012 |
transaction_account | 505 |
transaction_breakdown | 110011 |
tag | 31 |
tag_assigned_to_transaction | 125016 |
Оваа количина на податоци е доволна за PostgreSQL optimizer-от да започне да прави различни планови за извршување и да користи дел од предложените индекси кога тие се поисплатливи од Seq Scan.
Предложени дополнителни индекси
Следните индекси се предложени затоа што колоните често се користат во JOIN и WHERE услови во аналитичките извештаи:
CREATE INDEX IF NOT EXISTS idx_transaction_date ON "transaction"(date); CREATE INDEX IF NOT EXISTS idx_transaction_account_user_id ON transaction_account(user_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_transaction_id ON transaction_breakdown(transaction_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_account_id ON transaction_breakdown(transaction_account_id); CREATE INDEX IF NOT EXISTS idx_tag_assigned_transaction_id ON tag_assigned_to_transaction(transaction_id); CREATE INDEX IF NOT EXISTS idx_tag_assigned_tag_id ON tag_assigned_to_transaction(tag_id);
Индексот idx_transaction_date е наменет за извештаи кои филтрираат трансакции според временски период. Индексот idx_transaction_account_user_id е наменет за извештаи кои ги ограничуваат резултатите на конкретен корисник. Индексите на transaction_breakdown и tag_assigned_to_transaction се наменети за побрзо поврзување на трансакциите со сметки и тагови.
Сценарио 1: Трендови на трошење според тагови за корисник
Цел:
Овој извештај ја прикажува потрошувачката по тагови за конкретен корисник во последните шест месеци. Прашалникот е комплексен затоа што ги поврзува табелите tag_assigned_to_transaction, transaction, transaction_breakdown, transaction_account и tag, а дополнително врши филтрирање по корисник и датум, групирање по таг и месец, и сумирање на потрошената сума.
Анализиран SQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
tg.tag_name,
DATE_TRUNC('month', t.date) AS month,
SUM(tb.spent_amount) AS total_spent
FROM tag_assigned_to_transaction tat
JOIN "transaction" t
ON tat.transaction_id = t.transaction_id
JOIN transaction_breakdown tb
ON t.transaction_id = tb.transaction_id
JOIN transaction_account ta
ON tb.transaction_account_id = ta.transaction_account_id
JOIN tag tg
ON tat.tag_id = tg.tag_id
WHERE
ta.user_id = 5
AND t.date >= DATE_TRUNC('month', CURRENT_DATE - INTERVAL '6 MONTHS')
GROUP BY
tg.tag_name, month
ORDER BY
tg.tag_name, month;
Релевантни индекси за ова сценарио:
CREATE INDEX IF NOT EXISTS idx_transaction_date ON "transaction"(date); CREATE INDEX IF NOT EXISTS idx_transaction_account_user_id ON transaction_account(user_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_transaction_id ON transaction_breakdown(transaction_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_account_id ON transaction_breakdown(transaction_account_id); CREATE INDEX IF NOT EXISTS idx_tag_assigned_transaction_id ON tag_assigned_to_transaction(transaction_id); CREATE INDEX IF NOT EXISTS idx_tag_assigned_tag_id ON tag_assigned_to_transaction(tag_id);
Пред додавање на индексите:
GroupAggregate (cost=4898.54..4900.99 rows=98 width=52) (actual time=51.593..51.622 rows=7 loops=1)
Group Key: tg.tag_name, (date_trunc('month'::text, t.date))
Buffers: shared hit=4383
-> Sort (cost=4898.54..4898.79 rows=98 width=26) (actual time=51.574..51.582 rows=81 loops=1)
Sort Key: tg.tag_name, (date_trunc('month'::text, t.date))
Sort Method: quicksort Memory: 28kB
-> Hash Join (cost=2498.81..4895.30 rows=98 width=26) (actual time=28.248..51.522 rows=81 loops=1)
Hash Cond: (tat.tag_id = tg.tag_id)
-> Hash Join (cost=2497.11..4893.07 rows=98 width=18) (actual time=28.204..51.436 rows=81 loops=1)
Hash Cond: (tat.transaction_id = t.transaction_id)
-> Seq Scan on tag_assigned_to_transaction tat
(actual time=0.014..9.236 rows=125016 loops=1)
-> Hash
-> Nested Loop
-> Hash Join
Hash Cond: (tb.transaction_account_id = ta.transaction_account_id)
-> Seq Scan on transaction_breakdown tb
(actual time=0.012..8.429 rows=110011 loops=1)
-> Hash
-> Seq Scan on transaction_account ta
Filter: (user_id = 5)
Rows Removed by Filter: 500
-> Index Scan using transaction_pkey on transaction t
Index Cond: (transaction_id = tb.transaction_id)
Filter: (date >= date_trunc('month'::text, (CURRENT_DATE - '6 mons'::interval)))
-> Hash
-> Seq Scan on tag tg
Planning Time: 1.392 ms
Execution Time: 51.744 ms
По додавање на индексите:
GroupAggregate (cost=2490.16..2492.59 rows=97 width=52) (actual time=6.936..6.963 rows=7 loops=1)
Group Key: tg.tag_name, (date_trunc('month'::text, t.date))
Buffers: shared hit=3273 read=76
-> Sort (cost=2490.16..2490.40 rows=97 width=26) (actual time=6.921..6.928 rows=81 loops=1)
Sort Key: tg.tag_name, (date_trunc('month'::text, t.date))
Sort Method: quicksort Memory: 28kB
-> Nested Loop (cost=16.95..2486.96 rows=97 width=26) (actual time=0.155..6.835 rows=81 loops=1)
-> Nested Loop (cost=16.80..2480.67 rows=97 width=18) (actual time=0.135..6.624 rows=81 loops=1)
-> Nested Loop (cost=16.38..2443.39 rows=77 width=22) (actual time=0.104..5.104 rows=81 loops=1)
-> Nested Loop (cost=16.09..2050.95 rows=1089 width=10) (actual time=0.087..0.458 rows=1000 loops=1)
-> Bitmap Heap Scan on transaction_account ta
Recheck Cond: (user_id = 5)
-> Bitmap Index Scan on idx_transaction_account_user_id
Index Cond: (user_id = 5)
-> Bitmap Heap Scan on transaction_breakdown tb
Recheck Cond: (transaction_account_id = ta.transaction_account_id)
-> Bitmap Index Scan on idx_transaction_breakdown_account_id
Index Cond: (transaction_account_id = ta.transaction_account_id)
-> Index Scan using transaction_pkey on transaction t
Index Cond: (transaction_id = tb.transaction_id)
Filter: (date >= date_trunc('month'::text, (CURRENT_DATE - '6 mons'::interval)))
-> Index Scan using idx_tag_assigned_transaction_id on tag_assigned_to_transaction tat
Index Cond: (transaction_id = tb.transaction_id)
-> Memoize
Cache Key: tat.tag_id
-> Index Scan using tag_pkey on tag tg
Index Cond: (tag_id = tat.tag_id)
Planning Time: 1.738 ms
Execution Time: 7.055 ms
Споредба:
| Метрика | Пред индекси | По индекси |
|---|---|---|
| Planning Time | 1.392 ms | 1.738 ms |
| Execution Time | 51.744 ms | 7.055 ms |
| Подобрување на Execution Time | / | ~86.36% |
| Забрзување | / | ~7.33x |
| Тип на скенирање пред индекси | Seq Scan на големите табели | / |
| Тип на скенирање по индекси | / | Bitmap Index Scan, Bitmap Heap Scan, Index Scan
|
| Дали новите индекси се користат | Не | Да |
По додавање на индексите, PostgreSQL експлицитно ги користи:
idx_transaction_account_user_idпрекуBitmap Index Scanidx_transaction_breakdown_account_idпрекуBitmap Index Scanidx_tag_assigned_transaction_idпрекуIndex Scan
Индексот idx_transaction_date не е директно искористен во овој план, затоа што optimizer-от прво ги ограничува податоците преку корисничките сметки и breakdown записите, а потоа пристапува до трансакциите преку примарниот клуч transaction_pkey.
Заклучок:
Ова сценарио има најголемо подобрување. Времето на извршување се намали од 51.744 ms на 7.055 ms, што е приближно 86.36% подобрување, односно околу 7.33 пати побрзо извршување. Причината е што по додавање на индексите PostgreSQL повеќе не мора секвенцијално да ги чита transaction_account, transaction_breakdown и tag_assigned_to_transaction, туку директно ги лоцира релевантните записи преку индексите. Ова покажува дека индексите се корисни за извештаи кои филтрираат податоци за конкретен корисник и потоа ги поврзуваат трансакциите со тагови.
Сценарио 2: Сметки со највисоко вкупно трошење во изминатата година
Цел:
Овој извештај ги прикажува сметките со најголема вкупна потрошувачка во последната година. Прашалникот користи JOIN помеѓу transaction_breakdown, transaction и transaction_account, филтрирање по датум, SUM агрегација, GROUP BY, ORDER BY и LIMIT.
Анализиран SQL:
EXPLAIN (ANALYZE, BUFFERS) SELECT ta.account_name, COALESCE(SUM(tb.spent_amount), 0) AS total_spent FROM transaction_breakdown tb JOIN "transaction" t ON tb.transaction_id = t.transaction_id JOIN transaction_account ta ON tb.transaction_account_id = ta.transaction_account_id WHERE t.date >= NOW() - INTERVAL '1 YEAR' GROUP BY ta.account_name ORDER BY total_spent DESC LIMIT 10;
Релевантни индекси за ова сценарио:
CREATE INDEX IF NOT EXISTS idx_transaction_date ON "transaction"(date); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_transaction_id ON transaction_breakdown(transaction_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_account_id ON transaction_breakdown(transaction_account_id);
Пред додавање на индексите:
Limit (cost=4969.28..4969.31 rows=10 width=49) (actual time=73.712..73.717 rows=10 loops=1) Buffers: shared hit=1540 -> Sort (cost=4969.28..4970.54 rows=505 width=49) (actual time=73.711..73.714 rows=10 loops=1) Sort Key: (COALESCE(sum(tb.spent_amount), '0'::numeric)) DESC Sort Method: top-N heapsort Memory: 26kB -> HashAggregate (cost=4952.06..4958.37 rows=505 width=49) (actual time=73.611..73.658 rows=113 loops=1) Group Key: ta.account_name -> Hash Join (cost=2756.94..4883.26 rows=13760 width=23) (actual time=34.771..69.224 rows=13568 loops=1) Hash Cond: (tb.transaction_account_id = ta.transaction_account_id) -> Hash Join (cost=2740.57..4830.47 rows=13760 width=10) (actual time=34.564..66.184 rows=13568 loops=1) Hash Cond: (tb.transaction_id = t.transaction_id) -> Seq Scan on transaction_breakdown tb (actual time=0.012..8.780 rows=110011 loops=1) -> Hash -> Seq Scan on transaction t Filter: (date >= (now() - '1 year'::interval)) Rows Removed by Filter: 87672 -> Hash -> Seq Scan on transaction_account ta Planning Time: 0.538 ms Execution Time: 73.792 ms
По додавање на индексите:
Limit (cost=3675.91..3675.93 rows=10 width=49) (actual time=47.820..47.826 rows=10 loops=1) Buffers: shared hit=1540 read=36 -> Sort (cost=3675.91..3677.17 rows=505 width=49) (actual time=47.818..47.822 rows=10 loops=1) Sort Key: (COALESCE(sum(tb.spent_amount), '0'::numeric)) DESC Sort Method: top-N heapsort Memory: 26kB -> HashAggregate (cost=3658.68..3665.00 rows=505 width=49) (actual time=47.717..47.765 rows=113 loops=1) Group Key: ta.account_name -> Hash Join (cost=1463.44..3589.80 rows=13776 width=23) (actual time=9.049..43.449 rows=13568 loops=1) Hash Cond: (tb.transaction_account_id = ta.transaction_account_id) -> Hash Join (cost=1447.08..3536.98 rows=13776 width=10) (actual time=8.845..40.394 rows=13568 loops=1) Hash Cond: (tb.transaction_id = t.transaction_id) -> Seq Scan on transaction_breakdown tb (actual time=0.011..9.164 rows=110011 loops=1) -> Hash -> Bitmap Heap Scan on transaction t Recheck Cond: (date >= (now() - '1 year'::interval)) Heap Blocks: exact=834 -> Bitmap Index Scan on idx_transaction_date Index Cond: (date >= (now() - '1 year'::interval)) -> Hash -> Seq Scan on transaction_account ta Planning Time: 0.654 ms Execution Time: 47.902 ms
Споредба:
| Метрика | Пред индекси | По индекси |
|---|---|---|
| Planning Time | 0.538 ms | 0.654 ms |
| Execution Time | 73.792 ms | 47.902 ms |
| Подобрување на Execution Time | / | ~35.08% |
| Забрзување | / | ~1.54x |
| Тип на скенирање пред индекси | Seq Scan на transaction | / |
| Тип на скенирање по индекси | / | Bitmap Index Scan + Bitmap Heap Scan на transaction
|
| Дали новите индекси се користат | Не | Да |
По додавање на индексите, PostgreSQL го користи idx_transaction_date преку:
Bitmap Index Scan on idx_transaction_date Index Cond: (date >= (now() - '1 year'::interval))
Останатите табели, како transaction_breakdown и transaction_account, сè уште се читаат со Seq Scan, затоа што query-то пресметува агрегат за сите сметки, а не за еден конкретен корисник или една конкретна сметка.
Заклучок:
Времето на извршување се намали од 73.792 ms на 47.902 ms, што претставува приближно 35.08% подобрување. Ова подобрување е директно поврзано со индексот idx_transaction_date, бидејќи извештајот филтрира трансакции само од последната година. Пред индексот, PostgreSQL мораше да ја чита целата табела transaction и да отфрли 87672 редови преку filter. По додавање на индексот, PostgreSQL користи Bitmap Index Scan за побрзо да ги најде трансакциите од последната година.
Сценарио 3: Годишни трендови на трансакции за корисник
Цел:
Овој извештај ја прикажува потрошувачката по квартали за конкретен корисник во тековната година. Прашалникот користи JOIN помеѓу transaction_breakdown, transaction и transaction_account, филтрирање по корисник и временски период, SUM агрегации со CASE WHEN, GROUP BY и ORDER BY.
Во анализата условот за година е напишан како временски опсег, наместо само со EXTRACT(YEAR FROM t.date), за B-Tree индексот на date да може поефикасно да се користи кога табелата ќе порасне.
Анализиран SQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
ta.account_name,
SUM(CASE WHEN EXTRACT(QUARTER FROM t.date) = 1 THEN tb.spent_amount ELSE 0 END) AS q1_spent,
SUM(CASE WHEN EXTRACT(QUARTER FROM t.date) = 2 THEN tb.spent_amount ELSE 0 END) AS q2_spent,
SUM(CASE WHEN EXTRACT(QUARTER FROM t.date) = 3 THEN tb.spent_amount ELSE 0 END) AS q3_spent,
SUM(CASE WHEN EXTRACT(QUARTER FROM t.date) = 4 THEN tb.spent_amount ELSE 0 END) AS q4_spent
FROM transaction_breakdown tb
JOIN "transaction" t
ON tb.transaction_id = t.transaction_id
JOIN transaction_account ta
ON tb.transaction_account_id = ta.transaction_account_id
WHERE
ta.user_id = 5
AND t.date >= DATE_TRUNC('year', CURRENT_DATE)
AND t.date < DATE_TRUNC('year', CURRENT_DATE) + INTERVAL '1 YEAR'
GROUP BY
ta.account_name
ORDER BY
ta.account_name;
Релевантни индекси за ова сценарио:
CREATE INDEX IF NOT EXISTS idx_transaction_date ON "transaction"(date); CREATE INDEX IF NOT EXISTS idx_transaction_account_user_id ON transaction_account(user_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_transaction_id ON transaction_breakdown(transaction_id); CREATE INDEX IF NOT EXISTS idx_transaction_breakdown_account_id ON transaction_breakdown(transaction_account_id);
Пред додавање на индексите:
GroupAggregate (cost=2511.77..2514.21 rows=5 width=145) (actual time=26.493..26.495 rows=1 loops=1)
Group Key: ta.account_name
Buffers: shared hit=3706
-> Sort (cost=2511.77..2511.93 rows=67 width=31) (actual time=26.366..26.372 rows=70 loops=1)
Sort Key: ta.account_name
Sort Method: quicksort Memory: 28kB
-> Nested Loop (cost=11.67..2509.73 rows=67 width=31) (actual time=0.322..26.308 rows=70 loops=1)
-> Hash Join (cost=11.38..2103.69 rows=1089 width=27) (actual time=0.301..21.727 rows=1000 loops=1)
Hash Cond: (tb.transaction_account_id = ta.transaction_account_id)
-> Seq Scan on transaction_breakdown tb
(actual time=0.019..8.175 rows=110011 loops=1)
-> Hash
-> Seq Scan on transaction_account ta
Filter: (user_id = 5)
Rows Removed by Filter: 500
-> Index Scan using transaction_pkey on transaction t
Index Cond: (transaction_id = tb.transaction_id)
Filter: ((date >= date_trunc('year'::text, (CURRENT_DATE)::timestamp with time zone))
AND (date < (date_trunc('year'::text, (CURRENT_DATE)::timestamp with time zone) + '1 year'::interval)))
Rows Removed by Filter: 1
Planning Time: 0.495 ms
Execution Time: 26.571 ms
По додавање на индексите:
GroupAggregate (cost=2458.99..2461.40 rows=5 width=145) (actual time=5.086..5.088 rows=1 loops=1)
Group Key: ta.account_name
Buffers: shared hit=3023
-> Sort (cost=2458.99..2459.16 rows=66 width=31) (actual time=4.954..4.960 rows=70 loops=1)
Sort Key: ta.account_name
Sort Method: quicksort Memory: 28kB
-> Nested Loop (cost=16.38..2457.00 rows=66 width=31) (actual time=0.104..4.917 rows=70 loops=1)
-> Nested Loop (cost=16.09..2050.95 rows=1089 width=27) (actual time=0.086..0.420 rows=1000 loops=1)
-> Bitmap Heap Scan on transaction_account ta
Recheck Cond: (user_id = 5)
-> Bitmap Index Scan on idx_transaction_account_user_id
Index Cond: (user_id = 5)
-> Bitmap Heap Scan on transaction_breakdown tb
Recheck Cond: (transaction_account_id = ta.transaction_account_id)
-> Bitmap Index Scan on idx_transaction_breakdown_account_id
Index Cond: (transaction_account_id = ta.transaction_account_id)
-> Index Scan using transaction_pkey on transaction t
Index Cond: (transaction_id = tb.transaction_id)
Filter: ((date >= date_trunc('year'::text, (CURRENT_DATE)::timestamp with time zone))
AND (date < (date_trunc('year'::text, (CURRENT_DATE)::timestamp with time zone) + '1 year'::interval)))
Rows Removed by Filter: 1
Planning Time: 0.724 ms
Execution Time: 5.169 ms
Споредба:
| Метрика | Пред индекси | По индекси |
|---|---|---|
| Planning Time | 0.495 ms | 0.724 ms |
| Execution Time | 26.571 ms | 5.169 ms |
| Подобрување на Execution Time | / | ~80.55% |
| Забрзување | / | ~5.14x |
| Тип на скенирање пред индекси | Seq Scan на transaction_breakdown и transaction_account | / |
| Тип на скенирање по индекси | / | Bitmap Index Scan, Bitmap Heap Scan, Index Scan
|
| Дали новите индекси се користат | Не | Да |
По додавање на индексите, PostgreSQL ги користи:
idx_transaction_account_user_idпрекуBitmap Index Scanidx_transaction_breakdown_account_idпрекуBitmap Index Scan
Индексот idx_transaction_date не е директно искористен во овој план. Причината е што optimizer-от прво ги ограничува податоците преку конкретниот user_id и неговите сметки, а потоа пристапува до трансакциите преку transaction_pkey. Во овој случај филтрирањето по корисник е поселективно од самото филтрирање по датум.
Заклучок:
Времето на извршување се намали од 26.571 ms на 5.169 ms, што претставува приближно 80.55% подобрување, односно околу 5.14 пати побрзо извршување. Ова подобрување е резултат на тоа што PostgreSQL повеќе не ја чита целата табела transaction_breakdown и целата табела transaction_account, туку директно ги наоѓа сметките на конкретниот корисник и breakdown записите поврзани со тие сметки.
Финален заклучок од анализата
Првичната анализа со мал број податоци не покажа реална употреба на индексите, затоа што табелите беа премногу мали и PostgreSQL правилно избра Seq Scan. Поради тоа беше генериран поголем, логички поврзан сет на тест-податоци, кој содржи 100012 трансакции, 110011 breakdown записи и 125016 записи за доделени тагови.
По зголемување на количината на податоци, индексите почнаа да имаат значајно влијание врз execution plan-от и времето на извршување.
| Сценарио | Execution Time пред индекси | Execution Time по индекси | Подобрување | Индекси искористени |
|---|---|---|---|---|
| Трендови на трошење според тагови за корисник | 51.744 ms | 7.055 ms | ~86.36% | Да |
| Сметки со највисоко трошење во изминатата година | 73.792 ms | 47.902 ms | ~35.08% | Да |
| Годишни трендови на трансакции за корисник | 26.571 ms | 5.169 ms | ~80.55% | Да |
Најголемо подобрување се забележува кај извештаите кои филтрираат по конкретен корисник, затоа што индексите idx_transaction_account_user_id и idx_transaction_breakdown_account_id овозможуваат PostgreSQL директно да ги најде релевантните сметки и breakdown записи, наместо да ги чита целите табели.
Кај извештајот за сметки со највисоко трошење во последната година, најважен е индексот idx_transaction_date, затоа што условот по датум ја намалува количината на трансакции кои треба да се обработат.
Заклучокот е дека предложените индекси се оправдани и корисни за FEiN, особено кога базата содржи поголем број трансакции. Тие значително го намалуваат времето на извршување на аналитичките извештаи и ја подобруваат скалабилноста на системот.
3. Интегритет и конзистентност
Со цел самата база да биде отпорна на грешки, имплементирани се стриктни ограничувања (CONSTRAINTS) и тригери кои ја заштитуваат финансиската историја на корисниците.
ALTER TABLE "user" ADD CONSTRAINT user_email_unique UNIQUE (email);
- Надворешни клучеви: Релациите помеѓу табелите се реализирани преку надворешни клучеви (
FOREIGN KEY). Со тоа се спречува внесување на записи кои референцираат непостоечки корисници, сметки, трансакции или тагови. - Заштита на финансиска историја: За разлика од автоматско каскадно бришење, кај трансакциските сметки се користи тригер кој спречува бришење на сметка доколку таа има поврзани записи во
transaction_breakdown. Ова е важно затоа што бришење на сметка со постоечка историја може да доведе до губење на финансиски податоци и неточни извештаи. - Одржување на логичка конзистентност: Дополнително, системот користи тригер кој автоматски ја брише трансакцијата доколку по бришење на breakdown запис повеќе не постојат поврзани breakdown записи за таа трансакција. Со тоа се спречува постоење на „празни“ трансакции без финансиско значење.
- Ограничувања на уникатност: Корисничките мејлови се заштитени со
UNIQUEограничување, со што се гарантира дека во системот не можат да постојат два кориснички профили со иста емаил адреса. - Типови на податоци за финансиски вредности: Сите монетарни вредности во системот се складираат со типот
NUMERIC(10,2). Овој пристап обезбедува фиксна прецизност до две децимали и ги елиминира грешките кои можат да настанат при користење на типови со подвижна запирка(FLOATилиDOUBLE PRECISION) во финансиски пресметки.
4. Одржување на базата
- Зачувување на структурните промени: Секоја промена на структурата на базата се документира во Trac документацијата и се имплементира директно во PostgreSQL преку SQL наредби. На тој начин документацијата останува усогласена со реалната имплементација на системот.
- Чување на SQL скриптите: Секоја SQL скрипта извршена после првата DDL скрипта ја чувам хронолошки, за во случај да треба базата да се иницијализира повторно, да ги извршам редоследно и да ја добијам истата состојба која сум ја имал.
- Одговорност за инфраструктурата: FEiN е дизајниран да работи врз PostgreSQL сервер обезбеден од надворешна инфраструктура. Конфигурацијата на серверот, резервните копии и механизмите за обновување на податоците се надвор од опсегот на самата апликација и зависат од околината во која е поставена базата на податоци.
