SQL Server Data Types Explained: Every Major Type, and Which One to Actually Use
Every column has exactly one data type — SQL Server enforces it strictly, unlike a spreadsheet cell that’ll happily hold text or a number interchangeably. This lesson covers every data type category you’ll realistically encounter: exact and approximate numbers, character strings, dates and times, binary data, and the “special purpose” types like UNIQUEIDENTIFIER and XML.
Exact Numeric Types
Use these when the value must be precisely correct — counts, IDs, money. No rounding error, ever.
| Type | Storage | Range | Use for |
|---|---|---|---|
TINYINT |
1 byte | 0 to 255 | Small counters, status codes, ages |
SMALLINT |
2 bytes | -32,768 to 32,767 | Small ranges — year, quantity in a typical order |
INT |
4 bytes | ±2.1 billion | The default choice for IDs, counts, foreign keys |
BIGINT |
8 bytes | ±9.2 quintillion | High-volume identity columns (event logs, telemetry) that will exceed 2.1 billion rows |
DECIMAL(p,s) / NUMERIC(p,s) |
5–17 bytes (depends on precision) | Exact, defined by precision p and scale s |
Money, measurements, anything requiring exact arithmetic — DECIMAL and NUMERIC are functionally identical, DECIMAL is the conventional spelling |
MONEY |
8 bytes | ±922 trillion, 4 decimal places | Legacy currency type — most teams prefer DECIMAL(19,4) for portability and clearer rounding behavior |
SMALLMONEY |
4 bytes | ±214,748.3648 | Rarely used; same caveats as MONEY at a smaller range |
Approximate Numeric Types
| Type | Storage | Use for |
|---|---|---|
FLOAT |
4 or 8 bytes | Scientific/statistical values where tiny binary rounding error is acceptable — never for money |
REAL |
4 bytes | Lower-precision FLOAT(24); rarely chosen deliberately today |
FLOAT is a binary approximation and can accumulate tiny rounding errors — summing many FLOAT values can produce $100.00000000001 instead of exactly $100.00. DECIMAL stores exact values with zero rounding error, which is why it’s the correct choice for financial data.
Character String Types
| Type | Storage | Use for |
|---|---|---|
CHAR(n) |
Fixed n bytes, space-padded | Fixed-length codes — country codes, status flags (e.g. CHAR(2) for ‘US’) |
VARCHAR(n) |
Variable, up to n bytes | Variable-length ASCII/Latin text — names, addresses, single-language content |
VARCHAR(MAX) |
Up to 2GB | Long non-Unicode text — logs, descriptions |
NCHAR(n) |
Fixed 2n bytes | Fixed-length Unicode — rare |
NVARCHAR(n) |
Variable, up to 2n bytes | Variable-length Unicode text — the default recommendation for almost all text, since it safely holds any language or emoji |
NVARCHAR(MAX) |
Up to 2GB | Long Unicode text — articles, JSON payloads, free-form notes |
TEXT / NTEXT |
— | Deprecated. Use VARCHAR(MAX) / NVARCHAR(MAX) instead |
Rule of thumb: default to NVARCHAR. The storage cost difference versus VARCHAR is small, and it avoids an entire class of bugs where a user’s name or a customer’s address contains a character your ASCII column silently mangles.
Date and Time Types
| Type | Storage | Precision | Use for |
|---|---|---|---|
DATE |
3 bytes | Day | Birthdates, due dates — anything with no time component |
TIME |
3–5 bytes | Up to 100ns | A time of day with no date — daily opening hours, alarm times |
SMALLDATETIME |
4 bytes | Minute | Legacy; low precision, narrow range |
DATETIME |
8 bytes | ~3ms, rounded | Legacy — the older default; rounding quirks make DATETIME2 the better modern choice |
DATETIME2 |
6–8 bytes | Up to 100ns | The modern default for date + time — wider range and better precision than DATETIME, at equal or smaller storage |
DATETIMEOFFSET |
8–10 bytes | Up to 100ns | Date + time + UTC offset — required whenever you need to store a time zone alongside the timestamp (multi-region applications) |
Binary Types
| Type | Storage | Use for |
|---|---|---|
BINARY(n) |
Fixed n bytes | Fixed-length raw bytes — hashes of a known fixed length |
VARBINARY(n) |
Variable, up to n bytes | Variable-length raw bytes — small file attachments, encrypted blobs |
VARBINARY(MAX) |
Up to 2GB | Large binary objects — documents, images stored in-database |
IMAGE |
— | Deprecated. Use VARBINARY(MAX) instead |
Other Special-Purpose Types
| Type | Use for |
|---|---|
BIT |
True/false flags (0, 1, or NULL) — SQL Server packs up to 8 BIT columns into a single byte |
UNIQUEIDENTIFIER |
A 16-byte GUID — useful for IDs that must be unique across multiple databases/systems without coordination, at the cost of larger, less sequential index keys than INT |
XML |
Native XML storage with schema validation and XQuery support |
SQL_VARIANT |
Stores a value of almost any base type in one column — rare, generally avoided since it defeats type-checking and indexing efficiency |
ROWVERSION (formerly TIMESTAMP) |
An automatically-incrementing binary value per row, used for optimistic concurrency checks — not an actual date/time despite the old name |
HIERARCHYID |
Compact encoding of a position in a tree — org charts, category trees |
GEOGRAPHY / GEOMETRY |
Spatial types for round-earth (GEOGRAPHY) or planar (GEOMETRY) coordinates, with built-in distance/intersection methods |
There’s no dedicated JSON type in SQL Server — JSON is stored as NVARCHAR(MAX) and manipulated with built-in functions like JSON_VALUE, JSON_QUERY, and ISJSON.
A Real Table Using a Representative Mix
CREATE TABLE dbo.Example_DataTypes (
id INT IDENTITY(1,1) PRIMARY KEY,
public_id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
full_name NVARCHAR(100) NOT NULL,
country_code CHAR(2) NOT NULL,
price DECIMAL(10,2) NOT NULL,
quantity SMALLINT NOT NULL,
is_active BIT NOT NULL DEFAULT 1,
signed_up_on DATE NOT NULL DEFAULT GETDATE(),
last_login DATETIME2 NULL,
profile_json NVARCHAR(MAX) NULL,
avatar VARBINARY(MAX) NULL,
row_version ROWVERSION
);
Why DECIMAL, Never FLOAT, for Money
This is one of the most common code-review flags in real SQL Server codebases: FLOAT’s binary rounding makes it fundamentally unsuitable for currency, while DECIMAL guarantees exact values.
The Rule of Thumb
Pick the smallest type that can never realistically overflow, and default to Unicode (NVARCHAR) for text and DATETIME2 for timestamps unless you have a specific reason not to. Using NVARCHAR(MAX) or BIGINT everywhere “to be safe” wastes storage and slows indexes down — you’ll see exactly why once we get to performance tuning.
Enjoyed this?
Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.
📡 Subscribe via RSS |
Share on X |
Share on LinkedIn |
Share on Facebook
Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server Fundamentals, coming soon on this site.