Astăzi este prima lecție care chiar e despre scrierea de T-SQL. Tot ce a fost până acum a fost pregătire. De aici încolo, fiecare lecție începe cu o întrebare pe care chiar și-o pune business-ul Runehold — „care produse s-au vândut cel mai mult în Germania săptămâna trecută?”, „câte comenzi am livrat la timp în martie?”, „care e rata de churn pentru Runeboxes în Italia?” — și ajungem la SQL-ul care răspunde.
Instrucțiunea pe care o vom scrie cel mai mult în viața noastră este SELECT. Pare simplă. Are cel puțin nouă cuvinte cheie care pot intra în ea. Are o ordine logică de execuție care nu se potrivește cu ordinea în care o tastezi. Are un cuvânt cheie (*) care strică interogările în moduri invizibile până când nu mai sunt. Acoperim toate astea astăzi.
Cele șase părți ale unui SELECT
Fiecare SELECT are până la șase clauze majore, în această ordine atunci când le scrii:
SELECT <columns>
FROM <table>
WHERE <row filter>
GROUP BY <grouping>
HAVING <group filter>
ORDER BY <sorting>
Plus o serie de modificatori opționali (TOP, DISTINCT, OFFSET ... FETCH, JOIN-uri, funcții de fereastră) care se atașează la una dintre acele clauze.
Cel mai simplu SELECT legal are doar două clauze:
SELECT CustomerId, Email FROM Sales.Customer;
Cel mai simplu SELECT legal fără tabel are una singură:
SELECT GETDATE();
-- Returns the current time. No FROM needed.
Majoritatea interogărilor de producție le folosesc pe toate șase.
Ordinea logică nu este ordinea scrisă
Iată cel mai important fapt despre SELECT pe care nimeni nu ți-l spune explicit.
Ordinea în care SCRII clauzele nu este ordinea în care SQL Server le EXECUTĂ.
Ordinea logică de procesare a SQL Server arată cam așa:
1. FROM (pick the table or tables)
2. ON (apply any JOIN condition)
3. JOIN (combine rows from joined tables)
4. WHERE (filter rows)
5. GROUP BY (collapse rows into groups)
6. HAVING (filter groups)
7. SELECT (project the columns)
8. DISTINCT (deduplicate)
9. ORDER BY (sort)
10. TOP / OFFSET (limit)
Totul începe cu FROM. Lista SELECT — coloanele pe care le proiectezi — este aproape ultimul lucru evaluat. Pare ciudat pentru că scriem SELECT primul. Dar asta explică o duzină de greșeli de începător într-o singură propoziție:
- De ce nu poți folosi un alias de coloană în clauza
WHERE(alias-ul se creează la pasul 7;WHERErulează la pasul 4) - De ce poți folosi un alias de coloană în
ORDER BY(pasul 9 e după pasul 7) - De ce
GROUP BYcere numele fără alias - De ce un filtru
WHEREpe un agregat nu funcționează — trebuie să foloseștiHAVING
Notează-ți mintal această ordine. Te scapă de zece ore de „de ce nu merge asta” în primul an.
Proiecție: alegerea coloanelor
În interogări reale, niciodată SELECT *. Explic de ce într-un minut. În schimb, listează întotdeauna coloanele dorite:
USE Runehold;
SELECT CustomerId,
Name,
Country,
CreatedAt
FROM Sales.Customer;
Fiecare rând care se potrivește interogării produce un rând în rezultat, cu coloanele cerute în ordinea cerută.
Poți de asemenea să calculezi coloane noi din mers, numite expresii:
SELECT CustomerId,
Name,
UPPER(Country) AS CountryCode, -- case transform
DATEDIFF(DAY, CreatedAt, GETDATE()) AS DaysSinceSignup,
CreatedAt
FROM Sales.Customer;
AS <alias> dă expresiei un nume de coloană. Fără un alias, coloanele calculate apar în rezultate ca (No column name) — urât și inutilizabil pentru codul aplicației. Pune întotdeauna alias.
SELECT * miroase a neprofesionalism
SELECT * înseamnă „fiecare coloană din tabel, în ordinea în care le definește tabelul.” Fiecare tutorial SQL începe cu el pentru că e ușor de tastat. Fiecare ghid SQL de producție îl interzice.
Motive:
- Schimbările de schemă îți strică codul. Cineva adaugă o coloană;
INSERT INTO ... SELECT *are acum un număr greșit de coloane. Maparea cu tipuri puternice a aplicației se rupe. Acum ai un 500 luni dimineața. - Performanță.
SELECT *forțează SQL Server să citească fiecare coloană, ceea ce exclude indexurile non-clustered acoperitoare. O interogare care ar putea face seek pe un index și răspunde în 2ms acum face scan pe tot tabelul. - Lizibilitate.
SELECT Name, Email, Countryîi spune cititorului despre ce e interogarea.SELECT *nu îi spune nimic. - Cost de rețea. Returnarea unor coloane pe care aplicația nu le folosește irosește lățime de bandă între baza de date și serverul de aplicație.
Trei excepții în care SELECT * este acceptabil:
- Explorare ad-hoc în SSMS („ce-i în acest tabel?”). De aruncat.
- Subinterogări
EXISTS:WHERE EXISTS (SELECT * FROM ... WHERE ...)—*nu este niciodată materializat, e o convenție. COUNT(*)— nu e de fapt unSELECT *, e o formă specială care înseamnă „numără rândurile indiferent de NULL.”
Listează întotdeauna coloanele. Tu cel viitor îți vei mulțumi.
Alias-uri: funcția de calitate a vieții
Alias-urile de coloană redenumesc coloane calculate sau prea lungi:
SELECT o.OrderId AS Id,
o.Total AS AmountEUR,
o.CountryCode AS Country,
o.Total * o.VatRate AS VatAmount,
o.Total - (o.Total * o.VatRate / (1 + o.VatRate)) AS NetAmount
FROM Sales.Orders AS o;
Două stiluri: o.OrderId AS Id (cu AS) și o.OrderId Id (spațiu în loc de AS). Ambele funcționează. Folosește întotdeauna AS — e explicit și vizual neambiguu. Forma cu spațiu m-a prins când am scris accidental SELECT OrderId Total FROM ... și SQL Server a interpretat asta ca „dă-mi OrderId aliasat ca Total”, nu „dă-mi OrderId și Total”. Confuz, tăcut, enervant.
Alias-urile de tabel redenumesc tabelul într-o interogare, de obicei la ceva scurt:
SELECT o.OrderId, c.Name, c.Country
FROM Sales.Orders AS o
JOIN Sales.Customer AS c ON c.CustomerId = o.CustomerId;
o pentru Orders, c pentru Customer. Alias-urile îți permit să scrii o.OrderId în loc de Sales.Orders.OrderId. Pune întotdeauna alias la tabele în interogările cu mai multe tabele. Scrie întotdeauna o.OrderId în loc de doar OrderId când sunt mai multe tabele în joc, chiar dacă numele coloanei e neambiguu astăzi. Mâine adaugă cineva o coloană OrderId în Customer și interogarea ta nu se mai compilează.
TOP, OFFSET ... FETCH și DISTINCT
TOP (n) — returnează primele n rânduri. Fără un ORDER BY e la noroc care n rânduri primești:
SELECT TOP (10) OrderId, Total FROM Sales.Orders ORDER BY OrderDate DESC;
-- The 10 most recent orders
OFFSET ... FETCH — forma standard SQL pentru paginare. Necesită ORDER BY:
SELECT OrderId, Total
FROM Sales.Orders
ORDER BY OrderDate DESC
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;
-- Skip 20, take 10 — "page 3 of 10-per-page"
DISTINCT — deduplică rândurile după toate coloanele proiectate:
SELECT DISTINCT Country FROM Sales.Customer;
-- Each country appears once
DISTINCT e scump (SQL Server trebuie să sorteze sau să facă hash ca să deduplice). Dacă te trezești scriindu-l des, de obicei soluția reală e GROUP BY sau un alt model de JOIN.
Obiceiul listării complete a coloanelor
Hai să luăm o interogare realistă pe Runehold: „listează cele mai mari zece comenzi ale clienților italieni din acest trimestru, cele mai noi primele, arătând ID-ul comenzii, numele clientului, totalul în EUR și data.”
SELECT TOP (10)
o.OrderId,
c.Name AS CustomerName,
o.Total AS TotalEUR,
CAST(o.OrderDate AS DATE) AS OrderDay
FROM Sales.Orders AS o
JOIN Sales.Customer AS c
ON c.CustomerId = o.CustomerId
WHERE o.CountryCode = 'IT'
AND o.OrderDate >= '2026-01-01'
AND o.OrderDate < '2026-04-01'
ORDER BY o.Total DESC;
Fiecare coloană e numită. Fiecare tabel are alias. Filtrul pe OrderDate folosește un interval semi-deschis (>= la început, < la final), care e modul corect de a filtra pe o coloană datetime — mai multe despre de ce în lecția 15, dar e singura formă atât corectă, cât și prietenoasă cu indexurile. ORDER BY e explicit; fără el, TOP (10) ar fi un eșantion aleator de 10 rânduri.
Asta e forma unei interogări de producție. Obișnuiește-te cu ea.
Comentarii: lasă-ți firimituri
T-SQL are două stiluri de comentarii:
-- Single-line comment, like this
/*
Multi-line comment.
Useful for long explanations or
temporarily disabling a block of code.
*/
Scrie comentarii pentru contextul de business, nu pentru ceea ce SQL-ul face evident:
-- Bad comment: tells you what you can already read
-- Select customers
SELECT Name FROM Sales.Customer;
-- Good comment: tells you why
-- Italian customers, used by the marketing team's quarterly
-- "famiglie italiane" mailing campaign. Updated 2026-03-15.
SELECT Name FROM Sales.Customer WHERE Country = 'IT';
O interogare care va fi citită de o duzină de oameni timp de cinci ani are nevoie de comentarii. O explorare de aruncat nu. Folosește judecata.
Greșeli frecvente de începător, cu corecții
Greșeala 1: „Invalid column name” pe un alias care arată valid.
-- ERROR: 'Country' is invalid in WHERE
SELECT UPPER(CountryCode) AS Country FROM Sales.Orders WHERE Country = 'IT';
WHERE rulează înaintea SELECT în ordinea logică. Alias-ul nu există încă la pasul 4. Corectare:
SELECT UPPER(CountryCode) AS Country FROM Sales.Orders WHERE UPPER(CountryCode) = 'IT';
-- or better:
SELECT UPPER(CountryCode) AS Country FROM Sales.Orders WHERE CountryCode = 'IT';
A doua formă păstrează indexul pe CountryCode utilizabil (nicio funcție pe coloană).
Greșeala 2: Sortare după o coloană care nu e în lista SELECT.
De fapt, asta funcționează corect:
SELECT OrderId, Total FROM Sales.Orders ORDER BY OrderDate DESC;
ORDER BY rulează la pasul 9 și are acces la tot ce a produs FROM, nu doar la ce a ales SELECT. Rezultatul arată doar coloanele proiectate, dar poți sorta după orice coloană din tabelele sursă. Util și deseori uitat.
Greșeala 3: DISTINCT ca plasture pentru duplicate.
Dacă interogarea ta returnează rânduri duplicate la care nu te așteptai, cauza obișnuită este un JOIN care produce mai multe rânduri decât credeai. Adăugarea DISTINCT ascunde simptomul fără să rezolve cauza. Repară JOIN-ul.
Rulează asta pe propria mașină
În sfârșit punem niște date în Runehold și le interogăm. Lipește tot blocul dintr-o singură mișcare — se construiește pe baza de date din lecția 3.
USE Runehold;
GO
-- Create the Orders table if it doesn't exist
IF OBJECT_ID(N'Sales.Orders', N'U') IS NULL
BEGIN
CREATE TABLE Sales.Orders (
OrderId BIGINT IDENTITY(1,1) NOT NULL,
CustomerId INT NOT NULL,
OrderDate DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME(),
Total DECIMAL(19, 4) NOT NULL,
Currency CHAR(3) NOT NULL DEFAULT 'EUR',
CountryCode CHAR(2) NOT NULL,
VatRate DECIMAL(5, 4) NOT NULL,
CONSTRAINT pk_Orders PRIMARY KEY CLUSTERED (OrderId),
CONSTRAINT fk_Orders_Customer FOREIGN KEY (CustomerId)
REFERENCES Sales.Customer (CustomerId)
);
END;
-- Insert a few realistic orders (CustomerIds 1-4 from lesson 3)
INSERT INTO Sales.Orders (CustomerId, OrderDate, Total, CountryCode, VatRate)
VALUES (1, '2026-03-05 10:32:00', 59.00, 'NL', 0.2100),
(1, '2026-03-18 14:22:00', 29.00, 'NL', 0.2100),
(2, '2026-02-15 09:15:00', 149.00, 'IT', 0.2200),
(2, '2026-03-22 11:40:00', 89.50, 'IT', 0.2200),
(3, '2026-03-10 16:05:00', 199.00, 'DE', 0.1900),
(4, '2026-03-28 08:12:00', 42.42, 'RO', 0.1900);
-- Query 1: simple SELECT with aliases
SELECT OrderId,
CustomerId,
Total AS TotalEUR,
CountryCode AS Country
FROM Sales.Orders
ORDER BY Total DESC;
-- Query 2: compute a VAT breakdown on the fly
SELECT OrderId,
Total AS GrossEUR,
Total / (1 + VatRate) AS NetEUR,
Total - (Total / (1 + VatRate)) AS VatEUR,
CAST(VatRate * 100 AS DECIMAL(5,2)) AS VatPercent
FROM Sales.Orders
ORDER BY OrderDate;
-- Query 3: the top-3 biggest orders in Italy (joins across tables)
SELECT TOP (3)
o.OrderId,
c.Name AS CustomerName,
o.Total AS TotalEUR,
CAST(o.OrderDate AS DATE) AS OrderDay
FROM Sales.Orders AS o
JOIN Sales.Customer AS c ON c.CustomerId = o.CustomerId
WHERE o.CountryCode = 'IT'
ORDER BY o.Total DESC;
-- Query 4: notice the logical order — alias works in ORDER BY, not in WHERE
SELECT o.OrderId,
o.Total * 100 AS TotalInCents
FROM Sales.Orders AS o
WHERE o.Total > 50 -- filter uses original column
ORDER BY TotalInCents DESC; -- ORDER BY can use the alias
Citește rezultatele. Scrie câteva interogări de-ale tale. Sparge-le intenționat ca să vezi erorile.
Lecția următoare: WHERE, NULL și de ce filtrele tale uneori te mint. E mai scurtă decât asta și conține acel singur fapt T-SQL care îi pune piedică oricui a învățat vreodată SQL.