wiki:Normalization

Version 2 (modified by 201178, 6 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 email 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_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, бидејќи е веќе целосно нормализиран.

Note: See TracWiki for help on using the wiki.