SQL i bazy danych
Fundamenty SQL dla inżynierów: JOIN-y, indeksy, transakcje ACID, poziomy izolacji, normalizacja, N+1, SQL injection oraz wybór SQL czy NoSQL.
SQL i bazy danych
Fundamenty SQL dla inżynierów: JOIN-y, indeksy, transakcje ACID, poziomy izolacji, normalizacja, N+1, SQL injection oraz wybór SQL czy NoSQL.
JOIN-y: kiedy który jest poprawny
na egzaminieJOIN łączy wiersze z wielu tabel na podstawie warunku dopasowania kluczy. Zły typ JOIN nie jest błędem składniowym. Silnik wykona zapytanie, ale wynik będzie cicho niepoprawny: zgubione wiersze albo utrata danych, bez komunikatu błędu.
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.
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.
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.
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.
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.
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.
-- 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; 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 dopasowania musi być w ON.
| 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 |
LEFT JOIN zachowuje LEWĄ stronę w całości, jak mówi nazwa. 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.
Indeksy: przyspieszenie odczytu kontra koszt zapisu
na egzaminieIndeks 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.
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.
indeks pokrywający (covering index): Indeks zawierający wszystkie kolumny potrzebne do wykonania zapytania. Silnik w ogóle nie sięga wtedy do tabeli głównej (Index Only Scan w PostgreSQL, 'Using index' w EXPLAIN MySQL).
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). Nie przyspiesza zapytania filtrującego tylko po b lub c, bo B-drzewo jest posortowane od lewej kolumny.
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 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. Dlatego i tak wybierze Seq Scan, bo odczyt przez indeks byłby wolniejszy od skanu sekwencyjnego.
LIKE '%fraza%' z wiodącym znakiem wieloznacznym nie może wykorzystać zwykłego indeksu B-drzewa. Sortowanie po prefiksie nic nie daje, gdy szukana fraza jest w środku stringa. Potrzebny jest indeks trigramowy (pg_trgm) albo pełnotekstowy.
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.
| 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 |
- Sprawdź plan zapytania przez EXPLAIN ANALYZE Zobacz, czy planner faktycznie wybiera Index Scan zamiast Seq Scan.
- Zmierz selektywność kolumny Ile unikalnych wartości względem liczby wierszy w tabeli.
- Potwierdź obecność kolumny w WHERE, JOIN lub ORDER BY Indeks pomaga tylko tam, gdzie jest faktycznie używany do filtrowania lub sortowania.
- Przetestuj na środowisku testowym Porównaj czas wykonania zapytania przed i po dodaniu indeksu na realistycznym wolumenie danych.
- Zmierz wpływ na zapisy Sprawdź czas INSERT/UPDATE na tabeli o dużym ruchu zapisu przed i po dodaniu indeksu.
Transakcje i właściwości ACID
na egzaminieTransakcja 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.
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.
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.
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ą.
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.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; Seria niezależnych zapytań bez jawnej transakcji jest tak bezpieczna, jak najsłabsze ogniwo. Awaria połączenia po pierwszym UPDATE, a przed drugim, zostawia bazę w niespójnym stanie. Gwarancje ACID tego nie naprawią, bo zapytania nie były jedną transakcją.
PostgreSQL, MySQL/InnoDB i większość silników relacyjnych zapisują zmianę najpierw sekwencyjnie do dziennika WAL, a później do stron danych. Zapis sekwencyjny do jednego pliku jest szybszy niż losowy zapis po całej tabeli. Po awarii dziennik pozwala odtworzyć zmiany zatwierdzone i niezatwierdzone.
- 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).
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.
Poziomy izolacji i race condition, które kontrolują
na egzaminiePeł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ść.
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ć.
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.
phantom read (odczyt widmo): Ta sama transakcja wykonuje dwukrotnie ten sam warunek WHERE. Za drugim razem widzi nowe wiersze albo brak wcześniej widzianych, bo inna transakcja wstawiła lub usunęła pasujące rekordy.
| 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 |
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.
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; 'Repeatable Read' w MySQL i w standardzie SQL to nie to samo. Standard dopuszcza phantom read, a InnoDB w praktyce blokuje go w większości przypadków. Zakładanie identycznego zachowania silników bez sprawdzenia dokumentacji to częste źródło buga widocznego 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.
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.
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.
1NF: Każda kolumna przechowuje pojedynczą, atomową wartość, brak list czy zagnieżdżonych struktur w jednej komórce.
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).
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.
-- 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; 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.
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.
| 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 |
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'.
Problem N+1 i jak generują go ORM-y
na egzaminieN+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.
ORM-y domyślnie ładują relacje leniwie (lazy loading). Odwołanie do powiązanego obiektu w pętli wyzwala osobne zapytanie SQL dopiero wtedy, a nie z góry. Na małym zbiorze danych różnicy nie widać. Na produkcji, przy tysiącach rekordów, to sekundy albo minuty odpowiedzi.
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.
lazy loading (ładowanie leniwe): Powiązane dane są pobierane dopiero przy pierwszym odwołaniu do nich w kodzie. Jest to wygodne, ale domyślnie generuje N+1 przy iteracji po liście.
# 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ń 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 listy | Zapytań przy lazy loading | Zapytań przy eager loading |
|---|---|---|
| 10 | 11 | 1 |
| 1 000 | 1 001 | 1 |
| 100 000 | 100 001 | 1 |
- Włącz logowanie wykonywanych zapytań SQL W środowisku deweloperskim, żeby zobaczyć realną liczbę zapytań na request.
- Policz zapytania na request Django Debug Toolbar, gem Bullet w Rails, albo APM typu New Relic/Datadog.
- Testuj na realistycznym wolumenie danych Nie na trzech rekordach, N+1 jest niewidoczny przy małym zbiorze.
- Ustaw alert na liczbę zapytań per request W monitoringu produkcyjnym, żeby wykryć regresję zanim zgłoszą ją użytkownicy.
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.
SQL injection i parametryzacja zapytań
na egzaminieSQL injection powstaje, gdy dane od użytkownika trafiają bezpośrednio do treści zapytania SQL i zmieniają jego strukturę, a nie tylko wartość. Jedyna realna obrona to parametryzacja: oddzielenie kodu zapytania od danych na poziomie sterownika bazy, a nie ręczne escapowanie stringów.
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.
# 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,)
) 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.
Escapowanie cudzysłowów czy ręczna 'sanityzacja' stringa nie jest równoważna parametryzacji. Łatwo pominąć edge case (kodowanie znaków, kontekst zagnieżdżony). Wystarczy jedno przeoczone miejsce, żeby atak przeszedł.
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.
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.
| 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 |
OWASP od lat wskazuje parametryzację zapytań (prepared statements) jako podstawowy, obowiązkowy mechanizm obrony przed SQL injection. Dotyczy to wszystkich języków i sterowników baz danych. OWASP stawia ją przed escapowaniem i przed walidacją wejścia.
SQL vs NoSQL: kryteria decyzji
Wybór między bazą relacyjną a NoSQL to decyzja, gdzie ponieść koszt. Albo przy zapisie (silny schemat, spójność, JOIN-y), albo 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
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.
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.
Przy podziale sieci (Partition) system rozproszony musi wybrać między pełną Spójnością (Consistency) a Dostępnością (Availability) dla części, która utraciła łączność. Bazy relacyjne na jednym węźle rzadko muszą rozstrzygać ten kompromis. Systemy NoSQL na wiele regionów muszą to zrobić świadomie.
{
"_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"
} Wybór NoSQL 'pod skalę', zanim ktokolwiek zmierzył realne wąskie gardło dobrze zaindeksowanej bazy relacyjnej, to częsty przypadek przedwczesnej optymalizacji. Jeden 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ąż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ę
20 z 24 pytań, losowo, z wyjaśnieniem po każdym. Wynik zostaje na tym urządzeniu.
Rozpocznij quiz