| 30 | | **Лев и десен дел:** |
| 31 | | |
| 32 | | - **Леви:** `user_id, email, username, training_user_id, training_id, investor_user_id, asset_id, weight_user_id, daily_intake_id, discipline_user_id, custom_tracking_id, task_id, daily_completion_id, finance_user_id, income_id` |
| 33 | | - **Десни:** `password, training_gender, training_age, training_weight, training_date, training_type, training_duration, training_calories, asset_ticker_symbol, asset_buy_price, asset_buy_date, asset_quantity, weight_current, weight_height, weight_goal_weight, weight_goal_calories, daily_intake_date, daily_intake_calories, custom_tracking_name, name, is_finished, daily_completion_date, procent, finance_spending_budget, finance_saving_budget, finance_investing_budget, finance_donation_budget, finance_credit, income_date, income_amount` |
| 34 | | |
| 35 | | == **Глобална релација** |
| 36 | | |
| 37 | | `R = { user_id, email, username, password, training_user_id, training_gender, training_age, training_weight, training_id, training_date, training_type, training_duration, training_calories, investor_user_id, asset_id, asset_ticker_symbol, asset_buy_price, asset_buy_date, asset_quantity, weight_user_id, weight_current, weight_height, weight_goal_weight, weight_goal_calories, daily_intake_id, daily_intake_date, daily_intake_calories, discipline_user_id, custom_tracking_id, custom_tracking_name, task_id, name, is_finished, daily_completion_id, daily_completion_date, procent, finance_user_id, finance_spending_budget, finance_saving_budget, finance_investing_budget, finance_donation_budget, finance_credit, income_id, income_date, income_amount }` |
| 38 | | |
| 39 | | == **Покривачи** |
| 40 | | |
| 41 | | - **`user_id+`** = { `user_id`, `email`, `username`, `password` } -> **не ги содржи сите атрибути** |
| 42 | | - **`training_user_id+`** = { `training_user_id`, `training_gender`, `training_age`, `training_weight` } -> **не ги содржи сите атрибути** |
| 43 | | - **`training_id+`** = { `training_id`, `training_user_id`, `training_date`, `training_type`, `training_duration`, `training_calories` } -> **не ги содржи сите атрибути** |
| 44 | | - **`investor_user_id+`** = { `investor_user_id`, `user_id` } -> **не ги содржи сите атрибути** |
| 45 | | - **`asset_id+`** = { `asset_id`, `investor_user_id`, `asset_ticker_symbol`, `asset_buy_price`, `asset_buy_date`, `asset_quantity` } -> **не ги содржи сите атрибути** |
| 46 | | - **`weight_user_id+`** = { `weight_user_id`, `weight_current`, `weight_height`, `weight_goal_weight`, `weight_goal_calories` } -> **не ги содржи сите атрибути** |
| 47 | | - **`daily_intake_id+`** = { `daily_intake_id`, `weight_user_id`, `daily_intake_date`, `daily_intake_calories` } -> **не ги содржи сите атрибути** |
| 48 | | - **`discipline_user_id+`** = { `discipline_user_id`, `user_id` } -> **не ги содржи сите атрибути** |
| 49 | | - **`custom_tracking_id+`** = { `custom_tracking_id`, `user_id`, `custom_tracking_name` } -> **не ги содржи сите атрибути** |
| 50 | | - **`task_id+`** = { `task_id`, `discipline_user_id`, `custom_tracking_id`, `name`, `is_finished` } -> **не ги содржи сите атрибути** |
| 51 | | - **`daily_completion_id+`** = { `daily_completion_id`, `user_id`, `daily_completion_date`, `procent` } -> **не ги содржи сите атрибути** |
| 52 | | - **`finance_user_id+`** = { `finance_user_id`, `finance_spending_budget`, `finance_saving_budget`, `finance_investing_budget`, `finance_donation_budget`, `finance_credit` } -> **не ги содржи сите атрибути** |
| 53 | | - **`income_id+`** = { `income_id`, `finance_user_id`, `income_date`, `income_amount` } -> **не ги содржи сите атрибути** |
| 54 | | - **`{task_id, daily_completion_id}+`** = { `task_id`, `daily_completion_id` } -> **не ги содржи сите атрибути** |
| 55 | | |
| 56 | | == **Спојување покривачи** |
| 57 | | |
| 58 | | Бидејќи атрибутите што се појавуваат само на левата страна не можат да се изведат на друг начин, тие мора да бидат дел од примарниот клуч. |
| 59 | | |
| 60 | | Земајќи ги сите леви атрибути заедно, добиваме минимален суперклуч: |
| 61 | | |
| 62 | | `{ user_id, training_user_id, training_id, investor_user_id, asset_id, weight_user_id, daily_intake_id, discipline_user_id, custom_tracking_id, task_id, daily_completion_id, finance_user_id, income_id }` |
| 63 | | |
| 64 | | Во нашата нормализирана конструкција, ова е теоретскиот глобален кандидат-клуч на де-нормализираната релација. |
| 65 | | |
| 66 | | Избран примарен клуч: **`{ user_id, training_user_id, training_id, investor_user_id, asset_id, weight_user_id, daily_intake_id, discipline_user_id, custom_tracking_id, task_id, daily_completion_id, finance_user_id, income_id }`** |
| 67 | | |
| 68 | | == **Проверка за 1НФ** |
| 69 | | |
| 70 | | Бидејќи релацијата содржи повторливи групи и неатомски листи во де-нормализираниот поглед, **не ја задоволува 1НФ**. |
| 71 | | |
| 72 | | == **Декомпозиција по 1НФ** |
| 73 | | |
| 74 | | **Релација што се анализира** |
| 75 | | |
| 76 | | `R` |
| 77 | | |
| 78 | | **Проблем** |
| 79 | | |
| 80 | | Лист атрибутите и повторливите групи не се атомски. |
| 81 | | |
| 82 | | **Прва декомпозиција** |
| 83 | | |
| 84 | | - `TASKS(task_id, discipline_user_id, custom_tracking_id, name, is_finished)` |
| 85 | | - `TASK_DAILY_COMPLETION(task_id, daily_completion_id)` |
| 86 | | |
| 87 | | **Резултат** |
| 88 | | |
| 89 | | Се добиваат атомски вредности и релации со локални клучеви. |
| 90 | | |
| 91 | | == **Проверка за 2НФ** |
| 92 | | |
| 93 | | `R` **не ја задоволува 2НФ** поради парцијални зависности, на пример: |
| 94 | | |
| 95 | | - `user_id -> email, username, password` |
| 96 | | - `training_user_id -> training_gender, training_age, training_weight` |
| 97 | | - `asset_id -> asset_ticker_symbol, asset_buy_price, asset_buy_date, asset_quantity` |
| 98 | | |
| 99 | | == **Декомпозиција по 2НФ** |
| 100 | | |
| 101 | | Атрибутите се групираат според тоа од кој клуч зависат: |
| 102 | | |
| 103 | | - **`USERS(user_id, email, username, password)`** |
| 104 | | - **`TRAINING_USERS(user_id, gender, age, weight)`** |
| 105 | | - **`TRAINING_SESSIONS(training_id, training_user_id, date, type, duration, calories)`** |
| 106 | | - **`INVESTOR_USERS(user_id)`** |
| 107 | | - **`ASSETS(asset_id, user_id, ticker_symbol, buy_price, buy_date, quantity)`** |
| 108 | | - **`WEIGHT_USERS(user_id, weight, height, goal_weight, goal_calories)`** |
| 109 | | - **`DAILY_INTAKES(daily_intake_id, user_id, date, calories)`** |
| 110 | | - **`DISCIPLINE_USERS(user_id)`** |
| 111 | | - **`CUSTOM_TRACKING_CATEGORIES(custom_tracking_id, user_id, name)`** |
| 112 | | - **`TASKS(task_id, discipline_user_id, custom_tracking_id, name, is_finished)`** |
| 113 | | - **`DAILY_COMPLETION(daily_completion_id, user_id, date, procent)`** |
| 114 | | - **`TASK_DAILY_COMPLETION(task_id, daily_completion_id)`** |
| 115 | | - **`FINANCE_USERS(user_id, spending_budget, saving_budget, investing_budget, donation_budget, credit)`** |
| 116 | | - **`INCOMES(income_id, user_id, date, amount)`** |
| 117 | | |
| 118 | | == **Проверка за 3НФ** |
| 119 | | |
| 120 | | Ги разгледуваме релациите добиени по 2НФ. |
| 121 | | |
| 122 | | Проблематични транзитивни/изведени атрибути се: |
| 123 | | |
| 124 | | - **`num_tasks`** |
| 125 | | - **`tasks`** |
| 126 | | - **`weight_user_id`** во `TRAINING_SESSIONS` како непотребна зависност за оваа верзија на моделот |
| 127 | | |
| 128 | | == **Декомпозиција по 3НФ** |
| 129 | | |
| 130 | | 1. **`DISCIPLINE_USERS(user_id)`** -> се задржува само идентификаторот што ја врзува дисциплинската улога со `USERS`. |
| 131 | | |
| 132 | | 2. **`CUSTOM_TRACKING_CATEGORIES(custom_tracking_id, user_id, name)`** -> се задржува само името на категоријата, без броење и листи на задачи. |
| 133 | | |
| 134 | | 3. **`TRAINING_SESSIONS(training_id, training_user_id, date, type, duration, calories)`** -> сесијата се врзува само со `training_user_id`. |
| 135 | | |
| 136 | | **Резултат** |
| 137 | | |
| 138 | | Нема не-клучен атрибут што зависи од друг не-клучен атрибут во истата релација. |
| 139 | | |
| 140 | | == **Проверка за БКНФ** |
| 141 | | |
| 142 | | Сите добиени релации се во БКНФ, бидејќи секој детерминант е кандидат-клуч во својата релација. |
| 143 | | |
| 144 | | Особено: |
| 145 | | |
| 146 | | - **`USERS`** ги задржува алтернативните клучеви `email` и `username` |
| 147 | | - **`TASK_DAILY_COMPLETION(task_id, daily_completion_id)`** е спојна релација без не-клучни атрибути |
| 148 | | - профилните релации со `user_id` се чисти 1:1 продолжувања на `USERS` |
| 149 | | |
| 150 | | == **Финален резултат и дискусија** |
| 151 | | |
| 152 | | == **Нормализиран релациски модел** |
| 153 | | |
| 154 | | Финалниот модел што го користиме во тековната имплементација е: |
| 155 | | |
| 156 | | - **`USERS`** |
| 157 | | - **`TRAINING_USERS`** |
| 158 | | - **`TRAINING_SESSIONS`** |
| 159 | | - **`INVESTOR_USERS`** |
| 160 | | - **`ASSETS`** |
| 161 | | - **`WEIGHT_USERS`** |
| 162 | | - **`DAILY_INTAKES`** |
| 163 | | - **`DISCIPLINE_USERS`** |
| 164 | | - **`CUSTOM_TRACKING_CATEGORIES`** |
| 165 | | - **`TASKS`** |
| 166 | | - **`DAILY_COMPLETION`** |
| 167 | | - **`TASK_DAILY_COMPLETION`** |
| 168 | | - **`FINANCE_USERS`** |
| 169 | | - **`INCOMES`** |
| 170 | | |
| 171 | | == **Дискусија** |
| 172 | | |
| 173 | | Разлики во однос на претходниот дизајн: |
| 174 | | |
| 175 | | - Се отстранети **`num_tasks`** и **`tasks`** од дисциплинските табели. |
| 176 | | - Се отстранета непотребната релација **`weight_user_id`** од `TRAINING_SESSIONS`. |
| 177 | | - Се задржуваат профилните табели со **`user_id`** како заеднички примарен и странски клуч. |
| 178 | | |
| | 32 | == **Кандидат клучеви и примарен клуч** == |
| | 33 | |
| | 34 | === **Пресметување на затворања (closures)** === |
| | 35 | |
| | 36 | За да се најдат кандидат клучевите, пресметуваме затворање за секој атрибут (или комбинација) и проверуваме дали го покрива целиот `R`. |
| | 37 | |
| | 38 | **Атрибути кои се појавуваат само на десна страна** (никогаш детерминанти): |
| | 39 | - `email`, `username` — само преку `FD2`/`FD3` се детерминанти, но се и десни во `FD1` |
| | 40 | - `training_gender`, `training_age`, `training_weight`, `training_date`, `training_type`, `training_duration`, `training_calories` |
| | 41 | - `asset_ticker_symbol`, `asset_buy_price`, `asset_buy_date`, `asset_quantity` |
| | 42 | - `weight_current`, `weight_height`, `weight_goal_weight`, `weight_goal_calories` |
| | 43 | - `daily_intake_date`, `daily_intake_calories` |
| | 44 | - `custom_tracking_name`, `task_name`, `task_is_finished` |
| | 45 | - `daily_completion_date`, `daily_completion_procent` |
| | 46 | - `finance_spending_budget`, `finance_saving_budget`, `finance_investing_budget`, `finance_donation_budget`, `finance_credit` |
| | 47 | - `income_date`, `income_amount` |
| | 48 | |
| | 49 | Овие атрибути **не можат** да бидат дел од кандидат клуч бидејќи не детерминираат ништо надвор од себе. |
| | 50 | |
| | 51 | **Атрибути кои се само на лева страна** (мора да бидат во секој кандидат клуч): |
| | 52 | - `training_id`, `asset_id`, `daily_intake_id`, `task_id`, `daily_completion_id`, `income_id` |
| | 53 | |
| | 54 | **Останати детерминанти:** |
| | 55 | - `user_id`, `email`, `username`, `training_user_id`, `investor_user_id`, `weight_user_id`, `discipline_user_id`, `custom_tracking_id`, `finance_user_id` |
| | 56 | |
| | 57 | === **Затворање на минималниот суперклуч** === |
| | 58 | |
| | 59 | Пробуваме со комбинација на сите атрибути што се само леви + по еден претставник од групите поврзани со `user_id`: |
| | 60 | |
| | 61 | `K = { user_id, training_id, asset_id, daily_intake_id, task_id, daily_completion_id, income_id, training_user_id, investor_user_id, weight_user_id, discipline_user_id, custom_tracking_id, finance_user_id }` |
| | 62 | |
| | 63 | **`K+` пресметување:** |
| | 64 | - `user_id` -> `email, username, password` (FD1) |
| | 65 | - `training_user_id` -> `training_gender, training_age, training_weight` (FD4) |
| | 66 | - `training_id` -> `training_user_id, training_date, training_type, training_duration, training_calories` (FD5) |
| | 67 | - `investor_user_id` -> `user_id` (FD6) |
| | 68 | - `asset_id` -> `investor_user_id, asset_ticker_symbol, asset_buy_price, asset_buy_date, asset_quantity` (FD7) |
| | 69 | - `weight_user_id` -> `weight_current, weight_height, weight_goal_weight, weight_goal_calories` (FD8) |
| | 70 | - `daily_intake_id` -> `weight_user_id, daily_intake_date, daily_intake_calories` (FD9) |
| | 71 | - `discipline_user_id` -> `user_id` (FD10) |
| | 72 | - `custom_tracking_id` -> `user_id, custom_tracking_name` (FD11) |
| | 73 | - `task_id` -> `discipline_user_id, custom_tracking_id, task_name, task_is_finished` (FD12) |
| | 74 | - `daily_completion_id` -> `user_id, daily_completion_date, daily_completion_procent` (FD13) |
| | 75 | - `finance_user_id` -> `finance_spending_budget, finance_saving_budget, finance_investing_budget, finance_donation_budget, finance_credit` (FD15) |
| | 76 | - `income_id` -> `finance_user_id, income_date, income_amount` (FD16) |
| | 77 | |
| | 78 | `K+ = R` → **`K` е суперклуч** ✓ |
| | 79 | |
| | 80 | **Минималност:** Отстранувањето на кој било атрибут од `K` резултира во непокриени атрибути (на пример, без `training_id` не можеме да добиеме `training_date`, `training_type` итн.), па `K` е минимален. |
| | 81 | |
| | 82 | **Примарен клуч:** |
| | 83 | |
| | 84 | `PK = { user_id, training_id, asset_id, daily_intake_id, task_id, |
| | 85 | daily_completion_id, income_id, training_user_id, investor_user_id, |
| | 86 | weight_user_id, discipline_user_id, custom_tracking_id, finance_user_id }` |
| | 87 | |
| | 88 | |
| | 89 | **Напомена:** Постојат и алтернативни кандидат клучеви: |
| | 90 | - `email` може да замени `user_id` (FD2: `email -> user_id`) |
| | 91 | - `username` може да замени `user_id` (FD3: `username -> user_id`) |
| | 92 | |
| | 93 | == **Проверка за 1НФ** == |
| | 94 | |
| | 95 | Релацијата `R` **НЕ ја задоволува 1НФ** поради: |
| | 96 | 1. **Повторливи групи** — еден корисник има повеќе `training_sessions`, повеќе `incomes`, повеќе `assets` итн., што значи дека редот би морал да се повторува или атрибутите би биле листи. |
| | 97 | 2. **Не-атомски атрибути** — во оригиналниот дизајн постоеја `tasks` (`TEXT` листа) и `num_tasks` (изведена вредност) во `DISCIPLINE_USERS` и `CUSTOM_TRACKING_CATEGORIES`. |
| | 98 | |
| | 99 | == **Декомпозиција по 1НФ** == |
| | 100 | |
| | 101 | **Релација што се анализира:** `R` (глобална) |
| | 102 | |
| | 103 | **Проблем:** Повторливи групи и не-атомски атрибути. |
| | 104 | |
| | 105 | **Решение:** Секој ентитет и врска добива своја посебна релација со атомски атрибути и локален примарен клуч. |
| | 106 | |
| | 107 | **Посебен случај — `TASK_DAILY_COMPLETION`:** |
| | 108 | Задачите и дневните завршувања се во врска **многу-кон-многу**. Се воведува посредна релација: |
| | 109 | |
| | 110 | TASK_DAILY_COMPLETION(task_id, daily_completion_id) |
| | 111 | |
| | 112 | |
| | 113 | **Резултат по 1НФ декомпозиција:** |
| | 114 | - `R1: USERS(user_id, email, username, password)` |
| | 115 | - `R2: TRAINING_USERS(training_user_id, training_gender, training_age, training_weight)` |
| | 116 | - `R3: TRAINING_SESSIONS(training_id, training_user_id, training_date, training_type, training_duration, training_calories)` |
| | 117 | - `R4: INVESTOR_USERS(investor_user_id)` |
| | 118 | - `R5: ASSETS(asset_id, investor_user_id, asset_ticker_symbol, asset_buy_price, asset_buy_date, asset_quantity)` |
| | 119 | - `R6: WEIGHT_USERS(weight_user_id, weight_current, weight_height, weight_goal_weight, weight_goal_calories)` |
| | 120 | - `R7: DAILY_INTAKES(daily_intake_id, weight_user_id, daily_intake_date, daily_intake_calories)` |
| | 121 | - `R8: DISCIPLINE_USERS(discipline_user_id)` |
| | 122 | - `R9: CUSTOM_TRACKING_CATEGORIES(custom_tracking_id, user_id, custom_tracking_name)` |
| | 123 | - `R10: TASKS(task_id, discipline_user_id, custom_tracking_id, task_name, task_is_finished)` |
| | 124 | - `R11: DAILY_COMPLETION(daily_completion_id, user_id, daily_completion_date, daily_completion_procent)` |
| | 125 | - `R12: TASK_DAILY_COMPLETION(task_id, daily_completion_id)` |
| | 126 | - `R13: FINANCE_USERS(finance_user_id, finance_spending_budget, finance_saving_budget, finance_investing_budget, finance_donation_budget, finance_credit)` |
| | 127 | - `R14: INCOMES(income_id, finance_user_id, income_date, income_amount)` |
| | 128 | |
| | 129 | - Сите атрибути се атомски. ✓ |
| | 130 | - Нема повторливи групи. ✓ |
| | 131 | |
| | 132 | == **Проверка за 2НФ** == |
| | 133 | |
| | 134 | **2НФ бара:** релацијата е во 1НФ и **нема парцијална зависност** — секој не-клучен атрибут зависи од **целиот** примарен клуч, не само од дел од него. |
| | 135 | |
| | 136 | Парцијална зависност постои само кога примарниот клуч е **сложен**. Ги разгледуваме само релациите со сложен `PK`: |
| | 137 | |
| | 138 | **`R12: TASK_DAILY_COMPLETION(task_id, daily_completion_id)`** |
| | 139 | - `PK: {task_id, daily_completion_id}` |
| | 140 | - Нема не-клучни атрибути → нема парцијални зависности → во 2НФ ✓ |
| | 141 | |
| | 142 | Сите останати релации (`R1`–`R11`, `R13`–`R14`) имаат **прост примарен клуч**, па парцијална зависност е невозможна → автоматски во 2НФ ✓ |
| | 143 | |
| | 144 | **Заклучок:** Сите релации се во **2НФ**. Декомпозиција не е потребна. |
| | 145 | |
| | 146 | == **Декомпозиција по 2НФ** == |
| | 147 | |
| | 148 | Бидејќи сите релации веќе се во 2НФ, нема декомпозиција. Релациите од 1НФ чекорот се пренесуваат непроменети. |
| | 149 | |
| | 150 | == **Проверка за 3НФ** == |
| | 151 | |
| | 152 | **3НФ бара:** релацијата е во 2НФ и **нема транзитивна зависност** — не-клучен атрибут не смее да зависи од друг не-клучен атрибут. |
| | 153 | |
| | 154 | Ги разгледуваме релациите каде транзитивна зависност е можна: |
| | 155 | |
| | 156 | **`R3: TRAINING_SESSIONS`** |
| | 157 | - Атрибути: `training_id`, `training_user_id`, `training_date`, `training_type`, `training_duration`, `training_calories` |
| | 158 | - `PK`: `training_id` |
| | 159 | - `FDs`: `training_id -> training_user_id, training_date, training_type, training_duration, training_calories` |
| | 160 | - Не-клучни атрибути: `training_user_id`, `training_date`, `training_type`, `training_duration`, `training_calories` |
| | 161 | - Постои ли транзитивност? `training_user_id` е не-клучен, но не детерминира ништо во оваа релација → нема транзитивност → во 3НФ ✓ |
| | 162 | |
| | 163 | **`R5: ASSETS`** |
| | 164 | - Атрибути: `asset_id`, `investor_user_id`, `asset_ticker_symbol`, `asset_buy_price`, `asset_buy_date`, `asset_quantity` |
| | 165 | - `PK`: `asset_id` |
| | 166 | - `FDs`: `asset_id ->` сите атрибути |
| | 167 | - Не-клучен `investor_user_id` не детерминира ништо во оваа релација → во 3НФ ✓ |
| | 168 | |
| | 169 | **`R7: DAILY_INTAKES`** |
| | 170 | - Атрибути: `daily_intake_id`, `weight_user_id`, `daily_intake_date`, `daily_intake_calories` |
| | 171 | - `PK`: `daily_intake_id` |
| | 172 | - `FDs`: `daily_intake_id -> weight_user_id, daily_intake_date, daily_intake_calories` |
| | 173 | - `weight_user_id` не детерминира ништо во оваа релација → во 3НФ ✓ |
| | 174 | |
| | 175 | **`R9: CUSTOM_TRACKING_CATEGORIES`** |
| | 176 | - Атрибути: `custom_tracking_id`, `user_id`, `custom_tracking_name` |
| | 177 | - `PK`: `custom_tracking_id` |
| | 178 | - `FDs`: `custom_tracking_id -> user_id, custom_tracking_name` |
| | 179 | - `user_id` не детерминира ништо во оваа релација → во 3НФ ✓ |
| | 180 | |
| | 181 | **`R10: TASKS`** |
| | 182 | - Атрибути: `task_id`, `discipline_user_id`, `custom_tracking_id`, `task_name`, `task_is_finished` |
| | 183 | - `PK`: `task_id` |
| | 184 | - `FDs`: `task_id -> discipline_user_id, custom_tracking_id, task_name, task_is_finished` |
| | 185 | - Ниту `discipline_user_id` ниту `custom_tracking_id` детерминираат ништо во оваа релација → во 3НФ ✓ |
| | 186 | |
| | 187 | **`R14: INCOMES`** |
| | 188 | - Атрибути: `income_id`, `finance_user_id`, `income_date`, `income_amount` |
| | 189 | - `PK`: `income_id` |
| | 190 | - `FDs`: `income_id -> finance_user_id, income_date, income_amount` |
| | 191 | - `finance_user_id` не детерминира ништо во оваа релација → во 3НФ ✓ |
| | 192 | |
| | 193 | **Заклучок:** Сите релации се во **3НФ**. Декомпозиција не е потребна. |
| | 194 | |
| | 195 | == **Проверка за БКНФ** == |
| | 196 | |
| | 197 | **БКНФ бара:** за секоја нетривијална `FD X -> Y`, `X` мора да биде **суперклуч**. |
| | 198 | |
| | 199 | Ги разгледуваме релациите каде постојат повеќе кандидат клучеви: |
| | 200 | |
| | 201 | **`R1: USERS(user_id, email, username, password)`** |
| | 202 | - `FDs`: |
| | 203 | - `user_id -> email, username, password` |
| | 204 | - `email -> user_id` (FD2) |
| | 205 | - `username -> user_id` (FD3) |
| | 206 | - Кандидат клучеви: `{user_id}`, `{email}`, `{username}` |
| | 207 | - Проверка: |
| | 208 | - `user_id -> ...` : `user_id` е кандидат клуч ✓ |
| | 209 | - `email -> ...` : `email` е кандидат клуч ✓ |
| | 210 | - `username -> ...` : `username` е кандидат клуч ✓ |
| | 211 | - Сите детерминанти се кандидат клучеви → `USERS` е во БКНФ ✓ |
| | 212 | |
| | 213 | **`R10: TASKS`** |
| | 214 | - `FDs`: `task_id -> discipline_user_id, custom_tracking_id, task_name, task_is_finished` |
| | 215 | - Единствен кандидат клуч: `{task_id}` |
| | 216 | - `task_id` е кандидат клуч → во БКНФ ✓ |
| | 217 | |
| | 218 | **Напомена за CHECK constraint:** |
| | 219 | Условот `(discipline_user_id IS NOT NULL AND custom_tracking_id IS NULL) OR (discipline_user_id IS NULL AND custom_tracking_id IS NOT NULL)` не е функционална зависност туку ограничување на домен — не влијае на БКНФ. |
| | 220 | |
| | 221 | Сите останати релации имаат единствен кандидат клуч (нивниот сурогатен `PK`) и сите детерминанти во нив се суперклучеви → сите се во БКНФ ✓ |
| | 222 | |
| | 223 | **Заклучок:** **Целиот модел е во БКНФ.** |
| | 224 | |
| | 225 | === **Lossless Join проверка** === |
| | 226 | |
| | 227 | За секоја декомпозиција `R -> R1, R2`, `join`-от е lossless ако и само ако: |
| | 228 | `(R1 ∩ R2) -> R1` **ИЛИ** `(R1 ∩ R2) -> R2` |
| | 229 | |
| | 230 | Бидејќи декомпозицијата е вршена директно од глобалната релација `R` во посебни релации (не постепено `R->R1,R2->R3,R4...`), ја применуваме проверката за секој пар поврзани релации преку странски клуч: |
| | 231 | |
| | 232 | - `USERS` — `TRAINING_USERS`: Пресек `{user_id}` → `user_id` е `PK` во `USERS` → суперклуч ✓ |
| | 233 | - `USERS` — `FINANCE_USERS`: Пресек `{user_id}` → `user_id` е `PK` во `USERS` → суперклуч ✓ |
| | 234 | - `USERS` — `INVESTOR_USERS`: Пресек `{user_id}` → `user_id` е `PK` во `USERS` → суперклуч ✓ |
| | 235 | - `USERS` — `DISCIPLINE_USERS`: Пресек `{user_id}` → `user_id` е `PK` во `USERS` → суперклуч ✓ |
| | 236 | - `USERS` — `CUSTOM_TRACKING_CATEGORIES`: Пресек `{user_id}` → `user_id` е `PK` во `USERS` → суперклуч ✓ |
| | 237 | - `USERS` — `DAILY_COMPLETION`: Пресек `{user_id}` → `user_id` е `PK` во `USERS` → суперклуч ✓ |
| | 238 | - `TRAINING_USERS` — `TRAINING_SESSIONS`: Пресек `{training_user_id}` → `training_user_id` е `PK` во `TRAINING_USERS` → суперклуч ✓ |
| | 239 | - `INVESTOR_USERS` — `ASSETS`: Пресек `{investor_user_id}` → `investor_user_id` е `PK` во `INVESTOR_USERS` → суперклуч ✓ |
| | 240 | - `WEIGHT_USERS` — `DAILY_INTAKES`: Пресек `{weight_user_id}` → `weight_user_id` е `PK` во `WEIGHT_USERS` → суперклуч ✓ |
| | 241 | - `FINANCE_USERS` — `INCOMES`: Пресек `{finance_user_id}` → `finance_user_id` е `PK` во `FINANCE_USERS` → суперклуч ✓ |
| | 242 | - `DISCIPLINE_USERS` — `TASKS`: Пресек `{discipline_user_id}` → `discipline_user_id` е `PK` во `DISCIPLINE_USERS` → суперклуч ✓ |
| | 243 | - `CUSTOM_TRACKING_CATEGORIES` — `TASKS`: Пресек `{custom_tracking_id}` → `custom_tracking_id` е `PK` во `CUSTOM_TRACKING_CATEGORIES` → суперклуч ✓ |
| | 244 | - `TASKS` — `TASK_DAILY_COMPLETION`: Пресек `{task_id}` → `task_id` е `PK` во `TASKS` → суперклуч ✓ |
| | 245 | - `DAILY_COMPLETION` — `TASK_DAILY_COMPLETION`: Пресек `{daily_completion_id}` → `daily_completion_id` е `PK` во `DAILY_COMPLETION` → суперклуч ✓ |
| | 246 | |
| | 247 | **Заклучок:** Сите декомпозиции имаат **lossless join** својство. ✓ |
| | 248 | |
| | 249 | === **Dependency Preservation проверка** === |
| | 250 | |
| | 251 | Проверуваме дека секоја `FD` од оригиналниот сет е зачувана во барем една од финалните релации: |
| | 252 | |
| | 253 | - `FD1: user_id -> email, username, password` → `USERS` ✓ |
| | 254 | - `FD2: email -> user_id` → `USERS` ✓ |
| | 255 | - `FD3: username -> user_id` → `USERS` ✓ |
| | 256 | - `FD4: training_user_id -> training_gender, training_age, training_weight` → `TRAINING_USERS` ✓ |
| | 257 | - `FD5: training_id -> training_user_id, ...` → `TRAINING_SESSIONS` ✓ |
| | 258 | - `FD6: investor_user_id -> user_id` → `INVESTOR_USERS` (преку `FK`) ✓ |
| | 259 | - `FD7: asset_id -> investor_user_id, ...` → `ASSETS` ✓ |
| | 260 | - `FD8: weight_user_id -> weight_current, ...` → `WEIGHT_USERS` ✓ |
| | 261 | - `FD9: daily_intake_id -> weight_user_id, ...` → `DAILY_INTAKES` ✓ |
| | 262 | - `FD10: discipline_user_id -> user_id` → `DISCIPLINE_USERS` (преку `FK`) ✓ |
| | 263 | - `FD11: custom_tracking_id -> user_id, name` → `CUSTOM_TRACKING_CATEGORIES` ✓ |
| | 264 | - `FD12: task_id -> discipline_user_id, ...` → `TASKS` ✓ |
| | 265 | - `FD13: daily_completion_id -> user_id, ...` → `DAILY_COMPLETION` ✓ |
| | 266 | - `FD14: (task_id, daily_completion_id) -> /` → `TASK_DAILY_COMPLETION` ✓ |
| | 267 | - `FD15: finance_user_id -> finance_spending_budget, ...` → `FINANCE_USERS` ✓ |
| | 268 | - `FD16: income_id -> finance_user_id, ...` → `INCOMES` ✓ |
| | 269 | |
| | 270 | **Заклучок:** **Сите функционални зависности се зачувани.** ✓ |
| | 271 | |
| | 272 | == **Финален резултат и дискусија** == |
| | 273 | |
| | 274 | === **Нормализиран релациски модел** === |
| | 275 | |
| | 276 | USERS(user_id, email, username, password) |
| | 277 | CK: {user_id}, {email}, {username} |
| | 278 | |
| | 279 | TRAINING_USERS(user_id, gender, age, weight) |
| | 280 | PK: user_id FK: user_id -> USERS |
| | 281 | |
| | 282 | TRAINING_SESSIONS(training_id, training_user_id, date, type, duration, calories) |
| | 283 | PK: training_id FK: training_user_id -> TRAINING_USERS |
| | 284 | |
| | 285 | INVESTOR_USERS(user_id) |
| | 286 | PK: user_id FK: user_id -> USERS |
| | 287 | |
| | 288 | ASSETS(asset_id, user_id, ticker_symbol, buy_price, buy_date, quantity) |
| | 289 | PK: asset_id FK: user_id -> INVESTOR_USERS |
| | 290 | |
| | 291 | WEIGHT_USERS(user_id, weight, height, goal_weight, goal_calories) |
| | 292 | PK: user_id FK: user_id -> USERS |
| | 293 | |
| | 294 | DAILY_INTAKES(daily_intake_id, user_id, date, calories) |
| | 295 | PK: daily_intake_id FK: user_id -> WEIGHT_USERS |
| | 296 | |
| | 297 | DISCIPLINE_USERS(user_id) |
| | 298 | PK: user_id FK: user_id -> USERS |
| | 299 | |
| | 300 | CUSTOM_TRACKING_CATEGORIES(custom_tracking_id, user_id, name) |
| | 301 | PK: custom_tracking_id FK: user_id -> USERS |
| | 302 | |
| | 303 | TASKS(task_id, discipline_user_id, custom_tracking_id, name, is_finished) |
| | 304 | PK: task_id |
| | 305 | FK: discipline_user_id -> DISCIPLINE_USERS |
| | 306 | FK: custom_tracking_id -> CUSTOM_TRACKING_CATEGORIES |
| | 307 | CHECK: точно еден од двата FK е NOT NULL |
| | 308 | |
| | 309 | DAILY_COMPLETION(daily_completion_id, user_id, date, procent) |
| | 310 | PK: daily_completion_id FK: user_id -> USERS |
| | 311 | |
| | 312 | TASK_DAILY_COMPLETION(task_id, daily_completion_id) |
| | 313 | PK: {task_id, daily_completion_id} |
| | 314 | FK: task_id -> TASKS |
| | 315 | FK: daily_completion_id -> DAILY_COMPLETION |
| | 316 | |
| | 317 | FINANCE_USERS(user_id, spending_budget, saving_budget, investing_budget, donation_budget, credit) |
| | 318 | PK: user_id FK: user_id -> USERS |
| | 319 | |
| | 320 | INCOMES(income_id, user_id, date, amount) |
| | 321 | PK: income_id FK: user_id -> FINANCE_USERS |
| | 322 | |
| | 323 | |
| | 324 | === **Дискусија** === |
| | 325 | |
| | 326 | **Разлики во однос на претходниот дизајн (Phase 2):** |
| | 327 | |
| | 328 | 1. **Отстранет `weight_user_id` од `TRAINING_SESSIONS`.** |
| | 329 | Во оригиналниот DDL, `TRAINING_SESSIONS` имаше `FK` кон `WEIGHT_USERS`. Ова е непотребна вкрстена зависност — тренинг сесијата логички припаѓа на тренинг профилот, не на weight профилот. Двата профила независно се врзуваат со `USERS` преку `user_id`. |
| | 330 | |
| | 331 | 2. **Отстранети `num_tasks` и `tasks` од `DISCIPLINE_USERS` и `CUSTOM_TRACKING_CATEGORIES`.** |
| | 332 | `num_tasks` е изведен атрибут (`COUNT` на `TASKS`) — чување на изведени вредности го нарушува принципот на нормализација и создава ризик од инконзистентност. `tasks` (`TEXT` листа) е не-атомски атрибут и директно ја нарушува `1НФ`. |
| | 333 | |
| | 334 | 3. **Додадени `UNIQUE` constraints на `email` и `username` во `USERS`.** |
| | 335 | Нормализацијата формално идентификуваше `{email}` и `{username}` како кандидат клучеви, па DDL мора да ги енфорцира. |
| | 336 | |
| | 337 | 4. **`GENERATED BY DEFAULT AS IDENTITY` за сите сурогатни `PK`-а.** |
| | 338 | Технички подобрување усогласено со нормализираниот модел. |
| | 339 | |
| | 340 | **Модел за употреба во понатамошните фази:** нормализираниот модел (финалниот DDL од Phase 5). |