| | 1 | = P5: Нормализација = |
| | 2 | |
| | 3 | == 1. Почетна де-нормализирана релација == |
| | 4 | |
| | 5 | Почетно се разгледува една глобална де-нормализирана релација со уникатни имиња на атрибути: |
| | 6 | |
| | 7 | {{{ |
| | 8 | R( |
| | 9 | u_user_id, u_full_name, u_email, u_phone, u_created_at, u_password_hash, |
| | 10 | ur_user_id, ur_role_id, r_role_name, |
| | 11 | lc_card_id, lc_user_id, lc_points, |
| | 12 | a_appointment_id, a_appointment_time, a_end_time, a_notes, a_type, |
| | 13 | a_total_price, a_points_awarded, a_user_id, a_status_id, st_status_name, |
| | 14 | aps_appointment_id, aps_service_id, |
| | 15 | s_service_id, s_service_name, s_price, s_duration_minutes, |
| | 16 | s_category_id, c_category_id, c_category_name, |
| | 17 | p_payment_id, p_amount, p_method, p_timestamp, p_status, |
| | 18 | p_appointment_id, p_points_used, p_package_purchase_id, |
| | 19 | rv_review_id, rv_rating, rv_comment, rv_created_at, rv_payment_id, |
| | 20 | pkg_package_id, pkg_name, pkg_max_usage, pkg_total_price, |
| | 21 | ps_package_id, ps_service_id, ps_discounted_price, |
| | 22 | upp_purchase_id, upp_user_id, upp_package_id, |
| | 23 | upp_purchased_at, upp_total_uses, upp_remaining_uses, |
| | 24 | upp_status, upp_expires_at, |
| | 25 | apu_appointment_id, apu_purchase_id, apu_service_id, |
| | 26 | apu_used_units, apu_finalized_at, |
| | 27 | av_availability_id, av_date, av_start_time, |
| | 28 | av_end_time, av_is_closed |
| | 29 | ) |
| | 30 | }}} |
| | 31 | |
| | 32 | Во оваа форма податоците од повеќе независни ентитети и M:N релации се споени во една глобална релација, што создава: |
| | 33 | |
| | 34 | * повторување на податоци, |
| | 35 | * update/delete аномалии, |
| | 36 | * мултивредносни групи, |
| | 37 | * зависности меѓу атрибути што не се дел од ист ентитет. |
| | 38 | |
| | 39 | Глобалната релација концептуално ги опфаќа следните ентитети и релации: |
| | 40 | |
| | 41 | * `"User"` |
| | 42 | * `role` |
| | 43 | * `userrole` |
| | 44 | * `category` |
| | 45 | * `service` |
| | 46 | * `status` |
| | 47 | * `appointment` |
| | 48 | * `appointmentservice` |
| | 49 | * `payment` |
| | 50 | * `review` |
| | 51 | * `package` |
| | 52 | * `packageservice` |
| | 53 | * `userpackagepurchase` |
| | 54 | * `appointmentpackageusage` |
| | 55 | * `availability` |
| | 56 | * `loyaltycard` |
| | 57 | |
| | 58 | == 2. Функционални зависности == |
| | 59 | |
| | 60 | {{{ |
| | 61 | "User".user_id |
| | 62 | -> full_name, email, phone, created_at, password_hash |
| | 63 | |
| | 64 | "User".email |
| | 65 | -> user_id, full_name, phone, created_at, password_hash |
| | 66 | |
| | 67 | role.role_id |
| | 68 | -> role.name |
| | 69 | |
| | 70 | category.category_id |
| | 71 | -> category.name |
| | 72 | |
| | 73 | status.status_id |
| | 74 | -> status.name |
| | 75 | |
| | 76 | service.service_id |
| | 77 | -> service.name, service.price, |
| | 78 | service.duration_minutes, service.category_id |
| | 79 | |
| | 80 | appointment.appointment_id |
| | 81 | -> appointment_time, end_time, |
| | 82 | notes, type, total_price, |
| | 83 | user_id, status_id, points_awarded |
| | 84 | |
| | 85 | payment.payment_id |
| | 86 | -> amount, method, timestamp, |
| | 87 | status, appointment_id, |
| | 88 | points_used, package_purchase_id |
| | 89 | |
| | 90 | review.review_id |
| | 91 | -> rating, comment, created_at, payment_id |
| | 92 | |
| | 93 | package.package_id |
| | 94 | -> name, max_usage, total_price |
| | 95 | |
| | 96 | userpackagepurchase.purchase_id |
| | 97 | -> user_id, package_id, |
| | 98 | purchased_at, total_uses, |
| | 99 | remaining_uses, status, expires_at |
| | 100 | |
| | 101 | availability.availability_id |
| | 102 | -> date, start_time, end_time, is_closed |
| | 103 | |
| | 104 | (packageservice.package_id, packageservice.service_id) |
| | 105 | -> discounted_price |
| | 106 | }}} |
| | 107 | |
| | 108 | Дополнително: |
| | 109 | |
| | 110 | {{{ |
| | 111 | (userrole.user_id, userrole.role_id) |
| | 112 | -> / |
| | 113 | |
| | 114 | (appointmentservice.appointment_id, |
| | 115 | appointmentservice.service_id) |
| | 116 | -> / |
| | 117 | }}} |
| | 118 | |
| | 119 | Овие релации немаат не-клучни атрибути. |
| | 120 | |
| | 121 | == 3. Кандидат-клучеви и избор на примарен клуч == |
| | 122 | |
| | 123 | Во глобалната де-нормализирана релација не постои единечен атрибут што ги одредува сите останати атрибути, бидејќи се комбинирани повеќе независни repeating groups: |
| | 124 | |
| | 125 | * повеќе улоги по корисник, |
| | 126 | * повеќе услуги по термин, |
| | 127 | * повеќе услуги по пакет, |
| | 128 | * повеќе package purchases, |
| | 129 | * повеќе availability windows, |
| | 130 | * повеќе payments и reviews. |
| | 131 | |
| | 132 | Поради тоа, глобалниот кандидат-клуч мора да биде композитен. |
| | 133 | |
| | 134 | Теоретски кандидат-клуч: |
| | 135 | |
| | 136 | {{{ |
| | 137 | K_R = |
| | 138 | ( |
| | 139 | ur_user_id, |
| | 140 | ur_role_id, |
| | 141 | aps_appointment_id, |
| | 142 | aps_service_id, |
| | 143 | p_payment_id, |
| | 144 | ps_package_id, |
| | 145 | ps_service_id, |
| | 146 | upp_purchase_id, |
| | 147 | av_availability_id |
| | 148 | ) |
| | 149 | }}} |
| | 150 | |
| | 151 | Почетната глобална релација не ја задоволува 1НФ и се наоѓа во UNF форма. |
| | 152 | |
| | 153 | == 4. Декомпозиција до 1НФ == |
| | 154 | |
| | 155 | === Релација што се анализира === |
| | 156 | |
| | 157 | {{{ |
| | 158 | R |
| | 159 | }}} |
| | 160 | |
| | 161 | === Проблем === |
| | 162 | |
| | 163 | Во глобалната релација постојат повторувачки групи и мултивредносни атрибути: |
| | 164 | |
| | 165 | * повеќе улоги по корисник, |
| | 166 | * повеќе услуги по термин, |
| | 167 | * повеќе услуги по пакет, |
| | 168 | * повеќе package purchases, |
| | 169 | * повеќе availability windows. |
| | 170 | |
| | 171 | === Декомпозиција === |
| | 172 | |
| | 173 | {{{ |
| | 174 | "User"(user_id, full_name, email, phone, created_at, password_hash) |
| | 175 | |
| | 176 | role(role_id, name) |
| | 177 | |
| | 178 | userrole(user_id, role_id) |
| | 179 | |
| | 180 | category(category_id, name) |
| | 181 | |
| | 182 | service(service_id, name, price, |
| | 183 | duration_minutes, category_id) |
| | 184 | |
| | 185 | status(status_id, name) |
| | 186 | |
| | 187 | appointment( |
| | 188 | appointment_id, |
| | 189 | appointment_time, |
| | 190 | end_time, |
| | 191 | notes, |
| | 192 | type, |
| | 193 | total_price, |
| | 194 | user_id, |
| | 195 | status_id, |
| | 196 | points_awarded |
| | 197 | ) |
| | 198 | |
| | 199 | appointmentservice( |
| | 200 | appointment_id, |
| | 201 | service_id |
| | 202 | ) |
| | 203 | |
| | 204 | payment( |
| | 205 | payment_id, |
| | 206 | amount, |
| | 207 | method, |
| | 208 | timestamp, |
| | 209 | status, |
| | 210 | appointment_id, |
| | 211 | points_used, |
| | 212 | package_purchase_id |
| | 213 | ) |
| | 214 | |
| | 215 | review( |
| | 216 | review_id, |
| | 217 | rating, |
| | 218 | comment, |
| | 219 | created_at, |
| | 220 | payment_id |
| | 221 | ) |
| | 222 | |
| | 223 | package( |
| | 224 | package_id, |
| | 225 | name, |
| | 226 | max_usage, |
| | 227 | total_price |
| | 228 | ) |
| | 229 | |
| | 230 | packageservice( |
| | 231 | package_id, |
| | 232 | service_id, |
| | 233 | discounted_price |
| | 234 | ) |
| | 235 | |
| | 236 | userpackagepurchase( |
| | 237 | purchase_id, |
| | 238 | user_id, |
| | 239 | package_id, |
| | 240 | purchased_at, |
| | 241 | total_uses, |
| | 242 | remaining_uses, |
| | 243 | status, |
| | 244 | expires_at |
| | 245 | ) |
| | 246 | |
| | 247 | appointmentpackageusage( |
| | 248 | appointment_id, |
| | 249 | purchase_id, |
| | 250 | service_id, |
| | 251 | used_units, |
| | 252 | finalized_at |
| | 253 | ) |
| | 254 | |
| | 255 | availability( |
| | 256 | availability_id, |
| | 257 | date, |
| | 258 | start_time, |
| | 259 | end_time, |
| | 260 | is_closed |
| | 261 | ) |
| | 262 | |
| | 263 | loyaltycard( |
| | 264 | card_id, |
| | 265 | points, |
| | 266 | user_id |
| | 267 | ) |
| | 268 | }}} |
| | 269 | |
| | 270 | === Клучеви === |
| | 271 | |
| | 272 | Едноставни PK: |
| | 273 | |
| | 274 | {{{ |
| | 275 | user_id |
| | 276 | role_id |
| | 277 | category_id |
| | 278 | service_id |
| | 279 | status_id |
| | 280 | appointment_id |
| | 281 | payment_id |
| | 282 | review_id |
| | 283 | package_id |
| | 284 | purchase_id |
| | 285 | availability_id |
| | 286 | card_id |
| | 287 | }}} |
| | 288 | |
| | 289 | Композитни PK: |
| | 290 | |
| | 291 | {{{ |
| | 292 | userrole(user_id, role_id) |
| | 293 | |
| | 294 | appointmentservice( |
| | 295 | appointment_id, |
| | 296 | service_id |
| | 297 | ) |
| | 298 | |
| | 299 | packageservice( |
| | 300 | package_id, |
| | 301 | service_id |
| | 302 | ) |
| | 303 | }}} |
| | 304 | |
| | 305 | === Lossless Join === |
| | 306 | |
| | 307 | Декомпозицијата е lossless бидејќи сите релации се поврзани преку PK/FK врски. |
| | 308 | |
| | 309 | === Dependency Preservation === |
| | 310 | |
| | 311 | Функционалните зависности се зачувани во новите релации. |
| | 312 | |
| | 313 | == 5. Декомпозиција до 2НФ == |
| | 314 | |
| | 315 | === Релации што се анализираат === |
| | 316 | |
| | 317 | * `userrole` |
| | 318 | * `appointmentservice` |
| | 319 | * `packageservice` |
| | 320 | |
| | 321 | === Проверка === |
| | 322 | |
| | 323 | Во `userrole` и `appointmentservice` нема не-клучни атрибути. |
| | 324 | |
| | 325 | Во `packageservice`: |
| | 326 | |
| | 327 | {{{ |
| | 328 | (package_id, service_id) |
| | 329 | -> discounted_price |
| | 330 | }}} |
| | 331 | |
| | 332 | `discounted_price` зависи од целата комбинација, а не од дел од клучот. |
| | 333 | |
| | 334 | === Заклучок === |
| | 335 | |
| | 336 | Постојната шема веќе ја задоволува 2НФ. |
| | 337 | |
| | 338 | Нема потреба од дополнителна декомпозиција. |
| | 339 | |
| | 340 | == 6. Декомпозиција до 3НФ == |
| | 341 | |
| | 342 | === Транзитивни зависности === |
| | 343 | |
| | 344 | {{{ |
| | 345 | service.service_id |
| | 346 | -> service.category_id |
| | 347 | -> category.name |
| | 348 | |
| | 349 | appointment.appointment_id |
| | 350 | -> appointment.status_id |
| | 351 | -> status.name |
| | 352 | |
| | 353 | "User".user_id |
| | 354 | -> userrole.role_id |
| | 355 | -> role.name |
| | 356 | }}} |
| | 357 | |
| | 358 | === Решение === |
| | 359 | |
| | 360 | Транзитивните зависности се отстранети преку издвојување на lookup релации: |
| | 361 | |
| | 362 | * `category` |
| | 363 | * `status` |
| | 364 | * `role` |
| | 365 | |
| | 366 | Дополнително: |
| | 367 | |
| | 368 | * `review` е одвоена од `payment` |
| | 369 | * `payment` е одвоена од `appointment` |
| | 370 | * `package` е одвоена од `userpackagepurchase` |
| | 371 | |
| | 372 | === Заклучок === |
| | 373 | |
| | 374 | Релациите ја задоволуваат 3НФ. |
| | 375 | |
| | 376 | == 7. Проверка за BCNF == |
| | 377 | |
| | 378 | Во најголем дел релациите ја задоволуваат BCNF бидејќи детерминантите се кандидат-клучеви. |
| | 379 | |
| | 380 | Особено: |
| | 381 | |
| | 382 | * `"User"` |
| | 383 | * `role` |
| | 384 | * `category` |
| | 385 | * `status` |
| | 386 | * `service` |
| | 387 | * `appointment` |
| | 388 | * `payment` |
| | 389 | * `review` |
| | 390 | * `package` |
| | 391 | * `userpackagepurchase` |
| | 392 | * `availability` |
| | 393 | * `loyaltycard` |
| | 394 | |
| | 395 | Контролирани де-нормализирани атрибути: |
| | 396 | |
| | 397 | * `appointment.total_price` |
| | 398 | * `package.total_price` |
| | 399 | * `payment.amount` |
| | 400 | * `appointment.points_awarded` |
| | 401 | |
| | 402 | Овие атрибути се свесно задржани за: |
| | 403 | |
| | 404 | * перформанси, |
| | 405 | * историска конзистентност, |
| | 406 | * поедноставување на трансакциската логика. |
| | 407 | |
| | 408 | Конзистентноста се одржува преку DB-first логика: |
| | 409 | |
| | 410 | === Functions / Procedures === |
| | 411 | |
| | 412 | {{{ |
| | 413 | fn_recalculate_appointment |
| | 414 | fn_validate_appointment |
| | 415 | sp_create_appointment |
| | 416 | sp_apply_package_to_appointment |
| | 417 | sp_create_payment |
| | 418 | sp_mark_payment_paid |
| | 419 | sp_award_loyalty_points |
| | 420 | }}} |
| | 421 | |
| | 422 | === Triggers === |
| | 423 | |
| | 424 | {{{ |
| | 425 | appointmentservice_recalculate |
| | 426 | appointment_validate |
| | 427 | payment_validate |
| | 428 | appointment_prevent_price_change |
| | 429 | }}} |
| | 430 | |
| | 431 | == 8. Финален нормализиран модел == |
| | 432 | |
| | 433 | Финалниот нормализиран модел е составен од: |
| | 434 | |
| | 435 | {{{ |
| | 436 | "User" |
| | 437 | role |
| | 438 | userrole |
| | 439 | category |
| | 440 | service |
| | 441 | status |
| | 442 | appointment |
| | 443 | appointmentservice |
| | 444 | payment |
| | 445 | review |
| | 446 | package |
| | 447 | packageservice |
| | 448 | userpackagepurchase |
| | 449 | appointmentpackageusage |
| | 450 | availability |
| | 451 | loyaltycard |
| | 452 | }}} |
| | 453 | |
| | 454 | == 9. Споредба со Phase 2 дизајнот == |
| | 455 | |
| | 456 | Нормализацијата покажува дека тековниот модел веќе е во голем дел усогласен со 3НФ/BCNF. |
| | 457 | |
| | 458 | Во постојниот дизајн: |
| | 459 | |
| | 460 | * ентитетите се раздвоени, |
| | 461 | * M:N релациите се реализирани преку junction табели, |
| | 462 | * lookup податоците се издвоени, |
| | 463 | * транзитивните зависности се отстранети, |
| | 464 | * business logic е имплементирана во базата. |
| | 465 | |
| | 466 | === Дали е потребно реструктурирање? === |
| | 467 | |
| | 468 | Не е потребно значајно реструктурирање на базата. |
| | 469 | |
| | 470 | Опционални подобрувања: |
| | 471 | |
| | 472 | * конзистентно именување на табели, |
| | 473 | * дополнително стандардизирање на status вредности. |
| | 474 | |
| | 475 | == 10. Заклучок == |
| | 476 | |
| | 477 | Финалниот дизајн: |
| | 478 | |
| | 479 | * ја намалува редундантноста, |
| | 480 | * избегнува insert/update/delete аномалии, |
| | 481 | * ги зачувува функционалните зависности, |
| | 482 | * овозможува lossless join, |
| | 483 | * и поддржува DB-first имплементација преку SQL функции, процедури, тригери и views |