Normalizacja — dlaczego poprawna tabela może być złym projektem?
Od poprawnego raportu do bezpiecznej zmiany
22 października 2026 · wykład 5 · 2 × 45 minut.
W poprzednim wykładzie raport ośrodka „Dąb” zwracał 0, 3 i 4 wolne miejsca w edycjach E1, E2 i E3. Poprawny raport nie rozstrzyga jednak, czy projekt pozwala bezpiecznie zmieniać dane. Jeżeli nazwę sali zapisano przy każdym uczestniku, jedna zmiana nazwy wymaga odnalezienia wielu kopii.
Normalizacja porządkuje strukturę według znaczenia danych i ich zależności. Celem tego pokazu jest usunięcie konkretnych anomalii przy zachowaniu informacji potrzebnej do raportowania. Liczba tabel sama w sobie nie mierzy jakości projektu.
Pliki SQL do pokazu — pobierz ZIP.
Pokaz działa w PostgreSQL i pgAdmin, w osobnej bazie prba5_pokaz. Compose tworzy tę bazę pustą: budowanie tabel jest częścią demonstracji. W Query Tool kolejne pliki 00–09 wykonuje się po jednym razie, przy włączonym Auto commit. Pliki kontrolne 02 oraz 06 zawierają kilka zapytań: ich wyniki należy obejrzeć osobno. Klucze obce są sprawdzane przez serwer bez dodatkowego polecenia włączającego. Daty i godziny mają typ timestamp. Nie wykonujemy tego pokazu na tabelach z wykładu 4.
✅ Sprawdź się z wykładu 4
1. Kontrakt danych: kurs nie jest edycją
Kurs oznacza temat szkolenia; edycja jest konkretną realizacją w określonym terminie. Każda edycja ma jedną salę i jednego prowadzącego. Ten sam kurs może mieć wiele edycji w różnych salach i z różnymi prowadzącymi.
Uczestnik ma najwyżej jeden zapis do danej edycji. Zapis przechowuje bieżący status confirmed lub cancelled; ponowne zgłoszenie aktualizuje ten sam zapis po sprawdzeniu miejsc. Pełna historia statusów nie należy do tego modelu. Nazwy osób, sal i kursów nie muszą być unikalne; identyfikujemy obiekty przez ID.
Pojemność sali opisuje salę, a limit miejsc opisuje edycję. Limit jest dodatni i nie większy od pojemności. Edycje nie mogą zajmować tej samej sali ani prowadzącego w nakładających się przedziałach; styk końca i początku jest dozwolony. Nazwy w tym modelu są bieżące, nie stanowią historycznej treści zaświadczeń.
W pliku 00 zapisujemy cztery znane zgłoszenia, z pełnym zestawem 14 wartości w każdym wierszu INSERT INTO … VALUES. Daty i statusy odpowiadają poprzedniemu pokazowi; dodajemy jawne dane sal oraz prowadzących.
| Uczestnik | Edycja | Kurs | Sala | Pojemność | Limit | Status |
|---|---|---|---|---|---|---|
| Ada | E1 | K1 | S1 | 20 | 2 | confirmed |
| Bartek | E1 | K1 | S1 | 20 | 2 | confirmed |
| Celina | E2 | K1 | S1 | 20 | 4 | confirmed |
| Bartek | E2 | K1 | S1 | 20 | 4 | cancelled |
Dlaczego nie ma E3? Arkusz zawiera wyłącznie zapisy, a E3 nie ma uczestników. Nie da się odtworzyć E3 na podstawie tych czterech wierszy. Po zmianie struktury jej dane zostaną dodane osobno z wymagań klienta, a nie „odzyskane” przez JOIN.
2. Pierwsza postać normalna nie usuwa wszystkich problemów
1NF — First Normal Form, pierwsza postać normalna: wartości są atomowe w ramach przyjętych dziedzin. Tutaj wiersz opisuje jeden zapis jednej osoby na jedną edycję. Nie przechowuje list edycji ani list uczestników w komórce. Tabela flat_enrollments już spełnia 1NF w przyjętym modelu.
Kluczem jest para (participant_id, edition_id). Sama osoba może mieć kilka zapisów, a jedna edycja wielu uczestników. Status dotyczy całej pary.
Mimo tego nazwa sali S1 występuje w czterech wierszach. Plik 01 zmienia tylko jedną jej kopię:
UPDATE flat_enrollments SET room_name = 'Sala Alfa'
WHERE participant_id = 'U1' AND edition_id = 'E1';
SELECT edition_id, participant_id, room_id, room_name
FROM flat_enrollments ORDER BY edition_id, participant_id;Operacja nie narusza klucza głównego, a jednak ta sama sala otrzymuje sprzeczne nazwy. To anomalia aktualizacji. Dwa pozostałe problemy mają to samo źródło:
- Anomalia wstawiania: nie da się zapisać edycji bez uczestnika bez wymyślania fikcyjnego zapisu albo zmiany znaczenia wiersza.
- Anomalia usuwania: usunięcie jedynego zapisu danej osoby usuwa również jej jedyne dane z tego arkusza.
Klucz zapobiega powtórzeniu identyfikatora zapisu, ale nie wymusza zgodności wszystkich kopii opisów obiektów. Plik 02 naprawia celową zmianę na podstawie znanej wartości źródłowej i sprawdza, czy dane są zgodne przed podziałem.
3. Zależności: co wyznacza każdą wartość?
FD — Functional Dependency, zależność funkcjonalna, X → Y oznacza: wiersze zgodne na X muszą być zgodne na Y. Wynika z reguł dziedziny, nie z tego, że w kilku przykładowych rekordach przypadkiem wystąpiły te same wartości.
participant_id → participant_name
course_id → course_name
room_id → room_name, room_capacity
instructor_id → instructor_name
edition_id → course_id, starts_at, ends_at, room_id, instructor_id, seat_limit
(participant_id, edition_id) → status
Nie zakładamy course_id → room_id: kurs K1 może mieć realizacje w innych salach. Nie zakładamy również room_capacity → seat_limit. Sala na 20 osób może obsługiwać edycję z limitem 12, 4 albo 2.
Nadklucz jednoznacznie identyfikuje wiersz. Klucz kandydujący jest nadkluczem minimalnym: usunięcie dowolnego atrybutu odbiera mu tę właściwość. Klucz główny (PK — Primary Key) to wybrany klucz kandydujący. Klucz obcy (FK — Foreign Key) odwołuje się do klucza tabeli nadrzędnej.
4. Druga postać normalna: osoba, edycja i zapis
2NF — Second Normal Form: relacja jest w 1NF, a żaden atrybut niekluczowy (nienależący do żadnego klucza kandydującego) nie zależy od właściwej części któregokolwiek klucza kandydującego. W tym przykładzie jedynym kluczem kandydującym arkusza jest para identyfikatorów osoby i edycji.
Imię zależy tylko od osoby. Dane edycji zależą tylko od edycji. Są to zależności częściowe względem tej pary. Plik 03 wydziela:
| Tabela | Co przechowuje | Początkowo |
|---|---|---|
| participants_2nf | Osoby | 3 wiersze |
| editions_2nf | Edycje z powielonymi jeszcze opisami kursu, sali i prowadzącego | 2 wiersze |
| enrollments_2nf | Powiązania osób z edycjami i status | 4 wiersze |
INSERT INTO participants_2nf (participant_id, name)
SELECT DISTINCT participant_id, participant_name FROM flat_enrollments;
INSERT INTO enrollments_2nf (participant_id, edition_id, status)
SELECT participant_id, edition_id, status FROM flat_enrollments;Pełne tworzenie i wypełnienie trzech tabel znajduje się w pliku 03. DISTINCT usuwa identyczne kopie, ale nie naprawia sprzecznych opisów. Dlatego kontrola z pliku 02 poprzedza podział. Nie należy arbitralnie wybierać nazwy przez MIN.
Dodanie sztucznego jednokolumnowego PK do starego arkusza nie usuwa istniejących zależności. Warunek 2NF analizuje wszystkie klucze kandydujące, nie tylko PK.
5. Dlaczego 2NF nadal nie wystarcza?
Sala S1 występuje przy E1 i E2. Plik 04 zmienia nazwę tylko przy E1, zatem problem pozostaje, choć opis osoby już się nie powiela. W tabeli edycji obowiązuje:
edition_id → room_id → room_name, room_capacity
edition_id → course_id → course_name
edition_id → instructor_id → instructor_name
To zależności przechodnie: opis sali, kursu lub prowadzącego trafia do tabeli edycji za pośrednictwem identyfikatora innego obiektu. Jednokolumnowy klucz edycji nie usuwa tych powtórzeń.
6. Trzecia postać normalna: opis obiektu w jego tabeli
3NF — Third Normal Form: dla każdej nietrywialnej zależności X → A wynikającej z przyjętych reguł X jest nadkluczem lub A należy do pewnego klucza kandydującego. W tym przykładzie dalszy podział usuwa wskazane zależności przechodnie atrybutów niekluczowych.
Plik 05 najpierw naprawia celową zmianę z pliku 04, potem tworzy sześć tabel:
participants(participant_id PK, name)
courses(course_id PK, name)
rooms(room_id PK, name, capacity)
instructors(instructor_id PK, name)
editions(edition_id PK, course_id FK, starts_at, ends_at,
room_id FK, instructor_id FK, seat_limit)
enrollments(participant_id PK/FK, edition_id PK/FK, status)
Kurs, sala i prowadzący mogą mieć wiele edycji. Uczestnik może mieć wiele zapisów, a edycja wiele osób; tabela enrollments zachowuje te powiązania i ich statusy. Samo utworzenie katalogów osób i edycji nie zachowałoby informacji o zapisach.
Powtarzanie FK w zapisach jest potrzebne. Usuwamy zbędne kopie opisów, nie identyfikatory wyrażające relacje. Limit zostaje przy edycji, pojemność przy sali.
7. Czy podział zachował informacje?
Plik 05 tworzy enrollment_report, widok odtwarzający wszystkie 14 kolumn płaskiego arkusza przez złączenia po kluczach. VIEW przechowuje definicję zapytania; zwykły widok nie jest dodatkową utrzymywaną kopią jego wyniku.
Plik 06 porównuje dane w obu kierunkach, zanim wprowadzimy nowe zmiany:
SELECT * FROM flat_enrollments EXCEPT SELECT * FROM enrollment_report;
SELECT * FROM enrollment_report EXCEPT SELECT * FROM flat_enrollments;
SELECT conrelid::regclass AS table_name, conname, convalidated
FROM pg_constraint
WHERE connamespace = 'public'::regnamespace AND contype = 'f';Dwa wyniki EXCEPT mają być puste. Trzecie zapytanie wypisuje klucze obce: każdy powinien mieć convalidated = true. To kontrola stanu ograniczeń, a nie kolejny pusty raport ani kontrola wszystkich reguł biznesowych. Samo stwierdzenie „cztery wiersze przed i po” byłoby za słabe: raport mógłby mieć cztery inne wiersze. Klucze gwarantują tu unikalność pary zapisów, więc porównanie zbiorów nie ukrywa duplikatów tych samych zapisów.
Kontrola potwierdza zgodność dla przygotowanych danych. Uzasadnienie bezstratności podziału opiera się na zależnościach: zapis wskazuje jedną osobę i jedną edycję, a edycja jeden kurs, salę i prowadzącego. Złączenia po tych kluczach odtwarzają opisy bez arbitralnego dopasowywania nazw.
8. Trzy próby poprawionego projektu
Plik 07 — jedna zmiana nazwy. Aktualizacja jednego wiersza rooms sprawia, że raport pokazuje „Sala Alfa” we wszystkich czterech zapisach. Stare tabele etapowe nie synchronizują się z nowymi; pozostają tylko do porównania.
Plik 08 — edycja bez zapisów. Z wymagań klienta dodajemy K2 „Pierwsza pomoc”, salę S2 oraz E3, z limitem 4, bez żadnego uczestnika. To nowe dane względem arkusza, a nie część jego dekompozycji. Znany raport z poprzedniego wykładu daje:
| Edycja | Limit | Potwierdzone | Wolne miejsca |
|---|---|---|---|
| E1 | 2 | 2 | 0 |
| E2 | 4 | 1 | 3 |
| E3 | 4 | 0 | 4 |
Plik 09 — osoba pozostaje po usunięciu zapisu. Usuwamy jedyny zapis Ady i sprawdzamy, że nadal istnieje w participants. Plik następnie przywraca zapis. W praktycznym procesie rezygnacji używalibyśmy statusu cancelled; fizyczne usunięcie tutaj służy wyłącznie pokazaniu niezależności osoby od zapisu.
9. Czego normalizacja nie załatwia?
W skryptach są klucze, wymagane wartości, dodatnie limity i pojemności oraz kontrola dozwolonych statusów. Nie zaimplementowano jeszcze ochrony limitu potwierdzonych zapisów, limitu względem pojemności ani kolizji terminów. Nie są one automatycznym skutkiem osiągnięcia 3NF; wymagają osobnych mechanizmów.
Przechowywanie bieżącej nazwy osoby albo sali jest świadomym założeniem. Jeżeli zaświadczenie ma zachować dokładną treść z dnia wydania, potrzebujemy historii lub utrwalenia dokumentu. Taka kopia może mieć inne znaczenie biznesowe niż bieżący opis; nie każda powtórzona wartość jest błędem.
10. BCNF jako uzupełnienie
BCNF — Boyce–Codd Normal Form, postać normalna Boyce’a–Codda, wymaga, aby w każdej nietrywialnej zależności funkcjonalnej determinant był nadkluczem. Nie musi być minimalny, więc sformułowanie „zawsze klucz kandydujący” byłoby zbyt mocne. 3NF dopuszcza dodatkowo prawą stronę należącą do klucza kandydującego.
Przy jawnie przyjętych tutaj zależnościach końcowe tabele spełniają także BCNF. Nie potrzeba dodatkowego podziału tylko po to, żeby dopisać następny etap pokazu. W ogólności przy rozkładzie do BCNF trzeba również przeanalizować zachowanie zależności: bezstratność złączenia i możliwość lokalnego wymuszania wszystkich reguł to osobne właściwości.
Co wynika z tego dla projektu?
Dla każdej kolumny trzeba wskazać obiekt lub powiązanie, które opisuje, oraz identyfikator wyznaczający jej wartość. Podział ma usuwać konkretną anomalię, zachować fakty i umożliwić sprawdzenie raportu. Na kolejnym wykładzie przejdziemy do indeksów i planów wykonania: dobry model logiczny jest punktem wyjścia, ale nie zastępuje pomiaru wydajności.