C, PHP, VB, .NET

Дневникът на Филип Петров


* Пета нормална форма

Публикувано на 21 юни 2014 в раздел Бази от Данни.

Предишните нормални форми имаха строги и ясни правила. В общи линии с нарастване на практическия опит спазването им става интуитивно. А в практиката дори несъзнателно започваме да спазваме едно съвсем просто (но погрешно!) правило: „да има възможно най-малко на брой таблици, като същевременно с това да няма повторение на информация“. Това правило обикновено води до добре нормализирана база от данни. А защо все пак е погрешно?

Досега виждате, че когато нормализирахме базата данни, ние „разбивахме“ големите таблици на по-малки. Този процес се нарича декомпозиция. От практическите задачи обаче знаем, че впоследствие често ни се налага да „събираме обратно“ информацията чрез оператор JOIN в SELECT заявките. Процесът, при който свързваме множество по-малки таблици в една голяма, се нарича обединение. Е, може би интуитивно ще се досетите, че е важно ако декомпозираме една релация, после да можем да я възстановяваме чрез обединение. В противен случай ще казваме, че имаме „загуба на информация при процеса на декомпозиция“. Ако една таблица и всички нейни оригинални записи могат да бъдат реконструирани чрез SQL JOIN операция, то ще казваме, че има JOIN зависимост на данните.

Нека дам един прост пример за декомпозиция без загуба на информация. Нека вземем следната таблица:

CREATE TABLE users(
 id SERIAL PRIMARY KEY,
 EGN INT(10) UNSIGNED UNIQUE,
 name VARCHAR(255) NOT NULL,
 phone VARHCAR(20) NULL DEFAULT NULL
 ) ENGINE = InnoDB;

Ако я декомпозираме по следния начин:

CREATE TABLE users(
 id SERIAL PRIMARY KEY,
 name VARCHAR(255) NOT NULL,
 phone VARHCAR(20) NULL DEFAULT NULL
 ) ENGINE = InnoDB;

CREATE TABLE usersEGNs(
 userid SERIAL,
 FOREIGN KEY(userid) REFERENCES users(id),
 PRIMARY KEY(userid),
 EGN INT(10) UNSIGNED UNIQUE
 ) ENGINE=InnoDB;

ние няма да загубим никаква информация и същевременно с това можем да възстановим оригиналната таблица директно чрез следната заявка:

SELECT users.id, usersEGNs.EGN, users.name, users.phone
 FROM users JOIN usersEGNs ON users.id = usersEGNs.userid;

Нека обаче сега дам класическия пример за таблица, в която има излишество на информация, но не може да бъде декомпозирана без да се загуби информация. Дадена е следната таблица:

CREATE TABLE sales(
 seller_id INT UNSIGNED,
 manufacturer_id INT UNSIGNED,
 product_type_id INT UNSIGNED,
 PRIMARY KEY(seller_id, manufacturer_id, product_type_id)
 ) ENGINE=InnoDB;

От нея могат да се изкарват следните отчети:

  • Кои продавачи с кои производители работят;
  • Кои продавачи какви типове продукти продават;
  • Кои производители какви типове продукти произвеждат.

Виждате, че тези три отчета са свързани един с друг (не се нарушава 4НФ). Едно примерно попълване би било следното:

sales
+-----------+-----------------+-----------------+
| seller_id | manufacturer_id | product_type_id |
+-----------+-----------------+-----------------+
|        1  |               1 |               1 |
|        1  |               1 |               2 |
|        1  |               2 |               1 |
|        2  |               1 |               2 |
|        2  |               2 |               2 |
|        3  |               1 |               1 |
+-----------+-----------------+-----------------+

Тук има излишество на информация – например пазим два пъти информацията, че seller 1 продава продукт от тип 1, както и два пъти, че seller 1 продава продукти на manufacturer 1. В зависимост от данните и обема им, това при по-голяма таблица може да е проблем. Тогава защо не декомпозираме в отделни таблици? Това би могло да се случи по следния начин:

sellers_manufacturers
+-----------+-----------------+
| seller_id | manufacturer_id |
+-----------+-----------------+
|        1  |               1 |
|        1  |               2 |
|        2  |               1 |
|        2  |               2 |
|        3  |               1 |
+-----------+-----------------+

sellers_producttypes
+-----------+-----------------+
| seller_id | product_type_id |
+-----------+-----------------+
|        1  |               1 |
|        1  |               2 |
|        2  |               2 |
|        3  |               1 |
+-----------+-----------------+

manufacturers_producttypes
+-----------------+-----------------+
| manufacturer_id | product_type_id |
+-----------------+-----------------+
|               1 |               1 |
|               1 |               2 |
|               2 |               1 |
|               2 |               2 |
+-----------------+-----------------+

Декомпозирането на пръв поглед изглежда логично – ние нямаме вече повторения в отчетите и при писане на заявки няма да имаме нужда от оператор DISTINCT. За сметка на това обаче ние не можем вече да възстановим оригиналната таблица чрез SELECT заявка с JOIN (проверете и ще се убедите).

Защо това е така? Нека вземем например seller с id 1. От оригиналната таблица знаем, че той продава продукти от тип 1 и 2 на производител 1, както и продукт от тип 1 на производител 2. При декомпозираната таблица sallers_manufactures ние виждаме, че seller с id 1 продава продукти на производители 1 и 2 (дотук добре). От втората таблица sellers_producttypes виждаме, че seller с id 1 продава продукти с id 1 и 2 (това също е вярно). От декомпозираните таблици обаче ние по никакъв начин не можем да отчетем факта, че seller с id 1 НЕ продава продукти от тип 2 на производител 2 (!). Това е видно от първоначалната таблица, но е невъзстановимо от декомпозицията й.

С този пример се показва принципно рядък случай, но такъв, който може да има неприятни последици. Това може да се случи ако започнете да разработвате една информационна система, в която сте направили декомпозиция със загуба на информация (тя е вършила чудесна работа като начален старт), а в бъдеще ви е потрябвала именно тази загубена информация, когато сте решили да добавяте нови функционалности. Тогава ще се окаже, че вие реално сте събирали и сте имали налична информацията, която ви е нужна, но сте я загубили поради прекалената декомпозиция.

Пета нормална форма (5НФ) за релация R настъпва ако всяка нетривиална JOIN зависимост, която е в сила за R, се базира на кандидат-ключовете на R. Казано много по-просто – 5НФ настъпва тогава, когато:

  1. Таблицата е в 4НФ;
  2. Едно от двете е вярно:
    • Няма излишество на данни;
    • Има излишество, но не може да бъде декомпозирана без загуба на информация.

Практическата важност е да знаете, че не трябва да се прекалява с декомпозицията. Понякога се налага да приемем неизбежно излишеството на информация, защото в противен случай ще има загуба на такава. Ето защо простичкото правило в началото на статията („да има възможно най-малко на брой таблици, като същевременно да няма повторение на информация“) е погрешно – има частни случаи, в които то води до проблеми със загуба на информация – нарушаване на 5НФ.

Освен това винаги трябва да взимате предвид практическата взаимовръзка на информацията. Ако например към горния пример добавим едно допълнително правило – „продавачите са представители на производителите, т.е. те са длъжни да предоставят всички техни продукти“, – то вече таблицата sales ще нарушава 5НФ. Това е така, защото декомпозицията ѝ в три таблици вече ще бъде без загуба на информация.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*