| Version 1 (modified by , 7 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 | ден |
Во денормализираната табела постојат следниве основни функционални зависности:
- {user_id} → {username, password_hash, role_id, active}
- {role_id} → {role_name, description}
- {session_token} → {user_id, created_at, expires_at}
- {shift_id} → {user_id, start_time, end_time}
- {shift_close_id} → {shift_id, closed_by, total, order_count, closed_at}
- {table_id} → {table_number, capacity, status}
- {order_id} → {user_id, table_id, created_at, status}
- {order_id, item_number} → {product_id, quantity, unit_price, status}
- {product_id} → {category_id, name, description, price, active, min_stock}
- {category_id} → {name, description, active}
- {payment_id} → {order_id, amount, method, payment_date}
- {invoice_id} → {payment_id, invoice_number, issued_at}
- {ingredient_id} → {name, unit, min_stock, active}
- {recipe_id} → {product_id}
- {recipe_id, ingredient_id} → {quantity_needed}
- {inventory_id} → {product_id, quantity_change, operation_type, created_at}
- {ingredient_inventory_id} → {ingredient_id, quantity_change, operation_type, created_at}
- {log_id} → {user_id, action, entity_name, created_at}
- {setting_id} → {restaurant_name, vat_rate, currency}
Прва нормална форма (1NF)
Форма во која повторно има една табела, но овојпат таа е ограничена со следниве правила:
- Подредувањето на редовите не претставува никакво значење
- Во ќелиите на секоја колона има по една вредност
- Не се мешаат типови на податоци во една ќелија
- Табелата има примарен композитен клуч
- Нема повторувачки групи
Примарен композитен клуч: (user_id, order_id, item_number, payment_id, invoice_id, ingredient_id, recipe_id, inventory_id, ingredient_inventory_id, log_id, shift_id, shift_close_id, session_token, table_id, category_id, role_id, setting_id)
Овој клуч уникатно го идентификува секој ред бидејќи комбинира ги сите клучеви на ентитетите.
Избор на примарен клуч во 1NF
Поради големиот број на атрибути и сложеноста, композитниот клуч е многу голем. Ова е нормално за денормализирана форма и ќе се намали со декомпозиција.
Втора нормална форма (2NF)
Форма која ги следи овие правила:
- Веќе е во прва нормална форма
- Секој атрибут кој не е клуч, зависи од целосниот примарен клуч (нема парцијални зависности)
Проблем во 1NF: парцијални зависности
Во 1NF постојат атрибути кои зависат само од дел од примарниот клуч:
- {user_id} → {username, password_hash, role_id, active}
- {role_id} → {role_name, description}
- {session_token} → {user_id, created_at, expires_at}
- {shift_id} → {user_id, start_time, end_time}
- {table_id} → {table_number, capacity, status}
- {order_id} → {user_id, table_id, created_at, status}
- {product_id} → {category_id, name, description, price, active, min_stock}
- {category_id} → {name, description, active}
- {payment_id} → {order_id, amount, method, payment_date}
- {invoice_id} → {payment_id, invoice_number, issued_at}
- {ingredient_id} → {name, unit, min_stock, active}
- {recipe_id} → {product_id}
- {inventory_id} → {product_id, quantity_change, operation_type, created_at}
- {ingredient_inventory_id} → {ingredient_id, quantity_change, operation_type, created_at}
- {log_id} → {user_id, action, entity_name, created_at}
- {setting_id} → {restaurant_name, vat_rate, currency}
- {shift_close_id} → {shift_id, closed_by, total, order_count, closed_at}
Ова ја крши 2NF. Решение: декомпозиција - секоја група на атрибути која зависи од дел од композитниот клуч се издвојува во посебна табела каде тој дел станува примарен клуч.
R1: ROLE
{role_id} → {role_name, description}
| role_id | role_name | description |
| 1 | ADMIN | System administrator |
| 2 | WAITER | Restaurant waiter |
| 3 | MANAGER | Restaurant manager |
Lossless join test:
R ∩ R1 = {role_id, role_name, description}
Бидејќи {role_id} → {role_name, description}, а role_id ∈ (R ∩ R1), следи дека (R ∩ R1) → R1.
⇒ Декомпозицијата е lossless.
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 |
Lossless join test:
R1.1 ∩ R2 = {user_id, username, password_hash, role_id, first_name, last_name, email, active}
Бидејќи {user_id} → {username, password_hash, role_id, first_name, last_name, email, active}, а user_id ∈ (R1.1 ∩ R2), следи дека (R1.1 ∩ R2) → R2.
⇒ Декомпозицијата е lossless.
R2.1 = R1.1 - {username, password_hash, role_id, first_name, last_name, email, active}
R3: SESSION
{session_token} → {user_id, created_at, expires_at}
| token | user_id | created_at | expires_at |
(празна табела)
Lossless join test:
R2.1 ∩ R3 = {session_token, user_id, created_at, expires_at}
Бидејќи {session_token} → {user_id, created_at, expires_at}, а session_token ∈ (R2.1 ∩ R3), следи дека (R2.1 ∩ R3) → R3.
⇒ Декомпозицијата е lossless.
R3.1 = R2.1 - {user_id, created_at, expires_at}
R4: SHIFT
{shift_id} → {user_id, start_time, end_time}
| shift_id | user_id | start_time | end_time |
| 1 | 2 | 2026-01-10 08:00:00 | 2026-01-10 16:00:00 |
Lossless join test:
R3.1 ∩ R4 = {shift_id, user_id, start_time, end_time}
Бидејќи {shift_id} → {user_id, start_time, end_time}, а shift_id ∈ (R3.1 ∩ R4), следи дека (R3.1 ∩ R4) → R4.
⇒ Декомпозицијата е lossless.
R4.1 = R3.1 - {user_id, start_time, end_time}
R5: RESTAURANT_TABLE
{table_id} → {table_number, capacity, status}
| table_id | table_number | capacity | status |
| 1 | 1 | 4 | СЛОБОДНА |
| 2 | 2 | 4 | СЛОБОДНА |
| 3 | 3 | 6 | СЛОБОДНА |
| 4 | 4 | 4 | СЛОБОДНА |
| 5 | 5 | 4 | СЛОБОДНА |
| ... | ... | ... | ... |
Lossless join test:
R4.1 ∩ R5 = {table_id, table_number, capacity, status}
Бидејќи {table_id} → {table_number, capacity, status}, а table_id ∈ (R4.1 ∩ R5), следи дека (R4.1 ∩ R5) → R5.
⇒ Декомпозицијата е lossless.
R5.1 = R4.1 - {table_number, capacity, status}
R6: CATEGORY
{category_id} → {name, description, active}
| category_id | name | description | active |
| 1 | Кафе | (null) | t |
| 2 | Нес Кафе | (null) | t |
| 3 | Води | (null) | t |
| ... | ... | ... | ... |
Lossless join test:
R5.1 ∩ R6 = {category_id, name, description, active}
Бидејќи {category_id} → {name, description, active}, а category_id ∈ (R5.1 ∩ R6), следи дека (R5.1 ∩ R6) → R6.
⇒ Декомпозицијата е lossless.
R6.1 = R5.1 - {name, description, active}
R7: PRODUCT
{product_id} → {category_id, name, description, price, active, 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 |
| ... | ... | ... | ... | ... | ... | ... |
Lossless join test:
R6.1 ∩ R7 = {product_id, category_id, name, description, price, active, min_stock}
Бидејќи {product_id} → {category_id, name, description, price, active, min_stock}, а product_id ∈ (R6.1 ∩ R7), следи дека (R6.1 ∩ R7) → R7.
⇒ Декомпозицијата е lossless.
R7.1 = R6.1 - {category_id, name, description, price, active, min_stock}
R8: ORDERS
{order_id} → {user_id, table_id, created_at, 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 | ПЛАТЕНА |
| ... | ... | ... | ... | ... |
Lossless join test:
R7.1 ∩ R8 = {order_id, user_id, table_id, created_at, status}
Бидејќи {order_id} → {user_id, table_id, created_at, status}, а order_id ∈ (R7.1 ∩ R8), следи дека (R7.1 ∩ R8) → R8.
⇒ Декомпозицијата е lossless.
R8.1 = R7.1 - {user_id, table_id, created_at, 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 |
| ... | ... | ... | ... | ... |
Lossless join test:
R8.1 ∩ R9 = {payment_id, order_id, amount, method, payment_date}
Бидејќи {payment_id} → {order_id, amount, method, payment_date}, а payment_id ∈ (R8.1 ∩ R9), следи дека (R8.1 ∩ R9) → R9.
⇒ Декомпозицијата е lossless.
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 |
| ... | ... | ... | ... |
Lossless join test:
R9.1 ∩ R10 = {invoice_id, payment_id, invoice_number, issued_at}
Бидејќи {invoice_id} → {payment_id, invoice_number, issued_at}, а invoice_id ∈ (R9.1 ∩ R10), следи дека (R9.1 ∩ R10) → R10.
⇒ Декомпозицијата е lossless.
R10.1 = R9.1 - {payment_id, invoice_number, issued_at}
R11: INGREDIENT
{ingredient_id} → {name, unit, min_stock, active}
(празна табела)
Lossless join test:
R10.1 ∩ R11 = {ingredient_id, name, unit, min_stock, active}
Бидејќи {ingredient_id} → {name, unit, min_stock, active}, а ingredient_id ∈ (R10.1 ∩ R11), следи дека (R10.1 ∩ R11) → R11.
⇒ Декомпозицијата е lossless.
R11.1 = R10.1 - {name, unit, min_stock, active}
R12: RECIPE
{recipe_id} → {product_id}
(празна табела)
Lossless join test:
R11.1 ∩ R12 = {recipe_id, product_id}
Бидејќи {recipe_id} → {product_id}, а recipe_id ∈ (R11.1 ∩ R12), следи дека (R11.1 ∩ R12) → R12.
⇒ Декомпозицијата е lossless.
R12.1 = R11.1 - {product_id}
R13: INVENTORY
{inventory_id} → {product_id, quantity_change, operation_type, 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 |
| ... | ... | ... | ... | ... |
Lossless join test:
R12.1 ∩ R13 = {inventory_id, product_id, quantity_change, operation_type, created_at}
Бидејќи {inventory_id} → {product_id, quantity_change, operation_type, created_at}, а inventory_id ∈ (R12.1 ∩ R13), следи дека (R12.1 ∩ R13) → R13.
⇒ Декомпозицијата е lossless.
R13.1 = R12.1 - {product_id, quantity_change, operation_type, created_at}
R14: INGREDIENT_INVENTORY
{ingredient_inventory_id} → {ingredient_id, quantity_change, operation_type, created_at}
(празна табела)
Lossless join test:
R13.1 ∩ R14 = {id, ingredient_id, quantity_change, operation_type, created_at}
⇒ Декомпозицијата е lossless.
R14.1 = R13.1 - {ingredient_id, quantity_change, operation_type, created_at}
R15: AUDIT_LOG
{log_id} → {user_id, action, entity_name, created_at}
| log_id | user_id | action | entity_name | created_at |
| 1 | 1 | Направен е продукт Еспресо | PRODUCT | 2026-07-07 14:45 |
Lossless join test:
R14.1 ∩ R15 = {log_id, user_id, action, entity_name, created_at}
⇒ Декомпозицијата е lossless.
R15.1 = R14.1 - {user_id, action, entity_name, created_at}
R16: APP_SETTINGS
{id} → {restaurant_name, vat_rate, currency, printer_config}
| id | restaurant_name | vat_rate | currency | printer_config |
| 1 | Емпорио | 18.00 | ден | 2 |
Lossless join test:
R15.1 ∩ R16 = {id, restaurant_name, vat_rate, currency, printer_config}
⇒ Декомпозицијата е lossless.
R16.1 = R15.1 - {restaurant_name, vat_rate, currency, printer_config}
R17: SETTINGS
{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 |
Lossless join test:
R16.1 ∩ R17 = {id, restaurant_name, vat_percent, currency, printer_ip, printer_port}
⇒ Декомпозицијата е lossless.
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, status}
| order_id | item_number | product_id | quantity | unit_price | status |
| 54 | 1 | 14 | 1 | 70.00 | ПОРАЧАНО |
| 55 | 1 | 51 | 1 | 70.00 | ПОРАЧАНО |
| ... | ... | ... | ... | ... | ... |
Lossless join test:
R17.1 ∩ R18 = {order_id, item_number, product_id, quantity, unit_price, status}
⇒ Декомпозицијата е lossless.
R19: SHIFT_CLOSE
{shift_id} → {closed_at, closed_by, total, order_count}
| 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 |
| ... | ... | ... | ... | ... |
Lossless join test:
R18.1 ∩ R19 = {shift_id, closed_at, closed_by, total, order_count}
⇒ Декомпозицијата е lossless.
R20: RECIPE_ITEM
{recipe_id, ingredient_id} → {quantity_needed}
(празна табела)
Lossless join test:
R19.1 ∩ R20 = {recipe_id, ingredient_id, quantity_needed}
⇒ Декомпозицијата е lossless.
Трета нормална форма (3NF)
Форма која ги следи овие правила:
- Веќе е во втора нормална форма
- Се отстрануваат транзитивните функционални зависности
Проверка за транзитивни зависности
Во 2NF, сите транзитивни зависности се веќе отстранети бидејќи секоја табела има само еден кандидат-клуч и сите не-клучни атрибути зависат директно од него.
Единствена потенцијална транзитивна зависност:
- {recipe_id} → {product_id} → {category_id} (но recipe не содржи category_id, па нема транзитивност)
Сите табели се веќе во 3NF.
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_id PK, closed_at, closed_by FK, total, order_count) # 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, бидејќи е веќе целосно нормализиран.
