= Нормализација = == Денормализирана форма == Форма во која има една табела во која се внесени сите ентитети и нивните релации помеѓу себе, без никакви правила. Во денормализираната форма сите атрибути од сите 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, active` * `role_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_stock` * `category_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 || email || active || || 1 || admin || $2a$06$... || 1 || Georgi || Admin || admin@pos.com || f || || 2 || marko || $2a$06$... || 2 || Marko || Petrov || marko@pos.com || 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, бидејќи е веќе целосно нормализиран.