Ngày 4 — Database và SQL
Mục tiêu ngày 4
Sau ngày 4 bạn phải:
- Hiểu INDEX hoạt động bên trong (B-tree) — không chỉ "index giúp nhanh".
- Viết được mọi loại JOIN + tránh bẫy LEFT JOIN.
- Giải thích ACID + 4 isolation level ở mức cơ chế.
- Phân biệt MVCC vs Lock-based.
- Đọc execution plan và biết khi nào query non-sargable.
- Tối ưu được 1 query chậm: indexing, rewrite, EXPLAIN.
1. Schema demo + Mock data
1.1. Tạo schema
-- Cleanup nếu chạy lại
IF OBJECT_ID('OrderItems', 'U') IS NOT NULL DROP TABLE OrderItems;
IF OBJECT_ID('Orders', 'U') IS NOT NULL DROP TABLE Orders;
IF OBJECT_ID('Products', 'U') IS NOT NULL DROP TABLE Products;
IF OBJECT_ID('Customers', 'U') IS NOT NULL DROP TABLE Customers;
IF OBJECT_ID('Employees', 'U') IS NOT NULL DROP TABLE Employees;
IF OBJECT_ID('Users', 'U') IS NOT NULL DROP TABLE Users;
CREATE TABLE Customers (
Id INT PRIMARY KEY IDENTITY,
Name NVARCHAR(100) NOT NULL,
Email NVARCHAR(150),
City NVARCHAR(50),
CreatedAt DATETIME2 DEFAULT SYSUTCDATETIME()
);
CREATE TABLE Products (
Id INT PRIMARY KEY IDENTITY,
Name NVARCHAR(100) NOT NULL,
Price DECIMAL(18, 2) NOT NULL,
Stock INT NOT NULL DEFAULT 0
);
CREATE TABLE Orders (
Id INT PRIMARY KEY IDENTITY,
CustomerId INT NOT NULL,
OrderDate DATE NOT NULL,
Status NVARCHAR(20) NOT NULL DEFAULT 'Pending',
Total DECIMAL(18, 2),
CONSTRAINT FK_Orders_Customers FOREIGN KEY (CustomerId) REFERENCES Customers(Id)
);
CREATE TABLE OrderItems (
Id INT PRIMARY KEY IDENTITY,
OrderId INT NOT NULL,
ProductId INT NOT NULL,
Quantity INT NOT NULL,
UnitPrice DECIMAL(18, 2) NOT NULL,
CONSTRAINT FK_Items_Orders FOREIGN KEY (OrderId) REFERENCES Orders(Id),
CONSTRAINT FK_Items_Products FOREIGN KEY (ProductId) REFERENCES Products(Id)
);
CREATE TABLE Employees (
Id INT PRIMARY KEY IDENTITY,
Name NVARCHAR(100) NOT NULL,
ManagerId INT NULL,
Salary DECIMAL(18, 2),
Dept NVARCHAR(50),
CONSTRAINT FK_Emp_Manager FOREIGN KEY (ManagerId) REFERENCES Employees(Id)
);
CREATE TABLE Users (
Id INT PRIMARY KEY IDENTITY,
Name NVARCHAR(100),
Email NVARCHAR(150)
);
CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId);
CREATE INDEX IX_Orders_Date ON Orders(OrderDate);
CREATE INDEX IX_Items_OrderId ON OrderItems(OrderId);
1.2. Mock data
INSERT INTO Customers (Name, Email, City) VALUES
('Alice Nguyen', 'alice@gmail.com', 'HCM'),
('Bob Tran', 'bob@gmail.com', 'HN'),
('Charlie Le', 'charlie@gmail.com', 'HCM'),
('Dave Pham', 'dave@gmail.com', 'DN'),
('Eve Hoang', 'eve@gmail.com', 'HCM'),
('Frank Vo', 'frank@gmail.com', 'HCM'),
('Grace Bui', 'grace@gmail.com', 'HN'),
('Henry Do', NULL, 'CT'); -- Henry chưa từng mua (test LEFT JOIN)
INSERT INTO Products (Name, Price, Stock) VALUES
('Laptop Dell XPS', 32000000, 5),
('iPhone 15', 25000000, 12),
('AirPods Pro', 5500000, 30),
('Bàn phím cơ Keychron', 2200000, 25),
('Chuột Logitech MX', 1800000, 50),
('Màn hình LG 27 4K', 12000000, 8);
INSERT INTO Orders (CustomerId, OrderDate, Status, Total) VALUES
(1, '2026-01-15', 'Paid', 37500000),
(1, '2026-02-10', 'Paid', 5500000),
(1, '2026-03-22', 'Pending', 2200000),
(2, '2026-01-05', 'Paid', 25000000),
(3, '2026-02-18', 'Paid', 14200000),
(3, '2026-04-01', 'Cancelled', 1800000),
(4, '2026-03-12', 'Paid', 32000000),
(5, '2026-04-20', 'Pending', 18000000),
(5, '2026-05-02', 'Paid', 5500000),
(6, '2026-01-28', 'Paid', 37000000),
(6, '2026-05-15', 'Paid', 7700000),
(7, '2026-04-10', 'Pending', 12000000);
INSERT INTO Employees (Name, ManagerId, Salary, Dept) VALUES
('CEO An', NULL, 80000000, 'Executive'),
('CTO Bao', 1, 60000000, 'Tech'),
('CFO Cuong', 1, 55000000, 'Finance'),
('Lead Dev Dao', 2, 45000000, 'Tech'),
('Senior Dev Phong', 4, 35000000, 'Tech'),
('Junior Dev Giang', 4, 18000000, 'Tech'),
('Fresher Hieu', 4, 12000000, 'Tech');
INSERT INTO Users (Name, Email) VALUES
('User A', 'duplicate@mail.com'),
('User B', 'duplicate@mail.com'),
('User C', 'unique1@mail.com'),
('User D', 'another@mail.com'),
('User E', 'another@mail.com');
2. INDEX — kiến thức cốt lõi
2.1. Tại sao cần INDEX?
2.2. 🟡 B-tree — cấu trúc bên trong
- Root node (1 page) → trỏ tới các internal nodes.
- Internal nodes → trỏ tới các leaf nodes.
- Leaf nodes chứa key + pointer tới row thực, sorted.
- Mỗi node là 1 disk page (8 KB trong SQL Server).
[Root]
/ | \
[..] [..] [..] ← internal
/ | | \ | \
[L][L] [L][L] [L][L] ← leaf (sorted)
Tại sao O(log n) tìm? Mỗi node có nhiều children (vd 100). Tree depth = log_100(N). Với 100 triệu row → depth ~4. Chỉ 4 page reads thay vì scan 100 triệu row.
2.3. Clustered vs Non-clustered
Hình dung:
CLUSTERED (PK = Id) → data sorted by Id
Leaf:
[Id=1, full row data] → [Id=2, full row data] → [Id=3, ...]
NON-CLUSTERED (on Email) → tree riêng sorted by Email
Leaf:
[Email='a@b.com', Id=42] → [Email='b@c.com', Id=7] → ...
↓
Lookup Id=42 trong clustered → lấy full row
→ Non-clustered có 2 cấp: tìm leaf của index → lookup clustered theo PK.
2.4. Code ví dụ
-- Clustered (default = PK)
CREATE TABLE Orders (
Id INT PRIMARY KEY IDENTITY, -- = clustered tự động
...
);
-- Non-clustered đơn cột
CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId);
-- Non-clustered composite (thứ tự cột RẤT QUAN TRỌNG)
CREATE INDEX IX_Orders_Cust_Date ON Orders(CustomerId, OrderDate);
-- Covering — INCLUDE thêm cột để query không cần seek về bảng
CREATE INDEX IX_Orders_Cover ON Orders(CustomerId)
INCLUDE (OrderDate, Total, Status);
-- Filtered — chỉ index 1 phần dữ liệu
CREATE INDEX IX_Orders_Active ON Orders(CustomerId)
WHERE Status <> 'Cancelled';
-- Unique
CREATE UNIQUE INDEX IX_Users_Email ON Users(Email);
2.5. Left-most prefix rule
(A, B, C) dùng được cho:✅ WHERE A = ?✅ WHERE A = ? AND B = ?
✅ WHERE A = ? AND B = ? AND C = ?
✅ WHERE A = ? AND C = ? (dùng được A, không dùng C)❌ WHERE B = ? (bỏ qua A) — KHÔNG dùng được
❌ WHERE C = ? — KHÔNG dùng được→ Phải dùng cột đầu tiên của composite mới được. Đó là vì index sorted theo (A, B, C) — không seek được nếu không biết A.
2.6. Khi nào nên / không nên tạo INDEX?
- Cột thường trong WHERE, JOIN, ORDER BY, GROUP BY.
- Cột có selectivity cao (nhiều giá trị distinct, vd Email).
- Foreign key (SQL Server KHÔNG tự tạo!).
- Bảng nhỏ (vài nghìn row) — scan có khi nhanh hơn.
- Cột thay đổi rất thường xuyên (UPDATE phải update cả index).
- Selectivity thấp (Gender: 2 giá trị → index gần vô dụng).
- Bảng heavy INSERT, ít SELECT.
2.7. Sargable vs Non-sargable
-- ❌ Non-sargable — function lên cột
WHERE YEAR(OrderDate) = 2026 -- engine phải tính YEAR cho mỗi row
WHERE UPPER(Email) = 'A@B.COM' -- function lên cột
WHERE Total + Tax > 100 -- biểu thức lên cột
WHERE Status LIKE '%Active%' -- wildcard ở đầu
-- ✅ Sargable
WHERE OrderDate >= '2026-01-01' AND OrderDate < '2027-01-01'
WHERE Email = 'a@b.com' -- nếu collation case-insensitive
WHERE Total > 100 - Tax
WHERE Status LIKE 'Active%' -- wildcard cuối OK
⭐ Bẫy PV: "Tại sao WHERE YEAR(date) = 2026 chậm dù có index?" → Non-sargable. DB phải compute YEAR cho mọi row → full scan.
2.8. Verify index có được dùng
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT * FROM Orders WHERE CustomerId = 1;
-- Output:
-- Table 'Orders'. Scan count 1, logical reads 8, physical reads 2
-- CPU time = 0 ms, elapsed time = 12 ms
logical reads = số 8KB page đọc. Chỉ số quan trọng nhất để so trước/sau optimize.
3. JOIN — sâu hơn
3.1. 5 loại JOIN
-- INNER — chỉ row match cả 2
SELECT c.Name, o.Total
FROM Customers c
INNER JOIN Orders o ON o.CustomerId = c.Id;
-- LEFT — tất cả Customers + matching Orders (NULL nếu không có)
SELECT c.Name, o.Total
FROM Customers c
LEFT JOIN Orders o ON o.CustomerId = c.Id;
-- RIGHT — đảo lại (hiếm dùng, swap LEFT thuận)
-- FULL OUTER — tất cả row 2 bên
-- CROSS — tích Descartes m×n
3.2. 🟡 Anti-join — tìm "không có"
-- Tìm khách hàng chưa mua đơn nào — 3 cách:
-- Cách 1: LEFT JOIN ... IS NULL
SELECT c.* FROM Customers c
LEFT JOIN Orders o ON o.CustomerId = c.Id
WHERE o.Id IS NULL;
-- Cách 2: NOT EXISTS (THƯỜNG TỐT NHẤT)
SELECT c.* FROM Customers c
WHERE NOT EXISTS (SELECT 1 FROM Orders o WHERE o.CustomerId = c.Id);
-- Cách 3: NOT IN (cẩn thận NULL!)
SELECT c.* FROM Customers c
WHERE c.Id NOT IN (SELECT CustomerId FROM Orders WHERE CustomerId IS NOT NULL);
x NOT IN (1, 2, NULL) luôn UNKNOWN → query trả 0 rows. Tránh dùng NOT IN với subquery có thể NULL — dùng NOT EXISTS.3.3. 🔴 Bẫy LEFT JOIN + WHERE
-- ❌ Sai — biến LEFT JOIN thành INNER vô tình
SELECT c.* FROM Customers c
LEFT JOIN Orders o ON o.CustomerId = c.Id
WHERE o.Status = 'Paid'; -- ⚠ o.Status NULL bị loại → mất KH chưa mua
-- ✅ Đúng — điều kiện vào ON
SELECT c.* FROM Customers c
LEFT JOIN Orders o ON o.CustomerId = c.Id AND o.Status = 'Paid';
3.4. SELF JOIN — hierarchy
-- Employees có ManagerId trỏ về Employees.Id
SELECT e.Name AS Employee, m.Name AS Manager
FROM Employees e
LEFT JOIN Employees m ON m.Id = e.ManagerId;
-- Output:
-- CEO An | NULL
-- CTO Bao | CEO An
-- Lead Dev Dao | CTO Bao
-- ...
4. Window function — đột phá SQL hiện đại
4.1. Cấu trúc
function_name() OVER (
[PARTITION BY col1, col2 ...]
[ORDER BY col1 ...]
[ROWS|RANGE BETWEEN ... AND ...]
)
4.2. Ranking — ROW_NUMBER / RANK / DENSE_RANK
-- Cùng 100, 100, 90, 80
SELECT
Value,
ROW_NUMBER() OVER (ORDER BY Value DESC) AS Rn, -- 1, 2, 3, 4
RANK() OVER (ORDER BY Value DESC) AS Rk, -- 1, 1, 3, 4 (skip)
DENSE_RANK() OVER (ORDER BY Value DESC) AS Dk -- 1, 1, 2, 3 (no skip)
FROM Scores;
4.3. Top N per group — câu PV hot
-- Đơn hàng mới nhất của MỖI khách
WITH Ranked AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY CustomerId ORDER BY OrderDate DESC) AS rn
FROM Orders
)
SELECT * FROM Ranked WHERE rn = 1;
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...). Cách cũ (correlated subquery) chậm hơn nhiều.4.4. Running total + Moving average
SELECT
OrderDate, Total,
SUM(Total) OVER (ORDER BY OrderDate) AS RunningTotal,
AVG(Total) OVER (
ORDER BY OrderDate
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS MovingAvg7Days
FROM Orders;
5. Transaction & ACID
5.1. Cú pháp transaction
BEGIN TRANSACTION;
BEGIN TRY
UPDATE Accounts SET Balance = Balance - 1000 WHERE Id = 1;
UPDATE Accounts SET Balance = Balance + 1000 WHERE Id = 2;
IF (SELECT Balance FROM Accounts WHERE Id = 1) < 0
THROW 50001, 'Insufficient funds', 1;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
THROW;
END CATCH
5.2. ACID — định nghĩa cốt lõi
| Letter | Tên | Ý nghĩa |
|---|---|---|
| Atomicity | Nguyên tử | Tất cả hoặc không gì — commit hết hoặc rollback hết |
| Consistency | Nhất quán | Trước/sau transaction, DB ở trạng thái hợp lệ (PK, FK, CHECK) |
| Isolation | Cô lập | Transaction không thấy nháp của transaction khác |
| Durability | Bền vững | Sau COMMIT, dữ liệu lưu chắc chắn (kể cả crash) |
Ví dụ chuyển khoản: trừ 1tr tài khoản A, cộng 1tr tài khoản B:
- A: nếu trừ A xong mà cộng B fail → rollback cả 2 → không mất tiền.
- C: balance không bao giờ âm (CHECK constraint).
- I: transaction khác query A đang chạy → không thấy state "đã trừ A nhưng chưa cộng B".
- D: COMMIT xong, crash server → khởi động lại, transaction vẫn được áp dụng.
5.3. Durability — hoạt động ra sao?
- Mọi thay đổi ghi vào transaction log trước.
- Khi COMMIT, đảm bảo log đã flush xuống disk.
- Sau đó data page có thể flush async.
- Crash → khởi động lại → replay log từ checkpoint → restore state.
6. Isolation level — 4 anomaly
6.1. 3 anomaly cần thuộc
- Dirty read: đọc data CHƯA commit của transaction khác.
- Non-repeatable read: đọc cùng row 2 lần, lần 2 thấy khác (do transaction khác UPDATE+commit giữa chừng).
- Phantom read: query 1 range 2 lần, lần 2 thấy thêm/bớt row (do transaction khác INSERT/DELETE).
6.2. 4 isolation level
| Level | Dirty | Non-rep | Phantom |
|---|---|---|---|
| Read Uncommitted | ✅ Có | ✅ | ✅ |
| Read Committed (default SQL Server) | ❌ | ✅ | ✅ |
| Repeatable Read | ❌ | ❌ | ✅ |
| Serializable | ❌ | ❌ | ❌ |
| Snapshot (extra) | ❌ | ❌ | ❌ (MVCC) |
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
-- ...
COMMIT;
6.3. 🟡 SQL Server default vs Postgres default vs MySQL default
- SQL Server: READ COMMITTED (lock-based mặc định, có thể bật snapshot).
- Postgres: READ COMMITTED nhưng dùng MVCC — đọc không block ghi.
- MySQL InnoDB: REPEATABLE READ (cao hơn 2 cái trên) — chọn để statement-based replication consistent.
6.4. 🔴 MVCC — Multi-Version Concurrency Control
READ_COMMITTED_SNAPSHOT ON để dùng MVCC variant.Trade-off MVCC:- ✅ Concurrency cực tốt.
- ❌ Bảng phình to với version cũ → cần VACUUM (Postgres) / undo cleanup (InnoDB).
6.5. NOLOCK — đừng lạm dụng
SELECT * FROM Orders WITH (NOLOCK);
-- ↑ = READ UNCOMMITTED cho query này — cho phép dirty read
- Có thể đọc data đã rollback (dirty).
- Có thể đọc row 2 lần hoặc miss row (do page split khi đang scan).
7. Deadlock
Transaction A: Transaction B:
UPDATE Account 1 (lock A1) UPDATE Account 2 (lock A2)
UPDATE Account 2 ← chờ A2 UPDATE Account 1 ← chờ A1
(chờ vô tận — DEADLOCK)
SQL Server tự detect deadlock và pick "victim" (transaction tốn ít work) → rollback.
7.1. Cách tránh deadlock
- Lock cùng thứ tự trong mọi transaction (vd: luôn UPDATE Account nhỏ ID trước lớn).
- Giữ transaction ngắn (commit sớm).
- Update đúng cột cần, không lock thừa.
- Isolation thấp hơn nếu được (Snapshot thay vì Repeatable Read).
- Index tốt → giảm scope lock (lock row thay vì lock page/table).
8. Query optimization — 10 step
- ✅ EXPLAIN / Actual Execution Plan — xem operator gì đắt.
- ✅
SET STATISTICS IO ON—logical readschỉ số quan trọng nhất. - ✅ Có index trên cột WHERE/JOIN/ORDER BY chưa?
- ✅ WHERE sargable (không function lên cột)?
- ✅ Tránh
SELECT *— chọn cột cần (phá covering index). - ✅ JOIN cùng kiểu dữ liệu (tránh implicit conversion).
- ✅
EXISTSthay vìINvới subquery lớn. - ✅
UNION ALLthayUNIONnếu chắc không trùng. - ✅ Update statistics nếu plan dùng row estimate cũ.
- ✅ Defrag index khi fragmentation > 30%.
9. Câu hỏi tự test cuối ngày
- ACID là gì? Mỗi chữ giải thích bằng ví dụ chuyển khoản.
- INNER JOIN vs LEFT JOIN khác gì?
- Primary Key vs Unique Key khác ra sao?
(A, B, C) — query nào dùng được, query nào không?
6. Vì sao WHERE YEAR(date) = 2026 chậm dù có index trên date?
7. Bẫy LEFT JOIN + WHERE — giải thích.🟡 Mid:
8. Default isolation 3 DB (SQL Server, Postgres, MySQL) khác nhau gì?
9. MVCC là gì? Trade-off?
10. WAL (Write-Ahead Log) — vai trò Durability ra sao?
11. Top N per group — viết query bằng window function.🔴 Senior:
12. B+ tree khác B-tree ra sao? Vì sao DB chọn B+?
13. Clustered index thay đổi → ảnh hưởng non-clustered ra sao?
14. SNAPSHOT isolation vs READ_COMMITTED_SNAPSHOT — khác gì?
15. Parameter sniffing (SQL Server) là gì? Fix sao?Đáp án: Ngân hàng câu hỏi SQL.
➡️ Ngày mai: Ngày 5 — ASP.NET Core + EF Core
Ngày 3 — C# nâng cao
Generic internals, LINQ deferred execution, async/await state machine, delegate vs event, extension method. Giải thích sâu cho mọi level.
Ngày 5 — ASP.NET Core và EF Core
Middleware pipeline trace, DI lifetimes + captive dependency, EF Core change tracking internals, loading strategies với N+1 demo, migrations.