Przejdź do treści
← wszystkie przedmioty

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.

TEMATY EGZAMINACYJNE W TYM PRZEDMIOCIE
  • 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

★ egzamin

JOIN łą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.

Termin kluczowy
INNER JOIN - Zwraca tylko wiersze, dla których warunek dopasowania jest spełniony w obu tabelach. Klient bez zamówień znika z wyniku całkowicie.
Termin kluczowy
LEFT JOIN (LEFT OUTER JOIN) - Zwraca wszystkie wiersze z lewej tabeli, a z prawej dopasowane wartości lub NULL, gdy dopasowania brak. Domyślny wybór, gdy trzeba zachować 'właściciela' relacji nawet bez powiązanych rekordów.
Termin kluczowy
RIGHT JOIN (RIGHT OUTER JOIN) - Lustrzane odbicie LEFT JOIN: wszystkie wiersze z prawej tabeli, dopasowanie z lewej lub NULL. W praktyce rzadko używany, bo ten sam wynik da się uzyskać zamieniając kolejność tabel i pisząc LEFT JOIN, co jest czytelniejsze.
Termin kluczowy
FULL OUTER JOIN - Suma LEFT i RIGHT JOIN: wszystkie wiersze z obu tabel, NULL tam, gdzie dopasowania brak po którejkolwiek stronie. MySQL nie wspiera tej składni natywnie, trzeba ją emulować przez UNION dwóch LEFT JOIN.
Termin kluczowy
CROSS JOIN - Iloczyn kartezjański: każdy wiersz lewej tabeli z każdym wierszem prawej, bez warunku dopasowania. Liczba wynikowych wierszy to iloczyn liczności obu tabel, przy dużych tabelach to szybki sposób na wyczerpanie pamięci.
Termin kluczowy
SELF JOIN - Złączenie tabeli samej ze sobą przez dwa aliasy, typowe dla struktur hierarchicznych, np. pracownik i jego przełożony w tej samej tabeli employees.
LEFT JOIN zachowujący klientów bez zamówień sql
-- 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;
Pułapka
Filtrowanie kolumny z dołączonej tabeli w WHERE zamiast w ON cicho zamienia LEFT JOIN w INNER JOIN: WHERE o.status = 'paid' odrzuca wiersze z o.status IS NULL, więc klienci bez zamówień i tak znikają. Warunek dotyczący dopasowania musi siedzieć w ON, nie w WHERE.
Typ JOINWiersze z lewej bez dopasowaniaWiersze z prawej bez dopasowaniaTypowe zastosowanie
INNER JOINodrzuconeodrzuconełączenie dopasowanych rekordów obu stron, np. zamówienie z istniejącym produktem
LEFT JOINzachowane (NULL po prawej)odrzuconelista głównej encji niezależnie od istnienia powiązań, np. wszyscy klienci z liczbą zamówień
RIGHT JOINodrzuconezachowane (NULL po lewej)rzadko używany, zwykle zastępowany zamianą tabel i LEFT JOIN
FULL OUTER JOINzachowane (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 JOINn/d (iloczyn kartezjański)n/d (iloczyn kartezjański)generowanie kombinacji, np. każda data z każdym produktem w raporcie
Mnemonik
LEFT JOIN zachowuje LEWĄ stronę w całości, tak jak nazwa mówi wprost, więc gdy pytanie brzmi 'pokaż wszystkich klientów, także tych bez zamówień', odpowiedź to zawsze LEFT JOIN z tabeli klientów.
  • 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

★ egzamin

Indeks 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.

B-drzewo
Domyślna struktura indeksu w PostgreSQL, MySQL/InnoDB i SQL Server. Wyszukiwanie, wstawianie i usuwanie mają złożoność O(log n) względem liczby wierszy, dzięki czemu tabela z milionami rekordów jest przeszukiwana w kilkunastu porównaniach zamiast pełnego skanu.
Termin kluczowy
indeks pokrywający (covering index) - Indeks zawierający wszystkie kolumny potrzebne do wykonania zapytania, dzięki czemu silnik w ogóle nie sięga do tabeli głównej (Index Only Scan w PostgreSQL, 'Using index' w EXPLAIN MySQL).
Termin kluczowy
indeks złożony i reguła lewego prefiksu - Indeks na (a, b, c) przyspiesza zapytania filtrujące po a, po (a, b) i po (a, b, c), ale nie przyspiesza zapytania filtrującego wyłącznie po b lub c, bo B-drzewo jest posortowane od lewej kolumny.
Dodanie indeksu i weryfikacja planu wykonania sql
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
Pułapka
Indeks na kolumnie o niskiej kardynalności (np. flaga boolean is_active) rzadko pomaga: planner szacuje, że dopasowanych wierszy jest zbyt dużo względem całej tabeli, i tak czy inaczej wybierze Seq Scan, bo odczyt przez indeks byłby wolniejszy od skanu sekwencyjnego.
Pułapka
LIKE '%fraza%' z wiodącym znakiem wieloznacznym nie może wykorzystać zwykłego indeksu B-drzewa, bo sortowanie po prefiksie nic nie daje, gdy szukana fraza jest w środku stringa. Potrzebny jest indeks trigramowy (pg_trgm) albo pełnotekstowy.
Dlaczego zapisy zwalniają
Każdy INSERT, UPDATE modyfikujący indeksowaną kolumnę i DELETE wymaga zaktualizowania nie tylko wiersza w tabeli, ale też wpisu w każdym indeksie na tej tabeli. Dziesięć indeksów na tabeli oznacza dziesięć dodatkowych operacji zapisu przy każdej zmianie danych.
  • 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.
AspektBez indeksuZ indeksem
Czas odczytu przy filtrze WHERESkan całej tabeli (Seq Scan), rośnie liniowo z rozmiarem tabeliSkan przez B-drzewo (Index Scan), rośnie logarytmicznie
Czas zapisu (INSERT/UPDATE/DELETE)Tylko aktualizacja wiersza w tabeliDodatkowa aktualizacja wpisu w każdym powiązanym indeksie
Zajętość dyskuTylko dane tabeliDane tabeli plus struktura indeksu, zwykle kilkanaście do kilkudziesięciu procent dodatkowo
Decyzja planneraZawsze Seq ScanIndex Scan przy wysokiej selektywności, Seq Scan przy niskiej mimo istnienia indeksu
  1. 1 Sprawdź plan zapytania przez EXPLAIN ANALYZE Zobacz, czy planner faktycznie wybiera Index Scan zamiast Seq Scan.
  2. 2 Zmierz selektywność kolumny Ile unikalnych wartości względem liczby wierszy w tabeli.
  3. 3 Potwierdź obecność kolumny w WHERE, JOIN lub ORDER BY Indeks pomaga tylko tam, gdzie jest faktycznie używany do filtrowania lub sortowania.
  4. 4 Przetestuj na środowisku testowym Porównaj czas wykonania zapytania przed i po dodaniu indeksu na realistycznym wolumenie danych.
  5. 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

★ egzamin

Transakcja 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.

Termin kluczowy
Atomicity (atomowość) - Wszystkie operacje w transakcji wykonują się w całości albo nie wykonują się wcale. Jeśli drugi z trzech UPDATE-ów w transakcji zawiedzie, pierwszy musi zostać wycofany, nie może zostać zapisany częściowy stan.
Termin kluczowy
Consistency (spójność) - Transakcja przenosi bazę z jednego poprawnego stanu w drugi poprawny stan, respektując wszystkie ograniczenia: klucze obce, unikalność, CHECK. Baza nigdy nie zostaje zapisana w stanie łamiącym te reguły.
Termin kluczowy
Isolation (izolacja) - Współbieżne transakcje nie widzą swoich niedokończonych zmian w sposób, który psułby poprawność wyniku. Stopień tej izolacji jest regulowany przez poziom izolacji i jest kompromisem między poprawnością a przepustowością.
Termin kluczowy
Durability (trwałość) - Po zatwierdzeniu (COMMIT) zmiana przetrwa awarię zasilania czy restart procesu. Mechanizmem realizującym tę gwarancję jest write-ahead log (WAL): zmiana trafia najpierw na dysk do dziennika, zanim transakcja zostanie potwierdzona klientowi.
Atomowy przelew między dwoma kontami sql
BEGIN;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;
Pułapka
Seria niezależnych zapytań bez opakowania w jawną transakcję jest tak bezpieczna, jak najsłabsze ogniwo: awaria połączenia po pierwszym UPDATE, a przed drugim, zostawia bazę w niespójnym stanie, którego żadna z gwarancji ACID już nie naprawi, bo nigdy nie były traktowane jako jedna transakcja.
Write-Ahead Log
PostgreSQL, MySQL/InnoDB i większość silników relacyjnych zapisują zmianę najpierw sekwencyjnie do dziennika WAL, dopiero później do właściwych stron danych. Sekwencyjny zapis do jednego pliku jest szybszy niż losowy zapis po całej tabeli, a po awarii dziennik pozwala odtworzyć niezatwierdzone i zatwierdzone zmiany.
  • 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).
Mnemonik
ACID: Atomicity, wszystko albo nic. Consistency, reguły bazy zawsze spełnione. Isolation, transakcje nie depczą sobie po nogach. Durability, po COMMIT zmiana przeżyje nawet wyłączenie prądu.

4 · Poziomy izolacji i race condition, które kontrolują

★ egzamin

Peł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ść.

Termin kluczowy
dirty read (odczyt brudny) - Transakcja czyta dane zapisane przez inną transakcję, która jeszcze nie wykonała COMMIT i może się wycofać (ROLLBACK). Odczytana wartość mogła nigdy realnie nie istnieć.
Termin kluczowy
non-repeatable read (odczyt niepowtarzalny) - Ta sama transakcja czyta ten sam wiersz dwukrotnie i dostaje różne wartości, bo inna transakcja zmieniła go i zatwierdziła pomiędzy odczytami.
Termin kluczowy
phantom read (odczyt widmo) - Ta sama transakcja wykonuje dwukrotnie ten sam warunek WHERE i za drugim razem widzi nowe wiersze albo brak wcześniej widzianych, bo inna transakcja wstawiła lub usunęła pasujące rekordy.
Poziom izolacjiDirty readNon-repeatable readPhantom read
Read Uncommittedmożliwymożliwymożliwy
Read Committedzablokowanymożliwymożliwy
Repeatable Readzablokowanyzablokowanymożliwy wg standardu SQL
Serializablezablokowanyzablokowanyzablokowany
Domyślne poziomy w praktyce
PostgreSQL i Oracle domyślnie startują na Read Committed. MySQL/InnoDB domyślnie na Repeatable Read i dodatkowo blokuje phantom read przy odczytach blokujących (SELECT ... FOR UPDATE) dzięki next-key lockom, co jest silniejsze niż wymaga sam standard SQL dla tego poziomu.
Lost update: dwie transakcje nadpisują ten sam wiersz bez blokady text
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)
Naprawa przez blokadę wiersza (SELECT ... FOR UPDATE) sql
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;
Pułapka
'Repeatable Read' w MySQL i w czystym standardzie SQL to nie to samo zjawisko: standard dopuszcza tam phantom read, InnoDB w praktyce go blokuje dla większości przypadków. Zakładanie identycznego zachowania między silnikami bez sprawdzenia dokumentacji to częste źródło buga ujawniającego się dopiero pod obciążeniem.
  • 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.
Mnemonik
Im wyższy poziom izolacji, tym mniej zjawisk współbieżności przechodzi przez sito, ale tym więcej transakcji czeka w kolejce albo dostaje odrzucenie do ponowienia.

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.

Termin kluczowy
1NF - Każda kolumna przechowuje pojedynczą, atomową wartość, brak list czy zagnieżdżonych struktur w jednej komórce.
Termin kluczowy
2NF - Spełnia 1NF i każda kolumna niekluczowa zależy od całego klucza głównego, nie tylko od jego części (dotyczy kluczy złożonych).
Termin kluczowy
3NF - Spełnia 2NF i żadna kolumna niekluczowa nie zależy od innej kolumny niekluczowej, brak zależności przechodnich. To poziom, na którym w praktyce zatrzymuje się większość schematów OLTP.
To samo zapytanie w schemacie znormalizowanym i zdenormalizowanym sql
-- 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;
Pułapka
Nadmierna normalizacja w systemie transakcyjnym o wysokim ruchu odczytowym prowadzi do zapytań z pięcioma czy sześcioma JOIN-ami tam, gdzie wystarczyłaby jedna tabela raportowa. Każdy dodatkowy JOIN to dodatkowy koszt planowania i wykonania zapytania.
Pułapka
Denormalizacja bez mechanizmu utrzymania spójności (triggera, zadania synchronizującego albo aktualizacji w tej samej transakcji) prowadzi do rozjazdu danych: skopiowana nazwa klienta w tabeli zamówień nie odzwierciedla zmiany nazwy w tabeli klientów.
  • 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.
AspektZnormalizowany schematZdenormalizowany schemat
Ryzyko anomalii aktualizacjiNiskie, dane w jednym miejscuWyższe, wymaga mechanizmu synchronizacji kopii
Wydajność odczytu przy złożonych zapytaniachWymaga JOIN-ów, koszt rośnie z liczbą tabelMniej lub brak JOIN-ów, szybszy pojedynczy odczyt
Zajętość dyskuMniejsza, dane bez duplikacjiWiększa, dane częściowo zduplikowane
Złożoność zapisuProstsza, jeden rekord do zmianyWymaga zaktualizowania wszystkich kopii tej samej wartości
Zasada praktyczna
Projektuj domyślnie w 3NF, denormalizuj punktowo i świadomie tam, gdzie profilowanie pokazało realny problem wydajnościowy, nigdy z góry 'na wszelki wypadek'.

6 · Problem N+1 i jak generują go ORM-y

★ egzamin

N+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.

Skąd się bierze
ORM-y domyślnie ładują relacje leniwie (lazy loading): odwołanie do powiązanego obiektu w pętli wyzwala osobne zapytanie SQL w danym momencie, a nie z góry. W developmencie na małym zbiorze danych różnica jest niewidoczna, na produkcji przy tysiącach rekordów zamienia się w sekundy albo minuty odpowiedzi.
Termin kluczowy
eager loading (ładowanie zachłanne) - Pobranie powiązanych danych z góry, jednym dodatkowym zapytaniem albo jednym JOIN-em, zamiast osobnym zapytaniem na każdy wiersz głównej listy.
Termin kluczowy
lazy loading (ładowanie leniwe) - Powiązane dane są pobierane dopiero w momencie pierwszego odwołania do nich w kodzie, co jest wygodne, ale domyślnie generuje N+1 przy iteracji po liście.
N+1 w ORM-ie i naprawa przez eager loading python
# 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ń
Pułapka
N+1 rzadko wybucha na etapie code review, bo w testach jednostkowych operuje się na kilku rekordach. Ujawnia się dopiero na produkcyjnym wolumenie danych, często jako 'baza nagle zwolniła' bez żadnej zmiany w kodzie, tylko wzrost liczby wierszy.
  • 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 listyZapytań przy lazy loadingZapytań przy eager loading
10111
1 0001 0011
100 000100 0011
  1. 1 Włącz logowanie wykonywanych zapytań SQL W środowisku deweloperskim, żeby zobaczyć realną liczbę zapytań na request.
  2. 2 Policz zapytania na request Django Debug Toolbar, gem Bullet w Rails, albo APM typu New Relic/Datadog.
  3. 3 Testuj na realistycznym wolumenie danych Nie na trzech rekordach, N+1 jest niewidoczny przy małym zbiorze.
  4. 4 Ustaw alert na liczbę zapytań per request W monitoringu produkcyjnym, żeby wykryć regresję zanim zgłoszą ją użytkownicy.
Mnemonik
N+1 to zawsze ten sam kształt: 1 zapytanie po listę, plus N zapytań w pętli po szczegóły. Powtarzający się identyczny SELECT z inną wartością w WHERE w logu to właśnie ten wzorzec.

7 · SQL injection i parametryzacja zapytań

★ egzamin

SQL 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.

Mechanizm ataku
Baza danych nie odróżnia 'kodu' od 'danych' w gotowym stringu zapytania. Gdy username wstawiony przez konkatenację brzmi ' OR '1'='1, warunek WHERE username = '' OR '1'='1' jest zawsze prawdziwy i zwraca wszystkie wiersze, niezależnie od intencji zapytania.
Podatna konkatenacja kontra bezpieczne zapytanie parametryzowane python
# 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,)
)
Termin kluczowy
prepared statement / zapytanie parametryzowane - Zapytanie wysyłane do bazy z placeholderami (?, %s, $1 zależnie od sterownika), a wartości przekazywane osobno. Silnik bazy traktuje parametry wyłącznie jako dane, nigdy jako fragment składni SQL.
Pułapka
Escapowanie cudzysłowów czy ręcznie napisana 'sanityzacja' stringa nie jest równoważna parametryzacji: łatwo pominąć edge case (kodowanie znaków, kontekst zagnieżdżony), a wystarczy jedno przeoczone miejsce, żeby atak przeszedł.
Pułapka
ORM chroni domyślnie tylko wtedy, gdy używa się jego standardowego API. Metody typu raw() (Django), query() z surowym stringiem czy ręcznie budowany fragment WHERE przez konkatenację są równie podatne jak czyste SQL bez ORM-a.
  • 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.
Mnemonik
Konkatenacja stringów do zapytania SQL to zawsze czerwona flaga, niezależnie od tego, jak 'zaufane' wydaje się źródło danych. Parametr, nigdy string.
WzorzecBezpieczny?Dlaczego
Konkatenacja stringów (f-string, +)NieDane wejściowe stają się częścią składni SQL
Ręczne escapowanie znaków specjalnychCzęściowo, niewystarczającoŁatwo pominąć edge case, brak gwarancji na poziomie silnika bazy
ORM: metoda raw()/surowy string bez parametrówNieOmija bezpieczny query builder, działa jak konkatenacja
Zapytanie parametryzowane / prepared statementTakDane przekazywane osobno od kodu zapytania
ORM: standardowy query builder (filter/where)TakWewnętrznie generuje zapytanie parametryzowane
Rekomendacja branżowa
OWASP od lat wskazuje parametryzację zapytań (prepared statements) jako podstawowy, obowiązkowy mechanizm obrony przed SQL injection we wszystkich językach i sterownikach baz danych, stawiając ją przed escapowaniem i przed walidacją wejścia.

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
Termin kluczowy
eventual consistency (spójność ostateczna) - Po zapisie różne repliki bazy mogą przez krótki czas zwracać różne wartości tego samego rekordu, zanim zmiana się rozpropaguje. Akceptowalne dla licznika polubień, nieakceptowalne dla salda konta bankowego.
Termin kluczowy
schema-on-write vs schema-on-read - Baza relacyjna wymusza strukturę w momencie zapisu: błędny typ danych odrzuca INSERT. Wiele baz dokumentowych sprawdza strukturę dopiero w momencie odczytu przez aplikację, co daje elastyczność kosztem przesunięcia błędów w czasie.
CAP theorem w skrócie
W obliczu podziału sieci (Partition) system rozproszony musi wybrać między pełną Spójnością (Consistency) a Dostępnością (Availability) dla części systemu, która utraciła łączność. Bazy relacyjne w typowej konfiguracji jednego węzła rzadko muszą na co dzień rozstrzygać ten kompromis, systemy NoSQL projektowane pod wiele regionów muszą to zrobić świadomie.
Dokument w bazie NoSQL: dane zamówienia zagnieżdżone w jednym rekordzie json
{
  "_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"
}
Pułapka
Wybór NoSQL 'pod skalę', zanim ktokolwiek zmierzył realne wąskie gardło dobrze zaindeksowanej bazy relacyjnej, to częsty przypadek przedwczesnej optymalizacji: pojedynczy dobrze skonfigurowany PostgreSQL obsługuje miliony wierszy i tysiące zapytań na sekundę, zanim w ogóle trzeba myśleć o shardingu.
  • 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ążeniaRekomendacjaPrzykład technologii
Transakcje finansowe z wieloma powiązanymi tabelamiSQLPostgreSQL, MySQL
Katalog produktów o zmiennej, niejednorodnej strukturzeNoSQL dokumentoweMongoDB
Sesje i cache o krótkim czasie życiaNoSQL klucz-wartośćRedis
Sieć powiązań (rekomendacje, graf społecznościowy)NoSQL grafoweNeo4j
Metryki i logi w czasieBaza szeregów czasowychTimescaleDB, InfluxDB
Fiszki - aktywne przypominanie

Sprawdź się - testowanie to nauka

24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.

Rozpocznij quiz
Zbudowane przez Tenzan Logic