Czym są relacyjne bazy danych
Relacyjna baza danych to zbiór tabel, między którymi istnieją powiązania oparte na kluczach. Każda tabela to uporządkowana struktura kolumn (pól) i wierszy (rekordów). Model powstał w latach 70. za sprawą Edgara F. Codda i do dziś jest fundamentem większości systemów produkcyjnych — od bankowości po panele administracyjne małych sklepów internetowych.
Silnik relacyjny nie przechowuje danych „jak popadnie”. Każda kolumna ma typ (liczba, tekst, data), wiersze są identyfikowane przez klucz główny (ang. primary key), a relacje między tabelami realizuje się przez klucze obce (foreign keys). SQL to język, którym rozmawiasz z tym systemem: pytasz o dane, modyfikujesz je i definiujesz strukturę.
Jeśli piszesz cokolwiek poważniejszego niż skrypt na jeden wieczór, prędzej czy później trafisz na SQL. Ten artykuł przeprowadzi cię przez najważniejsze elementy: od prostych zapytań SELECT, przez JOIN-y i agregacje, aż po indeksy, normalizację i typowe błędy, które popełnia każdy początkujący.
Podstawy SELECT: pobieranie danych
Polecenie SELECT to najczęściej używana instrukcja w SQL. Określa kolumny, które chcesz zobaczyć, tabelę, z której mają pochodzić, oraz opcjonalnie filtry, sortowanie i limity. Dobre zrozumienie tych czterech elementów to 80% codziennej pracy z bazą.
-- Wszystkie kolumny z tabeli uzytkownicy
SELECT * FROM uzytkownicy;
-- Tylko wybrane kolumny
SELECT id, email, data_rejestracji FROM uzytkownicy;
-- Filtrowanie po warunku
SELECT email FROM uzytkownicy
WHERE kraj = 'PL' AND aktywny = TRUE;
-- Sortowanie malejąco i limit wyników
SELECT id, email FROM uzytkownicy
ORDER BY data_rejestracji DESC
LIMIT 10;
WHERE, ORDER BY, LIMIT
Klauzula WHERE filtruje wiersze przed zwróceniem wyniku. Możesz łączyć warunki operatorami AND, OR, NOT, a także korzystać z IN, BETWEEN, LIKE czy IS NULL. ORDER BY sortuje wynik — domyślnie rosnąco (ASC), albo malejąco po dodaniu DESC. LIMIT ogranicza liczbę wierszy i w parze z OFFSET służy do paginacji.
Unikaj SELECT * w kodzie produkcyjnym. Jawne wymienianie kolumn chroni cię przed niespodziankami, gdy ktoś doda nowe pole, a twoje zapytanie nagle zacznie ciągnąć kilobajty niepotrzebnych danych.
JOIN-y: łączenie tabel
Relacyjna moc SQL ujawnia się przy łączeniu tabel. JOIN pozwala zapytać o dane z kilku tabel naraz na podstawie wspólnego pola — najczęściej klucza obcego. Rodzajów JOIN-ów jest kilka i każdy ma inne zastosowanie.
-- INNER JOIN: tylko pasujące wiersze z obu tabel
SELECT u.email, z.numer, z.kwota
FROM uzytkownicy u
INNER JOIN zamowienia z ON z.uzytkownik_id = u.id;
-- LEFT JOIN: wszyscy użytkownicy, nawet ci bez zamówień
SELECT u.email, COUNT(z.id) AS liczba_zamowien
FROM uzytkownicy u
LEFT JOIN zamowienia z ON z.uzytkownik_id = u.id
GROUP BY u.email;
-- RIGHT JOIN: wszystkie zamówienia, nawet osierocone
SELECT u.email, z.numer
FROM uzytkownicy u
RIGHT JOIN zamowienia z ON z.uzytkownik_id = u.id;
-- FULL OUTER JOIN: wszystko z obu stron
SELECT u.email, z.numer
FROM uzytkownicy u
FULL OUTER JOIN zamowienia z ON z.uzytkownik_id = u.id;
Który JOIN wybrać
INNER JOIN zwraca tylko wiersze, dla których warunek ON jest spełniony w obu tabelach. LEFT JOIN zwraca wszystko z lewej tabeli i dopasowania z prawej (brakujące wartości to NULL). RIGHT JOIN działa odwrotnie. FULL OUTER JOIN zwraca wszystko z obu stron — w MySQL trzeba go emulować przez UNION, bo nie jest natywnie wspierany.
W praktyce 90% zapytań używa INNER JOIN albo LEFT JOIN. RIGHT JOIN da się zawsze zapisać jako LEFT JOIN po zamianie tabel i tak jest czytelniej.
GROUP BY i funkcje agregujące
Gdy chcesz policzyć coś zbiorczo — ile zamówień złożył każdy klient, jaka jest średnia cena w kategorii, które produkty sprzedały się najlepiej — potrzebujesz GROUP BY i funkcji agregujących: COUNT, SUM, AVG, MIN, MAX.
-- Liczba zamówień i suma kwot per kraj
SELECT
kraj,
COUNT(*) AS liczba,
SUM(kwota) AS laczna_kwota,
AVG(kwota) AS srednia
FROM zamowienia
GROUP BY kraj
HAVING COUNT(*) > 10
ORDER BY laczna_kwota DESC;
Ważna różnica: WHERE filtruje wiersze przed agregacją, HAVING filtruje grupy po agregacji. Jeśli chcesz tylko zamówienia z 2026 roku, użyj WHERE. Jeśli chcesz kraje, w których liczba zamówień przekracza 10, użyj HAVING.
INSERT, UPDATE, DELETE
Odczyt to dopiero połowa pracy. Do modyfikacji danych służą trzy polecenia: INSERT dodaje nowe wiersze, UPDATE zmienia istniejące, a DELETE je usuwa. Każde z nich może skasować ci dane z produkcji, jeśli zapomnisz o WHERE — w naszym zespole co parę miesięcy ktoś uczy się tego na własnej skórze.
-- Dodanie nowego wiersza
INSERT INTO uzytkownicy (email, kraj, aktywny)
VALUES ('anna@example.pl', 'PL', TRUE);
-- Wstawianie wielu wierszy naraz
INSERT INTO uzytkownicy (email, kraj) VALUES
('jan@example.pl', 'PL'),
('maria@example.de', 'DE');
-- Aktualizacja — ZAWSZE z WHERE
UPDATE uzytkownicy
SET aktywny = FALSE
WHERE data_ostatniego_logowania < '2024-01-01';
-- Usuwanie konkretnych wierszy
DELETE FROM zamowienia
WHERE status = 'anulowane'
AND data_utworzenia < '2024-01-01';
Przed każdym UPDATE lub DELETE uruchom najpierw SELECT z tym samym WHERE. Zobaczysz dokładnie, które wiersze zostaną dotknięte. W bazach transakcyjnych używaj BEGIN i ROLLBACK — jeśli coś pójdzie nie tak, wycofasz zmiany zanim się utrwalą.
Indeksy: czemu zapytanie jest wolne
Indeks to dodatkowa struktura danych (najczęściej B-drzewo), która pozwala silnikowi szybko znaleźć wiersze bez czytania całej tabeli. Bez indeksu każde SELECT ... WHERE email = '...' oznacza full table scan — baza przegląda wszystkie wiersze po kolei. Przy milionie rekordów różnica to milisekundy kontra sekundy.
-- Prosty indeks na pojedynczej kolumnie
CREATE INDEX idx_uzytkownicy_email
ON uzytkownicy (email);
-- Indeks złożony — kolejność ma znaczenie
CREATE INDEX idx_zamowienia_uzytkownik_data
ON zamowienia (uzytkownik_id, data_utworzenia);
-- Sprawdzenie planu zapytania (PostgreSQL)
EXPLAIN ANALYZE
SELECT * FROM zamowienia WHERE uzytkownik_id = 42;
Kiedy dodać indeks
Indeksuj kolumny używane w WHERE, JOIN ... ON i ORDER BY — zwłaszcza te z dużą liczbą unikalnych wartości. Klucze główne i obce zwykle są indeksowane automatycznie. Pamiętaj jednak, że każdy indeks spowalnia INSERT i UPDATE, bo baza musi go aktualizować. Nie indeksuj wszystkiego „na zapas” — używaj EXPLAIN, żeby zobaczyć, czego naprawdę brakuje.
Normalizacja w trzech krokach
Normalizacja to proces organizowania danych tak, żeby unikać duplikacji i anomalii aktualizacji. W praktyce ograniczysz się do trzech poziomów:
- 1NF — każda komórka zawiera jedną wartość atomową, nie listę. Zamiast pola
telefony: "123, 456"robisz osobną tabelętelefony. - 2NF — w tabelach z kluczem złożonym każda kolumna nie-kluczowa zależy od całego klucza, nie tylko jego części. W praktyce: nie mieszaj encji w jednej tabeli.
- 3NF — kolumny nie-kluczowe zależą tylko od klucza głównego, nie od innych kolumn. Zamiast trzymać
kod_pocztowyimiastow tabeli klientów (miasto zależy od kodu), wydziel tabelękody_pocztowe.
Zasada kciuka: projektuj w 3NF, potem świadomie denormalizuj w miejscach, gdzie wydajność odczytu jest krytyczna. W serwisach do analityki (hurtownie danych) częściej zobaczysz schematy gwiaździste niż znormalizowane — i to jest OK.
Typowe błędy początkujących
Większość problemów z bazami nie wynika z ezoterycznych bugów, tylko z trzech-czterech wzorców, które powtarzają się w każdym projekcie.
Problem N+1
Klasyczna pułapka ORM-ów. Pobierasz listę 100 zamówień, potem dla każdego osobnym zapytaniem pobierasz użytkownika. Zamiast 1 zapytania robisz 101. Rozwiązanie: JOIN albo eager loading (includes w Rails, select_related w Django, populate w Mongoose).
Brak indeksów na kluczach obcych
Niektóre bazy (np. PostgreSQL) nie tworzą ich automatycznie. Efekt: każdy JOIN lub DELETE CASCADE robi full scan. Sprawdź w swojej bazie, czy klucze obce są zaindeksowane.
SQL injection
Konkatenowanie stringów z wejściem użytkownika to największy grzech bezpieczeństwa. Zawsze używaj parametryzowanych zapytań albo prepared statements. W Pythonie: cursor.execute("SELECT * FROM u WHERE id = %s", (user_id,)), nigdy f"... WHERE id = {user_id}".
Każdy framework ma gotowe narzędzia do parametryzacji. Jeśli sklejasz zapytanie stringiem, robisz coś źle. To nie jest kwestia stylu — to podatność bezpieczeństwa z listy OWASP Top 10.
MySQL, PostgreSQL, SQLite: co wybrać
Wszystkie trzy mówią SQL-em, ale różnią się filozofią i możliwościami. Nasz zespół w LeePoint korzystał w różnych projektach ze wszystkich — oto subiektywne podsumowanie.
- SQLite — jeden plik na dysku, zero konfiguracji. Idealny do prototypów, aplikacji mobilnych, testów i małych narzędzi. Nie nadaje się do aplikacji z wieloma równoczesnymi zapisami.
- MySQL / MariaDB — najpopularniejszy wybór dla webu. Szybki w prostych odczytach, łatwy w administracji, ogromne wsparcie hostingów. Ma swoje dziwactwa: domyślne kodowanie i strict mode wymagają konfiguracji.
- PostgreSQL — najbardziej zaawansowany open source. Wspiera JSONB, full-text search, CTE, window functions, rozszerzenia (PostGIS dla geo). Jeśli nie masz konkretnego powodu, żeby go nie użyć — używaj.
SQL czy NoSQL
Relacyjna baza to dobry domyślny wybór, gdy twoje dane mają strukturę i relacje, gdy potrzebujesz transakcji ACID, albo gdy będziesz robić złożone raporty. NoSQL (MongoDB, Redis, DynamoDB) ma sens dla danych bez sztywnego schematu, masowej skali odczytu lub specyficznych wzorców dostępu — cache, kolejki, event log. Większość typowych aplikacji CRUD-owych będzie się dobrze czuła w PostgreSQL, a ty unikniesz przedwczesnej komplikacji.
Następnym krokiem po opanowaniu powyższego jest nauka transakcji i poziomów izolacji, widoków, wspólnych wyrażeń tablicowych CTE (WITH) oraz funkcji okiennych (OVER). Zainstaluj lokalnie PostgreSQL, pobierz publiczny zbiór danych (np. Chinook albo Pagila) i napisz dziesięć zapytań, które naprawdę odpowiadają na pytania biznesowe — to najszybsza droga od teorii do swobody w pracy z relacyjną bazą.

