Masz utworzyć tabelę na zamówienia i po chwili okazuje się, że sama lista kolumn nie wystarczy: trzeba jeszcze ustalić, które pola mogą być puste i jak powiązać zamówienie z klientem. Instrukcja CREATE TABLE definiuje strukturę tabeli, ale szczegóły składni zależą od silnika bazy danych.
Dobra definicja tabeli zapobiega błędom, zanim trafią do danych
CREATE TABLEtworzy tabelę z kolumnami, typami danych i ograniczeniami.PRIMARY KEYidentyfikuje wiersz, aFOREIGN KEYpilnuje powiązania z inną tabelą.NOT NULLblokuje brak wartości, aCHECKmoże ograniczyć dozwolone wartości.- Dialekt ma znaczenie: typy danych i mechanizm generowania identyfikatorów różnią się między PostgreSQL, MySQL, SQLite i SQL Server.
IF NOT EXISTSnie sprawdza, czy istniejąca tabela ma oczekiwaną strukturę.
CREATE TABLE opisuje kolumny i reguły dla nowych danych
Najprostsza instrukcja składa się z nazwy tabeli oraz definicji jej kolumn. Przy każdej kolumnie podajesz typ danych, a w razie potrzeby także reguły, które baza ma egzekwować.
CREATE TABLE tasks (
task_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title VARCHAR(200) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'new',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT tasks_status_allowed
CHECK (status IN ('new', 'in_progress', 'done'))
);
Ten przykład używa składni PostgreSQL. Identyfikator jest generowany przez bazę, tytuł nie może być pusty, a status dostaje domyślną wartość. Ograniczenie nazwane tasks_status_allowed odrzuca wartości spoza listy, więc literówka nie przejdzie po cichu do tabeli.
PRIMARY KEY oznacza klucz podstawowy, czyli unikatowy identyfikator wiersza. DEFAULT uzupełnia wartość, gdy instrukcja INSERT pomija kolumnę, ale nie zastępuje NOT NULL: te dwa zapisy odpowiadają na różne pytania.
Typ kolumny i generator identyfikatora zależą od silnika
Ten sam zamysł można zapisać inaczej w różnych bazach. Szczególnie łatwo pomylić automatyczny identyfikator z typem kolumny, bo PostgreSQL, MySQL, SQLite i SQL Server mają tu odmienne rozwiązania.
| Silnik | Przykładowy zapis identyfikatora | Na co uważać |
|---|---|---|
| PostgreSQL | INTEGER GENERATED ALWAYS AS IDENTITY | Można użyć BY DEFAULT, jeśli ręczne podanie wartości ma być dopuszczone w zwykłym INSERT. |
| MySQL | INT AUTO_INCREMENT | Składnię trzeba dopasować do pozostałych definicji kolumn i klucza. |
| SQLite | INTEGER PRIMARY KEY | Ten zapis ma specjalne znaczenie dla identyfikatora wiersza; nie jest prostym odpowiednikiem każdej konstrukcji z innych baz. |
| SQL Server | INT IDENTITY(1,1) | IDENTITY określa generowanie kolejnych wartości. |
Różnice nie kończą się na identyfikatorze. Dokumentacja PostgreSQL, MySQL, SQLite i SQL Server opisuje składnię właściwą dla poszczególnych silników. Jeśli kod ma działać na więcej niż jednym, sprawdź każdy dialekt osobno, zamiast zakładać, że zamiana nazwy typu danych wystarczy.
Uważam, że przy pierwszej tabeli lepiej jawnie zapisać ograniczenia, niż liczyć na domyślne zachowanie silnika. Taka definicja od razu pokazuje, które dane są wymagane i co baza odrzuci.
Klucz obcy chroni powiązanie, a nie tylko opisuje je w kodzie
Jeśli zadanie ma należeć do projektu, sama kolumna project_id nie gwarantuje, że wskazuje istniejący projekt. Klucz obcy zapisuje tę relację w bazie i pozwala silnikowi kontrolować odwołania.
CREATE TABLE tasks (
task_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
project_id INTEGER NOT NULL,
title VARCHAR(200) NOT NULL,
CONSTRAINT tasks_project_fk
FOREIGN KEY (project_id) REFERENCES projects(project_id)
);
Przed wykonaniem tej instrukcji tabela projects musi już istnieć, a wskazana kolumna powinna być kluczem lub mieć unikatowe wartości zgodnie z wymaganiami danego silnika. Przy usuwaniu projektu możesz też określić regułę, na przykład zablokować usunięcie, gdy istnieją zadania, albo skasować powiązane wiersze przez ON DELETE CASCADE. Drugi wariant jest wygodny, ale przy błędnym usunięciu może objąć cały zestaw danych.
Gdy pole może być nieznane, dopuszczenie NULL i ustawienie DEFAULT na NULL nie oznaczają tego samego co wymagana wartość domyślna. Z kolei CHECK (status IN (...)) sam nie zawsze zabrania braku statusu: na przykład SQLite nie uznaje wyniku NULL wyrażenia CHECK za naruszenie. Jeśli status jest obowiązkowy, dodaj również NOT NULL.
IF NOT EXISTS może ukryć niezgodny schemat
Skrypt uruchamia się ponownie i kończy bez błędu. Brzmi dobrze, dopóki istniejąca tabela nie okaże się starszą wersją bez nowej kolumny. IF NOT EXISTS pomija błąd przy istniejącej nazwie, ale nie porównuje definicji tabeli z tą w skrypcie.
To zachowanie jest opisane w dokumentacji PostgreSQL i MySQL. Dlatego taki zapis przydaje się do ograniczenia błędu „tabela już istnieje”, lecz nie zastępuje migracji schematu. Jeśli zmieniasz istniejącą strukturę, przygotuj osobną zmianę, na przykład przez ALTER TABLE, i sprawdź wynik w docelowej bazie.
| Sytuacja | Możliwy skutek | Co zrobić |
|---|---|---|
Tabela o tej nazwie już istnieje, a skrypt zawiera IF NOT EXISTS | Skrypt nie zgłasza błędu, choć kolumny mogą się różnić. | Sprawdź schemat tabeli i zastosuj migrację, jeśli definicja jest nieaktualna. |
| Klucz obcy wskazuje na tabelę, której jeszcze nie utworzono | Utworzenie ograniczenia może się nie udać. | Twórz najpierw tabelę nadrzędną, a później zależną. |
Wartość ma być wymagana, ale kolumna nie ma NOT NULL | Baza może przyjąć NULL, nawet gdy zastosowano CHECK. | Dodaj NOT NULL do kolumny wymaganej. |
| Składnia identyfikatora pochodzi z innego silnika | Instrukcja może zostać odrzucona albo działać inaczej, niż zakłada kod. | Dopasuj definicję do silnika używanego w projekcie. |
Przed uruchomieniem sprawdź trzy decyzje projektowe
Zanim zapiszesz definicję w repozytorium, ustal który silnik ją wykona, które kolumny muszą mieć wartość i jak tabela łączy się z resztą schematu. Te odpowiedzi prowadzą do właściwych typów, ograniczeń i kluczy.
- Jeśli aplikacja używa jednego silnika, pisz składnię dla niego i zweryfikuj ją w jego dokumentacji.
- Jeśli tabela ma być przenośna, ogranicz zależność od specyficznych typów i generatorów, a kod sprawdź w każdym docelowym silniku.
- Jeśli zmieniasz istniejącą tabelę, nie polegaj na
IF NOT EXISTS; zaplanuj migrację i sprawdź rzeczywistą strukturę.
Najlepsza definicja tabeli nie zgaduje, co autor miał na myśli. Zapisuje reguły w bazie, więc błędny status, pusty tytuł czy osierocone powiązanie zatrzymują się tam, gdzie powstały.
