| Kiểu | Bytes | Giá trị min | Giá trị max | Dùng khi |
tinyint | 1 | 0 | 255 | Cột nhỏ như status, age (0-120) |
smallint | 2 | -32,768 | 32,767 | ID nhỏ, year |
int | 4 | -2,147,483,648 | 2,147,483,647 | FK, PK cho hầu hết tables |
bigint | 8 | -9.2 × 10¹⁸ | 9.2 × 10¹⁸ | ID lớn, counts, Unix timestamps |
-- Ví dụ sử dụng
CREATE TABLE Products (
ProductId INT NOT NULL, -- PK thông thường
CategoryId TINYINT NOT NULL, -- Giả sử < 256 categories
StockCount SMALLINT NOT NULL, -- Tồn kho không quá 32767
TotalOrders BIGINT NOT NULL DEFAULT 0 -- Có thể rất lớn
);
-- Tính toán có thể overflow nếu dùng sai type
SELECT 2000000000 + 2000000000; -- Overflow với INT!
SELECT CAST(2000000000 AS BIGINT) + 2000000000; -- OK
-- Kiểm tra giới hạn
SELECT
'tinyint' AS type_name, CAST(255 AS TINYINT) AS max_val UNION ALL
SELECT 'smallint', CAST(32767 AS SMALLINT) UNION ALL
SELECT 'int', CAST(2147483647 AS INT) UNION ALL
SELECT 'bigint', CAST(9223372036854775807 AS BIGINT);
DECIMAL(p, s) và NUMERIC(p, s) là synonyms, lưu số thập phân chính xác:
- p (precision): tổng số chữ số (1-38)
- s (scale): số chữ số sau dấu thập phân (0 đến p)
| Precision | Storage |
| 1-9 | 5 bytes |
| 10-19 | 9 bytes |
| 20-28 | 13 bytes |
| 29-38 | 17 bytes |
-- Dùng cho tiền tệ và số thập phân cần chính xác
DECLARE @price DECIMAL(10, 2); -- Tối đa 99999999.99
DECLARE @rate DECIMAL(5, 4); -- Tối đa 9.9999 (ví dụ: tax rate)
DECLARE @qty NUMERIC(18, 3); -- Lượng với 3 chữ số thập phân
-- Ví dụ tính toán chính xác
SELECT CAST(0.1 AS DECIMAL(10,1)) + CAST(0.2 AS DECIMAL(10,1)); -- 0.3 chính xác
SELECT 0.1 + 0.2; -- 0.300000000000... (float, không chính xác)
| Kiểu | Bytes | Ký hiệu khoa học | Chính xác |
real | 4 | ~7 chữ số | Approximate |
float | 8 | ~15 chữ số | Approximate |
float(n) | 4 hoặc 8 | n = 1-53 | Approximate |
Cảnh báo: float và real là approximate - KHÔNG dùng cho tiền tệ, kế toán, hoặc bất kỳ tính toán nào cần độ chính xác tuyệt đối!
-- Vấn đề với float
SELECT CAST(0.1 AS FLOAT) + CAST(0.2 AS FLOAT);
-- Kết quả: 0.30000000000000004 (sai!)
-- Float thích hợp cho: tọa độ GPS, đo lường khoa học, phân tích thống kê
CREATE TABLE SensorData (
ReadingId INT PRIMARY KEY,
Latitude FLOAT NOT NULL, -- GPS coordinates
Longitude FLOAT NOT NULL,
Temperature REAL NOT NULL -- Nhiệt độ, sai số nhỏ chấp nhận được
);
| Kiểu | Bytes | Phạm vi | Chính xác |
money | 8 | ±922 trillion | 4 decimal places |
smallmoney | 4 | ±214,748 | 4 decimal places |
-- money type
DECLARE @price MONEY = 19.99;
DECLARE @tax MONEY = 0.10;
SELECT @price + @tax; -- 20.09
-- Vấn đề với money: phép chia có thể mất độ chính xác
DECLARE @total MONEY = 100;
SELECT @total / 3; -- 33.3333 (OK)
SELECT @total / 3 * 3; -- 99.9999 (mất 0.0001!)
-- Khuyến nghị: dùng DECIMAL(19, 4) thay vì MONEY để có kiểm soát tốt hơn
| Kiểu | Encoding | bytes/ký tự | Tối đa |
char(n) | Non-Unicode (ASCII) | 1 | 8,000 ký tự |
varchar(n) | Non-Unicode (ASCII) | 1 | 8,000 ký tự |
varchar(MAX) | Non-Unicode | 1 | 2 GB |
nchar(n) | Unicode (UTF-16) | 2 | 4,000 ký tự |
nvarchar(n) | Unicode (UTF-16) | 2 | 4,000 ký tự |
nvarchar(MAX) | Unicode | 2 | 2 GB |
-- char(n): fixed-length, padding bằng spaces nếu ngắn hơn n
DECLARE @fixed CHAR(10) = 'Hello';
SELECT LEN(@fixed); -- 5 (LEN bỏ trailing spaces)
SELECT DATALENGTH(@fixed); -- 10 (luôn 10 bytes)
-- varchar(n): variable-length, không padding
DECLARE @var VARCHAR(10) = 'Hello';
SELECT LEN(@var); -- 5
SELECT DATALENGTH(@var); -- 5
-- Khi nào dùng char: dữ liệu có độ dài cố định (mã code, country code)
CREATE TABLE Countries (
CountryCode CHAR(2) NOT NULL, -- Luôn đúng 2 ký tự: 'US', 'VN', 'JP'
PhoneCode CHAR(4), -- '+84 ', '+1 '
CountryName VARCHAR(100) NOT NULL
);
-- Dùng N prefix để tạo Unicode string literal
INSERT INTO Employees (FullName) VALUES (N'Nguyễn Văn An'); -- Đúng với dấu
INSERT INTO Employees (FullName) VALUES ('Nguyễn Văn An'); -- Có thể sai nếu column là nvarchar
-- Khi nào dùng nvarchar:
-- - Dữ liệu nhiều ngôn ngữ (tiếng Việt, tiếng Trung, Arabic, etc.)
-- - Tên người, địa chỉ
-- - Nội dung do user nhập
-- Khi nào dùng varchar:
-- - Codes, IDs, email addresses (ASCII-safe)
-- - Dữ liệu hệ thống không cần Unicode
-- - Tiết kiệm storage khi biết chắc chỉ dùng ASCII
-- So sánh storage
CREATE TABLE StorageDemo (
col_varchar VARCHAR(100) = 'Hello',
col_nvarchar NVARCHAR(100) = N'Hello'
);
-- varchar: 5 bytes
-- nvarchar: 10 bytes (2 bytes/ký tự)
-- Dùng cho dữ liệu text rất lớn (tới 2GB)
CREATE TABLE Articles (
ArticleId INT PRIMARY KEY,
Title NVARCHAR(500) NOT NULL,
Content NVARCHAR(MAX), -- Nội dung dài
HtmlContent VARCHAR(MAX) -- HTML content (ASCII)
);
-- Chú ý hiệu năng với MAX:
-- - Không thể là key trong index
-- - Storage khác: có thể lưu inline (nếu < 8000 bytes) hoặc LOB pages
-- - Dùng .WRITE() để update hiệu quả
UPDATE Articles
SET Content.WRITE(N' Thêm nội dung', DATALENGTH(Content)/2, 0)
WHERE ArticleId = 1;
-- KHÔNG dùng nữa từ SQL Server 2005+
-- text: tương tự varchar(MAX) nhưng deprecated
-- ntext: tương tự nvarchar(MAX) nhưng deprecated
-- Nếu thấy trong code cũ, convert sang varchar(MAX)/nvarchar(MAX):
ALTER TABLE OldTable ALTER COLUMN OldTextCol NVARCHAR(MAX);
| Kiểu | Bytes | Phạm vi | Độ chính xác | Timezone |
datetime | 8 | 1753-01-01 ~ 9999-12-31 | 3.33ms | Không |
smalldatetime | 4 | 1900-01-01 ~ 2079-06-06 | 1 phút | Không |
datetime2(n) | 6-8 | 0001-01-01 ~ 9999-12-31 | 100ns | Không |
date | 3 | 0001-01-01 ~ 9999-12-31 | 1 ngày | Không |
time(n) | 3-5 | 00:00:00 ~ 23:59:59.9… | 100ns | Không |
datetimeoffset(n) | 8-10 | 0001-01-01 ~ 9999-12-31 | 100ns | Có (+/-14h) |
-- datetime - loại cũ, tránh dùng cho code mới
DECLARE @dt DATETIME = '2026-04-01 10:30:00.000';
-- datetime2 - khuyến nghị thay datetime
DECLARE @dt2 DATETIME2(7) = '2026-04-01 10:30:00.1234567'; -- 7 decimal seconds
DECLARE @dt2s DATETIME2(0) = '2026-04-01 10:30:00'; -- Chỉ đến giây
-- date - chỉ cần ngày, không cần giờ
DECLARE @d DATE = '2026-04-01';
DECLARE @birthDate DATE = '1990-05-15';
-- time - chỉ cần giờ
DECLARE @t TIME(0) = '10:30:00'; -- Đến giây
DECLARE @t7 TIME(7) = '10:30:00.1234567'; -- Cao nhất
-- datetimeoffset - khi cần lưu timezone
DECLARE @dto DATETIMEOFFSET = '2026-04-01 10:30:00 +07:00';
SELECT SWITCHOFFSET(@dto, '+00:00'); -- Convert sang UTC
-- Ví dụ thực tế
CREATE TABLE Orders (
OrderId INT PRIMARY KEY,
OrderDate DATE NOT NULL, -- Chỉ cần ngày
DeliveryTime TIME(0), -- Chỉ cần giờ giao
CreatedAt DATETIME2(7) NOT NULL DEFAULT SYSDATETIME(), -- Timestamp đầy đủ
UpdatedAt DATETIME2(7),
ScheduledAt DATETIMEOFFSET -- Nếu có múi giờ
);
-- So sánh datetime vs datetime2
SELECT
CAST('2026-04-01' AS DATETIME) AS old_datetime,
CAST('2026-04-01' AS DATETIME2) AS new_datetime2;
-- datetime: '2026-04-01 00:00:00.000'
-- datetime2: '2026-04-01 00:00:00.0000000'
| Kiểu | Mô tả | Tối đa |
binary(n) | Fixed-length binary | 8,000 bytes |
varbinary(n) | Variable-length binary | 8,000 bytes |
varbinary(MAX) | Variable-length binary | 2 GB |
image | Deprecated, thay bằng varbinary(MAX) | 2 GB |
-- Dùng cho: hình ảnh, files, hashes, encrypted data
CREATE TABLE Documents (
DocumentId INT PRIMARY KEY,
FileName NVARCHAR(255) NOT NULL,
FileContent VARBINARY(MAX), -- File content
ContentHash BINARY(32), -- SHA-256 hash (32 bytes)
Thumbnail VARBINARY(MAX)
);
-- Lưu hash của password (KHÔNG lưu plain text!)
-- Trong thực tế nên hash ở application layer, không ở SQL
-- HASHBYTES để tạo hash
SELECT HASHBYTES('SHA2_256', N'password123'); -- Trả về varbinary(32)
-- Convert hex string sang binary và ngược lại
SELECT CONVERT(VARCHAR(64), HASHBYTES('SHA2_256', N'test'), 2); -- Hex string
-- bit: 0, 1, hoặc NULL. SQL Server tối ưu lưu 8 bit columns vào 1 byte
CREATE TABLE Settings (
SettingId INT PRIMARY KEY,
IsActive BIT NOT NULL DEFAULT 1,
IsDeleted BIT NOT NULL DEFAULT 0,
IsPublic BIT NOT NULL DEFAULT 0,
HasExpiry BIT NOT NULL DEFAULT 0
-- 4 bit columns ở trên dùng chỉ 1 byte storage
);
-- Convert từ string
SELECT CAST('true' AS BIT); -- Error! BIT không nhận 'true'
SELECT CAST(1 AS BIT); -- 1
SELECT CAST(0 AS BIT); -- 0
SELECT IIF(SomeCondition, CAST(1 AS BIT), CAST(0 AS BIT));
-- Lưu GUID (128-bit)
CREATE TABLE Sessions (
SessionId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
UserId INT NOT NULL,
CreatedAt DATETIME2 NOT NULL DEFAULT SYSDATETIME()
);
-- NEWID() - random GUID (gây index fragmentation vì random)
SELECT NEWID(); -- e.g., '6F9619FF-8B86-D011-B42D-00C04FC964FF'
-- NEWSEQUENTIALID() - sequential GUID (dùng làm PK clustered index tốt hơn)
-- Chỉ dùng được trong DEFAULT constraint
CREATE TABLE AuditLogs (
LogId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
Action NVARCHAR(100),
CreatedAt DATETIME2 DEFAULT SYSDATETIME()
);
-- So sánh NEWID() vs NEWSEQUENTIALID()
-- NEWID(): Hoàn toàn random, gây page splits khi INSERT nhiều
-- NEWSEQUENTIALID(): Tăng dần, giảm fragmentation như IDENTITY
-- Convert GUID sang string và ngược lại
DECLARE @id UNIQUEIDENTIFIER = NEWID();
SELECT CAST(@id AS VARCHAR(36)); -- '6F961...'
SELECT CAST('6F9619FF-8B86-D011-B42D-00C04FC964FF' AS UNIQUEIDENTIFIER);
-- Lưu XML data với validation tùy chọn
CREATE TABLE Configurations (
ConfigId INT PRIMARY KEY,
ConfigKey NVARCHAR(100),
ConfigXml XML -- Untyped XML
);
-- Insert XML
INSERT INTO Configurations VALUES (1, 'AppSettings',
'<settings><timeout>30</timeout><maxRetry>3</maxRetry></settings>');
-- Query XML với XQuery
SELECT
ConfigId,
ConfigXml.value('(/settings/timeout)[1]', 'INT') AS timeout,
ConfigXml.value('(/settings/maxRetry)[1]', 'INT') AS max_retry
FROM Configurations
WHERE ConfigKey = 'AppSettings';
-- XML methods: value(), query(), nodes(), exist(), modify()
DECLARE @xml XML = '<employees><emp id="1">Alice</emp><emp id="2">Bob</emp></employees>';
SELECT
emp.value('@id', 'INT') AS emp_id,
emp.value('.', 'NVARCHAR(100)') AS emp_name
FROM @xml.nodes('/employees/emp') AS t(emp);
-- SQL Server không có native JSON type, dùng NVARCHAR + OPENJSON
-- Validate JSON
SELECT ISJSON('{"name": "Alice", "age": 30}'); -- 1 (valid)
SELECT ISJSON('not json'); -- 0
-- JSON_VALUE - lấy scalar value
DECLARE @json NVARCHAR(MAX) = '{"name": "Alice", "age": 30, "city": "Hanoi"}';
SELECT JSON_VALUE(@json, '$.name'); -- 'Alice'
SELECT JSON_VALUE(@json, '$.age'); -- '30'
-- JSON_QUERY - lấy object hoặc array
DECLARE @json2 NVARCHAR(MAX) = '{"person": {"name": "Alice", "skills": ["SQL", "C#"]}}';
SELECT JSON_QUERY(@json2, '$.person'); -- '{"name": "Alice", "skills": [...]}'
SELECT JSON_QUERY(@json2, '$.person.skills'); -- '["SQL", "C#"]'
-- OPENJSON - parse JSON thành rows
SELECT *
FROM OPENJSON('{"orders": [{"id":1,"total":100}, {"id":2,"total":200}]}', '$.orders')
WITH (
order_id INT '$.id',
order_total DECIMAL(10,2) '$.total'
);
-- FOR JSON - convert query result sang JSON
SELECT EmployeeId, FullName, Salary
FROM Employees
FOR JSON AUTO;
SELECT EmployeeId, FullName, Salary
FROM Employees
FOR JSON PATH, ROOT('employees');
-- Có thể lưu nhiều kiểu dữ liệu khác nhau (tránh dùng nếu có thể)
DECLARE @v SQL_VARIANT;
SET @v = 42;
SELECT @v, SQL_VARIANT_PROPERTY(@v, 'BaseType'); -- 'int'
SET @v = 'Hello';
SELECT @v, SQL_VARIANT_PROPERTY(@v, 'BaseType'); -- 'varchar'
-- Tự động tăng khi row được INSERT/UPDATE, dùng cho optimistic concurrency
CREATE TABLE Products (
ProductId INT PRIMARY KEY,
ProductName NVARCHAR(200),
Price DECIMAL(10,2),
RowVer ROWVERSION NOT NULL -- Tự cập nhật, 8 bytes
);
-- timestamp là synonym của rowversion (deprecated alias)
-- Dùng để detect concurrency conflicts
SELECT ProductId, ProductName, CAST(RowVer AS BIGINT) AS version
FROM Products;
-- N prefix tạo Unicode literal
SELECT 'Xin chào'; -- Mất dấu nếu collation không hỗ trợ
SELECT N'Xin chào'; -- Unicode, luôn đúng
-- Collation ảnh hưởng đến sorting và comparison
SELECT * FROM sys.fn_helpcollations() WHERE name LIKE 'Vietnamese%';
-- Khai báo explicit collation
CREATE TABLE MultiLanguage (
NameEn NVARCHAR(200) COLLATE Latin1_General_CI_AS,
NameVn NVARCHAR(200) COLLATE Vietnamese_CI_AS
);
-- So sánh cross-collation cần COLLATE trong query
SELECT * FROM t1
JOIN t2 ON t1.Name = t2.Name COLLATE SQL_Latin1_General_CP1_CI_AS;
-- CAST - chuẩn ANSI SQL
SELECT CAST(42 AS VARCHAR(10)); -- '42'
SELECT CAST('2026-04-01' AS DATE); -- 2026-04-01
SELECT CAST(3.14 AS INT); -- 3 (truncation)
-- CONVERT - SQL Server specific, hỗ trợ format code cho ngày
SELECT CONVERT(VARCHAR(10), GETDATE(), 103); -- '01/04/2026' (dd/mm/yyyy)
SELECT CONVERT(VARCHAR(10), GETDATE(), 120); -- '2026-04-01' (yyyy-mm-dd)
SELECT CONVERT(VARCHAR(8), GETDATE(), 112); -- '20260401' (yyyymmdd)
SELECT CONVERT(INT, '42');
-- TRY_CAST - trả về NULL thay vì error nếu convert thất bại
SELECT TRY_CAST('abc' AS INT); -- NULL (không báo lỗi)
SELECT TRY_CAST('42' AS INT); -- 42
-- TRY_CONVERT - tương tự TRY_CAST với format code
SELECT TRY_CONVERT(DATE, '2026-13-01'); -- NULL (tháng 13 không tồn tại)
SELECT TRY_CONVERT(DATE, '2026-04-01'); -- 2026-04-01
-- Implicit conversion - SQL Server tự convert (CÓ THỂ GÂY VẤN ĐỀ!)
SELECT * FROM Employees WHERE EmployeeId = '42';
-- SQL Server convert '42' sang INT: OK nhưng có thể ảnh hưởng performance
-- Implicit conversion GÂY VẤN ĐỀ với indexes:
-- Nếu column là VARCHAR và bạn so sánh với INT, SQL Server phải convert TỪNG GIÁ TRỊ TRONG TABLE
-- -> Index không được dùng -> Table scan!
SELECT * FROM Employees WHERE VarcharId = 42; -- BAD: convert mỗi row
SELECT * FROM Employees WHERE VarcharId = '42'; -- GOOD: dùng index
-- Kiểm tra implicit conversion với execution plan:
-- Tìm "CONVERT_IMPLICIT" trong execution plan để phát hiện vấn đề
| From \ To | INT | VARCHAR | DATE | FLOAT | BIT |
| INT | - | ✅ Explicit | ❌ | ✅ Implicit | ✅ Implicit |
| VARCHAR | ✅ Explicit | - | ✅ Explicit | ✅ Explicit | ❌ |
| DATE | ❌ | ✅ Explicit | - | ❌ | ❌ |
| FLOAT | ✅ Explicit (truncate) | ✅ Explicit | ❌ | - | ✅ Implicit |
-- 1. Chọn kiểu nhỏ nhất phù hợp
-- BAD: dùng BIGINT cho tất cả
CREATE TABLE BadDesign (OrderId BIGINT, Status BIGINT, YearsExp BIGINT);
-- GOOD: chọn đúng kích thước
CREATE TABLE GoodDesign (
OrderId INT NOT NULL, -- Đủ cho hầu hết use cases
Status TINYINT NOT NULL, -- 0-255 là đủ cho status
YearsExp TINYINT NOT NULL -- 0-120 năm kinh nghiệm
);
-- 2. Không dùng float/real cho tiền tệ
-- BAD:
CREATE TABLE BadMoney (Price FLOAT); -- Sai về độ chính xác!
-- GOOD:
CREATE TABLE GoodMoney (Price DECIMAL(19, 4)); -- Chính xác
-- 3. Luôn dùng nvarchar cho user-input text trong ứng dụng đa ngôn ngữ
CREATE TABLE UserProfiles (
UserId INT PRIMARY KEY,
UserName VARCHAR(50) NOT NULL, -- Username: ASCII-safe
FullName NVARCHAR(200) NOT NULL, -- Tên: cần Unicode
Email VARCHAR(320) NOT NULL, -- Email: ASCII-safe
Bio NVARCHAR(MAX) -- Bio: user input
);
-- 4. Dùng DATE thay DATETIME khi không cần time component
-- BAD: lưu ngày sinh với DATETIME
CREATE TABLE BadDates (BirthDate DATETIME); -- Lưu '1990-05-15 00:00:00.000'
-- GOOD:
CREATE TABLE GoodDates (BirthDate DATE); -- Chỉ '1990-05-15', tiết kiệm 5 bytes/row
-- 5. Dùng DATETIME2 thay DATETIME cho timestamps mới
-- datetime: range 1753+, precision 3.33ms, 8 bytes
-- datetime2: range 0001+, precision 100ns, 6-8 bytes
CREATE TABLE AuditLog (
CreatedAt DATETIME2(7) NOT NULL DEFAULT SYSDATETIME()
);
-- 6. Tránh varchar(MAX) khi biết trước giới hạn
-- BAD: dùng MAX cho mọi thứ
CREATE TABLE BadVarchar (Name NVARCHAR(MAX), Code NVARCHAR(MAX));
-- GOOD: giới hạn phù hợp
CREATE TABLE GoodVarchar (
Name NVARCHAR(200), -- Tên người
Code NVARCHAR(20), -- Mã code ngắn
Description NVARCHAR(2000), -- Mô tả
Content NVARCHAR(MAX) -- Nội dung thực sự cần MAX
);
-- 7. NULL vs NOT NULL
-- Tránh allow NULL trừ khi thực sự cần thiết về mặt business logic
-- NULL gây phức tạp trong queries và có thể ảnh hưởng index efficiency
| Tình huống | Khuyến nghị |
| ID / Primary Key (web app) | INT hoặc BIGINT |
| ID / Primary Key (distributed) | UNIQUEIDENTIFIER với NEWSEQUENTIALID() |
| Tiền tệ | DECIMAL(19, 4) |
| Phần trăm, tỉ lệ | DECIMAL(5, 4) |
| Tọa độ GPS | FLOAT |
| Tên người, địa chỉ | NVARCHAR(n) |
| Email, username | VARCHAR(n) |
| Mã code (fixed) | CHAR(n) |
| Ngày sinh, ngày hợp đồng | DATE |
| Timestamp tạo/sửa | DATETIME2(7) |
| Thời gian với timezone | DATETIMEOFFSET |
| Flag on/off | BIT |
| Trạng thái nhỏ (< 255 options) | TINYINT |
| File, image, binary data | VARBINARY(MAX) |
| Config JSON/XML | NVARCHAR(MAX) (JSON) hoặc XML |
| Audit/concurrent control | ROWVERSION |