| Version 3 (modified by , 5 days ago) ( diff ) |
|---|
Нормализација
Денормализирана форма
Форма во која има една табела во која се внесени сите ентитети и нивните релации помеѓу себе, без никакви правила.
Во денормализираната форма сите атрибути од сите 20 табели се обединети во една глобална релација. Поради големиот број на атрибути (над 50), таа не може практично да се прикаже како хоризонтална табела. Затоа, еден примерок ред е прикажан вертикално:
| Колона | Вредност |
| user_id | 3 |
| username | petar_waiter |
| password_hash | $2a$06$... |
| role_id | 3 |
| role_name | WAITER |
| session_token | a1b2c3d4-... |
| shift_id | 1 |
| shift_start | 2026-01-10 08:00 |
| shift_end | 2026-01-10 16:00 |
| shift_close_id | 1 |
| closed_by | 1 |
| total | 12234.35 |
| order_count | 18 |
| table_id | 1 |
| table_number | 1 |
| capacity | 4 |
| table_status | СЛОБОДНА |
| order_id | 43 |
| order_status | ПЛАТЕНА |
| order_created | 2026-07-10 14:47 |
| item_number | 1 |
| product_id | 14 |
| product_name | Безкофеинско Еспресо |
| price | 70.00 |
| quantity | 1 |
| unit_price | 70.00 |
| item_status | ПОРАЧАНО |
| category_id | 1 |
| category_name | Кафе |
| payment_id | 63 |
| amount | 6.50 |
| method | КЕШ |
| payment_date | 2026-07-10 14:47 |
| invoice_id | 39 |
| invoice_number | INV-43 |
| issued_at | 2026-07-10 14:47 |
| ingredient_id | 1 |
| ingredient_name | Кафе во зрно |
| unit | g |
| min_stock | 500 |
| recipe_id | 1 |
| recipe_item_qty | 8 |
| inv_id | 1 |
| inv_change | -1 |
| inv_op | ПРОДАЖБА |
| log_id | 1 |
| log_action | LOGIN |
| log_entity | app_user |
| log_created | 2026-07-07 |
| setting_id | 1 |
| restaurant_name | ЕМПОРИО |
| vat_rate | 18.00 |
| currency | ден |
Функционални зависности (почетно множество)
Во денормализираната табела постојат следниве основни функционални зависности:
F = {
f1: {user_id} → {username, password_hash, role_id, first_name, last_name, email, active}
f2: {role_id} → {role_name, description}
f3: {session_token} → {user_id, session_created_at, expires_at}
f4: {shift_id} → {user_id, shift_start_time, shift_end_time}
f5: {shift_close_id} → {shift_id, closed_by, total, order_count, closed_at}
f6: {table_id} → {table_number, capacity, table_status}
f7: {order_id} → {user_id, table_id, order_created_at, order_status}
f8: {order_id, item_number} → {product_id, quantity, unit_price, item_status}
f9: {product_id} → {category_id, product_name, description, price, active, product_min_stock}
f10: {category_id} → {category_name, description, active}
f11: {payment_id} → {order_id, amount, method, payment_date}
f12: {invoice_id} → {payment_id, invoice_number, issued_at}
f13: {ingredient_id} → {ingredient_name, unit, ingredient_min_stock, active}
f14: {recipe_id} → {product_id}
f15: {recipe_id, ingredient_id} → {quantity_needed}
f16: {inventory_id} → {product_id, quantity_change, operation_type, inventory_created_at}
f17: {ingredient_inventory_id} → {ingredient_id, quantity_change, operation_type, ingredient_inventory_created_at}
f18: {log_id} → {user_id, action, entity_name, log_created_at}
f19: {setting_id} → {restaurant_name, vat_rate, currency}
}
Забелешка: Поради преклопување на имиња на атрибути (на пр. name во PRODUCT, CATEGORY, INGREDIENT; status во RESTAURANT_TABLE и ORDER_ITEM; min_stock во PRODUCT и INGREDIENT; created_at во SESSION, ORDERS, INVENTORY, INGREDIENT_INVENTORY, AUDIT_LOG), во глобалната релација истите се преименувани со префикс за да нема дупликати.
Canonical cover (минимално покритие)
За формална анализа се користи canonical cover F_c — минимално множество ФЗ еквивалентно на F, без излишни атрибути и без излишни зависности.
Чекор 1 — Дескомпозиција на десните страни (RHS) на единечни атрибути:
f1: user_id → username, user_id → password_hash, user_id → role_id,
user_id → first_name, user_id → last_name, user_id → email, user_id → active
f2: role_id → role_name, role_id → description
f3: session_token → user_id, session_token → session_created_at, session_token → expires_at
f4: shift_id → user_id, shift_id → shift_start_time, shift_id → shift_end_time
f5: shift_close_id → shift_id, shift_close_id → closed_by, shift_close_id → total,
shift_close_id → order_count, shift_close_id → closed_at
f6: table_id → table_number, table_id → capacity, table_id → table_status
f7: order_id → user_id, order_id → table_id, order_id → order_created_at, order_id → order_status
f8: (order_id, item_number) → product_id, (order_id, item_number) → quantity,
(order_id, item_number) → unit_price, (order_id, item_number) → item_status
f9: product_id → category_id, product_id → product_name, product_id → description,
product_id → price, product_id → active, product_id → product_min_stock
f10: category_id → category_name, category_id → description, category_id → active
f11: payment_id → order_id, payment_id → amount, payment_id → method, payment_id → payment_date
f12: invoice_id → payment_id, invoice_id → invoice_number, invoice_id → issued_at
f13: ingredient_id → ingredient_name, ingredient_id → unit, ingredient_id → ingredient_min_stock,
ingredient_id → active
f14: recipe_id → product_id
f15: (recipe_id, ingredient_id) → quantity_needed
f16: inventory_id → product_id, inventory_id → quantity_change, inventory_id → operation_type,
inventory_id → inventory_created_at
f17: ingredient_inventory_id → ingredient_id, ingredient_inventory_id → quantity_change,
ingredient_inventory_id → operation_type, ingredient_inventory_id → ingredient_inventory_created_at
f18: log_id → user_id, log_id → action, log_id → entity_name, log_id → log_created_at
f19: setting_id → restaurant_name, setting_id → vat_rate, setting_id → currency
Чекор 2 — Отстранување на излишни атрибути од левите страни (LHS):
Сите LHS се единечни атрибути освен f8 и f15:
(order_id, item_number): нитуorder_idсам, нитуitem_numberсам не определуваproduct_id,quantity,unit_price,item_status→ ниту еден атрибут не е излишен.(recipe_id, ingredient_id): нитуrecipe_idсам, нитуingredient_idсам не определуваquantity_needed→ ниту еден атрибут не е излишен.
Чекор 3 — Отстранување на излишни ФЗ:
Ниту една ФЗ не е излишна (не може да се изведе од останатите):
product_id → category_idне може да се изведе одcategory_id → ...(обратна насока).recipe_id → product_idне може да се изведе одproduct_id → ....invoice_id → payment_idне може да се изведе одpayment_id → ....
Заклучок: F_c = F (почетното множество е веќе минимално покритие по единечни атрибути).
Кандидат-клучеви и избор на примарен клуч
Формална дефиниција
Нека R е денормализираната релација со множество атрибути U и множество ФЗ F. Кандидат-клуч (К.К.) е подмножество K ⊆ U такво што:
# Уникатност:
K → U(K е супер-клуч). # Минималност: не постои вистинско подмножествоK' ⊂ KсоK' → U.
Примарен клуч (PK) е еден избран кандидат-клуч.
Пресметка на затворање на атрибути (attribute closure)
За секој атрибут/подмножество се пресметува X⁺ (затворање) со алгоритмот:
result := X
repeat
for each FD Y → Z in F:
if Y ⊆ result then result := result ∪ Z
until no change
return result
Кандидат-клуч е X ако X⁺ = U и не постои вистинско подмножество со истото својство.
Определување на кандидат-клучеви
Атрибути кои НЕ се појавуваат на RHS на ниту една ФЗ (мора да бидат во секој кандидат-клуч):
session_token, shift_close_id, inventory_id, ingredient_inventory_id, log_id, setting_id, item_number, recipe_id, invoice_id
Овие 9 атрибути мора да бидат дел од секој кандидат-клуч. Да го пресметаме нивното затворање:
X0 = {session_token, shift_close_id, inventory_id, ingredient_inventory_id,
log_id, setting_id, item_number, recipe_id, invoice_id}
X0⁺ = X0
∪ {user_id, session_created_at, expires_at} (од session_token)
∪ {shift_id, closed_by, total, order_count, closed_at} (од shift_close_id)
∪ {product_id, quantity_change, operation_type, inventory_created_at} (од inventory_id)
∪ {ingredient_id, ..., ingredient_inventory_created_at} (од ingredient_inventory_id)
∪ {user_id, action, entity_name, log_created_at} (од log_id)
∪ {restaurant_name, vat_rate, currency} (од setting_id)
∪ {quantity_needed} (од (recipe_id, ingredient_id) → quantity_needed)
∪ {payment_id, invoice_number, issued_at} (од invoice_id → payment_id, ...)
Ова не е целото U — недостигаат order_id, table_id, category_id, role_id, username, password_hash, first_name, last_name, email, active, role_name, description, table_number, capacity, table_status, order_status, order_created_at, quantity, unit_price, item_status, product_name, price, product_min_stock, category_name, amount, method, payment_date, ingredient_name, unit, ingredient_min_stock, shift_start_time, shift_end_time.
Значи X0 не е супер-клуч. Треба да се додадат атрибути.
Додавање на order_id и payment_id:
X1 = X0 ∪ {order_id, payment_id}
X1⁺ = X0⁺ ∪ {user_id, table_id, order_created_at, order_status} (од order_id → ...)
∪ {amount, method, payment_date} (од payment_id → ...)
Сега преку order_id → user_id:
user_id → username, password_hash, role_id, first_name, last_name, email, activerole_id → role_name, description
Преку order_id → table_id:
table_id → table_number, capacity, table_status
Преку product_id (веќе во X0 преку inventory_id и recipe_id):
product_id → category_id, product_name, description, price, active, product_min_stockcategory_id → category_name, description, active
Сега X1⁺ = U. Значи:
K = {session_token, shift_close_id, inventory_id, ingredient_inventory_id,
log_id, setting_id, order_id, item_number, payment_id, invoice_id, recipe_id}
е супер-клуч.
Проверка на минималност: Да провериме дали некој атрибут е излишен:
- Отстранување на
session_token→session_created_at,expires_atне можат да се изведат → неопходен. - Отстранување на
shift_close_id→shift_id,closed_by,total,order_count,closed_atне можат да се изведат → неопходен. - Отстранување на
inventory_id→quantity_change,operation_type,inventory_created_atне можат да се изведат → неопходен. - Отстранување на
ingredient_inventory_id→ingredient_idне може да се изведе → неопходен. - Отстранување на
log_id→action,entity_name,log_created_atне можат да се изведат → неопходен. - Отстранување на
setting_id→restaurant_name,vat_rate,currencyне можат да се изведат → неопходен. - Отстранување на
order_id→table_id,order_created_at,order_statusне можат да се изведат → неопходен. - Отстранување на
item_number→quantity,unit_price,item_statusне можат да се изведат → неопходен. - Отстранување на
payment_id→amount,method,payment_dateне можат да се изведат → неопходен. - Отстранување на
invoice_id→invoice_number,issued_atне можат да се изведат → неопходен. - Отстранување на
recipe_id→quantity_neededне може да се изведе → неопходен.
Заклучок: K е минимален супер-клуч, значи е кандидат-клуч.
Дали има друг кандидат-клуч?
Секој кандидат-клуч мора да ги содржи сите атрибути што не се појавуваат на RHS:
{session_token, shift_close_id, inventory_id, ingredient_inventory_id,
log_id, setting_id, item_number, recipe_id, invoice_id}
Понатаму:
- За да се добијат
order_id-зависните атрибути (table_id,order_created_at,order_status), мора да имаorder_id(илиpayment_idод кој се изведуваorder_id). - За
amount,method,payment_dateмора да имаpayment_id(бидејќиorder_id → payment_idне постои). - За
invoice_number,issued_atмора да имаinvoice_id.
Значи секој кандидат-клуч мора да го содржи множеството:
K_min = {session_token, shift_close_id, inventory_id, ingredient_inventory_id,
log_id, setting_id, item_number, recipe_id, invoice_id, order_id, payment_id}
Ова е токму K. Бидејќи K е минимален и секој кандидат-клуч мора да го содржи K_min = K, следи дека не постои друг кандидат-клуч.
Избор на примарен клуч
За примарен клуч се избира кандидат-клучот K:
PK = (session_token, shift_close_id, inventory_id, ingredient_inventory_id,
log_id, setting_id, order_id, item_number, payment_id, invoice_id, recipe_id)
Забелешка: Овој PK е многу голем (11 атрибути) поради денормализираната природа. Со декомпозицијата тој ќе се намали.
Во која NF е денормализираната релација?
- 1NF: Да — сите ќелии содржат атомски вредности (по една вредност), нема повторувачки групи, нема мешање на типови.
- 2NF: Не — постојат парцијални зависности: многу не-клучни атрибути зависат само од дел од композитниот клуч (на пр.
user_id → username, ...кадеuser_idе дел од PK). - 3NF: Не — поради парцијалните зависности (2NF не е задоволен).
- BCNF: Не.
Заклучок: Денормализираната релација е во 1NF, но не е во 2NF. Декомпозицијата започнува од 2NF.
1NF декомпозиција
Релацијата е веќе во 1NF (види погоре). Не е потребна дополнителна декомпозиција за 1NF. Се преминува на 2NF.
2NF декомпозиција
Проблем: Во 1NF постојат парцијални зависности — не-клучни атрибути зависат само од дел од композитниот PK. Секоја таква група се издвојува во посебна табела каде тој дел станува PK.
Постапка: За секоја ФЗ X → Y каде X ⊂ PK, се создава нова релација R_i(X ∪ Y) со PK = X. Потоа се проверува lossless join и preservation of FDs.
Проверка на lossless join (теорема): Ако R1 и R2 се декомпозиција на R, и R1 ∩ R2 → R1 или R1 ∩ R2 → R2, тогаш декомпозицијата е lossless.
Проверка на preservation of FDs: Се проверува дали (F1 ∪ F2 ∪ ... ∪ Fn)⁺ = F⁺, каде Fi се ФЗ што важат во новата релација Ri.
R1: ROLE
{role_id} → {role_name, description}
| role_id | role_name | description |
| 1 | ADMIN | System administrator |
| 2 | WAITER | Restaurant waiter |
| 3 | MANAGER | Restaurant manager |
- ФЗ што важат:
{role_id} → {role_name, description}. - К.К.:
{role_id}(единствен;role_nameне е уникатен во општ случај). - PK:
role_id. - NF: BCNF (единствена ФЗ, LHS = PK = супер-клуч).
- Lossless join test:
R ∩ R1 = {role_id, role_name, description}. Бидејќи{role_id} → {role_name, description}иrole_id ∈ (R ∩ R1), следи(R ∩ R1) → R1. ⇒ lossless. - Preservation of FDs:
f2е зачувана во R1. ⇒ preserved. - Остаток:
R1.1 = R − {role_name, description}.
R2: APP_USER
{user_id} → {username, password_hash, role_id, first_name, last_name, email, active}
| user_id | username | password_hash | role_id | first_name | last_name | active | |
| 1 | admin | $2a$06$... | 1 | Georgi | Admin | admin@… | f |
| 2 | marko | $2a$06$... | 2 | Marko | Petrov | marko@… | f |
| 6 | (null) | $2a$06$... | 1 | Георги | Паунков | (null) | t |
| 7 | (null) | $2a$06$... | 2 | Georgi | Paunkov | (null) | t |
- ФЗ што важат:
{user_id} → {username, password_hash, role_id, first_name, last_name, email, active}. - К.К.:
{user_id}(единствен;usernameиemailимаат NULL вредности, не се уникатни). - PK:
user_id. - NF: BCNF.
- Lossless join test:
R1.1 ∩ R2 = {user_id, username, password_hash, role_id, first_name, last_name, email, active}. Бидејќи{user_id} → {...}иuser_id ∈ (R1.1 ∩ R2), следи(R1.1 ∩ R2) → R2. ⇒ lossless. - Preservation of FDs:
f1е зачувана. ⇒ preserved. - Остаток:
R2.1 = R1.1 − {username, password_hash, role_id, first_name, last_name, email, active}.
R3: SESSION
{session_token} → {user_id, session_created_at, expires_at}
| token | user_id | created_at | expires_at |
(празна табела)
- ФЗ што важат:
{session_token} → {user_id, session_created_at, expires_at}. - К.К.:
{session_token}. - PK:
token. - NF: BCNF.
- Lossless join test:
R2.1 ∩ R3 = {session_token, user_id, session_created_at, expires_at}.{session_token} → {...}иsession_token ∈ (R2.1 ∩ R3)⇒(R2.1 ∩ R3) → R3. ⇒ lossless. - Preservation of FDs:
f3зачувана. ⇒ preserved. - Остаток:
R3.1 = R2.1 − {user_id, session_created_at, expires_at}.
R4: SHIFT
{shift_id} → {user_id, shift_start_time, shift_end_time}
| shift_id | user_id | start_time | end_time |
| 1 | 2 | 2026-01-10 08:00:00 | 2026-01-10 16:00:00 |
- ФЗ што важат:
{shift_id} → {user_id, shift_start_time, shift_end_time}. - К.К.:
{shift_id}. - PK:
shift_id. - NF: BCNF.
- Lossless join test:
R3.1 ∩ R4 = {shift_id, user_id, shift_start_time, shift_end_time}.{shift_id} → {...}⇒(R3.1 ∩ R4) → R4. ⇒ lossless. - Preservation of FDs:
f4зачувана. ⇒ preserved. - Остаток:
R4.1 = R3.1 − {user_id, shift_start_time, shift_end_time}.
R5: RESTAURANT_TABLE
{table_id} → {table_number, capacity, table_status}
| table_id | table_number | capacity | status |
| 1 | 1 | 4 | СЛОБОДНА |
| 2 | 2 | 4 | СЛОБОДНА |
| 3 | 3 | 6 | СЛОБОДНА |
| 4 | 4 | 4 | СЛОБОДНА |
| 5 | 5 | 4 | СЛОБОДНА |
| ... | ... | ... | ... |
- ФЗ што важат:
{table_id} → {table_number, capacity, table_status}. - К.К.:
{table_id}. - PK:
table_id. - NF: BCNF.
- Lossless join test:
R4.1 ∩ R5 = {table_id, table_number, capacity, table_status}.{table_id} → {...}⇒(R4.1 ∩ R5) → R5. ⇒ lossless. - Preservation of FDs:
f6зачувана. ⇒ preserved. - Остаток:
R5.1 = R4.1 − {table_number, capacity, table_status}.
R6: CATEGORY
{category_id} → {category_name, description, active}
| category_id | name | description | active |
| 1 | Кафе | (null) | t |
| 2 | Нес Кафе | (null) | t |
| 3 | Води | (null) | t |
| ... | ... | ... | ... |
- ФЗ што важат:
{category_id} → {category_name, description, active}. - К.К.:
{category_id}. - PK:
category_id. - NF: BCNF.
- Lossless join test:
R5.1 ∩ R6 = {category_id, category_name, description, active}.{category_id} → {...}⇒(R5.1 ∩ R6) → R6. ⇒ lossless. - Preservation of FDs:
f10зачувана. ⇒ preserved. - Остаток:
R6.1 = R5.1 − {category_name, description, active}.
R7: PRODUCT
{product_id} → {category_id, product_name, description, price, active, product_min_stock}
| product_id | category_id | name | description | price | active | min_stock |
| 1 | 1 | Еспресо Хаусбрандт | 100% Арабика | 70.00 | t | 0 |
| 2 | 1 | Макијато | 100% Арабика | 80.00 | t | 0 |
| ... | ... | ... | ... | ... | ... | ... |
- ФЗ што важат:
{product_id} → {category_id, product_name, description, price, active, product_min_stock}. - К.К.:
{product_id}. - PK:
product_id. - NF: BCNF.
- Lossless join test:
R6.1 ∩ R7 = {product_id, category_id, product_name, description, price, active, product_min_stock}.{product_id} → {...}⇒(R6.1 ∩ R7) → R7. ⇒ lossless. - Preservation of FDs:
f9зачувана. ⇒ preserved. - Остаток:
R7.1 = R6.1 − {category_id, product_name, description, price, active, product_min_stock}.
R8: ORDERS
{order_id} → {user_id, table_id, order_created_at, order_status}
| order_id | user_id | table_id | created_at | status |
| 43 | 1 | 1 | 2026-07-10 14:47:39 | ПЛАТЕНА |
| 44 | 1 | 1 | 2026-07-10 14:48:34 | ПЛАТЕНА |
| ... | ... | ... | ... | ... |
- ФЗ што важат:
{order_id} → {user_id, table_id, order_created_at, order_status}. - К.К.:
{order_id}. - PK:
order_id. - NF: BCNF.
- Lossless join test:
R7.1 ∩ R8 = {order_id, user_id, table_id, order_created_at, order_status}.{order_id} → {...}⇒(R7.1 ∩ R8) → R8. ⇒ lossless. - Preservation of FDs:
f7зачувана. ⇒ preserved. - Остаток:
R8.1 = R7.1 − {user_id, table_id, order_created_at, order_status}.
R9: PAYMENT
{payment_id} → {order_id, amount, method, payment_date}
| payment_id | order_id | amount | method | payment_date |
| 63 | 43 | 6.50 | КЕШ | 2026-07-10 14:47:43 |
| 64 | 44 | 6.50 | КЕШ | 2026-07-10 14:48:38 |
| ... | ... | ... | ... | ... |
- ФЗ што важат:
{payment_id} → {order_id, amount, method, payment_date}. - К.К.:
{payment_id}. - PK:
payment_id. - NF: BCNF.
- Lossless join test:
R8.1 ∩ R9 = {payment_id, order_id, amount, method, payment_date}.{payment_id} → {...}⇒(R8.1 ∩ R9) → R9. ⇒ lossless. - Preservation of FDs:
f11зачувана. ⇒ preserved. - Остаток:
R9.1 = R8.1 − {order_id, amount, method, payment_date}.
R10: INVOICE
{invoice_id} → {payment_id, invoice_number, issued_at}
| invoice_id | payment_id | invoice_number | issued_at |
| 39 | 63 | INV-43 | 2026-07-10 14:47:43 |
| 40 | 64 | INV-44 | 2026-07-10 14:48:38 |
| ... | ... | ... | ... |
- ФЗ што важат:
{invoice_id} → {payment_id, invoice_number, issued_at}. - К.К.:
{invoice_id}. - PK:
invoice_id. - NF: BCNF.
- Lossless join test:
R9.1 ∩ R10 = {invoice_id, payment_id, invoice_number, issued_at}.{invoice_id} → {...}⇒(R9.1 ∩ R10) → R10. ⇒ lossless. - Preservation of FDs:
f12зачувана. ⇒ preserved. - Остаток:
R10.1 = R9.1 − {payment_id, invoice_number, issued_at}.
R11: INGREDIENT
{ingredient_id} → {ingredient_name, unit, ingredient_min_stock, active}
(празна табела)
- ФЗ што важат:
{ingredient_id} → {ingredient_name, unit, ingredient_min_stock, active}. - К.К.:
{ingredient_id}. - PK:
ingredient_id. - NF: BCNF.
- Lossless join test:
R10.1 ∩ R11 = {ingredient_id, ingredient_name, unit, ingredient_min_stock, active}.{ingredient_id} → {...}⇒(R10.1 ∩ R11) → R11. ⇒ lossless. - Preservation of FDs:
f13зачувана. ⇒ preserved. - Остаток:
R11.1 = R10.1 − {ingredient_name, unit, ingredient_min_stock, active}.
R12: RECIPE
{recipe_id} → {product_id}
(празна табела)
- ФЗ што важат:
{recipe_id} → {product_id}. - К.К.:
{recipe_id}. - PK:
recipe_id. - NF: BCNF.
- Lossless join test:
R11.1 ∩ R12 = {recipe_id, product_id}.{recipe_id} → {product_id}⇒(R11.1 ∩ R12) → R12. ⇒ lossless. - Preservation of FDs:
f14зачувана. ⇒ preserved. - Остаток:
R12.1 = R11.1 − {product_id}.
R13: INVENTORY ==={{{
{inventory_id} → {product_id, quantity_change, operation_type, inventory_created_at} }}}
| inventory_id | product_id | quantity_change | operation_type | created_at |
| 1 | 14 | -1 | ПРОДАЖБА | 2026-07-10 17:17 |
| 2 | 51 | -1 | ПРОДАЖБА | 2026-07-10 17:31 |
| ... | ... | ... | ... | ... |
- ФЗ што важат:
{inventory_id} → {product_id, quantity_change, operation_type, inventory_created_at}. - К.К.:
{inventory_id}. - PK:
inventory_id. - NF: BCNF.
- Lossless join test:
R12.1 ∩ R13 = {inventory_id, product_id, quantity_change, operation_type, inventory_created_at}.{inventory_id} → {...}⇒(R12.1 ∩ R13) → R13. ⇒ lossless. - Preservation of FDs:
f16зачувана. ⇒ preserved. - Остаток:
R13.1 = R12.1 − {product_id, quantity_change, operation_type, inventory_created_at}.
R14: INGREDIENT_INVENTORY
{ingredient_inventory_id} → {ingredient_id, quantity_change, operation_type, ingredient_inventory_created_at}
(празна табела)
- ФЗ што важат:
{ingredient_inventory_id} → {ingredient_id, quantity_change, operation_type, ingredient_inventory_created_at}. - К.К.:
{ingredient_inventory_id}. - PK:
ingredient_inventory_id(во финалниот дизајнid). - NF: BCNF.
- Lossless join test:
R13.1 ∩ R14 = {ingredient_inventory_id, ingredient_id, quantity_change, operation_type, ingredient_inventory_created_at}.{ingredient_inventory_id} → {...}⇒(R13.1 ∩ R14) → R14. ⇒ lossless. - Preservation of FDs:
f17зачувана. ⇒ preserved. - Остаток:
R14.1 = R13.1 − {ingredient_id, quantity_change, operation_type, ingredient_inventory_created_at}.
R15: AUDIT_LOG
{log_id} → {user_id, action, entity_name, log_created_at}
| log_id | user_id | action | entity_name | created_at |
| 1 | 1 | Направен е продукт Еспресо | PRODUCT | 2026-07-07 14:45 |
- ФЗ што важат:
{log_id} → {user_id, action, entity_name, log_created_at}. - К.К.:
{log_id}. - PK:
log_id. - NF: BCNF.
- Lossless join test:
R14.1 ∩ R15 = {log_id, user_id, action, entity_name, log_created_at}.{log_id} → {...}⇒(R14.1 ∩ R15) → R15. ⇒ lossless. - Preservation of FDs:
f18зачувана. ⇒ preserved. - Остаток:
R15.1 = R14.1 − {user_id, action, entity_name, log_created_at}.
R16: APP_SETTINGS
{setting_id} → {restaurant_name, vat_rate, currency, printer_config}
| id | restaurant_name | vat_rate | currency | printer_config |
| 1 | Емпорио | 18.00 | ден | 2 |
- ФЗ што важат:
{setting_id} → {restaurant_name, vat_rate, currency, printer_config}. - К.К.:
{setting_id}. - PK:
setting_id(во финалниот дизајнid). - NF: BCNF.
- Lossless join test:
R15.1 ∩ R16 = {setting_id, restaurant_name, vat_rate, currency, printer_config}.{setting_id} → {...}⇒(R15.1 ∩ R16) → R16. ⇒ lossless. - Preservation of FDs:
f19зачувана. ⇒ preserved. - Остаток:
R16.1 = R15.1 − {restaurant_name, vat_rate, currency, printer_config}.
R17: SETTINGS
{setting_id} → {restaurant_name, vat_percent, currency, printer_ip, printer_port}
| id | restaurant_name | vat_percent | currency | printer_ip | printer_port |
| 1 | EMPORIO CAFFE | 18 | ден | (null) | 9100 |
- ФЗ што важат:
{setting_id} → {restaurant_name, vat_percent, currency, printer_ip, printer_port}. - К.К.:
{setting_id}. - PK:
setting_id(во финалниот дизајнid). - NF: BCNF.
- Lossless join test:
R16.1 ∩ R17 = {setting_id, restaurant_name, vat_percent, currency, printer_ip, printer_port}.{setting_id} → {...}⇒(R16.1 ∩ R17) → R17. ⇒ lossless. - Preservation of FDs: зачувана. ⇒ preserved.
- Остаток:
R17.1 = R16.1 − {restaurant_name, vat_percent, currency, printer_ip, printer_port}.
R18: ORDER_ITEM
{order_id, item_number} → {product_id, quantity, unit_price, item_status}
| order_id | item_number | product_id | quantity | unit_price | status |
| 54 | 1 | 14 | 1 | 70.00 | ПОРАЧАНО |
| 55 | 1 | 51 | 1 | 70.00 | ПОРАЧАНО |
| ... | ... | ... | ... | ... | ... |
- ФЗ што важат:
{order_id, item_number} → {product_id, quantity, unit_price, item_status}. - К.К.:
{order_id, item_number}(единствен; нитуorder_idсам, нитуitem_numberсам не е уникатен). - PK:
(order_id, item_number). - NF: BCNF.
- Lossless join test:
R17.1 ∩ R18 = {order_id, item_number, product_id, quantity, unit_price, item_status}.{order_id, item_number} → {...}⇒(R17.1 ∩ R18) → R18. ⇒ lossless. - Preservation of FDs:
f8зачувана. ⇒ preserved.
R19: SHIFT_CLOSE
{shift_close_id} → {shift_id, closed_by, total, order_count, closed_at}
| shift_id | closed_at | closed_by | total | order_count |
| 1 | 2026-07-09 11:32 | 1 | 12234.35 | 18 |
| 2 | 2026-07-09 11:32 | 1 | 0 | 0 |
| ... | ... | ... | ... | ... |
- ФЗ што важат:
{shift_close_id} → {shift_id, closed_by, total, order_count, closed_at}. - К.К.:
{shift_close_id}(единствен;shift_idе уникатен само ако секој shift има најмногу едно затворање — што е случај, но формалноshift_close_idе деклариран како PK;shift_idможе да се смета за алтернативен К.К. доколку е уникатен). - PK:
shift_close_id. - NF: BCNF.
- Lossless join test:
R18.1 ∩ R19 = {shift_close_id, shift_id, closed_by, total, order_count, closed_at}.{shift_close_id} → {...}⇒(R18.1 ∩ R19) → R19. ⇒ lossless. - Preservation of FDs:
f5зачувана. ⇒ preserved.
R20: RECIPE_ITEM
{recipe_id, ingredient_id} → {quantity_needed}
(празна табела)
- ФЗ што важат:
{recipe_id, ingredient_id} → {quantity_needed}. - К.К.:
{recipe_id, ingredient_id}(композитен; ниту еден дел сам не е уникатен). - PK:
(recipe_id, ingredient_id). - NF: BCNF.
- Lossless join test:
R19.1 ∩ R20 = {recipe_id, ingredient_id, quantity_needed}.{recipe_id, ingredient_id} → {quantity_needed}⇒(R19.1 ∩ R20) → R20. ⇒ lossless. - Preservation of FDs:
f15зачувана. ⇒ preserved.
3NF декомпозиција
Форма која ги следи овие правила:
- Веќе е во втора нормална форма.
- Се отстрануваат транзитивните функционални зависности.
Проверка за транзитивни зависности
Во 2NF, сите транзитивни зависности се веќе отстранети бидејќи секоја табела има само еден кандидат-клуч и сите не-клучни атрибути зависат директно од него.
Единствена потенцијална транзитивна зависност:
{recipe_id} → {product_id} → {category_id}(но RECIPE не содржиcategory_id, па нема транзитивност)
Сите табели се веќе во 3NF.
Preservation of FDs (глобална проверка): Сите ФЗ f1–f19 се зачувани во поединечните табели. ⇒ (F1 ∪ ... ∪ F20)⁺ = F⁺.
Lossless join (глобална проверка): Секоја декомпозиција во 2NF беше lossless. Со композиција на lossless декомпозиции, финалната декомпозиција е lossless.
BCNF (Boyce-Codd Normal Form)
Форма која ги следи овие правила:
- Веќе е во трета нормална форма.
- За секоја функционална зависност
X → Y,Xмора да биде супер-клуч.
Проверка за BCNF
Сите функционални зависности во сите табели имаат X кој е примарен клуч (или композитен клуч), што значи X е супер-клуч.
Сите табели се веќе во BCNF.
Финални табели (BCNF)
- ROLE (role_id PK, role_name, description)
- APP_USER (user_id PK, role_id FK, username, password_hash, first_name, last_name, email, active)
- SESSION (token PK, user_id FK, created_at, expires_at)
- SHIFT (shift_id PK, user_id FK, start_time, end_time)
- SHIFT_CLOSE (shift_close_id PK, shift_id FK, closed_by FK, total, order_count, closed_at)
- RESTAURANT_TABLE (table_id PK, table_number, capacity, status)
- CATEGORY (category_id PK, name, description, active)
- PRODUCT (product_id PK, category_id FK, name, description, price, active, min_stock)
- ORDERS (order_id PK, user_id FK, table_id FK, created_at, status)
- ORDER_ITEM (order_id PK, item_number PK, product_id FK, quantity, unit_price, status)
- PAYMENT (payment_id PK, order_id FK, amount, method, payment_date)
- INVOICE (invoice_id PK, payment_id FK, invoice_number, issued_at)
- INGREDIENT (ingredient_id PK, name, unit, min_stock, active)
- RECIPE (recipe_id PK, product_id FK)
- RECIPE_ITEM (recipe_id PK, ingredient_id PK, quantity_needed)
- INVENTORY (inventory_id PK, product_id FK, quantity_change, operation_type, created_at)
- INGREDIENT_INVENTORY (id PK, ingredient_id FK, quantity_change, operation_type, created_at)
- AUDIT_LOG (log_id PK, user_id FK, action, entity_name, created_at)
- APP_SETTINGS (id PK, restaurant_name, vat_rate, currency, printer_config)
- SETTINGS (id PK, restaurant_name, vat_percent, currency, printer_ip, printer_port)
Заклучок
Со примената на формална анализа на функционални зависности, правилен избор на примарни клучеви и проверка со lossless-join тест се елиминираат сите аномалии, се обезбедува логичка коректност и се добива шема погодна за имплементација во релациона датабаза.
Разлики помеѓу нормализираниот дизајн и дизајнот од фаза P2
Нормализираниот дизајн е идентичен со дизајнот од фаза P2. Ова значи дека дизајнот од P2 беше веќе правилно нормализиран.
Сите табели се во BCNF, сите функционални зависности се запазени, и сите декомпозиции се lossless.
За следните фази на проектот ќе се користи истиот дизајн од P2, бидејќи е веќе целосно нормализиран.
