1. Minimum SQL do odczytywania danych
Gdy zaczynasz pisać backend, bardzo szybko okazuje się: większość czasu nie „zapisujesz danych", lecz je odczytujesz. Pokazujesz katalog, szukasz produktów, budujesz listę zamówień, pobierasz szczegóły jednego zamówienia. Do tego nie potrzebujesz „kursu o optymalizatorze PostgreSQL", ale potrzebujesz stabilnego podstawowego słownictwa: SELECT, filtry, sortowanie, ograniczenie wyniku i umiejętność sklejenia danych z kilku tabel.
SQL — to język deklaratywny: opisujesz co chcesz dostać, a nie jak dokładnie baza ma to obliczyć. To zarazem wygodne i podstępne. Wygodne — bo zapytanie czyta się jak pytanie. Podstępne — bo baza nie musi zwracać wierszy w jakimś „naturalnym porządku", jeśli tego porządku nie ustaliłeś. I tu zaczynają się „a dlaczego mi skacze lista?" i „a dlaczego paginacja zwraca duplikaty?".
Żeby łatwiej trzymać obraz w głowie, od razu ustalmy mini-ściągawkę — bez fanatyzmu:
| Część zapytania | Co robi | Ludzkie pytanie |
|---|---|---|
|
wybiera kolumny/wyrażenia do wyniku | „Co chcę widzieć w odpowiedzi?" |
|
wskazuje źródło danych | „Skąd to czytam?" |
|
filtruje wiersze | „Które wiersze pasują?" |
|
ustala porządek wyniku | „W jakiej kolejności pokazywać?" |
|
bierze „kawałek" listy | „Ile wierszy wziąć i od którego zacząć?" |
|
łączy tabele | „Jak złożyć powiązaną odpowiedź z kilku tabel?" |
Dalej omówimy to wszystko po kolei i za każdym razem będziemy odnosić do domeny mini-shop: kategorie, produkty, stany magazynowe i pozycje zamówień.
2. SELECT i FROM: podstawowy wybór
Gdy po raz pierwszy widzisz SQL, wydaje się, że SELECT — to po prostu „polecenie pobrania danych", a FROM — „skąd pobrać". To prawda, ale warto dodać jeszcze jedną myśl: SELECT ustala formę odpowiedzi, czyli które dokładnie pola trafią do wyniku. I tu u początkujących niemal zawsze pojawia się pokusa napisania SELECT *, bo „tak prościej". Rzeczywiście prościej… dopóki masz jedną kolumnę i żadnych kolegów. Potem zaczyna się cyrk.
Wyobraź sobie, że SELECT * — to jak przyjść do kawiarni i powiedzieć: „Proszę mi przynieść *całe menu*. Potem wybiorę". Kelner oczywiście się zdziwi, ale przyniesie. A potem będziesz za to płacić — czasem dosłownie, bo zbędny odczyt danych oznacza zbędną pracę.
Dla przykładów przyjmiemy, że mamy tabelę product z kolumnami jak id, sku, name, price, status, category_id. To ten sam schemat edukacyjny mini-shop: kategorie, produkty, stany magazynowe i zamówienia już rozłożone w osobnych tabelach, po prostu teraz bierzemy z nich tylko te kawałki, które są potrzebne do odczytu danych. Gdy niżej pojawią się category, stock_item czy order_item, to nie nowe abstrakcyjne tabele, lecz sąsiednie części tego samego schematu.
Mini-przykład 1: najbardziej „leniwy" wariant (do nauki — można, jako nawyk — lepiej nie):
-- Wariant edukacyjny: pobieramy wszystkie kolumny z tabeli
-- W prawdziwym kodzie lepiej jawnie wymieniać potrzebne pola
SELECT *
FROM product;
Dlaczego to jest niebezpieczne jako styl? Bo wynik zależy od struktury tabeli. Dodano kolumnę — zmienił się wynik. W prawdziwym kodzie często zamienia się to w „coś się zepsuło, bo ktoś dodał kolumnę". Brzmi absurdalnie, ale to rzeczywistość.
Mini-przykład 2: wybieramy tylko potrzebne kolumny (to już bardziej dojrzałe podejście):
-- Jawnie wymieniamy kolumny, które naprawdę są potrzebne odpowiedzi
SELECT id, sku, name, price
FROM product;
Mini-przykład 3: aliasy kolumn — przydatne, gdy składasz „witrynę" i chcesz zrozumiałe nazwy:
-- Zmieniamy nazwy pól w wyniku, żeby odpowiedź była samodokumentująca
SELECT
sku AS product_sku,
name AS product_name,
price AS product_price
FROM product;
Mini-przykład 4: wyrażenia w SELECT (SQL umie nie tylko „dostarczać", ale i obliczać proste rzeczy). Na przykład pokazać cenę z 10% rabatem:
-- Dodajemy obliczone pole do wyniku (to nie zmienia danych w tabeli)
SELECT
sku,
price,
price * 0.90 AS discounted_price
FROM product;
Ważne tu zauważyć: SELECT nie zmienia danych w tabeli. To „wirtualny wynik", jakbyś narysował nową tabelkę nad starą.
Mini-przykład 5: aliasy tabel (przy pojedynczej tabeli nie są konieczne, ale przy JOIN — niemal obowiązkowe). Nawet jeśli teraz wydaje się to zbędne, lepiej przyzwyczajać się do stylu:
-- Alias tabeli wygodny przy JOIN i pomaga unikać niejednoznaczności nazw kolumn
SELECT p.id, p.sku, p.name
FROM product p;
Czyta się to tak: „Wybierz pola z product, którą będziemy nazywać p". W kolejnych sekcjach zaoszczędzi nam to nerwów i ochroni przed „ambiguous column".
3. WHERE: filtrowanie wierszy
Jeśli SELECT odpowiada na pytanie „które kolumny pokazać", to WHERE odpowiada na pytanie „które wiersze w ogóle pasują". To moment, gdy SQL zaczyna przypominać filtry w sklepie internetowym: „pokaż tylko aktywne produkty", „cena od X do Y", „kategoria taka i taka".
Najważniejsze, co musi poczuć początkujący: WHERE — to nie schemat „najpierw odczytamy wszystko, a potem przefiltrujemy, jak w kodzie Java przez if". W SQL od razu opisujesz warunek, i baza zwraca już przefiltrowany wynik. Dlatego właściwym nawykiem jest formułowanie warunków po stronie bazy, a nie ciąganie wszystkiego do aplikacji na zasadzie „weźmiemy wszystko i tam się zorientujemy".
Na początek wystarczy najprostszych operatorów: =, <> (nie równa się), >, >=, <, <=, plus logika AND/OR. I osobno ważny temat — NULL, ale do niego podejdziemy ostrożnie.
Mini-przykład 1: filtrowanie produktów po cenie:
-- Filtrujemy wiersze po wartości w kolumnie price
SELECT id, sku, name, price
FROM product
WHERE price >= 100;
Mini-przykład 2: filtrowanie po statusie (niech status jest przechowywany jako string):
-- Zostawiamy tylko produkty o konkretnym statusie
SELECT sku, name, status
FROM product
WHERE status = 'ACTIVE';
Mini-przykład 3: kilka warunków (czyta się jako „i to, i to"):
-- AND oznacza, że oba warunki muszą być prawdziwe
SELECT sku, name, price, category_id
FROM product
WHERE category_id = 10
AND price < 500;
Mini-przykład 4: OR i nawiasy. Bez nawiasów łatwo dostać „nie to":
-- Nawiasy ustalają "ludzki" sens wyrażenia z AND/OR
SELECT sku, name, status, price
FROM product
WHERE (status = 'ACTIVE' OR status = 'DRAFT')
AND price >= 100;
Jeśli nawiasy usuniesz, sens może się zmienić. SQL, podobnie jak matematyka, nie zgaduje twojej myśli — respektuje reguły priorytetu operatorów. A twoja myśl potem się obraża.
Mini-przykład 5: wyszukiwanie po fragmencie tekstu przez LIKE. To podstawowa rzecz do „wyszukiwania po nazwie" na poziomie prostego SQL:
-- LIKE + % daje proste wyszukiwanie "podciąg gdzieś wewnątrz"
SELECT sku, name
FROM product
WHERE name LIKE '%Lego%';
% oznacza „dowolna liczba znaków". Ważne: wielkość liter i szczegóły porównania zależą od DBMS i collation. Na razie wystarczy idea: LIKE — proste narzędzie do „zawiera podciąg".
Mini-przykład 6: ostrożnie z NULL. W SQL NULL — to „nieznane" lub „brak wartości", i nie można go porównywać przez =. Potrzebne IS NULL / IS NOT NULL:
-- NULL sprawdzamy przez IS NULL / IS NOT NULL, a nie przez =
SELECT id, sku, name
FROM product
WHERE category_id IS NULL;
W naszym projekcie category_id będzie obowiązkowy, więc to zapytanie jest raczej edukacyjne. Ale błąd „sprawdzam = NULL" jest tak popularny, że lepiej zobaczyć go teraz niż potem pół godziny szukać wzrokiem.
Jeśli potrzebujesz „ściągawki z warunkami", oto kompaktowa tabela. I to prawie wszystko, czego teraz potrzebujesz:
| Czego chcemy | Jak piszemy w WHERE |
|---|---|
| równa się | |
| nie równa się | |
| większe/mniejsze | |
| kilka warunków | |
| „zawiera tekst" | |
| puste/niepuste | |
4. ORDER BY: ustalamy porządek
Bardzo ludzkie oczekiwanie początkującego brzmi tak: „Przecież dodawałem wiersze po kolei, więc baza też zwróci je po kolei". Niestety (lub na szczęście), baza danych nie musi spełniać naszych oczekiwań. Tabela — to nie lista, a zbiór wierszy, i bez ORDER BY kolejność wyniku nie jest gwarantowana. Czasem będzie ci się wydawać, że jest „stabilna", ale to tylko szczęście, a nie umowa.
ORDER BY — to miejsce, gdzie ustalasz kontrakt: „pokaż mi wynik po cenie malejąco" lub „po nazwie rosnąco". A jeszcze ORDER BY staje się krytycznie ważny, gdy chcemy pobierać wynik porcjami przez LIMIT/OFFSET (następna sekcja). Bez porządku porcje będą „pływały".
Mini-przykład 1: sortowanie produktów po cenie, najdroższe na górze:
-- DESC sortuje malejąco (droższy -> tańszy)
SELECT sku, name, price
FROM product
ORDER BY price DESC;
DESC — descending, malejąco. Jeśli napisać ASC (ascending) lub nic, zazwyczaj zakłada się rosnąco.
Mini-przykład 2: sortowanie po kilku polach. Na przykład najpierw po statusie, potem po nazwie:
-- Sortowanie po kilku kolumnach stosowane jest od lewej do prawej
SELECT sku, name, status
FROM product
ORDER BY status ASC, name ASC;
Czyta się to tak: „najpierw pogrupuj po statusach (w porządku alfabetycznym), a wewnątrz każdego statusu posortuj po nazwie".
Mini-przykład 3: sortowanie + filtrowanie — najczęstszy „prawdziwy" scenariusz:
-- Najpierw zawężamy wynik (WHERE), potem ustalamy porządek (ORDER BY)
SELECT sku, name, price
FROM product
WHERE status = 'ACTIVE'
ORDER BY name;
Tu najpierw zawężamy wynik, a potem nadajemy mu zrozumiały porządek. I tak, choć w tekście zapytania WHERE stoi przed ORDER BY, lepiej myśleć nie „jak napisano", lecz „co robi": baza najpierw tworzy zestaw pasujących wierszy, a potem go sortuje.
5. LIMIT i OFFSET: paginacja
W prawdziwej aplikacji niemal nigdy nie można „uczciwie zwrócić wszystkich produktów". Katalog może być ogromny, i nawet jeśli nie jest, użytkownik nie ma ochoty patrzeć na 50 tysięcy wierszy. Dlatego pobieramy wynik porcjami. Minimum SQL do tego — LIMIT (ile wierszy wziąć) i OFFSET (ile wierszy pominąć).
Ale tu jest jedna zasada, którą warto wykuć w kamieniu: LIMIT/OFFSET niemal zawsze powinny iść razem z ORDER BY. Bez porządku bierzesz „przypadkowe" 20 wierszy, potem „następne przypadkowe" 20, i to już nie wygląda jak normalna paginacja — to bardziej generator niespodzianek.
Mini-przykład 1: „pierwsze 20 produktów" (w zrozumiałym porządku):
-- Bierzemy pierwsze 20 wierszy w stabilnym porządku (ORDER BY obowiązkowy przy paginacji)
SELECT id, sku, name
FROM product
ORDER BY id
LIMIT 20;
Mini-przykład 2: „druga strona po 20 produktów" (czyli pomiń 20 i weź kolejne 20):
-- OFFSET pomija wskazaną liczbę wierszy, LIMIT ogranicza rozmiar strony
SELECT id, sku, name
FROM product
ORDER BY id
LIMIT 20 OFFSET 20;
Żeby się nie mylić w głowie, przydatne jest zapamiętanie prostej formuły:
offset = (numer_strony - 1) * rozmiar_strony
Jeśli strony liczone są od 1. Jeśli od 0 — formuła jest jeszcze prostsza: offset = page * size. Sam pomysł jest ważniejszy niż konkretna numeracja.
Oto mała tabela dla rozmiaru strony 20 (strony 1-based):
| Strona | OFFSET |
|---|---|
| 1 | |
| 2 | |
| 3 | |
| 4 | |
Mini-przykład 3: „pokaż 10 najdroższych aktywnych produktów":
-- Łączymy filtr, sortowanie i ograniczenie wyniku
SELECT sku, name, price
FROM product
WHERE status = 'ACTIVE'
ORDER BY price DESC
LIMIT 10;
To już prawie prawdziwy scenariusz: „top 10" lub „witryna".
I jeszcze jeden bardzo ważny szczegół z praktyki: LIMIT/OFFSET działają na wynik po sortowaniu. Czyli najpierw posortowałeś, a potem już „odciąłeś kawałek". Dlatego porządek — kluczowy element stabilności.
6. JOIN: powiązane dane
Gdyby projekt składał się z jednej tabeli everything, JOIN nie byłby potrzebny. Ale już omówiliśmy normalizację: kategorie żyją osobno, produkty osobno, zamówienia osobno, pozycje zamówień osobno. I to dobrze. Cena za to — konieczność czasem składania „obrazu" z kilku tabel. Tu właśnie pojawia się JOIN.
JOIN można postrzegać jako „sklejanie wierszy" z dwóch tabel po warunku ON. Zazwyczaj ON łączy klucz obcy i klucz główny: np. product.category_id wskazuje na category.id. Wtedy JOIN pozwala odczytać produkt i jego kategorię w jednym wyniku, nie kopiując category.name do każdego wiersza produktu.
Na początku będziemy używać zwykłego JOIN (w większości DBMS to inner join). O różnych rodzajach złączeń porozmawiamy później, a teraz potrzebujemy podstawowej mechaniki.
Mini-przykład 1: pokazanie produktów razem z nazwą kategorii:
-- JOIN łączy tabele po warunku ON (zazwyczaj FK -> PK)
SELECT
p.sku,
p.name,
c.name AS category_name
FROM product p
JOIN category c ON c.id = p.category_id
ORDER BY p.name;
Tu od razu kilka ważnych umiejętności:
Po pierwsze, p i c — aliasy tabel. Po drugie, pola precyzujemy jako p.name i c.name, inaczej nie będzie wiadomo, o które dokładnie name chodzi (w obu tabelach może być kolumna name). Po trzecie, w wyniku dajemy zrozumiały alias category_name.
Mini-przykład 2: pokazanie składu konkretnego zamówienia (zamówienie + pozycje + produkt). Przyjmijmy, że są tabele customer_order, order_item, product:
-- Składamy "witrynę zamówienia": zamówienie + pozycje + dane produktów
SELECT
o.order_number,
p.sku,
p.name AS product_name,
oi.quantity,
oi.line_total
FROM customer_order o
JOIN order_item oi ON oi.order_id = o.id
JOIN product p ON p.id = oi.product_id
WHERE o.order_number = 'ORD-1001'
ORDER BY p.name;
To już całkiem „prawdziwe" zapytanie: użytkownik chce zobaczyć zamówienie, i potrzebuje nie tylko product_id, lecz nazwę produktu, ilość i sumę pozycji.
Mini-przykład 3: pokazanie stanów magazynowych produktów. Przyjmijmy, że są stock_item (stany) i product (katalog):
-- Wyświetlamy stany razem ze zrozumiałymi polami produktu (sku, name)
SELECT
p.sku,
p.name,
si.available_quantity,
si.reserved_quantity
FROM stock_item si
JOIN product p ON p.id = si.product_id
ORDER BY si.available_quantity ASC;
W wyniku dostajemy „tabelkę dla panelu admina": które produkty się kończą.
Mini-przykład 4: to samo zapytanie, ale tylko dla „mało zostało" (umownie mniej niż 5):
-- Dodajemy filtr "mało zostało" i ograniczamy wynik dla panelu admina
SELECT
p.sku,
p.name,
si.available_quantity
FROM stock_item si
JOIN product p ON p.id = si.product_id
WHERE si.available_quantity < 5
ORDER BY si.available_quantity ASC
LIMIT 20;
Zwróć uwagę, jak ładnie układają się kawałki zapytania: JOIN daje powiązany zestaw danych, WHERE filtruje, ORDER BY sprawia, że porządek jest zrozumiały, a LIMIT nie pozwala przypadkowo zwrócić „całego magazynu".
7. Mentalny model wykonania zapytania
Na początku SQL często jest mylący: zapytanie napisane w jednej kolejności, a logicznie działa jak przenośnik. Jeśli trzymać ten przenośnik w głowie, dużo rzadziej piszesz dziwne zapytania i dużo szybciej czytasz cudze. Ustalmy prosty, edukacyjny, ale użyteczny model: najpierw określamy źródła (FROM + JOIN), potem filtrujemy (WHERE), potem formujemy odpowiedź (SELECT), potem sortujemy (ORDER BY), i dopiero potem przycinamy wynik (LIMIT/OFFSET).
Oto schemat wizualny:
flowchart TD
A["FROM + JOIN
zebraliśmy źródło wierszy"] --> B["WHERE
odfiltrowaliśmy wiersze"]
B --> C["SELECT
wybraliśmy kolumny/wyrażenia"]
C --> D["ORDER BY
ustaliliśmy porządek"]
D --> E["LIMIT/OFFSET
wzięliśmy porcję wyniku"]
Teraz weźmy jedno zapytanie i „przeczytajmy" je oczami przez ten przenośnik:
-- Przykład zapytania "witrynowego": produkt + kategoria + filtr + sortowanie + paginacja
SELECT
p.sku,
p.name,
c.name AS category_name,
p.price
FROM product p
JOIN category c ON c.id = p.category_id
WHERE p.status = 'ACTIVE'
ORDER BY p.price DESC
LIMIT 10 OFFSET 0;
Logika jest prosta: najpierw „skleiliśmy" produkty z kategoriami, potem zostawiliśmy tylko aktywne, potem wybraliśmy potrzebne pola, następnie posortowaliśmy po cenie i dopiero potem wzięliśmy pierwsze 10. To nie magia — to czytelny przenośnik.
I dlatego porządek jest ważny: jeśli usunąć ORDER BY, LIMIT 10 zamieni się w „daj 10 przypadkowych aktywnych produktów". Czasem będziesz miał szczęście i będą „podobne do prawidłowych". Ale to już obszar szamanizmu, a nasz cel — podejście inżynierskie.
8. Typowe błędy przy SELECT/JOIN
Błędy w SQL rzadko wyglądają jak „czerwona linia kompilatora na cały ekran". Częściej wyglądają jak „zapytanie działa, ale zwraca coś dziwnego", a to jest nawet groźniejsze. Dlatego lepiej zawczasu przyzwyczaić się do kilku typowych pułapek i rozpoznawać je po sposobie chodzenia.
Błąd nr 1: SELECT * jako styl życia.
Podczas nauki SELECT * pomaga „zobaczyć tabelę". Ale jeśli zaczniesz tak pisać zapytania dla konkretnej odpowiedzi, to czynisz wynik kruchym i ciężkim. Przy dodaniu nowej kolumny „nagle" zmienia się wynik i ilość danych, a ty nawet nie zauważasz, że zacząłeś ciągać zbędne rzeczy. Nawyk jawnego wymieniania pól sprawia, że zapytania są bardziej zrozumiałe i stabilne.
Błąd nr 2: LIMIT/OFFSET bez ORDER BY.
Takie zapytanie często „działa" i tworzy iluzję poprawnej paginacji. A potem nagle widzisz duplikaty na różnych stronach lub pominięcia. Powód prosty: bez ORDER BY baza nie obiecuje porządku. Dlatego LIMIT zamienia się w „pierwsze N jak wyjdzie".
Błąd nr 3: warunki logiczne bez nawiasów (zwłaszcza z AND/OR).
Gdy piszesz A OR B AND C, SQL nie czyta tego „po ludzku". Czyta po regułach priorytetu, i wynik może różnić się od twojej myśli. Jeśli w warunku mieszają się AND i OR, lepiej raz postawić nawiasy i nie próbować wygrać konkursu „najbardziej oszczędny na znaki".
Błąd nr 4: nieprawidłowy warunek JOIN (łączymy nie te kolumny).
Najpopularniejszy scenariusz: ktoś widzi dwa pola id i łączy product.id = category.id, bo „no przecież id". A potem dostaje dziwny wynik albo w ogóle nic. Łączyć trzeba dokładnie klucz obcy i to, na co wskazuje: product.category_id = category.id. Jeśli trzymać w głowie powiązania (FK → PK), JOIN staje się mechaniczny i przewidywalny.
Błąd nr 5: „ambiguous column" i zamieszanie nazw przy JOIN.
Gdy łączysz tabele, nazwy kolumn często się powtarzają: id, name, created_at. Dlatego nawyk pisania SELECT name bez prefiksu szybko prowadzi do błędu lub nieoczywistości. Aliasy tabel (p, c, oi) i jawne p.name, c.name — to nie biurokracja, a lekarstwo na chaos.
GO TO FULL VERSION