Więzy integralności w SQL - Primary Key, Foreign Key, CASCADE, UNIQUE, CHECK
Mechanizmy obronne bazy danych - Primary Key (INT IDENTITY kontra GUID), Foreign Key i integralność referencyjna, trzy strategie ON DELETE (CASCADE/SET NULL/NO ACTION), constraints UNIQUE i CHECK jako walidacja na poziomie bazy. Dwudziesta czwarta część kursu C#.
W poprzednim wpisie SQL w praktyce - DDL i DML poznałeś fundamenty języka SQL. Teraz zejdziemy głębiej w mechanizmy obronne bazy danych - więzy integralności (constraints), czyli reguły, które silnik bazy wymusza automatycznie, odrzucając zapytania naruszające spójność danych.
Więzy integralności są w tabeli tym, czym walidacja formularza w UI - ale na znacznie niższym poziomie. Nawet gdy aplikacja ma bug i próbuje zapisać cenę -100 zł albo wskazać nieistniejącego użytkownika, baza mówi “nie”. W tej części poznasz cztery główne typy constraints (PK, FK, UNIQUE, CHECK), trzy strategie kaskadowego zachowania przy usuwaniu (CASCADE/SET NULL/NO ACTION), oraz kluczowy wybór projektowy: INT IDENTITY kontra GUID jako typ Primary Key.
1. Primary Key - fundament identyfikacji #
Klucz Główny to unikalny identyfikator wiersza. Baza używa go do:
- Szybkiego wyszukiwania konkretnego rekordu (
WHERE Id = 42) - Wymuszania relacji przez Foreign Keys w innych tabelach
- Automatycznego tworzenia indeksu klastrowego (SQL Server) - fizyczne uporządkowanie danych na dysku
Trzy wymagane cechy PK #
| Cecha | Wymóg |
|---|---|
| UNIQUE | Żadne dwa wiersze nie mogą mieć tej samej wartości |
| NOT NULL | Każdy wiersz musi mieć wartość (NULL zabroniony) |
| Jeden na tabelę | Dokładnie jeden PK; może być złożony z wielu kolumn |
Konwencja: pojedyncza kolumna Id (INT, BIGINT lub UNIQUEIDENTIFIER) - łatwo współpracuje z ORM (Entity Framework Core rozpoznaje ją automatycznie).
Dwa typy PK w produkcji #
Wybór A: INT IDENTITY (lub BIGINT)
CREATE TABLE Users (
Id INT IDENTITY(1,1) PRIMARY KEY, -- 1, 2, 3, 4...
Name NVARCHAR(50)
);
| Zalety | Wady |
|---|---|
| Mały rozmiar (4 B), szybkie sortowanie | Przewidywalność - konkurencja z /order/502 w URL |
| Sekwencyjny → efektywny indeks klastrowy | Konflikty przy łączeniu baz z wielu regionów |
| Naturalna kolejność chronologiczna | Nie da się generować po stronie klienta |
Wybór B: GUID / UNIQUEIDENTIFIER
CREATE TABLE Users (
Id UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY,
Name NVARCHAR(50)
);
W C#:
public class User
{
public Guid Id { get; set; } = Guid.NewGuid(); // generowane przed wysłaniem do bazy
public string Name { get; set; }
}
| Zalety | Wady |
|---|---|
| Globalna unikalność - zero kolizji między bazami | 16 B (4× większy niż INT) |
| Można generować w aplikacji przed INSERT | Losowy → fragmentacja indeksu klastrowego |
Trudne do zgadnięcia (/order/6F9619FF-8B86-D011-...) | Wolniejsze sortowanie, JOINy |
Reguła kciuka:
- Aplikacja monolityczna, jedna baza → INT IDENTITY (prostota, wydajność)
- Rozproszony system z wieloma bazami, publiczne URL wymagające nieprzewidywalności, generowanie ID w aplikacji przed zapisem → GUID
Dla większości aplikacji biznesowych INT wystarcza. GUID sięgaj tylko gdy realnie potrzebujesz globalnej unikalności.
2. Foreign Key - integralność referencyjna #
Foreign Key łączy dwa wiersze - w tej samej lub innej tabeli. Kolumna “dziecka” zawiera wartość Primary Key “rodzica”. FK to nie tylko relacja - to gwarancja spójności:
Bez FK aplikacja może swobodnie tworzyć orphan records (osierocone rekordy):
-- Bez FK - baza pozwala
INSERT INTO Orders (UserId, Product) VALUES (999, 'Laptop');
-- Order wskazuje na nieistniejącego usera. Później aplikacja próbuje
-- wyświetlić order i wyświetla 'null' zamiast nazwy klienta. Bug.
Z FK baza odrzuca takie próby:
CREATE TABLE Orders (
Id INT IDENTITY(1,1) PRIMARY KEY,
UserId INT NOT NULL,
Product NVARCHAR(100),
CONSTRAINT FK_Orders_Users
FOREIGN KEY (UserId) REFERENCES Users(Id)
);
INSERT INTO Orders (UserId, Product) VALUES (999, 'Laptop');
-- ❌ ERROR: The INSERT statement conflicted with the FOREIGN KEY constraint...
To jest integralność referencyjna - baza sama pilnuje, by relacje między tabelami były poprawne. Aplikacja może mieć błąd, próbować dowolnych wartości - baza jest ostatnią linią obrony.
Pełny przykład Departamenty ↔ Pracownicy #
-- Tabela rodzic
CREATE TABLE Departamenty (
Id INT IDENTITY(1,1) PRIMARY KEY,
Nazwa NVARCHAR(100) NOT NULL
);
-- Tabela dziecko z FK
CREATE TABLE Pracownicy (
Id INT IDENTITY(1,1) PRIMARY KEY,
Imie NVARCHAR(50) NOT NULL,
Nazwisko NVARCHAR(50) NOT NULL,
DepartamentId INT NOT NULL,
CONSTRAINT FK_Pracownicy_Departamenty
FOREIGN KEY (DepartamentId)
REFERENCES Departamenty(Id)
);
Silnik gwarantuje:
- INSERT pracownika z
DepartamentId = 5gdy departament 5 nie istnieje → odrzucone - DELETE departamentu 5 gdy istnieją pracownicy z
DepartamentId = 5→ odrzucone (domyślnie) - UPDATE pracownika ustawiający
DepartamentId = 999gdy 999 nie istnieje → odrzucone
3. Trzy strategie ON DELETE #
Domyślnie próba usunięcia rodzica z istniejącymi dziećmi kończy się błędem (bezpieczna blokada). Możesz to zmienić deklarując zachowanie:
ON DELETE NO ACTION (domyślne) / RESTRICT #
CONSTRAINT FK_Pracownicy_Departamenty
FOREIGN KEY (DepartamentId) REFERENCES Departamenty(Id)
ON DELETE NO ACTION
Silnik odrzuca próbę usunięcia rodzica gdy istnieją dzieci. Musisz najpierw ręcznie posprzątać. Bezpieczna decyzja - wymusza świadome podjęcie decyzji.
ON DELETE CASCADE - kaskadowe usuwanie #
CONSTRAINT FK_KoszykZawartosc_Koszyki
FOREIGN KEY (KoszykId) REFERENCES Koszyki(Id)
ON DELETE CASCADE
Usunięcie rodzica automatycznie kasuje wszystkie powiązane dzieci.
Dobre użycie:
Koszyk→PozycjaKoszyka(usunąć koszyk = usunąć pozycje)Plik→TagiPliku(usunąć plik = usunąć tagi)WpisBloga→Komentarze(usunąć wpis = usunąć komentarze)
Anti-pattern (klasyczna katastrofa):
Klient→Fakturaz CASCADE - usunięcie klienta kasuje historię faktur wymaganą przez USUzytkownik→LogAudytowyz CASCADE - usunięcie usera zaciera ślad audytowyKonto→Transakcjez CASCADE - usunięcie konta kasuje historię finansową
Reguła: CASCADE tylko dla ściśle podrzędnych danych bez wartości historycznej.
ON DELETE SET NULL #
CONSTRAINT FK_Pracownik_Menedzer
FOREIGN KEY (MenedzerId) REFERENCES Pracownicy(Id)
ON DELETE SET NULL
Usunięcie rodzica ustawia FK dzieci na NULL. Warunek: kolumna FK musi zezwalać na NULL (INT NULL, nie INT NOT NULL).
Klasyczne zastosowanie: luźne powiązanie, gdzie dziecko żyje bez rodzica:
Pracownik.MenedzerId → Pracownik.Id- menedżer odchodzi z firmy, jego podwładni pracują dalej bez szefa (do czasu nowego przydziału)Artykul.KategoriaId → Kategoria.Id- kategoria usunięta, artykuł zostaje jako “nieskategoryzowany”
Podsumowanie strategii #
| Strategia | Zachowanie przy usunięciu rodzica | Kiedy używać |
|---|---|---|
| NO ACTION / RESTRICT | Błąd - odrzuca operację | Domyślne, bezpieczne; wymusza świadome działanie |
| CASCADE | Automatyczne usunięcie dzieci | Ściśle podrzędne dane (koszyk↔pozycje, plik↔tagi) |
| SET NULL | FK dzieci → NULL | Luźne powiązania (menedżer↔podwładni) |
4. UNIQUE - unikalność bez bycia PK #
Primary Key jest jeden na tabelę. Ale kolumn wymagających unikalności może być kilka. UNIQUE to constraint zapewniający unikalność bez bycia PK:
CREATE TABLE Users (
Id INT IDENTITY(1,1) PRIMARY KEY, -- PK
Email VARCHAR(100) UNIQUE NOT NULL, -- unikalny email
Username NVARCHAR(50) UNIQUE NOT NULL, -- unikalny login
Pesel CHAR(11) UNIQUE, -- unikalny PESEL (może być NULL)
Name NVARCHAR(100)
);
Próba INSERT drugiego usera z tym samym emailem → błąd bazy.
Różnica PK kontra UNIQUE:
| Primary Key | UNIQUE | |
|---|---|---|
| Ilość w tabeli | Dokładnie 1 | Dowolna liczba |
| NULL | ❌ Niedozwolone | ✅ Dozwolone (jeden NULL w SQL Server) |
| Cel | Identyfikacja rekordu | Wymuszenie unikalności biznesowej |
| Indeks klastrowy | Domyślnie tak | Domyślnie nie (tylko non-clustered) |
Typowe zastosowania UNIQUE:
- Email użytkownika (można się zalogować tylko jednym kontem na maila)
- Numer telefonu (jeśli ma być unikalny)
- Kod kreskowy produktu
- PESEL, NIP, REGON
- Alias URL (
slug) -/posts/my-post-title- dwa posty nie mogą mieć tego samego slug
5. CHECK - walidacja logiczna #
CHECK wymusza dowolny warunek logiczny na kolumnie lub zestawie kolumn. To ostatnia linia obrony przed niepoprawnymi danymi.
Proste warunki #
CREATE TABLE Produkty (
Id INT IDENTITY(1,1) PRIMARY KEY,
Nazwa NVARCHAR(100) NOT NULL,
Cena DECIMAL(10, 2) NOT NULL,
-- Cena musi być dodatnia
CONSTRAINT CHK_Cena_Dodatnia CHECK (Cena > 0)
);
INSERT INTO Produkty (Nazwa, Cena) VALUES (N'Laptop', -100);
-- ❌ ERROR: CHK_Cena_Dodatnia
Zakresy i wartości domenowe #
CREATE TABLE Uzytkownicy (
Id INT IDENTITY(1,1) PRIMARY KEY,
Wiek INT NOT NULL CHECK (Wiek BETWEEN 0 AND 120),
Status NVARCHAR(20) NOT NULL
CHECK (Status IN ('Active', 'Paused', 'Deleted')),
Email VARCHAR(100) NOT NULL
CHECK (Email LIKE '%@%.%')
);
CHK_Status_Domena blokuje dowolny status inny niż trzy wartości - baza nie pozwoli wstawić 'Fake', 'ACTIVE' (wielkość znaków!), NULL.
Warunki między kolumnami #
CREATE TABLE Zamowienia (
Id INT IDENTITY(1,1) PRIMARY KEY,
DataZlozenia DATETIME2 NOT NULL,
DataDostawy DATETIME2 NULL,
-- Data dostawy nie może być przed datą złożenia
CONSTRAINT CHK_Daty_Kolejnosc
CHECK (DataDostawy IS NULL OR DataDostawy >= DataZlozenia)
);
CHECK sprawdza logiczną spójność między dwoma kolumnami tego samego wiersza.
Wielowarstwowa walidacja #
W dojrzałych aplikacjach walidacja żyje na wielu poziomach:
| Warstwa | Rodzaj walidacji | Przykład |
|---|---|---|
| UI (frontend) | Natychmiastowy feedback | ”Cena musi być liczbą” w formularzu |
| Aplikacja (backend) | Walidacja biznesowa | ”Cena musi mieszać się w widełkach 1-99999” |
| CHECK (baza) | Constraint na poziomie danych | CHECK (Cena > 0) |
Każda warstwa łapie inne rodzaje błędów: UI - literówki użytkownika; aplikacja - reguły biznesowe; baza - błędy aplikacji, bezpośrednie połączenia SQL, migracje danych z zewnątrz. Wszystkie trzy są potrzebne.
Podsumowanie tematu #
W tej części kursu poznałeś:
- Więzy integralności (constraints) - mechanizmy obronne bazy; naruszające zapytania są automatycznie odrzucane, chroniąc spójność danych
- Primary Key - unikalny identyfikator wiersza; UNIQUE + NOT NULL + jeden na tabelę; wybór INT IDENTITY (mały, sekwencyjny) kontra GUID (globalnie unikalny, dla rozproszonych systemów)
- Foreign Key - relacja między dwoma wierszami; integralność referencyjna blokuje orphan records; przykład Departamenty↔Pracownicy
- Trzy strategie ON DELETE - NO ACTION (domyślne, blokada), CASCADE (kaskadowe usuwanie - niebezpieczne dla historii biznesowej), SET NULL (luźne powiązania)
- UNIQUE - unikalność kolumny bez bycia PK; klasyczne użycia: email, telefon, kod kreskowy, PESEL; tabela różnic PK/UNIQUE
- CHECK - warunek logiczny na kolumnie lub wielu; walidacja zakresów, wartości domenowych, spójności między kolumnami
- Wielowarstwowa walidacja - UI + aplikacja + CHECK w bazie; każda warstwa łapie inne błędy
W następnych wpisach zagłębimy się w transakcje (BEGIN/COMMIT/ROLLBACK - jak baza gwarantuje atomowość operacji), indeksy (jak przyspieszać zapytania i jaki koszt tego płacimy), oraz poziomy izolacji transakcji.
Quiz i zadanie poniżej. Zadanie symuluje więzy integralności w C# - DatabaseSimulator z metodą AddOrder egzekwującą FK check (User istnieje?) i CHECK constraint (Price > 0?), rzucającą InvalidOperationException przy naruszeniu. To dokładny wzorzec, jak myśli prawdziwa baza przy każdym INSERT.
Podobne wpisy
SQL w praktyce - DDL i DML - CREATE, INSERT, SELECT, UPDATE, DELETE, JOIN
Praktyczny SQL dla programisty C# - podział na DDL/DML/DCL/TCL, CREATE TABLE z kluczami głównymi i obcymi, CRUD (Wielka Czwórka DML), INNER JOIN, klasyczne pułapki UPDATE/DELETE bez WHERE, mapowanie SQL na LINQ w C#. Dwudziesta trzecia część kursu C#.
Relacyjne bazy danych w C# - architektura RDBMS, schemat tabel, mapowanie typów SQL
Architektura silnika RDBMS (Query Optimizer, Storage Engine, Transaction Log), projektowanie tabel z PK/FK, mapowanie typów SQL Server na typy C# (INT, NVARCHAR, DECIMAL, DATETIME2, DATE). Dwudziesta druga część kursu C#.
ArrayPool i MemoryPool w C# - wypożyczalnia tablic kontra LOH
Recykling dużych buforów dla hot path - LOH (Large Object Heap) i jego pułapki, ArrayPool<T>.Shared z Rent/Return, pułapka nadmiarowego rozmiaru, MemoryPool<T> z IMemoryOwner i using dla async. Dwudziesta pierwsza część kursu C#.
🔗 Linkują tu