SQL i bazy danych
Praktyczne fundamenty SQL i baz relacyjnych dla pracujących inżynierów: JOIN-y, indeksy, transakcje ACID, poziomy izolacji, normalizacja, N+1, SQL injection i wybór SQL vs NoSQL.
- Różnice między typami JOIN i sytuacje, w których zły wybór cicho gubi dane
- Poziomy izolacji transakcji: które race condition każdy z nich dopuszcza, a które blokuje
- Kompromis indeksów: przyspieszenie odczytu kontra spowolnienie zapisu i dodatkowe miejsce na dysku
- Cztery gwarancje ACID oraz mechanizm (write-ahead log), który realizuje trwałość (Durability)
- Problem N+1 w ORM-ach: rozpoznanie i naprawa przez eager loading
- SQL injection: dlaczego jedyną realną obroną jest parametryzacja zapytań, nie escapowanie
1 · JOIN-y: kiedy który jest poprawny
★ egzaminJOIN łączy wiersze z wielu tabel na podstawie warunku dopasowania kluczy. Wybór złego typu JOIN nie jest błędem składniowym: silnik wykona zapytanie i zwróci wynik, tylko że będzie on cicho niepoprawny, ze zgubionymi wierszami albo utratą danych bez żadnego komunikatu błędu.
-- Klienci wraz z liczbą zamówień, łącznie z klientami bez żadnego zamówienia
SELECT c.id, c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY order_count DESC; | Typ JOIN | Wiersze z lewej bez dopasowania | Wiersze z prawej bez dopasowania | Typowe zastosowanie |
|---|---|---|---|
| INNER JOIN | odrzucone | odrzucone | łączenie dopasowanych rekordów obu stron, np. zamówienie z istniejącym produktem |
| LEFT JOIN | zachowane (NULL po prawej) | odrzucone | lista głównej encji niezależnie od istnienia powiązań, np. wszyscy klienci z liczbą zamówień |
| RIGHT JOIN | odrzucone | zachowane (NULL po lewej) | rzadko używany, zwykle zastępowany zamianą tabel i LEFT JOIN |
| FULL OUTER JOIN | zachowane (NULL po prawej) | zachowane (NULL po lewej) | pełne zestawienie rozbieżności między dwoma zbiorami, np. audyt synchronizacji dwóch systemów |
| CROSS JOIN | n/d (iloczyn kartezjański) | n/d (iloczyn kartezjański) | generowanie kombinacji, np. każda data z każdym produktem w raporcie |
- Potrzebujesz tylko dopasowanych par obu stron: INNER JOIN.
- Musisz zachować wszystkie rekordy 'głównej' encji, nawet bez powiązań: LEFT JOIN z tej encji jako lewej tabeli.
- Potrzebujesz pełnego zestawienia rozbieżności między dwoma zbiorami: FULL OUTER JOIN.
- Generujesz kombinacje, np. kalendarz dat razy lista produktów: CROSS JOIN.
- Porównujesz wiersze tej samej tabeli między sobą: SELF JOIN.
2 · Indeksy: przyspieszenie odczytu kontra koszt zapisu
★ egzaminIndeks to dodatkowa struktura danych, najczęściej B-drzewo, utrzymywana obok tabeli, która pozwala silnikowi znaleźć wiersze bez przeglądania całej tabeli. Ten sam mechanizm, który przyspiesza SELECT-y, dokłada pracę do każdego INSERT, UPDATE i DELETE, bo indeks trzeba aktualizować razem z danymi.
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 42;
-- Przed indeksem:
-- Seq Scan on orders (cost=0.00..18334.00 rows=12 width=72) (actual time=45.231..89.442 rows=12 loops=1)
-- Planning Time: 0.112 ms
-- Execution Time: 89.478 ms
-- Po indeksie:
-- Index Scan using idx_orders_customer_id on orders (cost=0.43..8.45 rows=12 width=72) (actual time=0.028..0.041 rows=12 loops=1)
-- Planning Time: 0.098 ms
-- Execution Time: 0.067 ms - Dodawaj indeks na kolumnach używanych w JOIN (klucze obce) i często filtrowanych w WHERE.
- Sprawdź selektywność: indeks pomaga, gdy zawęża wynik do niewielkiego ułamka tabeli.
- Unikaj nadmiarowych indeksów na tabelach z bardzo wysokim ruchem zapisu, każdy kolejny indeks to dodatkowy koszt na każdy zapis.
- Kolumny w ORDER BY i GROUP BY też są dobrymi kandydatami, indeks może wyeliminować osobne sortowanie.
| Aspekt | Bez indeksu | Z indeksem |
|---|---|---|
| Czas odczytu przy filtrze WHERE | Skan całej tabeli (Seq Scan), rośnie liniowo z rozmiarem tabeli | Skan przez B-drzewo (Index Scan), rośnie logarytmicznie |
| Czas zapisu (INSERT/UPDATE/DELETE) | Tylko aktualizacja wiersza w tabeli | Dodatkowa aktualizacja wpisu w każdym powiązanym indeksie |
| Zajętość dysku | Tylko dane tabeli | Dane tabeli plus struktura indeksu, zwykle kilkanaście do kilkudziesięciu procent dodatkowo |
| Decyzja plannera | Zawsze Seq Scan | Index Scan przy wysokiej selektywności, Seq Scan przy niskiej mimo istnienia indeksu |
- 1 Sprawdź plan zapytania przez EXPLAIN ANALYZE Zobacz, czy planner faktycznie wybiera Index Scan zamiast Seq Scan.
- 2 Zmierz selektywność kolumny Ile unikalnych wartości względem liczby wierszy w tabeli.
- 3 Potwierdź obecność kolumny w WHERE, JOIN lub ORDER BY Indeks pomaga tylko tam, gdzie jest faktycznie używany do filtrowania lub sortowania.
- 4 Przetestuj na środowisku testowym Porównaj czas wykonania zapytania przed i po dodaniu indeksu na realistycznym wolumenie danych.
- 5 Zmierz wpływ na zapisy Sprawdź czas INSERT/UPDATE na tabeli o dużym ruchu zapisu przed i po dodaniu indeksu.
3 · Transakcje i właściwości ACID
★ egzaminTransakcja grupuje wiele operacji w jedną niepodzielną całość: albo wszystkie się wykonają, albo żadna. ACID to cztery gwarancje, które silnik bazy danych musi utrzymać, żeby ta obietnica miała sens także przy awarii zasilania czy współbieżnym dostępie wielu klientów.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; - Operacje modyfikujące więcej niż jedną tabelę, które muszą się powieść razem.
- Odczyt-modyfikacja-zapis tej samej wartości, np. stan magazynowy.
- Import wsadowy, gdzie częściowy import jest gorszy niż brak importu.
- Operacje wymagające blokady wiersza do końca logiki biznesowej (SELECT ... FOR UPDATE).
4 · Poziomy izolacji i race condition, które kontrolują
★ egzaminPełna izolacja (Serializable) daje absolutną poprawność, ale kosztuje przepustowość: więcej blokad, więcej oczekiwania, więcej odrzuconych transakcji do ponowienia. Standard SQL definiuje cztery poziomy izolacji, z których każdy świadomie dopuszcza pewne zjawiska współbieżności w zamian za wydajność.
| Poziom izolacji | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
| Read Uncommitted | możliwy | możliwy | możliwy |
| Read Committed | zablokowany | możliwy | możliwy |
| Repeatable Read | zablokowany | zablokowany | możliwy wg standardu SQL |
| Serializable | zablokowany | zablokowany | zablokowany |
Transakcja A Transakcja B
BEGIN; BEGIN;
SELECT balance FROM accounts WHERE id=1; SELECT balance FROM accounts WHERE id=1;
-- odczytano 500 -- odczytano 500
-- lokalnie: 500 - 100 = 400 -- lokalnie: 500 - 50 = 450
UPDATE accounts SET balance = 400
WHERE id = 1;
COMMIT; UPDATE accounts SET balance = 450
WHERE id = 1;
COMMIT;
-- wynik końcowy: 450, zmiana transakcji A zgubiona (lost update) BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- blokuje wiersz do końca transakcji
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT; - Odczyty raportowe bez modyfikacji równoległych: Read Committed wystarcza w większości przypadków.
- Logika odczytaj-zmodyfikuj-zapisz na tym samym wierszu: SELECT FOR UPDATE albo blokada optymistyczna z kolumną wersji.
- Wielokrokowa logika biznesowa wymagająca spójnego 'zdjęcia' całej bazy przez cały czas transakcji: Repeatable Read lub Serializable.
- Wysoka konkurencja o te same wiersze: unikaj Serializable, rosnąca liczba odrzuceń transakcji wymaga kosztownych retry.
5 · Normalizacja i świadoma denormalizacja
Normalizacja eliminuje duplikację danych, dzieląc informację na osobne tabele połączone kluczami obcymi. Denormalizacja robi odwrotnie: świadomie kopiuje dane, żeby ograniczyć liczbę JOIN-ów kosztem ryzyka niespójności.
-- Znormalizowane (3NF): dane klienta tylko w tabeli customers
SELECT o.id, o.total, c.name, c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.id = 1001;
-- Zdenormalizowane: nazwa i e-mail klienta skopiowane do orders
SELECT id, total, customer_name, customer_email
FROM orders
WHERE id = 1001; - Warstwa raportowa/analityczna (data warehouse, tabele faktów) czytana dużo częściej niż zapisywana.
- Pola liczone i cache'owane, np. suma zamówienia zamiast sumowania pozycji przy każdym odczycie.
- Dane historyczne, które nie powinny się zmieniać nawet gdy zmieni się rekord źródłowy, np. cena produktu w momencie zakupu.
- Ekstremalnie odczytowe endpointy API, gdzie JOIN w czasie rzeczywistym jest wąskim gardłem.
| Aspekt | Znormalizowany schemat | Zdenormalizowany schemat |
|---|---|---|
| Ryzyko anomalii aktualizacji | Niskie, dane w jednym miejscu | Wyższe, wymaga mechanizmu synchronizacji kopii |
| Wydajność odczytu przy złożonych zapytaniach | Wymaga JOIN-ów, koszt rośnie z liczbą tabel | Mniej lub brak JOIN-ów, szybszy pojedynczy odczyt |
| Zajętość dysku | Mniejsza, dane bez duplikacji | Większa, dane częściowo zduplikowane |
| Złożoność zapisu | Prostsza, jeden rekord do zmiany | Wymaga zaktualizowania wszystkich kopii tej samej wartości |
6 · Problem N+1 i jak generują go ORM-y
★ egzaminN+1 to wzorzec, w którym pobranie listy encji kosztuje jedno zapytanie, a dociągnięcie powiązanych danych dla każdej z nich kosztuje kolejne zapytanie na rekord. Przy 1000 wierszy oznacza to 1001 zapytań zamiast jednego lub dwóch.
# N+1: 1 zapytanie po listę + N zapytań po powiązane rekordy
orders = Order.objects.all() # 1 zapytanie
for order in orders:
print(order.customer.name) # N zapytań, jedno na każdy order
# Naprawa: eager loading jednym JOIN-em
orders = Order.objects.select_related("customer") # wciąż 1 zapytanie
for order in orders:
print(order.customer.name) # brak dodatkowych zapytań - select_related / prefetch_related (Django), includes (Rails, Prisma): eager loading relacji z góry.
- DataLoader (batching i cache w obrębie jednego requestu) w GraphQL.
- Ręczny JOIN i mapowanie wyniku zamiast polegania na leniwym ładowaniu ORM-a.
- Denormalizacja albo cache dla relacji odczytywanych bardzo często i rzadko zmienianych.
| Liczba wierszy listy | Zapytań przy lazy loading | Zapytań przy eager loading |
|---|---|---|
| 10 | 11 | 1 |
| 1 000 | 1 001 | 1 |
| 100 000 | 100 001 | 1 |
- 1 Włącz logowanie wykonywanych zapytań SQL W środowisku deweloperskim, żeby zobaczyć realną liczbę zapytań na request.
- 2 Policz zapytania na request Django Debug Toolbar, gem Bullet w Rails, albo APM typu New Relic/Datadog.
- 3 Testuj na realistycznym wolumenie danych Nie na trzech rekordach, N+1 jest niewidoczny przy małym zbiorze.
- 4 Ustaw alert na liczbę zapytań per request W monitoringu produkcyjnym, żeby wykryć regresję zanim zgłoszą ją użytkownicy.
7 · SQL injection i parametryzacja zapytań
★ egzaminSQL injection powstaje, gdy dane od użytkownika trafiają bezpośrednio do treści zapytania SQL i zmieniają jego strukturę, a nie tylko wartość. Jedyną realną obroną jest parametryzacja: oddzielenie kodu zapytania od danych na poziomie sterownika bazy danych, nie na poziomie ręcznego escapowania stringów.
# PODATNE: string interpolation buduje zapytanie z danych użytkownika
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
# username = "' OR '1'='1" zwraca wszystkich użytkowników
# BEZPIECZNE: parametryzowane zapytanie, silnik bazy oddziela kod od danych
cursor.execute(
"SELECT * FROM users WHERE username = %s",
(username,)
) - Zasada najmniejszych uprawnień: konto aplikacji nie ma prawa DROP/ALTER, tylko operacje potrzebne do działania.
- Walidacja typu i formatu danych wejściowych przed dotarciem do warstwy zapytań.
- Logowanie i alerty na nietypowe wzorce zapytań oraz błędy składni SQL zwracane do klienta.
- Web Application Firewall jako dodatkowa warstwa, nigdy jako jedyna linia obrony.
| Wzorzec | Bezpieczny? | Dlaczego |
|---|---|---|
| Konkatenacja stringów (f-string, +) | Nie | Dane wejściowe stają się częścią składni SQL |
| Ręczne escapowanie znaków specjalnych | Częściowo, niewystarczająco | Łatwo pominąć edge case, brak gwarancji na poziomie silnika bazy |
| ORM: metoda raw()/surowy string bez parametrów | Nie | Omija bezpieczny query builder, działa jak konkatenacja |
| Zapytanie parametryzowane / prepared statement | Tak | Dane przekazywane osobno od kodu zapytania |
| ORM: standardowy query builder (filter/where) | Tak | Wewnętrznie generuje zapytanie parametryzowane |
8 · SQL vs NoSQL: kryteria decyzji
Wybór między bazą relacyjną a NoSQL to decyzja o tym, gdzie chcesz ponieść koszt: przy zapisie (silny schemat, spójność, JOIN-y) czy przy odczycie i skalowaniu poziomym (elastyczny schemat, replikacja, częściowa spójność).
SQL (relacyjne)
- Silny, wymuszony schemat i typy danych
- Transakcje ACID na wielu tabelach
- JOIN-y jako natywna operacja łącząca dane
- Dojrzała integralność referencyjna (klucze obce)
- Skalowanie głównie pionowe, poziome trudniejsze (ręczny sharding)
NoSQL (dokumentowe, kolumnowe, grafowe, klucz-wartość)
- Elastyczny lub brak schematu (schema-on-read)
- Natywne skalowanie poziome i replikacja między regionami
- Zwykle brak JOIN-ów, dane denormalizowane z góry w dokumencie
- Model spójności często eventual consistency zamiast pełnej ACID
- Wyspecjalizowany wybór pod kształt danych: graf, szereg czasowy, cache
{
"_id": "order_1001",
"customer": { "name": "Jan Kowalski", "email": "jan@example.com" },
"items": [
{ "sku": "ABC123", "qty": 2, "price": 49.99 },
{ "sku": "XYZ789", "qty": 1, "price": 129.00 }
],
"status": "paid"
} - Czy dane mają naturalną strukturę relacyjną z wieloma powiązaniami wymagającymi JOIN-ów, czy raczej kształt pojedynczego samodzielnego dokumentu?
- Czy potrzebujesz transakcji ACID obejmujących wiele rekordów lub tabel naraz?
- Jaka jest realna skala zapisu i czy pojedynczy węzeł bazy relacyjnej z odpowiednim indeksowaniem faktycznie jej nie udźwignie?
- Czy dopuszczalna jest chwilowa niespójność między replikami, czy dana domena (płatności, stany magazynowe) tego nie toleruje?
| Charakter obciążenia | Rekomendacja | Przykład technologii |
|---|---|---|
| Transakcje finansowe z wieloma powiązanymi tabelami | SQL | PostgreSQL, MySQL |
| Katalog produktów o zmiennej, niejednorodnej strukturze | NoSQL dokumentowe | MongoDB |
| Sesje i cache o krótkim czasie życia | NoSQL klucz-wartość | Redis |
| Sieć powiązań (rekomendacje, graf społecznościowy) | NoSQL grafowe | Neo4j |
| Metryki i logi w czasie | Baza szeregów czasowych | TimescaleDB, InfluxDB |
Sprawdź się - testowanie to nauka
24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.