KajoData / SQL
SELECT w SQL: kolumny, filtrowanie i kolejność wyniku
Zacznij od SELECT * i zbuduj wynik, który potrafisz sprawdzić. Pokazuję wybór kolumn, filtrowanie, braki danych i rozstrzyganie remisów przed LIMIT.

Pierwszy działający SELECT daje sporo satysfakcji. Wpisujesz kilka słów, naciskasz przycisk i nagle masz przed sobą dane. Potem przychodzi drugie pytanie: skąd wiadomo, że oglądasz właśnie te dane, których potrzebujesz? Na tym etapie zaczyna się przydatna nauka SQL.
Pokażę Ci, jak przejść od SELECT * FROM orders do krótkiego raportu, którego wynik da się sprawdzić ręcznie. Będziemy wybierać kolumny, filtrować zamówienia i porządkować wynik. Przy okazji wyjaśnię, dlaczego dwie osoby mogą uruchomić pozornie podobne zapytania i dostać inne odpowiedzi. Wystarczy drobna różnica w obsłudze braku albo remisu.
Wszystkie liczby pochodzą z syntetycznego przykładu. Kod jest przeznaczony do SQLite; bloki uruchamiaj kolejno w jednym pustym połączeniu. Dzięki małemu zbiorowi możesz samodzielnie policzyć oczekiwany wynik. Ten nawyk przyda Ci się później również przy tabeli, której nie da się już ogarnąć wzrokiem.
Co oznacza SELECT * FROM table
SELECT określa, co ma znaleźć się w wyniku, a FROM wskazuje źródło. Gwiazdka oznacza wszystkie dostępne kolumny tego źródła. W miejsce słowa table wpisujesz rzeczywistą nazwę tabeli. Sam wyraz jest tu tylko popularnym sposobem pokazywania składni, więc nie przepisuj go automatycznie do każdej bazy.
Wynik zapytania można sobie wyobrazić jako kolejną tabelę: ma nazwy kolumn i pewną liczbę wierszy. Może również nie zawierać żadnego wiersza. Pusty wynik bywa poprawną odpowiedzią, na przykład gdy żadne zamówienie nie spełnia warunku. Nie musi oznaczać błędu połączenia ani usunięcia danych.
Zwykły SELECT w naszych przykładach odczytuje dane i nie zmienia zapisanych zamówień. Kolejne zapytanie może wybrać inne kolumny, nie naruszając poprzedniego źródła. Tworzenie i wstawianie danych pojawi się tylko w przygotowaniu ćwiczenia. Rozdzielam te czynności, żeby od początku było jasne, które polecenia budują zbiór, a które go analizują.
Szczegóły składni i przetwarzania opisuje dokumentacja SELECT w SQLite. SQL ma wiele dialektów. Podstawowy pomysł jest wspólny, ale zapis limitu, dat albo funkcji tekstowych może się różnić. Gdy przykład z internetu nie działa, najpierw sprawdzam silnik i komunikat błędu, a dopiero później podejrzewam własne zdolności do analizy danych.
Przygotuj sześć zamówień
Użyję tabeli orders. Jeden jej wiersz oznacza jedno zamówienie, a order_id jest unikalnym identyfikatorem. Klient może zamawiać kilka razy. To ważna różnica: sześć zamówień nie musi oznaczać sześciu klientów. Zanim napiszę zapytanie, ustalam właśnie takie znaczenie jednego wiersza.
amount to umowna wartość punktowa zamówienia, bez waluty i bez rozliczeń finansowych. status opisuje stan, a ordered_at zawiera datę w jednolitym formacie rok-miesiąc-dzień. W tym ćwiczeniu wszystkie daty mają tę samą strukturę. Nie przenoś porównań tekstowych bez sprawdzenia na kolumnę z przypadkowymi formatami dat.
-- example: fixture
CREATE TEMP TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
status TEXT,
amount INTEGER,
ordered_at TEXT
);
INSERT INTO orders VALUES
(1, 10, 'paid', 120, '2026-01-03'),
(2, 10, 'cancelled', 80, '2026-01-04'),
(3, 20, 'paid', 200, '2026-01-04'),
(4, 30, 'pending', NULL, '2026-01-05'),
(5, 20, 'paid', 200, '2026-01-06'),
(6, 40, 'paid', 0, '2026-02-01');
TEMP tworzy tabelę tymczasową dla tego połączenia. Nie potrzebujesz pliku z danymi klienta ani produkcyjnej bazy. Wklejenie przygotowania drugi raz w tym samym połączeniu zgłosi, że tabela już istnieje. Najprościej rozpocząć wtedy nowe puste połączenie testowe i uruchomić ćwiczenie od początku.
Celowo zostawiłem kilka pułapek. Klienci 10 i 20 powtarzają się, dwie kwoty wynoszą 200, jedno zamówienie ma zero, a jedno nie ma podanej kwoty. Dzięki temu każde kolejne zapytanie sprawdza coś więcej niż szczęśliwy przypadek, w którym wszystkie dane są różne i kompletne.
Gwiazdka pomaga obejrzeć źródło
Na początek wyświetlę wszystkie kolumny. Dodaję sortowanie po identyfikatorze, żeby wynik ćwiczenia był jednoznaczny. Samo SELECT * FROM orders nie daje obietnicy kolejności wierszy. To, że baza przez kilka uruchomień pokazuje je tak samo, nie zastępuje jawnego warunku sortowania.
-- example: all-orders
SELECT *
FROM orders
ORDER BY order_id;
Zobaczysz sześć wierszy i pięć kolumn, zgodnie z przygotowaniem. W tym miejscu sprawdzam nazwy, przykładowe wartości i znaczenie braków. Jeżeli nazwa tabeli jest nieznana, warto wcześniej obejrzeć listę tabel udostępnioną przez używany program. Widoczność zależy od bazy i przyznanego dostępu.
Gwiazdka dobrze sprawdza się podczas pierwszego rozpoznania małej tabeli. W gotowym raporcie wolę wymienić kolumny. Odbiorca otrzymuje wtedy określony zestaw pól, a dodanie nowej kolumny do źródła nie poszerza automatycznie eksportu. Czytelnik kodu od razu widzi też, które dane były potrzebne do odpowiedzi.
Nie wynika z tego, że gwiazdka jest zawsze błędem albo że jej usunięcie automatycznie przyspieszy każde zapytanie. Koszt zależy od silnika, przechowywania i planu wykonania. Tutaj chodzi przede wszystkim o jasną umowę: jakie pola mają znaleźć się w wyniku oraz czy wszystkie są rzeczywiście potrzebne.
Wybierz kolumny i nadaj im nazwy
Teraz chcę zobaczyć identyfikator, klienta i kwotę. Kolejność kolumn w SELECT określa kolejność kolumn wyniku. Nie muszę zachowywać układu źródłowej tabeli. To prosty sposób przygotowania eksportu w formie, której oczekuje osoba korzystająca z raportu.
-- example: projection
SELECT order_id, customer_id, amount AS order_amount
FROM orders
ORDER BY order_id;
AS order_amount nadaje kolumnie alias w wyniku. Nie zmienia nazwy amount w tabeli. W kolejnym niezależnym zapytaniu nadal odwołasz się do źródłowej nazwy. Alias może opisywać obliczenie albo usuwać niejasność, ale warto unikać skrótów, których znaczenie pamięta tylko autor kodu.
Listę kolumn oddzielam przecinkami. Za ostatnią kolumną przed FROM nie ma przecinka. To drobiazg, ale właśnie takie drobiazgi potrafią zatrzymać pierwsze ćwiczenie na pół godziny. Przy błędzie składni sprawdź nie tylko wskazane słowo, lecz także fragment bezpośrednio przed nim.
Możesz również umieścić w SELECT wyrażenie, na przykład amount * 2 AS doubled_amount. Wynik obliczenia nie zapisuje się automatycznie do źródła. Dla brakującej kwoty pozostanie brak. Najpierw ustaliłbym jednak, po co w raporcie potrzebne jest podwojenie, bo poprawne mnożenie samo w sobie jeszcze nie tworzy sensownego wskaźnika.
WHERE wybiera wiersze
Pytanie brzmi teraz: które opłacone zamówienia mają co najmniej 150 punktów? Potrzebuję dwóch warunków połączonych przez AND. Pierwszy sprawdza status, drugi kwotę. Zanim uruchomię zapytanie, patrzę na dane: powinny zostać zamówienia 3 i 5. Oczekiwanie zapisane przed wynikiem jest bardzo dobrym testem.
-- example: filtered
SELECT order_id, amount
FROM orders
WHERE status = 'paid'
AND amount >= 150
ORDER BY order_id;
| order_id | amount |
|---|---|
| 3 | 200 |
| 5 | 200 |
Tekst paid zapisuję w pojedynczych cudzysłowach. Nazwy kolumn pozostają bez nich. Gdyby zamiast nazwy kolumny podać tekstowy literał, baza mogłaby porównywać stały napis z innym napisem. Zapytanie czasem nadal się wykona, tylko przestanie odpowiadać na planowane pytanie.
AND wymaga spełnienia obu warunków. OR pozwala spełnić przynajmniej jeden. Gdy mieszam je w dłuższym filtrze, stosuję nawiasy pokazujące zamierzony podział. Na przykład warunek dla opłaconych lub oczekujących zamówień, ograniczony dodatkowo do stycznia, powinien czytelnie wydzielać alternatywę statusów. Lepiej dopisać nawiasy niż później tłumaczyć nieoczekiwane wiersze.
Filtr nie usuwa odrzuconych zamówień z tabeli. Ogranicza wynik tego zapytania. Wciąż możesz uruchomić pierwsze zapytanie i zobaczyć wszystkie sześć pozycji. Przy nauce warto zrobić to samodzielnie, żeby oswoić różnicę między wyborem danych a ich trwałą zmianą.
NULL i zero odpowiadają na inne pytania
Brak kwoty oznaczyłem jako NULL. Zamówienie 4 istnieje, ale jego kwoty nie znamy. Zamówienie 6 ma znaną kwotę równą zero. Jeśli połączysz te przypadki bez ustalenia znaczenia biznesowego, raport może wyglądać porządniej, lecz straci informację o jakości źródła.
Do wyszukiwania braków używam IS NULL. Porównanie amount = NULL nie jest poprawnym sposobem zadania pytania o brak. Zwykłe porównania z NULL prowadzą do nieznanego wyniku, a WHERE zachowuje wiersze, dla których warunek jest prawdziwy. Reguły operatorów opisuje dokumentacja wyrażeń SQLite.
-- example: missing
SELECT order_id
FROM orders
WHERE amount IS NULL
ORDER BY order_id;
To zapytanie zwraca tylko identyfikator 4. Gdy zamienisz warunek na amount = 0, otrzymasz 6. Przy amount > 0 nie pojawi się ani 4, ani 6. Pierwszy odpada przez brak znanej kwoty, drugi przez niespełnienie progu. Ten sam brak w wyniku może więc mieć różne przyczyny.
W praktycznym raporcie często pokazuję osobno liczbę brakujących wartości. Dzięki temu odbiorca widzi, czy suma albo średnia opiera się na pełnym materiale. Automatyczna zamiana każdego braku na zero wymaga uzasadnienia. Sam fakt, że funkcja potrafi coś zastąpić, nie mówi jeszcze, czy powinna.

ORDER BY i LIMIT muszą współpracować
Załóżmy, że potrzebuję dwóch opłaconych zamówień o najwyższej kwocie. Sortuję malejąco po amount, a potem rosnąco po identyfikatorze. Drugie kryterium rozstrzyga remis między zamówieniami 3 i 5. Dzięki temu ustalam pełną kolejność, zanim ograniczę liczbę wierszy.
-- example: top-two
SELECT order_id, amount
FROM orders
WHERE status = 'paid'
ORDER BY amount DESC, order_id ASC
LIMIT 2;
Wynik to kolejno (3, 200) i (5, 200). Przy limicie jednego wiersza zostałoby zamówienie 3. Gdybym nie określił drugiego kryterium, samo sortowanie po kwocie nie wskazywałoby, które z dwóch równych zamówień ma pierwszeństwo. To szczególnie ważne przy porównywaniu kolejnych eksportów.
LIMIT 2 oznacza dwa wiersze, a nie dwie różne kwoty. Nie oznacza również wszystkich zamówień mieszczących się na dwóch pierwszych poziomach rankingu. Takie pytanie wymaga innej reguły, na przykład funkcji rankingowej. Najpierw nazwij dokładnie oczekiwany rezultat, potem wybierz składnię.
Limit małych wyników pomaga w eksploracji, ale nie gwarantuje małego kosztu całego zapytania. Baza może nadal potrzebować odczytać lub uporządkować wiele danych. Również limit widoczny w programie do obsługi bazy bywa czymś innym niż limit zapisany w SQL. Sprawdzam ustawienia konkretnego klienta i komunikaty o pobranych wierszach.
DISTINCT nie naprawia każdego powtórzenia
Klient 20 występuje w dwóch opłaconych zamówieniach. Jeżeli pytam o klientów, którzy dokonali opłaconego zakupu, potrzebuję listy różnych identyfikatorów. Wtedy świadomie stosuję DISTINCT do wybranej kolumny. Wynik ma inną szczegółowość niż tabela zamówień.
-- example: customers
SELECT DISTINCT customer_id
FROM orders
WHERE status = 'paid'
ORDER BY customer_id;
Otrzymuję klientów 10, 20 i 40. Dodanie order_id do listy kolumn zmieniłoby sens: unikalna stałaby się cała para klient-zamówienie, więc oba zakupy klienta 20 pozostałyby w wyniku. DISTINCT porównuje komplet wybranych wartości, a nie jedną domyślnie najważniejszą kolumnę.
Nie dodaję go odruchowo po każdym złączeniu, które pomnożyło wiersze. Powtórzenia mogą wynikać z prawidłowej relacji jeden-do-wielu albo z błędnego warunku łączenia. Usunięcie widocznych duplikatów nie wyjaśnia przyczyny. Zanim skrócisz tabelę, sprawdź, co reprezentuje każdy jej wiersz i czy nie tracisz legalnych zdarzeń.
Kolejność zapisu i myślenia o wyniku
W zapisie zaczynamy od SELECT, potem pojawia się FROM, ewentualne złączenia, WHERE, GROUP BY, HAVING, ORDER BY i limit właściwy dla dialektu. Nie każda klauzula jest obowiązkowa. Nasze przykłady korzystają tylko z części możliwości, co na początku zupełnie wystarcza.
Do rozumowania przydaje się inna kolejność: ustal źródło, wybierz wiersze, wykonaj potrzebne grupowanie i obliczenia, a następnie uporządkuj wynik. To model logiczny. Nie jest instrukcją odczytania rzeczywistego planu wykonania, bo optymalizator może przekształcić sposób realizacji zapytania, zachowując jego znaczenie.
Jeżeli dojdziesz do podsumowań, pamiętaj, że WHERE filtruje wiersze przed agregacją, a HAVING filtruje grupy. Zamiast wciskać wszystko do pierwszego zapytania, przejdź potem do filtrowania i grupowania w SQL. Dłuższe obliczenia możesz uporządkować za pomocą CTE.
Gdy zapytanie nie działa albo wynik zaskakuje
Najpierw odróżniam błąd wykonania od błędnej odpowiedzi. Komunikat o nieistniejącej tabeli oznacza inny problem niż poprawnie wyświetlona tabela z niewłaściwymi klientami. W pierwszej sytuacji sprawdzam połączenie, nazwę źródła i to, czy uruchomiłem przygotowanie danych. W drugiej wracam do znaczenia filtrów. Losowe dopisywanie kolejnych warunków zwykle utrudnia ustalenie przyczyny.
Jeśli program podkreśla nazwę kolumny, porównuję ją dokładnie z definicją tabeli. Literówka, inne podkreślenie albo odwołanie do aliasu w niewłaściwym miejscu wystarczą, żeby zapytanie przestało działać. W ćwiczeniu warto zacząć od pierwszego prostego odczytu. Gdy on działa, dokładam jedną zmianę i ponawiam sprawdzenie. Dzięki temu wiem, która zmiana wprowadziła problem.
Przy nieoczekiwanej liczbie wierszy na chwilę usuwam limit i oglądam identyfikatory. Być może wynik był poprawny, ale część rekordów nie mieściła się w podglądzie. Sprawdzam również, czy w edytorze zaznaczyłem całe zapytanie. Niektóre programy uruchamiają zaznaczony fragment, inne bieżące polecenie. To właściwość narzędzia, którą trzeba poznać osobno od składni języka.
Gdy filtr zwraca zero pozycji, sprawdzam wartości występujące w kolumnie. Tekst może zawierać dodatkową spację albo inną wielkość liter. Nie zakładam jednak automatycznie, że trzeba wszystko zamienić na małe litery. Reguły porównywania tekstu zależą od kolacji i silnika, a standaryzacja może zmienić znaczenie identyfikatora. Najpierw oglądam konkretny przykład, potem wybieram poprawkę.
Zapisuję też pytanie, na które miała odpowiadać kwerenda. „Pokaż najlepszych klientów” jest zbyt nieprecyzyjne, by ocenić wynik. „Pokaż identyfikatory klientów z opłaconym zamówieniem o wartości co najmniej 150 punktów w styczniu” daje już zestaw warunków do sprawdzenia. Taki opis pomaga zauważyć, że selekcja zamówień i selekcja klientów mogą wymagać innej listy kolumn.
Na koniec zapisuję działającą wersję razem z krótką informacją o danych wejściowych. Za tydzień łatwiej będzie ustalić, dlaczego oczekiwałem dwóch wierszy. Sam zrzut wyniku bez zapytania i przygotowania źródła nie pozwala odtworzyć obliczenia. Przy małym ćwiczeniu to drobna wygoda, przy regularnym raporcie to podstawa sensownego sprawdzania zmian.
Ćwiczenie: sprawdź odpowiedź przed uruchomieniem
Wybierz opłacone zamówienia ze stycznia, pokaż identyfikator i kwotę, a następnie posortuj je od najnowszej daty. W przypadku tej samej daty pierwszeństwo ma mniejszy identyfikator. Zanim napiszesz kod, zapisz na kartce przewidywaną kolejność i liczbę pozycji.
-- example: exercise
SELECT order_id, amount
FROM orders
WHERE status = 'paid'
AND ordered_at >= '2026-01-01'
AND ordered_at < '2026-02-01'
ORDER BY ordered_at DESC, order_id ASC;
Powinieneś otrzymać kolejno 5, 3 i 1, z kwotami 200, 200 i 120. Zamówienie 6 znajduje się dokładnie na górnej granicy i dlatego odpada. Przedział domknięty z lewej, otwarty z prawej dobrze pokazuje intencję: cały styczeń, bez pierwszego dnia lutego.
Na koniec zmień warunek statusu na wartość, której nie ma w danych. Pusty wynik jest wtedy oczekiwany. Wróć do pierwotnego filtra i porównaj efekt usunięcia sortowania. Nie oceniaj poprawności tylko po tym, czy silnik zgłosił błąd. Sprawdź kolumny, liczbę wierszy, konkretne identyfikatory, braki i granice. Tak buduję zaufanie do zapytania, zanim zamieni się w cykliczny raport.
Najważniejsze w pigułce
Co oznacza SELECT * FROM table?
Gwiazdka wybiera wszystkie kolumny wskazanego źródła. W miejsce table wpisujesz nazwę tabeli. Bez ORDER BY zapytanie nie gwarantuje kolejności wierszy.
Czy WHERE usuwa dane z tabeli?
Nie. WHERE w zwykłym SELECT ogranicza wynik odczytu. Odrzucone wiersze pozostają w tabeli i mogą zostać odczytane innym zapytaniem.
Jak znaleźć brakujące wartości w SQL?
Użyj IS NULL. Zero i pusty tekst są w SQLite wartościami innymi niż NULL, a porównanie kolumny przez = NULL nie jest testem braku.
