KajoData / SQL
SELECT w SQL: kolumny, obliczenia i czytelny wynik
Pokazuję, jak z czterech zamówień wybrać potrzebne kolumny i obliczyć kwoty. Wyjaśniam też duplikaty, aliasy oraz kolejność wierszy.

Piszesz SELECT *, dostajesz tabelę i przez chwilę wszystko wygląda dobrze. Potem ktoś pyta, co oznacza jeden wiersz, dlaczego kwota pojawia się dwa razy i czy pierwsze zamówienie jest najstarsze. Samo pobranie danych jeszcze na te pytania nie odpowiada. Ja zaczynam naukę SELECT od tego, jaki wynik chcę komuś przekazać.
Pokażę to na kilku syntetycznych zamówieniach. Wszystkie przykłady dotyczą PostgreSQL 18. Nie potrzebujesz dostępu do firmowej bazy ani dużego zbioru. Potrzebujesz umieć przeczytać wynik i wyjaśnić, skąd wzięła się każda kolumna. To bardzo dobry początek pracy z SQL.
SELECT wybiera postać wyniku
Instrukcja SELECT pobiera dane, a jej lista wyrażeń określa kolumny wyniku. Mogą to być kolumny źródła, obliczenia albo stałe. FROM wskazuje źródło. Tak rozdziela te role dokumentacja listy SELECT.
Dla mnie praktyczne pytanie brzmi: czy odbiorca potrzebuje wszystkich szczegółów zamówienia, czy tylko identyfikatora i kwoty? Już ten wybór wpływa na czytelność raportu. Jeśli pokażę piętnaście technicznych pól, użytkownik będzie musiał sam odgadnąć, które są istotne.
SELECT nie musi oznaczać jednej fizycznej tabeli. Źródłem może być także widok, podzapytanie albo wynik połączenia. Na początek zostanę przy jednym źródle, żeby nie mieszać wyboru kolumn z łączeniem danych. Do JOIN warto przejść wtedy, gdy umiesz powiedzieć, czym jest pojedynczy rekord wejścia.
Małe dane, które można przeczytać w całości
Załóżmy, że źródło orders wygląda tak. Kwoty są umownymi jednostkami, a wszystkie rekordy powstały wyłącznie do tego przykładu.
| order_id | customer_name | city |
|---|---|---|
| 1 | Ada | Krakow |
| 2 | Ben | Gdansk |
| 3 | Ada | Krakow |
| 4 | Celina | Krakow |
Ilość i cena tych samych zamówień są poniżej. Identyfikator łączy obie części prezentacji; źródło nadal ma cztery wiersze.
| order_id | quantity | unit_price |
|---|---|---|
| 1 | 2 | 10.00 |
| 2 | 1 | 30.00 |
| 3 | 3 | 10.00 |
| 4 | 1 | NULL |
Jeden wiersz oznacza tutaj jedno zamówienie jednego rodzaju produktu. W prawdziwym sklepie zamówienie może mieć wiele pozycji. Wtedy mnożenie ilości przez cenę opisuje pozycję, a sumę zamówienia trzeba dopiero zbudować. To założenie zapisuję przed liczeniem, bo inaczej poprawne działanie matematyczne może dać błędnie nazwany wskaźnik.
Jeżeli chcesz odtworzyć źródło w edytorze PostgreSQL, możesz użyć poniższego fragmentu. WITH nadaje nazwę wynikowi, a VALUES podaje wiersze. Dokumentacja VALUES opisuje taki sposób budowania małej tabeli bez zapisywania jej na dysku.
WITH orders(order_id, customer_name, city, quantity, unit_price) AS (
VALUES
(1, 'Ada', 'Krakow', 2, 10.00),
(2, 'Ben', 'Gdansk', 1, 30.00),
(3, 'Ada', 'Krakow', 3, 10.00),
(4, 'Celina', 'Krakow', 1, NULL)
)
SELECT order_id, customer_name
FROM orders
ORDER BY order_id;
W kolejnych przykładach zakładam dostępność tego samego źródła orders. Przy użyciu CTE trzeba zachować część WITH i wymienić następujące po niej zapytanie. Nazwa CTE obowiązuje w obrębie jednej instrukcji; po średniku nie powstaje trwała tabela.
Pierwsze zapytanie i jego odczytanie
SELECT order_id, customer_name
FROM orders
ORDER BY order_id;
Wynik zawiera kolejno pary 1, Ada, 2, Ben, 3, Ada, 4, Celina. Wybór dwóch kolumn zachował cztery wiersze. Żaden warunek nie wykluczył zamówienia, a żadne grupowanie nie połączyło rekordów.
Czytam to następująco: z orders weź identyfikator zamówienia i nazwę klienta, następnie pokaż je według identyfikatora. Kolejność kolumn wynika z listy po SELECT. Gdy zamienię miejscami nazwy kolumn, nazwa klienta znajdzie się po lewej, ale znaczenie źródłowego rekordu pozostanie takie samo.
Przecinki rozdzielają kolejne wyrażenia. Po ostatniej kolumnie przed FROM nie dopisuję przecinka. Średnik kończy instrukcję. Słowa kluczowe zapisuję wielkimi literami dla czytelności, natomiast nazwy pól małymi. To konwencja, która pomaga zobaczyć strukturę, a nie warunek poprawności zapytania.
Dlaczego ograniczam SELECT gwiazdka
SELECT *
FROM orders
ORDER BY order_id;
Gwiazdka zwraca wszystkie kolumny udostępnione przez źródło. W naszym przykładzie będzie ich pięć. Przy pierwszym oglądaniu nieznanej tabeli to wygodne. Widzę nazwy pól i mogę ustalić, które są potrzebne. W zapytaniu przekazywanym dalej wolę jawną listę.
Powód jest bardzo konkretny: lista kolumn stanowi umowę z odbiorcą. Jeżeli raport potrzebuje numeru zamówienia i miasta, chcę to widzieć w kodzie. Dodanie nowego pola do źródła nie powinno przypadkiem poszerzać eksportu. Jawna lista ułatwia też zrozumienie, dlaczego określone pole w ogóle trafiło do raportu.
Nie obiecuję przy tym konkretnego przyspieszenia. Czas wykonania zależy od bazy, planu i danych. W tym miejscu chodzi mi o przewidywalność wyniku. Samo zastąpienie gwiazdki trzema nazwami nie dowodzi, że całe zapytanie działa sprawniej. Taką ocenę trzeba oprzeć na osobnej analizie planu.
Aliasy nadają kolumnom sens
SELECT
order_id AS order_number,
customer_name AS customer
FROM orders
ORDER BY order_number;
Dane pozostają takie same, ale nagłówki wyniku to order_number i customer. Alias porządkuje prezentację oraz pozwala nazwać obliczenie. Nie zmienia nazwy pola w źródle ani nie przebudowuje tabeli. Następne zapytanie nadal może odwołać się do order_id.
W PostgreSQL alias kolumny wyjściowej można wykorzystać w ORDER BY. Nie jest natomiast dostępny w WHERE tego samego poziomu. Regułę tę opisuje instrukcja SELECT. Gdy alias ma być wejściem kolejnego etapu, mogę umieścić obliczenie w podzapytaniu.
Wybieram nazwy, które da się zrozumieć bez odgadywania skrótów. value2 niewiele mówi. line_amount przynajmniej wskazuje, że mowa o kwocie pozycji. Nazwa nie zastąpi jednak definicji: w dokumentacji raportu trzeba jeszcze wyjaśnić walutę, podatek i uwzględniane korekty, jeżeli dane rzeczywiście je zawierają.
Obliczenie w SELECT powstaje dla wiersza
SELECT
order_id,
quantity * unit_price AS line_amount
FROM orders
ORDER BY order_id;
Dla pierwszego wiersza mnożę dwa razy dziesięć, więc otrzymuję 20.00. Dla drugiego raz trzydzieści daje 30.00. Dla trzeciego trzy razy dziesięć również daje 30.00. W czwartym brakuje ceny, dlatego kwota pozostaje NULL. Sposób wyświetlenia końcowych zer może zależeć od programu, w którym oglądasz wynik.
| order_id | line_amount |
|---|---|
| 1 | 20.00 |
| 2 | 30.00 |
| 3 | 30.00 |
| 4 | NULL |
Mnożenie w PostgreSQL działa na typach liczbowych. Tutaj ceny zapisano jako liczby dziesiętne. Nie wyprowadzam reguł dla dowolnego typu tekstowego, w którym ktoś umieścił cenę z dopiskiem jednostki. Taki import trzeba najpierw uporządkować.
Samo obliczenie nie zapisuje wartości do źródła. Wynik jest wyliczony na potrzeby zapytania. Dla osoby zaczynającej naukę to istotne rozróżnienie: mogę porównać kilka wersji formuły bez dodawania kolejnych kolumn do tabeli. W tym artykule korzystam z prostych zapytań odczytujących dane.
NULL zostawiam widoczny
Czwarty rekord mówi, że cena jest nieznana. Nie mam podstaw, by wpisać tam zero. Zero oznaczałoby znaną wartość równą zero, co prowadziłoby do innej interpretacji. W raporcie mogę osobno pokazać liczbę pozycji z brakującą ceną i wyjaśnić, że suma nie opisuje pełnego zbioru.
Na tym etapie najważniejsze jest nauczyć się rozpoznawać brak. Pusta komórka wyświetlana przez narzędzie może być wizualną reprezentacją NULL. Zanim uznam ją za pusty tekst, sprawdzam ustawienia programu i typ kolumny. Wygląd w siatce wyników nie rozstrzyga o znaczeniu danych.
Jeśli potrzebujesz zastępowania braków, przeczytaj hasło o COALESCE. Ja nie dodaję tej funkcji automatycznie do każdego obliczenia. Najpierw ustalam, czy wartość zastępcza ma uzasadnienie w pytaniu, na które odpowiadam.

Dwa identyczne wiersze wyniku mogą być poprawne
SELECT customer_name, city
FROM orders
ORDER BY customer_name, city;
Ada i Krakow pojawią się dwa razy, ponieważ Ada ma dwa zamówienia. Usunięcie order_id z widoku nie połączyło tych zamówień. Po prostu przestałem pokazywać informację, która pozwalała je łatwo odróżnić.
To częsta pułapka podczas oceny jakości danych. Powtarzająca się nazwa klienta może oznaczać normalne kolejne zdarzenie. Zanim nazwę rekord duplikatem, określam klucz i poziom szczegółowości. W naszym zbiorze kluczem zamówienia jest order_id. Nazwa klienta nie pełni takiej roli.
Jeżeli rzeczywiście potrzebuję listy różnych par klient i miasto, dodaję DISTINCT. Wynikiem będą trzy pary: Ada i Krakow, Ben i Gdansk, Celina i Krakow. DISTINCT porównuje cały wybrany wiersz. Dodanie identyfikatora zamówienia do listy ponownie odróżni wszystkie cztery rekordy. Więcej przykładów znajdziesz w haśle SELECT DISTINCT.
ORDER BY ustala kolejność, LIMIT ogranicza liczbę
Bez ORDER BY baza nie gwarantuje kolejności wierszy. Nawet jeżeli przy kilku odczytach zobaczysz identyczny układ, nie traktuj go jak obietnicy. Dokumentacja sortowania wyraźnie oddziela zwrócenie danych od ich uporządkowania.
SELECT order_id, quantity
FROM orders
ORDER BY quantity DESC, order_id
LIMIT 2;
Najpierw otrzymasz zamówienie 3 z ilością 3, potem zamówienie 1 z ilością 2. Drugi klucz porządkuje ewentualne remisy. W tej tabeli order_id jednoznacznie identyfikuje zamówienie, więc pozwala domknąć porządek.
LIMIT 2 znaczy najwyżej dwa wiersze. Jeżeli filtr pozostawi jeden, wynik będzie miał jeden. Nie jest to polecenie znalezienia dwóch największych wartości; o tym, co trafi na początek, decyduje ORDER BY. Opis LIMIT wyjaśnia też, dlaczego ograniczenie bez jednoznacznego porządku może dawać nieprzewidywalny podzbiór.
SELECT i WHERE odpowiadają na różne pytania
SELECT order_id, customer_name, quantity
FROM orders
WHERE city = 'Krakow'
ORDER BY order_id;
Teraz wynik ma trzy rekordy: zamówienia 1, 3 i 4. SELECT wskazał widoczne pola, WHERE dopuścił tylko rekordy spełniające warunek. Miasto bierze udział w filtrowaniu, chociaż nie jest wyświetlane. Nie muszę pokazywać każdej kolumny, której używa warunek.
Warto umieć opisać oba wybory osobno. Prośba „pokaż tylko klientów z Krakowa” dotyczy wierszy. Prośba „pokaż tylko numer i klienta” dotyczy kolumn. Gdy ktoś przekazuje mi nieprecyzyjny opis raportu, te dwa pytania pomagają ustalić zakres bez zgadywania.
Tekstową wartość zapisuję w apostrofach. Nazwa kolumny city jest identyfikatorem. W PostgreSQL podwójne cudzysłowy służą do cytowania identyfikatorów, więc zastąpienie nimi apostrofów zmieni sens zapisu. Szczegóły opisuje struktura leksykalna SQL.
SELECT może pokazać stałą i prostą kalkulację
W PostgreSQL mogę napisać SELECT 2 + 3 AS total; bez FROM. Dostanę jeden wiersz z wartością 5. To dobry sposób, by zrozumieć, że lista SELECT zawiera wyrażenia. Kolumna źródłowa jest jednym z możliwych wyrażeń, ale nie jedynym. Nie muszę szukać w bazie tabeli z liczbami dwa i trzy.
Stała przydaje się również obok danych. Gdy do naszego zapytania dodam 'example' AS dataset_label, każdy z czterech wierszy otrzyma etykietę example. Apostrofy mówią, że to wartość tekstowa. Jeżeli zapiszę example bez apostrofów, baza będzie szukała takiej nazwy w dostępnym zakresie. Podobieństwo wizualne tych zapisów jest spore, a ich znaczenie zupełnie inne.
Nie wykorzystuję stałej etykiety do ukrywania ograniczeń źródła. Jeśli wpiszę 'complete', nie sprawi to, że brakująca cena Celiny nagle stanie się znana. Takie pole powinno opisywać rzeczywiste pochodzenie albo stan danych. Zgrabny nagłówek w raporcie nie jest dowodem kompletności. Dlatego etykiety w przykładach nazywam wprost przykładowymi.
Przy okazji warto odróżnić alias kolumny od aliasu źródła. Zapis FROM orders AS o pozwala użyć o.order_id w liście SELECT. Skrót o wskazuje, skąd pochodzi pole; AS order_number określa nazwę tego pola w wyniku. Ta druga nazwa nie zastępuje pierwszej we wszystkich miejscach zapytania. Takie rozróżnienie przyda się szczególnie przy dwóch tabelach mających własne pole id.
Typ wyniku jest równie ważny jak jego wygląd
Liczba i tekst złożony z cyfr mogą wyglądać identycznie w siatce. Dla kodu produktu 0012 początkowe zera mogą być istotną częścią identyfikatora. Jeżeli potraktuję go jak liczbę, mogę stracić informację potrzebną do porównania z innym systemem. Dlatego nie zamieniam każdego pola zawierającego cyfry na typ liczbowy.
Z kolei pola przeznaczone do obliczeń wymagają świadomego typu. Dokumentacja typów liczbowych PostgreSQL rozróżnia liczby całkowite, dokładne dziesiętne i przybliżone zmiennoprzecinkowe. Na początek wystarczy wiedzieć, że baza nie przechowuje każdej wartości w jeden identyczny sposób. Przygotowanie poprawnego typu jest częścią przygotowania analizy.
Widać to przy dzieleniu: w PostgreSQL SELECT 5 / 2 AS result; daje 2, bo oba argumenty są całkowite. Zapis SELECT 5.0 / 2 AS result; daje wartość liczbową równą 2.5. To dwa różne obliczenia pod względem typów, mimo że człowiek czyta je podobnie. Regułę dzielenia opisują przywołane wcześniej operatory matematyczne.
Jeżeli później policzysz udział zamówień z Krakowa, nie chcesz przypadkiem otrzymać zera tylko dlatego, że licznik i mianownik były całkowite. Zanim zajmiesz się formatem procentowym, upewnij się, że samo wyrażenie zwraca właściwą wartość. Wyświetlenie dwóch miejsc po przecinku nie naprawia wyniku obliczonego wcześniej jako liczba całkowita.
Na tym etapie nie musisz znać wszystkich typów PostgreSQL. Wystarczy prosty nawyk: przy nowym polu ustal, co ono oznacza, jak jest zapisane i do jakich działań będzie używane. Ta minuta na początku często oszczędza późniejsze poszukiwanie błędu w kilku kolejnych warstwach raportu.
Jak sprawdzam, czy wynik odpowiada na pytanie
Zaczynam od krótkiego zdania: jeden wiersz wyniku przedstawia jedno zamówienie z Krakowa. Potem porównuję tę deklarację z kolumnami i filtrem. Jeżeli zniknął identyfikator, pytam, czy nadal potrafię odróżnić zdarzenia. Jeśli dodałem DISTINCT, sprawdzam, czy świadomie zmieniłem poziom szczegółowości.
Następnie wybieram jeden rekord, dla którego potrafię ręcznie opisać drogę od źródła do wyniku. Zamówienie 3 przechodzi filtr miasta, pokazuje nazwę Ada i ma ilość 3. W przykładzie z kwotą daje 30.00. Taki mały punkt kontrolny szybko ujawnia pomylenie pola albo założenia.
Na końcu czytam nazwy i jednostki oczami odbiorcy. Kolumna quantity bez informacji, czy chodzi o sztuki, opakowania czy godziny, wymaga doprecyzowania. SQL wykona polecenie zgodnie ze składnią. To ja odpowiadam za to, czy powstała tabela daje się uczciwie zinterpretować.
Ćwiczenie i następny krok
Przygotuj zapytanie pokazujące order_id, customer_name oraz quantity * unit_price AS line_amount tylko dla Krakowa, w kolejności identyfikatora. Przewidywany wynik ma trzy wiersze, kwoty 20.00, 30.00 i NULL. Przed odczytaniem rozwiązania wypisz je samodzielnie z małej tabeli.
Następnie usuń identyfikator i cenę, pozostawiając klienta oraz miasto. Zastanów się, dlaczego nadal widzisz dwa zamówienia Ady. Dopiero potem dodaj DISTINCT i nazwij nowy poziom szczegółowości: różna para klient i miasto. Ta zmiana znaczenia jest ważniejsza od zapamiętania kolejnego słowa.
Dalej polecam filtrowanie WHERE oraz logiczną kolejność klauzul SQL. Gdy rozumiesz już, skąd biorą się wiersze i kolumny, łatwiej ocenisz późniejsze grupowanie i połączenia. Masz wtedy konkretny punkt odniesienia, zamiast kolekcji zaklęć do wklejania.
Film o SELECT
Poniżej zachowuję polski materiał o SELECT. Tekst jest samodzielnym rozwinięciem z własnymi danymi i określonym dialektem.
Najważniejsze w pigułce
Czy SELECT zmienia dane w tabeli?
Proste zapytania w tym artykule odczytują dane i tworzą wynik. Alias oraz obliczenie nie zmieniają nazwy ani wartości kolumn w źródle.
Czy SELECT usuwa duplikaty?
Domyślnie zachowuje wszystkie wiersze wyniku. DISTINCT usuwa powtórzenia całych wybranych wierszy, co może zmienić poziom szczegółowości.
Czy pierwszy wiersz jest najstarszy?
Bez ORDER BY kolejność nie jest gwarantowana. Do wskazania najstarszego zdarzenia potrzebujesz właściwej daty i rozstrzygnięcia remisów.
Przy pracy nad artykułem użyto w zautomatyzowany sposób sztucznej inteligencji. Jeśli też chcesz tak umieć, sprawdź ten projekt.
