Prima dată când am văzut un tabel de producție unde fiecare coloană era NVARCHAR(MAX), am crezut că trebuie să fie o bază de date de test pe care a uitat cineva s-o curețe. Nu era. Era masterul de clienți. Cineva, cu ani în urmă, decisese că „MAX înseamnă maxim, și vrem să fim în siguranță”, iar tabelul crescuse la patru terabytes în timp ce returna câte un client la fiecare query.
Tipurile de date nu sunt plictisitoare. Sunt cel mai mare lucru pe care-l poți face bine în prima zi care-ți va salva viitorul de la a plânge la 3 dimineața. Decid cât spațiu pe disc iau datele tale, cât de repede rulează query-urile, cum estimează SQL Server cardinalitatea, dacă indexurile funcționează, dacă poți sorta corect, dacă backup-urile încap pe banda de noapte. Decid dacă un plan de execuție alege un index seek sau un table scan. Decid dacă un JOIN folosește hash sau merge. Decid dacă șeful te concediază.
Lecția asta acoperă tipurile pe care le vei folosi 95% din timp, ce să alegi, ce să eviți și de ce. Printeaz-o. Lipește-o pe monitor. Citește-o din nou peste o săptămână.
Cele trei categorii
SQL Server are vreo 30 de tipuri de date. Cad în trei categorii:
- Numerice — numere întregi, zecimale, bani.
- Caracter / șir — șiruri pe un singur byte, șiruri Unicode, șiruri scurte de lungime fixă, blob-uri uriașe.
- Dată și oră — date, ore, datetimes, datetimes cu fus orar.
Plus o mână de tipuri speciale (BIT, UNIQUEIDENTIFIER, VARBINARY, XML, JSON în 2025+, tipuri spațiale, sql_variant) pe care le vei folosi mai rar.
Hai să trecem prin fiecare categorie cu opinii.
Tipuri numerice
Întregi: TINYINT, SMALLINT, INT, BIGINT
Patru tipuri de întregi, în ordinea mărimii:
| Tip | Bytes | Interval |
|---|---|---|
TINYINT | 1 | 0 până la 255 (fără negative!) |
SMALLINT | 2 | -32.768 până la +32.767 |
INT | 4 | ~-2,1 miliarde până la +2,1 miliarde |
BIGINT | 8 | ~-9,2 cvintilioane până la +9,2 cvintilioane |
Modelul mental: folosește-l pe cel mai mic care nu va rămâne fără spațiu vreodată.
TINYINT e excelent pentru lucruri ca status code-uri, numere de luni sau o enumerare mică. Dacă valorile încap în 0-255, folosește-l.
SMALLINT pentru numere de an, numărători mici, lucruri care nu vor depăși 30k.
INT e calul de tracțiune. Chei primare auto-incrementale, contoare, cantități, aproape tot. Intervalul de două miliarde de rânduri e mai mult decât suficient pentru 95% din cazuri.
BIGINT pentru când vei depăși două miliarde de rânduri sau ai nevoie de numărători mari. Tabele de evenimente, tabele de log, sisteme OLTP cu volum mare. Iată regula: orice tabel unde te aștepți la peste 500 de milioane de rânduri pe parcursul vieții lui ar trebui să aibă o cheie primară BIGINT din ziua unu. Conversia unui PK INT la BIGINT mai târziu e posibilă dar dureroasă, iar dacă ești referențiat de 20 de chei străine le vei schimba pe toate.
Zecimale: DECIMAL(p, s) / NUMERIC(p, s)
Același lucru, nume diferit. Folosește DECIMAL. p e precizia (total cifre), s e scala (cifre după virgulă). DECIMAL(18, 2) poate stoca numere precum 1234567890123456,78.
Folosește DECIMAL pentru toți banii, toate procentele și orice trebuie să însumezi sau să compari exact. E precis. Fără erori de rotunjire. Mai lent decât întregii dar merită.
Floats: FLOAT și REAL
Virgulă mobilă aproximativă. FLOAT(53) e dublă precizie pe 8 bytes. REAL e simplă precizie pe 4 bytes. Rapid, dar erorile de rotunjire sunt reale. SELECT 0.1 + 0.2 returnează 0.3; SELECT CAST(0.1 AS FLOAT) + CAST(0.2 AS FLOAT) returnează 0.30000000000000004.
Folosește FLOAT doar pentru valori științifice unde precizia nu trebuie să fie exactă: citiri de senzori, măsuri statistice, latitudini estimate. Niciodată să nu folosești FLOAT pentru bani. Am văzut un job de reconciliere financiară care producea o discrepanță de 0,0000001€ în fiecare lună din exact motivul ăsta. Constatarea de audit a durat trei luni să fie închisă.
Bani: MONEY și SMALLMONEY
Nu. MONEY arată ca și cum ar fi proiectat pentru valută, dar are comportament ciudat de precizie, reguli proaste de conversie de tip, iar economia de stocare față de DECIMAL(19, 4) e neglijabilă. Consensul industriei de cincisprezece ani e folosește DECIMAL(19, 4) pentru valută, nu MONEY.
Booleeni: BIT
SQL Server n-are un tip BOOLEAN propriu-zis. Are BIT, care stochează 0, 1 sau NULL. Până la 8 coloane BIT pe un rând împart un singur byte, deci sunt eficiente.
Atenție: coloanele BIT nu se pot folosi în toate locurile ca întregii. Nu poți face WHERE IsActive = TRUE — nu există cuvântul cheie TRUE în SQL. Folosește WHERE IsActive = 1.
Tipuri de șiruri
Aici fac majoritatea oamenilor cele mai multe greșeli. Citește atent.
Fix vs variabil: CHAR vs VARCHAR, NCHAR vs NVARCHAR
CHAR(n)/NCHAR(n)— lungime fixă. Mereuncaractere, completate cu spații. Risipește spațiu dacă majoritatea valorilor sunt mai scurte.VARCHAR(n)/NVARCHAR(n)— lungime variabilă. Stochează exact șirul pe care i-l dai, plus 2 bytes de metadate de lungime.
Aproape mereu folosește VARCHAR sau NVARCHAR. CHAR cu lungime fixă e potrivit doar când coloana e cu adevărat fixă (cod de țară CHAR(2), cod de valută CHAR(3)), pentru că completarea cu spații doare iar lungimea variabilă e mai flexibilă.
Single-byte vs Unicode: VARCHAR vs NVARCHAR
VARCHAR— 1 byte per caracter (pentru ASCII). Folosește collation-ul pe care-l are coloana, ceea ce-i determină setul de caractere și ordinea de sortare. Poate stoca caractere cu accent în collation-uri vest-europene la 1 byte per caracter folosind intervalul extins 0-255. Nu poate stoca chineză, arabă, emoji, sau majoritatea scrierilor ne-latine.NVARCHAR— 2 bytes per caracter (UTF-16). Poate stoca orice caracter Unicode. Stocare dublă pentru date doar-ASCII, dar nu-i pasă de drama collation-ului.
În 2026, regula mea e: implicit folosește NVARCHAR decât dacă știi că nu ai nevoie de Unicode. Da, dublează stocarea pentru date ASCII. Discul e ieftin; rapoartele de bug-uri de la clienți japonezi cărora baza ta de date le corupe numele nu sunt. Singura excepție: coduri interne pe care le controlezi (status code-uri, coduri ISO de țară, SKU-uri) unde garantezi doar ASCII. Acelea sunt ok ca VARCHAR.
SQL Server 2019 a adăugat un collation UTF-8 care permite coloanelor VARCHAR să stocheze UTF-8. Dacă ești pe 2019+ și fericit să fixezi un collation specific, VARCHAR cu un collation UTF-8 îți dă ce-i mai bun din ambele lumi: codare cu lățime variabilă care e încă single-byte pentru ASCII. E o mișcare de profesionist; dacă te încurcă, folosește NVARCHAR și mergi mai departe.
Cât de mare ar trebui să fie n?
O greșeală foarte comună: VARCHAR(50) pentru nume, VARCHAR(255) pentru email-uri, VARCHAR(max) „doar ca să fie în siguranță.”
Regula: dimensionează pentru utilizarea din lumea reală, nu pentru cazuri extreme limită.
- Nume:
NVARCHAR(100)e mult. Aproape niciun nume real nu e mai lung de 100 de caractere. - Email-uri:
NVARCHAR(254)— ăsta e limita RFC. - Numere de telefon:
NVARCHAR(30)— lasă loc pentru prefixe de țară și formatare. - URL-uri:
NVARCHAR(2048)— browserele plafonează de obicei la cam 2000. - Note de formă liberă:
NVARCHAR(4000)sauNVARCHAR(MAX)dacă ai cu adevărat nevoie de mare.
De ce nu doar NVARCHAR(MAX) peste tot? Pentru că MAX e diferit sub capotă. Valorile până la 8000 bytes sunt stocate in-row. Valorile mai mari ajung să fie stocate în pagini LOB separate, cu un pointer în rând. Motorul tratează coloanele MAX mai prudent: nu pot fi parte dintr-o cheie de index, au restricții în anumite funcționalități T-SQL, iar sortarea on-the-fly și grant-urile de memorie devin ciudate. Implicit folosește tipuri dimensionate. Folosește MAX când chiar ai nevoie de nelimitat.
TEXT, NTEXT, IMAGE
Nu. Astea sunt deprecated, sunt deprecated din SQL Server 2005 și vor fi eventual eliminate. Dacă le vezi într-o bază de date veche, planifică o migrare la VARCHAR(MAX), NVARCHAR(MAX) sau VARBINARY(MAX). Le mai găsesc ocazional în sălbăticie, mereu în cod care datează din administrația Bush.
Tipuri de dată și oră
DATETIME — cel vechi
Tipul mai vechi. 8 bytes. Precizie de 3,33 milisecunde (rotunjește la .000, .003, .007). Interval de la 1753 la 9999. Asta folosesc bazele de date mai vechi.
Nu-l folosi pentru cod nou. Precizia e ciudată, intervalul e uriaș, și e înlocuit de tipuri mai bune.
DATETIME2(n) — cel modern
6 până la 8 bytes. Precizie până la 100 nanosecunde. Interval de la 0001 la 9999.
DATETIME2(0) — fără secunde fracționare. Precizie la secundă. 6 bytes. Excelent pentru majoritatea timestamp-urilor.
DATETIME2(3) — precizie la milisecundă. 7 bytes. Folosește-l dacă trebuie să corelezi evenimente sub-secundă.
DATETIME2(7) — precizie completă de 100ns. 8 bytes. Folosește pentru log-uri și date de evenimente.
Implicit folosește DATETIME2(0) sau DATETIME2(3). Mai precis decât DATETIME, mai mic, iar intervalul e rezonabil.
DATE și TIME
Încă două pentru când vrei doar data sau doar ora.
DATE— 3 bytes. Interval 0001 la 9999. Folosește pentru zile de naștere, date de evenimente, orice unde ora din zi e irelevantă.TIME(n)— 3 până la 5 bytes. Folosește rar; de cele mai multe ori vrei un datetime complet.
DATETIMEOFFSET(n) — cu fus orar
10 bytes. Stochează datetime-ul plus un offset de la UTC.
Asta e alegerea corectă dacă trebuie să păstrezi informația de fus orar a evenimentului original. A stoca „utilizatorul a apăsat la 14:30 ora locală în Milano, care e 13:30 UTC” e legitim diferit de a stoca „13:30 UTC fără idee la ce se gândea.” Vom avea o lecție întreagă pe mlaștina fusurilor orare (lecția 15). Pentru moment: dacă chiar ai nevoie de conștientizare a fusului orar, folosește DATETIMEOFFSET. Dacă nu, folosește DATETIME2 și stochează totul în UTC.
SMALLDATETIME — nu
4 bytes, precizie la minut, interval 1900 la 2079. Obscur, economisește câțiva bytes, te vede în 2079. Sari peste.
Tipurile speciale care merită știute
UNIQUEIDENTIFIER
16 bytes. Stochează un GUID. Generează cu NEWID() (aleator) sau NEWSEQUENTIALID() (monoton).
Util pentru sisteme distribuite, chei primare care trebuie generate în afara bazei de date și integrare cu sisteme care folosesc GUID-uri. Alegere proastă pentru un index clustered (vezi lecția 21). Folosește-l când chiar ai nevoie de un ID global unic; nu-l folosi pentru că pare modern.
VARBINARY(n) / VARBINARY(MAX)
Pentru date binare — fișiere, imagini, blob-uri criptate. MAX pentru orice e mare. În majoritatea aplicațiilor probabil ar trebui să stochezi fișiere mari în blob storage (S3, Azure Blob) și URL-ul în SQL Server, dar VARBINARY(MAX) există pentru când chiar ai nevoie de binar în baza de date.
BIT
Booleeni. Acoperit mai sus.
XML și JSON
XML e un tip XML cu funcționalități complete și suport de interogare. Rar e alegerea potrivită în 2026 dacă nu ești deja greu pe XML.
JSON a fost, ani buni, „pune-l pur și simplu în NVARCHAR(MAX) și SQL Server are funcții să-l interogheze.” SQL Server 2025 introduce un tip de date JSON real cu stocare binară. Dacă ești pe o versiune mai veche, NVARCHAR(MAX) + JSON_VALUE() / JSON_QUERY() / OPENJSON() e idiomul.
Conversii implicite: ucigașul tăcut
Fiecare alegere de tip de date are un efect domino: comparațiile și join-urile merg mai bine când tipurile se potrivesc.
Dacă ai CustomerId INT și scrii:
SELECT * FROM Customer WHERE CustomerId = '42';
SQL Server face o conversie implicită pe partea coloanei. Șirul '42' se convertește la INT întâi (de fapt motorul convertește fiecare CustomerId la VARCHAR și apoi compară, după regulile încorporate de precedență a tipurilor). Rezultatul: indexul tău pe CustomerId nu mai poate face seek. Tocmai ai retrogradat un seek într-un scan, și nu vei vedea nicio eroare, doar un query mai lent.
Iată regula pe care Microsoft o numește „SARGability” (Search ARGument-able): un predicat e SARGable când SQL Server îl poate folosi pentru a face seek pe un index. Funcțiile, conversiile implicite și aritmetica pe coloana indexată sparg toate SARGability-ul. Bine:
WHERE CreatedAt >= '2025-01-01' AND CreatedAt < '2026-01-01'
Rău:
WHERE YEAR(CreatedAt) = 2025
Rău:
WHERE CAST(CreatedAt AS DATE) = '2025-12-25'
Bine:
WHERE CreatedAt >= '2025-12-25' AND CreatedAt < '2025-12-26'
Potrivește-ți tipurile. Nu pune funcții pe coloana indexată. Indexurile tale îți vor mulțumi.
Collation: schimbarea de scenariu
Fiecare coloană de șir are un collation — un set de reguli care determină cum se compară și se sortează caracterele. SQL_Latin1_General_CP1_CI_AS e un default comun (case-insensitive, accent-sensitive vest-european). Latin1_General_100_CS_AS_SC_UTF8 e un collation Unicode case-sensitive UTF-8 introdus în SQL Server 2019.
Collation-urile afectează:
- Dacă
'foo' = 'FOO'e adevărat (sensibilitate la majuscule) - Dacă
'cafe' = 'café'e adevărat (sensibilitate la accent) - Ordinea de sortare (alfabetică? specifică unei culturi?)
Amestecarea collation-urilor în join-uri cauzează conflicte de collation care produc eroarea distractivă: Cannot resolve the collation conflict between "X" and "Y" in the equal to operation. O rezolvi castând explicit o parte: col1 COLLATE Latin1_General_CI_AS = col2.
Lecție: alege un collation pentru baza ta de date, ține-te de el pe toate coloanele de șir și nu importa date cu un collation diferit decât dacă ești gata pentru distracție.
Rulează asta pe propria mașină
Un mic demo despre de ce contează tipurile de date. Copiază-lipește, rulează, citește timpii.
USE tempdb;
GO
-- Două tabele, unul bine tipizat, unul neglijent
CREATE TABLE dbo.WellTyped (
CustomerId INT NOT NULL,
Name NVARCHAR(100) NOT NULL,
CreatedAt DATETIME2(0) NOT NULL
);
CREATE TABLE dbo.Sloppy (
CustomerId NVARCHAR(50) NOT NULL, -- numeric stocat ca șir
Name NVARCHAR(MAX) NOT NULL, -- nelimitat fără motiv
CreatedAt DATETIME NOT NULL -- tipul vechi
);
-- Inserează 1M de rânduri în fiecare
WITH Nums AS (
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY a.object_id) AS n
FROM sys.all_objects a CROSS JOIN sys.all_objects b
)
INSERT INTO dbo.WellTyped (CustomerId, Name, CreatedAt)
SELECT n,
CONCAT(N'Customer ', n),
DATEADD(SECOND, n, '2020-01-01')
FROM Nums;
WITH Nums AS (
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY a.object_id) AS n
FROM sys.all_objects a CROSS JOIN sys.all_objects b
)
INSERT INTO dbo.Sloppy (CustomerId, Name, CreatedAt)
SELECT CAST(n AS NVARCHAR(50)),
CONCAT(N'Customer ', n),
DATEADD(SECOND, n, '2020-01-01')
FROM Nums;
-- Compară mărimile
SELECT OBJECT_NAME(ps.object_id) AS table_name,
SUM(ps.reserved_page_count) * 8 / 1024 AS reserved_mb
FROM sys.dm_db_partition_stats ps
WHERE ps.object_id IN (OBJECT_ID('dbo.WellTyped'), OBJECT_ID('dbo.Sloppy'))
GROUP BY ps.object_id;
-- Query pe coloana CustomerId — tip nepotrivit
SET STATISTICS IO, TIME ON;
SELECT Name FROM dbo.WellTyped WHERE CustomerId = 12345;
SELECT Name FROM dbo.Sloppy WHERE CustomerId = '12345'; -- potrivire șir cu șir
SET STATISTICS IO, TIME OFF;
-- Curățenie
DROP TABLE dbo.WellTyped;
DROP TABLE dbo.Sloppy;
Rulează-l. Notează diferența de spațiu rezervat, timpii de query, statisticile IO. Tabelul neglijent va fi semnificativ mai mare. Într-un sistem real cu 500 de milioane de rânduri, diferența e diferența dintre a încăpea în fereastra ta de backup și a nu încăpea.
Lecția următoare: CREATE, ALTER, DROP — facerea și desfacerea tabelelor, cu pattern-uri practice și exercițiul „o, nu, am dat drop la ce nu trebuie”.