SELECT – definiowanie wyniku

Klauzula SELECT, musi znaleźć się w każdym zapytaniu. Bez niej, nie wykonamy żadnej kwerendy, bo to właśnie tutaj, określamy w jaki sposób chcemy oglądać wynik zapytania.

Pomimo że występuje zawsze jako pierwsza, jej treść jest przetwarzana jako jedna z ostatnich (jeśli nie korzystamy z sortowania zbioru wynikowego ORDER BY, będzie to faktycznie ostatni krok).

(5)     SELECT
(1)	FROM
(2)	WHERE
(3)	GROUP BY
(4)     HAVING
(6)     ORDER BY

Ważne jest, aby zrozumieć, że język SQL pomimo swojej ustrukturyzowanej formy, jest elastyczny. Strukturyzowany, czyli ściśle określony pod względem szyku, słów kluczowych, konstrukcji, ale dający nam swobodę w ramach tej struktury.

W tym artykule, zaprezentuję pełen zakres możliwości jakie mamy do dyspozycji w SELECT, czyli wszystko co możemy użyć pomiędzy słowami kluczowymi SELECT … FROM do kształtowania wyniku kwerendy.


Pierwszy SELECT

Każda kwerenda, jako rezultat zwraca tabelę – wirtualną (Virtual Table, VT). Zgodnie z teorią zbiorów, omawianą w rozdziale pierwszym, tabela to zbiór elementów (wierszy) opisanych przez atrybuty (kolumny).
Wiemy już, że każde zapytanie musi zawierać SELECT. Czy potrzeba czegoś więcej? Zazwyczaj odwołujemy się w kwerendach do tabel, ale nie jest to koniczne (co prawda mało sensowne na pierwszy rzut oka, ale dobrze obrazuje sedno, które chcę Ci pokazać)

select 'Hello World !' as Kolumna1

SELECT_01

Jak widać, najprostsze z możliwych zapytań (nie odwołuje się nawet do żadnej tabeli) – zwraca nam zbiór jednoelementowy, opisany jednym atrybutem (jedna kolumna) o nazwie Kolumna1. Tego typu zbiór to nic innego jak wartość skalarna. Wartością skalarną tutaj jest zarówno wartość atrybutu (kolumny) jak i cały zbiór wynikowy. Skomplikujmy zatem zapytanie :

select 'pierwsza' as Kol1, 'druga' as Kol2, 'trzecia'

SELECT_02

Tym razem znów otrzymaliśmy zbiór jednoelementowy, ale opisany trzema atrybutami (jeden bez nazwy). Warto zauważyć, że tu również każdy atrybut elementu opisany jest przez wartość skalarną (‘pierwsza’, ‘druga’ i ‘trzecia’).

Wyciąganie danych z tabeli

Zróbmy jednak coś bardziej użytecznego – czyli pierwszy krok do pisania sensownych zapytań – wyciągnijmy w końcu jakieś dane z tabeli.
Załóżmy na początek, że chcemy pobrać wszystko z tabeli, aby w ogóle zobaczyć co tam jest przechowywane :

USE Northwind
GO

select * from dbo.Employees

SELECT_04
Za pomocą tej najprostszej z możliwych kwerend, otrzymamy tabelę wynikową o identycznej strukturze (te same nazwy kolumn, liczba wierszy, typy danych w kolumnach) jak tabela z której dane wyciągamy.
Na razie bez filtrowania, bez kombinacji – wyciągamy wszystko jak leci.

Tu konieczna uwaga – jeśli będziesz w ten sposób chciał odpytać tabelę, która zawierającą dużo rekordów – lepiej użyć dodatkowej klauzuli ograniczającej liczbę zwracanych wierszy czyli TOP, opisanej na końcu tego artykułu. W naszym przypadku nie ma to znaczenia, ale w systemach rzeczywistych ma i warto o tym pamiętać. Nigdy nie wyciągajmy czegoś, czego nie potrzebujemy (obciążenie serwera). Więcej na ten temat dowiesz się w rozdziale ostatnim czyli w praktycznych aspektach pisania zapytań.

Symbol ‘*’ zastępuje nazwy wszystkich kolumn z tabeli (lub tabel) do których odwołujemy się w klauzuli FROM. Stosowanie symbolu ‘*’ również nie jest dobrą praktyką – wyjaśniam to szerzej w rozdziale dotyczącym wydajności i dobrych praktyk. Na razie uznamy jednak, że do celów testowych, naukowych 🙂 będziemy go czasem używać.

Widzimy zatem, że podstawową cechą klauzuli SELECT jest określenie atrybutów elementów, zwracanych w zbiorze wynikowym kwerendy.

Każda kwerenda w wyniku zwraca zbiór elementów (tabelę VT). Czasem jest to zbiór pusty (gdy zapytanie nic nie zwraca), czasem jednoelementowy (może być opisany przez jeden lub wiele atrybutów), ale zazwyczaj zawiera szereg wierszy (elementów) i kolumn (opisanych przez wiele atrybutów). Kształt tabeli wynikowej jest określany za pomocą SELECT. To tutaj określamy w jaki sposób chcemy definiować elementy zbioru wynikowego (jakimi atrybutami czyli nazwami kolumn, będą opisane). Czy wiersze (elementy) będą opisane przez wszystkie kolumny z tabel źródłowych (symbol *) czy też tylko przez ich podzbiór.

Przeplatam tutaj świadomie definicje z teorii zbiorów (elementy, atrybuty) z nazwami obiektów stosowanych w bazach (kolumny, tabele) ponieważ zależy mi, abyś spojrzał na wynik kwerendy jak na zbiór elementów. Jeśli to dostrzeżesz, tworzenie trudniejszych zapytań będzie dla Ciebie intuicyjne.

Zatem, jeśli chcemy otrzymać zbiór elementów z tabeli źródłowej, opisany tylko poprzez kilka atrybutów (kolumn) – wymieniamy je w SELECT w postaci listy.

select LastName, FirstName from dbo.Employees

SELECT_03
Widzimy po raz kolejny zbiór wynikowy = tabela wirtualna, tym razem dwukolumnowa czyli każdy jej element opisany jest przez dwa atrybuty.

Co możemy umieścić w SELECT

Zasada generalna

W SELECT, możemy umieścić wszystko co jest (lub będzie) wartością skalarną, jako atrybut opisujący element zbioru wynikowego (VT).

Nazwy kolumn

Przede wszystkim i najczęściej będą to nazwy kolumn, zbiorów (tabel lub widoków) z których pobieramy dane. Nawiązując znów do teorii, kolumny to atrybuty zawierające wartości skalarne, opisujące elementy zbioru – czyli jak najbardziej się nadają !
Kolejność kolumn nie ma specjalnego znaczenia – jest to jedynie właściwość odczuwalna przez użytkowników w warstwie prezentacji. Podobnie jak nazwy kolumn, które są zazwyczaj nadawane zgodnie z przyjętym standardem i niekoniecznie muszą być intuicyjne. Dlatego tutaj, w SELECT, możesz nadać im bardziej zrozumiałe nazwy czyli aliasy. Użyłem ich w pierwszym przykładzie tego artykułu, więc trochę już je znasz. Do ich tworzenia, zazwyczaj stosujemy słowo kluczowe AS ale nie jest ono obowiązkowe. Jeśli nazwa zawiera spacje, musi być ona ujęta w [] lub „” ewentualnie w apostrofy ”. Trzy najpopularniejsze sposoby tworzenia aliasów :

select LastName AS Nazwisko, FirstName Imie, LastName [Pierwsze Imię]
from dbo.Employees

SELECT_05
Co istotne – zgodnie z kolejnością logicznego przetwarzania zapytań – użyty alias, będzie „widoczny” we wszystkich kolejnych krokach przetwarzania zapytania, następujących po SELECT, czyli tylko w ORDER BY. Istnieje jeszcze jeden sposób na tworzenie aliasów z operatorem porównania, ale uważam że nie jest on intuicyjny i bardzo rzadko się go spotyka :

-- alias Nazwisko, utworzony dla atrybutu (kolumny) LastName z =
select Nazwisko = LastName from dbo.Employees

Wszystkie kolumny z widoku, tabeli, wyrażenia tablicowego

Wspominany już symbol * zastępuje wszystkie kolumny ze wszystkich tabel do których się odwołujemy we FROM. Jeśli jednak zależy nam na wszystkich kolumnach tylko z konkretnej tabeli, możemy działanie ograniczyć tylko do kolumn wskazanej tableli, w tym przypadku – wszystkie kolumny tabeli dbo.Products :

-- aliasy możemy stosować również do nazw tabel - więcej na ten temat, 
-- przy okazji omawiania FROM

select p.* 
from dbo.Products as p 
	inner join dbo.Categories as c on p.CategoryID = c.CategoryID

Wartości stałe

SELECT określa atrybuty elementów, które mogą być zarówno nazwami kolumn, jak i samodzielnymi zbiorami (ważne aby były wartościami skalarnymi czyli zbiorami jednoelementowymi). Możemy tu zadeklarować wartość dodatkowego atrybutu np.

select LastName, FirstName, 'Oddział 1' as Division
from dbo.Employees

SELECT_06
Wtedy każdy element VT dostanie ekstra atrybut o nazwie Division, o wartość 'Oddział 1′. Szczególnie przydatne, przy łączeniach zbiorów (kwerend) lub do innych przekształceń.

Funkcje skalarne

Funkcje skalarne podobnie jak w innych językach programowania, na podstawie podanych argumentów zwracają jakąś wartość skalarną. W SQL Server możemy tworzyć własne funkcje skalarne, jak również mamy do dyspozycji, szereg wbudowanych funkcji, za pomocą których możemy dokonywać różnych przekształceń na danych. Są funkcje związane z datą i czasem, funkcje tekstowe, matematyczne czy systemowe, ale zawsze jeśli mówimy o funkcjach skalarnych, mamy na myśli takie, które zwracają pojedynczy element opisany jedną wartością. Najbardziej praktyczne, wbudowane funkcje skalarne SQL opisałem w dalszej części tego kursu.

Nie ważne czy będzie to wartość tekstowa, liczbowa, binarna, XML. Skalar = jedna wartość i takie funkcje możemy użyć w SELECT. Np. użycie funkcji związanej z datą :

-- przykład zastosowania funkcji daty i czasu
-- YEAR zwraca rok z danej daty, DATEDIFF różnicę dwóch dat np. w latach

select LastName, FirstName, BirthDate, 
       YEAR(BirthDate) as BirthYear,
       DATEDIFF(yy, BirthDate, getdate()) as Years
from dbo.Employees

SELECT_07

W tym przykładzie użyłem także funkcji getdate(), która wyświetla informacje o dacie i czasie systemowym.

Wyrażenia arytmetyczne, tekstowe

Możemy wykonywać dowolne (sensowne) operacje na danych np. skomplikowane 🙂 operacje arytmetyczne :

-- można kalkulować bez tabeli ;) 
Select 2+2 as wynik

-- Przydatniejsze są obliczenia na wartościach z kolumn
-- funkcja ROUND zaokrągla wynik do n miejsc po przecinku

select UnitPrice, Quantity, Discount, 
	Round((UnitPrice*Quantity*(1-Discount))*1.23 , 2) as TotalBrutto
from dbo.[Order Details]

SELECT_08
Wyrażenia tekstowe np. konkatenancja stringów czyli po ludzku – złączenie tekstów

select LastName + ' ' +  FirstName as ImieNazwisko, 
LEFT(FirstName,1) + '. ' +  LastName as ImieNazwisko2
from dbo.Employees

SELECT_09
Dlaczego podkreślam, że można tu wykonywać dowolne SENSOWNE operacje na danych. Otóż, wszystko co chcemy zrobić, musi mieć jednoznaczny i przewidywalny rezultat.
Jeśli wykonujemy operacje arytmetyczne, to powinny to być operacje na liczbach. Jeśli dodajesz teksty, powinny to być teksty itp. Itd. Jest to związane z silnym typizowaniem danych w SQL co pozwala zapewnić danym spójność.

Wyrażenia warunkowe

Język SQL, nie tylko służy do pisania kwerend, ale także za jego pomocą możemy tworzyć pewną logikę programistyczną. Czasem całkiem skomplikowaną. T-SQL oferuje szereg komend sterujących przepływem wykonywania skryptów, polecenia warunkowe, obsługi błędów, pętle.

Jedyną w zasadzie komendą z tej grupy, którą możemy stosować w kwerendach to konstrukcja warunkowa – CASE WHEN, pozwalająca definiowanie wartości atrybutu w oparciu o warunek (lub warunki) i na tej podstawie wyświetlanie odpowiedniej wartości.

Ogólna struktura CASE WHEN

                         CASE WHEN <warunek1>  THEN <jeśli spełniony to wartość 1>
<opcjonalnie kolejne warunki> WHEN <warunekN> THEN <jeśli spełniony to wartość n>
           <opcjonalnie else> ELSE <jeśli żaden z warunków nie spełniony to wartość else> END

W tej konstrukcji obowiązkowe jest zdefiniowanie przynajmniej jednego warunku. Jeśli nie określimy wyrażenia ELSE, wszystko co nie spełni postawionego warunku (lub warunków) dostanie wartość nieokreśloną czyli NULL. Najlepiej zobrazować to na przykładzie :

Use AdventureWorks2008
GO

select p.FirstName + ' ' + p.LastName as FullName, e.Gender, e.MaritalStatus,
       CASE WHEN Gender = 'F' THEN 'Female' 
	    ELSE 'Male' END as Gender,
       CASE WHEN MaritalStatus = 'M' THEN 'Married' END as MarriedSt 
from HumanResources.Employee e 
inner join Person.Person p on e.BusinessEntityID = p.BusinessEntityID

SELECT_10

Podzapytania zwracające pojedynczy, skalarny element

Jak już doskonale pamiętasz, możemy w SELECT umieścić wszystko co jest skalarem, czyli także wynik podzapytania zwracający jednoelementowy zbiór, opisany jednym atrybutem. Wróćmy znów do podstaw – najprostsza kwerenda zwracała nam taki właśnie zbiór, użyjmy jej jako podzapytania.

select ( select 'dzien dobry' ) as SubQuery_Result

SELECT_11
Zróbmy coś ciekawszego. Tym razem będzie to zapytanie skorelowane, ale nie koncentruj się na nim (więcej o tego typu podzapytaniach znajdziesz tutaj) – tutaj ważna jest sama idea SELECT – możliwość umieszczania skalarnego wyniku podzapytania jako jednego z atrybutów opisujących element zbioru wynikowego.
W wyniku poniższego podzapytania, zwracana jest wartość najdroższej pozycji w ramach zamówienia i atrybut ten (kolumnę) nazwiemy the_most_expensive_position.

select sod.SalesOrderID, ProductID, LineTotal, 

	 ( -- wartość atrybutu, określona przez podzapytanie
	   select MAX(LineTotal) from Sales.SalesOrderDetail 
	   where SalesOrderID=sod.SalesOrderID 
	  ) as the_most_expensive_position

from Sales.SalesOrderDetail as sod

SELECT_12

Zmienne

Kolejną grupą wartości skalarnych z którymi możesz się zetknąć i które możesz użyć w klauzuli SELECT są zmienne. Jak już wspominałem, język SQL to nie tylko zapytania, ale także język skryptowy, pozwalający na deklarowanie zmiennych i przechowywanie w nich wartości (zupełnie tak jak w innych językach np. C#). Jeśli zmienna jest skalarna – to oczywiście możemy ją również użyć w SELECT, do określenia wartości atrybutu jak również we wszelkich przekształceniach, kalkulacjach czy jako przekazywany argument do funkcji.

USE NORTHWIND
GO

declare @stawaVat decimal (2,2)
Set @stawaVat = 0.23

select @stawaVat as Stawka_VAT

select UnitPrice, Quantity, Discount, 
   (UnitPrice*Quantity*(1-Discount))*(@stawaVat+1) as TotalBrutto
from dbo.[Order Details]

-- przykład użycia zmiennych globalnych systemowych
Select @@Version as Wersja, @@Servername as NazwaSerwera

SELECT_13
Jeśli chodzi o pozostałe typy obiektów, jakie mogą pojawiać się w SELECT to mam dla Ciebie dobrą wiadomość – nic więcej Cię nie zaskoczy! Zawsze to będzie obiekt skalarny który będzie definiował atrybut elementu wynikowego (wartość kolumny, wiersza w tabeli wynikowej).

Pozostałe funkcjonalności SELECT

Oprócz funkcji wbudowanych, których jest zbyt wiele, aby je tutaj opisywać, istnieje jeszcze kilka funkcjonalności, rozszerzających polecenie SELECT, które są stosowane tylko i wyłącznie w tej klauzuli.

Usuwanie duplikatów elementów DISTINCT

Zbiór wynikowy, może zawierać wiele elementów, również duplikaty wierszy. Jeśli chcesz otrzymać unikalny zbiór bez duplikatów – możesz to zrobić za pomocą DISTINCT.

-- DISTINCT usunie wszytkie duplikaty
select DISTINCT FirstName
from   Person.Person 
where FirstName  = 'Ben'

TOP – ograniczenie wyniku zbioru do n elementów

Za pomocą słowa kluczowego TOP, możesz ograniczyć liczbę zwracanych wierszy do liczby bezwzględnej lub procentowej. Możliwości jakie daje TOP :

-- tylko 10 wierszy (teoretycznie losowych, bo nie ma sortowania)
select TOP 10 FirstName, LastName
from Person.Person  

-- 10 Procent wierszy 
select TOP 10 PERCENT FirstName, LastName
from Person.Person  

Jest jeszcze jedna opcja związana z TOP, którą najlepiej obrazuje to przykład TOP 3 w zawodach sportowych.
Jeśli dwóch zawodników uzyska ten sam, rekordowy rezultat – dostają ten sam medal, więc teoretyczne, może być więcej niż 3 medalistów. To właśnie załatwia nam opcja WITH TIES.
Ogranicza ona liczbę zbioru wyniku rekordów do zadanej wartości liczbowej lub procentowej podobnie jak samo TOP.
Dodatkowo dołącza do zbioru elementy, które posiadają identyczną wartość, jak ostatni wiersz z danego zakresu, według reguły sortowania.

-- 10 wierszy ale również z "remisowymi" czyli z ogonkiem 
-- WITH TIES tylko z ORDER BY
select TOP 10 WITH TIES FirstName, LastName
from Person.Person  
order by LastName

SELECT_16
Czyli w przypadku WITH TIES, konieczne jest jawne umieszczenie w kwerendzie klauzuli sortowania ORDER BY.

W dokumentacji BOL, znajdziesz jeszcze jedną opcję, której jednak nigdy nie spotkałem w zastosowaniu – SELECT ALL (przeciwieństwo DISTINCT), która to jest zawsze domyślnie stosowana. Określa ona zbiór wynikowy łącznie z duplikatami co jest tak naturalne, że nikt tego jawnie nie używa (bo i po co).

SELECT INTO

Pozostała jeszcze ostatnie polecenie w ramach SELECT z którym możesz się spotkać.
Nie służy ono do formowania wyniku i nie jest używane w kwerendach do wyciągania i pokazywania danych dlatego nie będę się tutaj na jego temat zbytnio rozpisywał. Jednak, aby być kompletnym i z uwagi na to, że jest to bardzo przyjemna funkcjonalność parę słów się należy.

Konstrukcja SELECT INTO przekierowuje wynik zwracany przez zapytanie w proces materializowania tabeli wynikowej czyli tworzy tabelę wynikową (wcześniej VT) z całą zawartością jaka jest zwracana przez kwerendę.
SELECT INTO to łatwy, szybki i bardzo wygodny sposób na przechowanie wyniku zapytania np. w tabeli tymczasowej. Wszystkie typy danych i nazwy kolumn będą również skopiowane z tabel źródłowych do których się odwołujemy w zapytaniu.

10 Responses

  • Hej
    zastanawiam się czy ten przyklad z TOP 3 jest wlasciwy do WITH TIES:
    „Jest jeszcze jedna opcja związana z TOP, którą najlepiej obrazuje to przykład TOP 3 w zawodach sportowych.
    Jeśli dwóch zawodników uzyska ten sam, rekordowy rezultat – dostają ten sam medal, więc teoretyczne, może być więcej niż 3 medalistów. To właśnie załatwia nam opcja WITH TIES. ”
    Bo czy jeśli bedą dwa pierwsze miejsca (albo dwa drugie miejsca a jedno pierwsze), a my chcemy wyciągnąć TOP 3 w zawodach sportowych, to w tym przypadku, tak naprawde dostaniemy tylko pierwsze i drugie miejsce? TOP 3 sprawdzi się tylko, gdy bedzie wiecej niż jedna osoba, ktora zajmie trzecie miejsce

    • Masz rację, lepiej byłoby tu urzyć funkcji okna RANK() OVER() – w przypadku faktycznych 3 miejsc, żeby nie pominąć brązowych medalistów (1 złoto, 4 srebra ;))

  • W jaki sposób mogę wyświetlić dane z jednej tabeli, gdy jedna z kolumn zawiera dwa rodzaje danych, a ja chcę mieć dwie wartości, będące sumami tych dwóch rodzajów ?

    • W bazach relacyjnych jest to błąd projektowy. Atrybuty powinny mieć tylko jeden typ danych. Jeśli naprawdę już musisz, to pozostaje tylko rzutowanie typóœ (np. funkcje CAST / CONVERT)

  • Mam tabele z pracownika i kilkoma jeszcze informacjami. Chciałbym uzyskać unikalne cale rekordy, dla stanowisk i departamentów. Czyli distinct ale na całe rekordy. Prosty przykład poniżej.

    stanowisko departament
    sprzedawca 10
    manager 15
    sprzedawca 10
    sprzedawca 20

    stanowisko departament
    sprzedawca 10
    manager 15
    sprzedawca 20

  • Mam jedno pytanie.
    Mam tabelę „Spis” z kolumnami m.in. „kraj” i „owoc”.
    Chciałabym wybrać te rezultaty z kolumny „kraj”, które są unikalne – żeby zobaczyć, które z krajów są głównymi producentami tylko jednego owocu.

    Czyli np jeśli kolumna zawierałaby Polska Anglia Polska Włochy Niemcy Włochy, chciałabym uzyskać zbiór o treści „Anglia Niemcy”.

    Czy jest to możliwe?

    Dziękuję za pomoc!

    • Oczywiście, ale musiałabyś pobawić się w grupowanie i filtrowanie grup
      ..
      GROUP BY Kraj
      HAVING COUNT(Kraj) = 1

      BTW. to pytanie typowe na forum (przy takich wątpliwościach najlepiej pisz na wss.pl tam jest b. fajne forum dot. SQL)

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.