CodeGym /Kursy /Spring Data JPA /Minimum SQL do odczytywania danych

Minimum SQL do odczytywania danych

Spring Data JPA
Poziom 2, Lekcja 3
Dostępny

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
SELECT ...
wybiera kolumny/wyrażenia do wyniku „Co chcę widzieć w odpowiedzi?"
FROM ...
wskazuje źródło danych „Skąd to czytam?"
WHERE ...
filtruje wiersze „Które wiersze pasują?"
ORDER BY ...
ustala porządek wyniku „W jakiej kolejności pokazywać?"
LIMIT ... OFFSET ...
bierze „kawałek" listy „Ile wierszy wziąć i od którego zacząć?"
JOIN ... ON ...
łą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ę
x = 10
nie równa się
x <> 10
większe/mniejsze
x >= 10, x < 10
kilka warunków
a AND b, a OR b
„zawiera tekst"
name LIKE '%abc%'
puste/niepuste
col IS NULL, col IS NOT NULL

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
0
2
20
3
40
4
60

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.

1
Zadanie
Spring Data JPA, poziom 2, lekcja 3
Niedostępne
Filtrowanie, sortowanie i paginacja produktów
Filtrowanie, sortowanie i paginacja produktów
1
Zadanie
Spring Data JPA, poziom 2, lekcja 3
Niedostępne
Skład jednego zamówienia przez JOIN
Skład jednego zamówienia przez JOIN
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION