T-SQL Cơ bản
Kiến trúc SQL Server
Các thành phần chính của SQL Server Engine
SQL Server Engine được chia thành hai phần lớn: Relational Engine (hay Query Processor) và Storage Engine.
SQL Server Instance
├── Relational Engine (Query Processor)
│ ├── Command Parser -- Phân tích cú pháp T-SQL
│ ├── Query Optimizer -- Lựa chọn execution plan tối ưu
│ └── Query Executor -- Thực thi execution plan
└── Storage Engine
├── Buffer Manager -- Quản lý bộ nhớ (Buffer Pool)
├── Log Manager -- Quản lý Transaction Log
├── Access Methods -- B-tree traversal, heap scans
└── Lock Manager -- Quản lý locking và concurrency
Relational Engine (Query Processor)
| Thành phần | Vai trò |
|---|---|
| Command Parser | Kiểm tra cú pháp T-SQL, tạo parse tree |
| Query Optimizer | Tạo execution plan dựa trên statistics và cost model |
| Query Executor | Thực thi từng bước trong execution plan |
Storage Engine
| Thành phần | Vai trò |
|---|---|
| Buffer Manager | Quản lý Buffer Pool (cache data pages trong RAM) |
| Log Manager | Ghi WAL (Write-Ahead Logging) vào transaction log |
| Access Methods | Duyệt B-tree index, heap tables |
| Lock Manager | Quản lý locking, deadlock detection |
Database Files
SQL Server tổ chức dữ liệu trên disk theo 3 loại file:
| File | Extension | Vai trò |
|---|---|---|
| Primary Data File | .mdf | File dữ liệu chính, chứa system tables và user data |
| Secondary Data File | .ndf | File dữ liệu bổ sung (filegroups khác nhau) |
| Log File | .ldf | Transaction log, dùng cho recovery và replication |
-- Xem thông tin files của database hiện tại
SELECT
name,
physical_name,
type_desc,
size * 8 / 1024 AS size_mb,
max_size,
growth
FROM sys.database_files;
Lưu ý: Log file (
.ldf) ghi tuần tự (sequential write) nên nên đặt trên disk riêng với data files để tối ưu I/O.
Pages và Extents
Pages (Trang)
- Đơn vị lưu trữ nhỏ nhất trong SQL Server là page, kích thước cố định 8KB (8192 bytes).
- Mỗi page có header 96 bytes, còn lại ~8096 bytes để lưu data.
- Các loại page chính:
| Page Type | Mô tả |
|---|---|
| Data | Chứa dữ liệu của heap tables |
| Index | Chứa B-tree index nodes |
| LOB | Large Object data (nvarchar(MAX), varbinary(MAX)) |
| IAM | Index Allocation Map - theo dõi pages thuộc object nào |
| PFS | Page Free Space - theo dõi không gian trống |
Extents (Nhóm trang)
- Extent = 8 pages liền kề = 64KB.
- Hai loại extent:
| Loại | Mô tả |
|---|---|
| Uniform Extent | Tất cả 8 pages thuộc cùng 1 object (dùng khi table lớn) |
| Mixed Extent | Các pages có thể thuộc nhiều objects khác nhau (dùng khi table nhỏ ≤ 8 pages) |
-- Xem số pages của từng table
SELECT
t.name AS table_name,
p.rows,
SUM(a.total_pages) * 8 AS total_kb,
SUM(a.used_pages) * 8 AS used_kb
FROM sys.tables t
JOIN sys.indexes i ON t.object_id = i.object_id
JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
JOIN sys.allocation_units a ON p.partition_id = a.container_id
GROUP BY t.name, p.rows
ORDER BY total_kb DESC;
Memory Architecture
Buffer Pool
Buffer Pool là vùng nhớ lớn nhất trong SQL Server, được dùng để cache data pages đọc từ disk.
Memory (RAM)
└── Buffer Pool
├── Data Page Cache -- Cache các 8KB data pages
├── Plan Cache -- Cache các execution plans
├── Log Cache -- Cache log records trước khi flush
└── Other caches -- Connection info, metadata, etc.
Nguyên tắc hoạt động:
- SQL Server đọc page từ disk → lưu vào Buffer Pool
- Lần sau đọc page đó → lấy trực tiếp từ RAM (không đọc disk)
- Khi RAM đầy → dùng LRU (Least Recently Used) để evict pages cũ
-- Xem Buffer Pool usage
SELECT
database_id,
COUNT(*) AS cached_pages,
COUNT(*) * 8 / 1024 AS cached_mb
FROM sys.dm_os_buffer_descriptors
GROUP BY database_id
ORDER BY cached_pages DESC;
Plan Cache
Execution plans được cache lại để tái sử dụng:
-- Xem các plans đang được cache
SELECT
qs.execution_count,
qs.total_elapsed_time / qs.execution_count AS avg_elapsed_us,
SUBSTRING(qt.text, (qs.statement_start_offset/2) + 1,
((qs.statement_end_offset - qs.statement_start_offset)/2) + 1) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_elapsed_time DESC;
SQL Server Instance vs Database
| Instance | Database | |
|---|---|---|
| Định nghĩa | Một cài đặt SQL Server Engine | Tập hợp dữ liệu logic trong instance |
| Cấu trúc | Quản lý memory, logins, linked servers | Tables, views, stored procedures, schemas |
| Multiple | Nhiều databases trong 1 instance | Nhiều schemas trong 1 database |
| Isolation | Shared engine resources | Cross-database queries được phép |
-- Xem thông tin instance
SELECT
@@SERVERNAME AS server_name,
@@VERSION AS version,
SERVERPROPERTY('Edition') AS edition,
SERVERPROPERTY('ProductVersion') AS product_version;
-- Xem các databases trong instance
SELECT name, state_desc, recovery_model_desc
FROM sys.databases
ORDER BY name;
T-SQL Cơ bản
SELECT, WHERE, ORDER BY
-- SELECT cơ bản
SELECT column1, column2, column3
FROM TableName;
-- Lấy tất cả cột (tránh dùng trong production)
SELECT * FROM Employees;
-- WHERE để lọc dữ liệu
SELECT EmployeeId, FullName, Salary
FROM Employees
WHERE DepartmentId = 5
AND Salary > 50000
AND HireDate >= '2020-01-01';
-- ORDER BY để sắp xếp
SELECT EmployeeId, FullName, Salary
FROM Employees
ORDER BY Salary DESC, FullName ASC;
GROUP BY và HAVING
-- GROUP BY - nhóm và tổng hợp
SELECT
DepartmentId,
COUNT(*) AS employee_count,
AVG(Salary) AS avg_salary,
MAX(Salary) AS max_salary,
MIN(Salary) AS min_salary
FROM Employees
GROUP BY DepartmentId;
-- HAVING - lọc trên kết quả aggregate (khác WHERE)
SELECT
DepartmentId,
COUNT(*) AS employee_count,
AVG(Salary) AS avg_salary
FROM Employees
WHERE IsActive = 1 -- WHERE lọc trước GROUP BY
GROUP BY DepartmentId
HAVING COUNT(*) >= 5 -- HAVING lọc sau GROUP BY
AND AVG(Salary) > 60000
ORDER BY avg_salary DESC;
Thứ tự thực thi:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
Aliases và DISTINCT
-- Column alias
SELECT
e.EmployeeId AS emp_id,
e.FirstName + ' ' + e.LastName AS full_name,
d.DepartmentName AS dept
FROM Employees e -- Table alias
JOIN Departments d ON e.DepartmentId = d.DepartmentId;
-- DISTINCT - loại bỏ hàng trùng lặp
SELECT DISTINCT DepartmentId FROM Employees;
-- DISTINCT với nhiều cột
SELECT DISTINCT DepartmentId, JobTitle
FROM Employees
ORDER BY DepartmentId;
TOP và OFFSET-FETCH
-- TOP N - lấy N hàng đầu
SELECT TOP 10 EmployeeId, FullName, Salary
FROM Employees
ORDER BY Salary DESC;
-- TOP với PERCENT
SELECT TOP 10 PERCENT EmployeeId, FullName
FROM Employees;
-- TOP WITH TIES - bao gồm các hàng có giá trị bằng nhau
SELECT TOP 5 WITH TIES EmployeeId, Salary
FROM Employees
ORDER BY Salary DESC;
-- OFFSET-FETCH (SQL Server 2012+) - phân trang chuẩn
DECLARE @PageSize INT = 10;
DECLARE @PageNumber INT = 2; -- Trang thứ 2
SELECT EmployeeId, FullName, Salary
FROM Employees
ORDER BY EmployeeId
OFFSET (@PageNumber - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;
Lưu ý:
OFFSET-FETCHyêu cầuORDER BY. Ưu tiên dùngOFFSET-FETCHthay vìTOPcho phân trang vì chuẩn hơn.
LIKE và Wildcard
-- % - khớp 0 hoặc nhiều ký tự
SELECT * FROM Employees WHERE LastName LIKE 'Smith%'; -- Bắt đầu bằng Smith
SELECT * FROM Employees WHERE Email LIKE '%@gmail.com'; -- Kết thúc bằng @gmail.com
SELECT * FROM Employees WHERE FullName LIKE '%Nguyen%'; -- Chứa Nguyen
-- _ - khớp đúng 1 ký tự
SELECT * FROM Products WHERE ProductCode LIKE 'A__'; -- A + 2 ký tự bất kỳ
-- [] - khớp 1 ký tự trong tập hợp
SELECT * FROM Employees WHERE LastName LIKE '[ABC]%'; -- Bắt đầu bằng A, B hoặc C
-- [^] - khớp 1 ký tự KHÔNG trong tập hợp
SELECT * FROM Employees WHERE LastName LIKE '[^ABC]%'; -- Không bắt đầu bằng A, B, C
-- Thoát ký tự đặc biệt với ESCAPE
SELECT * FROM Products WHERE Description LIKE '50\%' ESCAPE '\'; -- Chứa "50%"
Xử lý NULL
-- IS NULL / IS NOT NULL (không dùng = NULL)
SELECT * FROM Employees WHERE ManagerId IS NULL;
SELECT * FROM Employees WHERE ManagerId IS NOT NULL;
-- ISNULL(expression, replacement) - SQL Server specific
SELECT
EmployeeId,
ISNULL(MiddleName, '') AS middle_name,
ISNULL(Phone, 'N/A') AS phone
FROM Employees;
-- COALESCE(val1, val2, ...) - trả về giá trị non-NULL đầu tiên (ANSI standard)
SELECT
EmployeeId,
COALESCE(MobilePhone, OfficePhone, HomePhone, 'No phone') AS contact_phone
FROM Employees;
-- NULLIF(expr1, expr2) - trả về NULL nếu hai giá trị bằng nhau
-- Dùng để tránh chia cho 0
SELECT
Revenue,
Cost,
Revenue / NULLIF(Cost, 0) AS revenue_ratio
FROM FinancialData;
-- NULL trong aggregate functions (NULL bị bỏ qua)
SELECT
COUNT(*) AS total_rows, -- Đếm tất cả hàng
COUNT(Commission) AS has_commission, -- Đếm hàng có Commission != NULL
AVG(Commission) AS avg_comm, -- AVG bỏ qua NULL
SUM(Commission) AS total_comm
FROM Employees;
CASE Expressions
-- Simple CASE (so sánh bằng)
SELECT
EmployeeId,
FullName,
Status,
CASE Status
WHEN 'A' THEN 'Active'
WHEN 'I' THEN 'Inactive'
WHEN 'T' THEN 'Terminated'
ELSE 'Unknown'
END AS status_description
FROM Employees;
-- Searched CASE (điều kiện logic)
SELECT
EmployeeId,
FullName,
Salary,
CASE
WHEN Salary < 30000 THEN 'Entry Level'
WHEN Salary BETWEEN 30000 AND 60000 THEN 'Mid Level'
WHEN Salary BETWEEN 60001 AND 100000 THEN 'Senior Level'
WHEN Salary > 100000 THEN 'Executive'
ELSE 'Unknown'
END AS salary_band
FROM Employees;
-- CASE trong ORDER BY
SELECT EmployeeId, FullName, Priority
FROM Tasks
ORDER BY
CASE Priority
WHEN 'High' THEN 1
WHEN 'Medium' THEN 2
WHEN 'Low' THEN 3
ELSE 4
END;
-- CASE trong UPDATE
UPDATE Employees
SET Salary = Salary *
CASE
WHEN YearsOfService >= 10 THEN 1.10 -- Tăng 10%
WHEN YearsOfService >= 5 THEN 1.07 -- Tăng 7%
ELSE 1.05 -- Tăng 5%
END;
String Functions (Hàm xử lý chuỗi)
-- LEN - độ dài chuỗi (không tính trailing spaces)
SELECT LEN('Hello World'); -- 11
SELECT LEN(' Hello '); -- 9 (không tính 2 spaces cuối)
-- SUBSTRING(string, start, length) - cắt chuỗi (1-indexed)
SELECT SUBSTRING('Hello World', 7, 5); -- 'World'
SELECT SUBSTRING(Email, 1, CHARINDEX('@', Email) - 1) AS username
FROM Employees;
-- CHARINDEX(pattern, string [, start]) - tìm vị trí chuỗi con
SELECT CHARINDEX('@', 'user@example.com'); -- 5
SELECT CHARINDEX('a', 'banana', 3); -- 4 (bắt đầu tìm từ vị trí 3)
-- REPLACE(string, old, new) - thay thế
SELECT REPLACE('Hello World', 'World', 'SQL'); -- 'Hello SQL'
SELECT REPLACE(Phone, '-', '') AS cleaned_phone FROM Employees;
-- TRIM, LTRIM, RTRIM
SELECT TRIM(' Hello '); -- 'Hello' (SQL Server 2017+)
SELECT LTRIM(' Hello '); -- 'Hello '
SELECT RTRIM(' Hello '); -- ' Hello'
SELECT TRIM('x' FROM 'xxxHelloxxx'); -- 'Hello' (SQL Server 2017+)
-- UPPER, LOWER
SELECT UPPER('hello world'); -- 'HELLO WORLD'
SELECT LOWER('HELLO WORLD'); -- 'hello world'
-- CONCAT - nối chuỗi (NULL-safe)
SELECT CONCAT('Hello', ' ', 'World', '!'); -- 'Hello World!'
SELECT CONCAT(FirstName, ' ', LastName) AS full_name FROM Employees;
-- CONCAT_WS - nối với separator (SQL Server 2017+)
SELECT CONCAT_WS(', ', City, State, Country); -- 'Hanoi, HN, Vietnam'
-- FORMAT - định dạng (costly, tránh dùng trong queries lớn)
SELECT FORMAT(12345.678, 'N2'); -- '12,345.68'
SELECT FORMAT(GETDATE(), 'dd/MM/yyyy'); -- '01/04/2026'
SELECT FORMAT(Salary, 'C', 'vi-VN'); -- Định dạng tiền VN
-- REPLICATE - lặp chuỗi
SELECT REPLICATE('*', 10); -- '**********'
-- REVERSE
SELECT REVERSE('Hello'); -- 'olleH'
-- STRING_SPLIT (SQL Server 2016+)
SELECT value FROM STRING_SPLIT('a,b,c,d', ',');
-- LEFT, RIGHT
SELECT LEFT('Hello World', 5); -- 'Hello'
SELECT RIGHT('Hello World', 5); -- 'World'
-- PATINDEX - tìm pattern (hỗ trợ wildcard)
SELECT PATINDEX('%[0-9]%', 'abc123def'); -- 4 (vị trí số đầu tiên)
Date Functions (Hàm ngày tháng)
-- Hàm lấy ngày/giờ hiện tại
SELECT GETDATE(); -- datetime, local time
SELECT GETUTCDATE(); -- datetime, UTC time
SELECT SYSDATETIME(); -- datetime2(7), local time, cao hơn độ chính xác
SELECT SYSUTCDATETIME(); -- datetime2(7), UTC time
SELECT SYSDATETIMEOFFSET(); -- datetimeoffset, kèm timezone offset
-- DATEADD(part, number, date) - thêm/bớt thời gian
SELECT DATEADD(DAY, 30, GETDATE()); -- 30 ngày sau
SELECT DATEADD(MONTH, -3, GETDATE()); -- 3 tháng trước
SELECT DATEADD(YEAR, 1, '2025-01-01'); -- '2026-01-01'
SELECT DATEADD(HOUR, 2, GETDATE()); -- 2 giờ sau
SELECT DATEADD(MINUTE, -30, GETDATE()); -- 30 phút trước
-- DATEDIFF(part, start, end) - khoảng cách thời gian
SELECT DATEDIFF(DAY, '2025-01-01', '2026-01-01'); -- 365
SELECT DATEDIFF(MONTH, HireDate, GETDATE()) AS months_employed FROM Employees;
SELECT DATEDIFF(YEAR, BirthDate, GETDATE()) AS age FROM Employees;
-- DATEPART(part, date) - lấy phần của ngày
SELECT DATEPART(YEAR, GETDATE()); -- 2026
SELECT DATEPART(MONTH, GETDATE()); -- 4
SELECT DATEPART(DAY, GETDATE()); -- 1
SELECT DATEPART(WEEKDAY, GETDATE()); -- Ngày trong tuần (1=Sunday)
SELECT DATEPART(HOUR, GETDATE()); -- Giờ
-- DATENAME - tương tự DATEPART nhưng trả text
SELECT DATENAME(MONTH, GETDATE()); -- 'April'
SELECT DATENAME(WEEKDAY, GETDATE()); -- 'Wednesday'
-- DAY(), MONTH(), YEAR() - shorthand
SELECT DAY(GETDATE()), MONTH(GETDATE()), YEAR(GETDATE());
-- FORMAT với ngày (chậm hơn CONVERT, dùng cho display)
SELECT FORMAT(GETDATE(), 'dd/MM/yyyy'); -- '01/04/2026'
SELECT FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm:ss'); -- '2026-04-01 10:30:00'
-- CONVERT với ngày (nhanh hơn FORMAT)
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(DATE, GETDATE()); -- Cắt phần time
-- EOMONTH - ngày cuối tháng (SQL Server 2012+)
SELECT EOMONTH(GETDATE()); -- Ngày cuối tháng hiện tại
SELECT EOMONTH('2026-02-01'); -- '2026-02-28'
-- DATEFROMPARTS, DATETIMEFROMPARTS (SQL Server 2012+)
SELECT DATEFROMPARTS(2026, 4, 1); -- '2026-04-01'
SELECT DATETIMEFROMPARTS(2026, 4, 1, 10, 30, 0, 0); -- '2026-04-01 10:30:00'
-- Ví dụ thực tế: tìm nhân viên thuê trong 90 ngày qua
SELECT EmployeeId, FullName, HireDate
FROM Employees
WHERE HireDate >= DATEADD(DAY, -90, GETDATE())
AND HireDate < GETDATE();
Math Functions (Hàm toán học)
-- ROUND(number, decimal_places [, truncate])
SELECT ROUND(3.14159, 2); -- 3.14
SELECT ROUND(3.145, 2); -- 3.15 (round half up)
SELECT ROUND(3.145, 2, 1); -- 3.14 (truncate, không round)
-- ABS - giá trị tuyệt đối
SELECT ABS(-42); -- 42
SELECT ABS(3.14); -- 3.14
-- CEILING - làm tròn lên (trần)
SELECT CEILING(3.1); -- 4
SELECT CEILING(-3.9); -- -3
-- FLOOR - làm tròn xuống (sàn)
SELECT FLOOR(3.9); -- 3
SELECT FLOOR(-3.1); -- -4
-- POWER(base, exponent)
SELECT POWER(2, 10); -- 1024
SELECT POWER(3.0, 3); -- 27.0
-- SQRT - căn bậc hai
SELECT SQRT(144); -- 12.0
-- SQUARE
SELECT SQUARE(5); -- 25
-- PI
SELECT PI(); -- 3.14159265358979
-- LOG, LOG10, EXP
SELECT LOG(100); -- Logarithm tự nhiên
SELECT LOG(100, 10); -- Log base 10 = 2 (SQL Server 2012+)
SELECT LOG10(1000); -- 3
-- SIGN - dấu của số
SELECT SIGN(-5); -- -1
SELECT SIGN(0); -- 0
SELECT SIGN(5); -- 1
-- Ví dụ thực tế: tính commission
SELECT
EmployeeId,
Sales,
ROUND(Sales * 0.05, 2) AS commission,
CEILING(Sales / 1000.0) AS bonus_units
FROM SalesData;
Aggregate Functions (Hàm tổng hợp)
-- COUNT
SELECT COUNT(*) AS total FROM Employees; -- Đếm tất cả hàng (kể cả NULL)
SELECT COUNT(Email) AS has_email FROM Employees; -- Đếm hàng có Email NOT NULL
SELECT COUNT(DISTINCT DepartmentId) AS dept_count FROM Employees; -- Đếm distinct values
-- SUM
SELECT SUM(Salary) AS total_salary FROM Employees;
SELECT SUM(Salary) AS total_salary FROM Employees WHERE IsActive = 1;
-- AVG (bỏ qua NULL)
SELECT AVG(Salary) AS avg_salary FROM Employees;
-- Chú ý: AVG(int) trả về int! Dùng AVG(CAST(col AS decimal))
SELECT AVG(CAST(Score AS DECIMAL(10,2))) AS avg_score FROM Students;
-- MIN, MAX
SELECT MIN(Salary) AS min_salary, MAX(Salary) AS max_salary FROM Employees;
SELECT MIN(HireDate) AS first_hire, MAX(HireDate) AS last_hire FROM Employees;
-- Kết hợp với GROUP BY
SELECT
d.DepartmentName,
COUNT(e.EmployeeId) AS headcount,
MIN(e.Salary) AS min_salary,
MAX(e.Salary) AS max_salary,
AVG(e.Salary) AS avg_salary,
SUM(e.Salary) AS total_payroll
FROM Departments d
LEFT JOIN Employees e ON d.DepartmentId = e.DepartmentId
GROUP BY d.DepartmentName
ORDER BY total_payroll DESC;
-- STRING_AGG - gộp chuỗi (SQL Server 2017+)
SELECT
DepartmentId,
STRING_AGG(FullName, ', ') WITHIN GROUP (ORDER BY FullName) AS employees
FROM Employees
GROUP BY DepartmentId;
Joins (Phép kết bảng)
-- INNER JOIN - chỉ trả về hàng có khớp ở cả 2 bảng
SELECT e.EmployeeId, e.FullName, d.DepartmentName
FROM Employees e
INNER JOIN Departments d ON e.DepartmentId = d.DepartmentId;
-- LEFT JOIN - tất cả từ bảng trái, NULL với bảng phải nếu không khớp
SELECT e.EmployeeId, e.FullName, d.DepartmentName
FROM Employees e
LEFT JOIN Departments d ON e.DepartmentId = d.DepartmentId;
-- RIGHT JOIN - tất cả từ bảng phải
SELECT e.EmployeeId, e.FullName, d.DepartmentName
FROM Employees e
RIGHT JOIN Departments d ON e.DepartmentId = d.DepartmentId;
-- FULL OUTER JOIN - tất cả từ cả 2 bảng
SELECT e.EmployeeId, e.FullName, d.DepartmentName
FROM Employees e
FULL OUTER JOIN Departments d ON e.DepartmentId = d.DepartmentId;
-- CROSS JOIN - tích Descartes (n × m rows)
SELECT p.ProductName, c.ColorName
FROM Products p
CROSS JOIN Colors c;
-- SELF JOIN - bảng join với chính nó
SELECT
e.EmployeeId,
e.FullName AS employee,
m.FullName AS manager
FROM Employees e
LEFT JOIN Employees m ON e.ManagerId = m.EmployeeId;
-- Multi-table join
SELECT
o.OrderId,
c.CustomerName,
e.FullName AS salesperson,
SUM(od.Quantity * od.UnitPrice) AS order_total
FROM Orders o
JOIN Customers c ON o.CustomerId = c.CustomerId
JOIN Employees e ON o.EmployeeId = e.EmployeeId
JOIN OrderDetails od ON o.OrderId = od.OrderId
GROUP BY o.OrderId, c.CustomerName, e.FullName;