Buổi 10: Index & Transaction + Review toàn bộ khóa
Đây là buổi cuối của khóa. Nửa đầu học hai kỹ thuật quyết định tốc độ và độ tin cậy của một hệ thống chứng khoán: Index (tìm nhanh trong hàng triệu lệnh, giao dịch) và Transaction (chuyển tiền không bao giờ "bốc hơi" giữa chừng). Nửa sau là ôn tập toàn bộ khóa và làm Capstone project: bộ báo cáo phân tích khách hàng cho phòng chăm sóc khách hàng.
Lưu ý: bộ dữ liệu là hnx_securities (xem mục "Bộ dữ liệu mẫu của khóa học" ở Buổi 1). "Hôm nay" cố định là 2026-09-25. Buổi này có nhiều phần không chấm tự động (execution plan, hai phiên đồng thời, kết nối Excel); các phần đó được trình bày thành các bước làm tuần tự để bạn tự thực hành trên SSMS/Workbench và Excel của công ty.
Mục tiêu buổi học
Sau buổi này bạn có thể:
- Giải thích index là gì, B-Tree hoạt động thế nào, lợi ích (tăng tốc
SELECT) và chi phí (chậm ghi, tốn dung lượng); biết khi nào nên và không nên tạo index. - Phân biệt các loại index: PRIMARY KEY (clustered), UNIQUE, composite (quy tắc cột trái nhất), covering (
INCLUDE), FULLTEXT; nắm khác biệt SQL Server / MySQL (NULL trong unique index, index khóa ngoại). - Tạo, xem, xóa index (
CREATE INDEX,DROP INDEX,sys.indexes/INFORMATION_SCHEMA.STATISTICS); đọc execution plan trên SSMS vàEXPLAINtrên MySQL; hiểu index hints /FORCE INDEX. - Viết transaction (
BEGIN/START TRANSACTION,COMMIT,ROLLBACK,SAVEPOINT) tôn trọng ACID và xây procedure chuyển tiền hoàn nguyên khi lỗi. - Hiểu isolation level (dirty / non-repeatable / phantom read), lock và deadlock cùng cách phòng tránh.
- Viết truy vấn hiệu quả (sargable), biết công cụ monitoring, biết kết nối và trích xuất dữ liệu SQL vào Excel, và hoàn thành Capstone.
Bản đồ buổi học
Buổi học 180 phút: khoảng 33% lý thuyết + demo, 67% thực hành. Bài tập nằm xen ngay trong bài giảng; Capstone đặt ở cuối buổi.
| Phần | Nội dung | Hình thức | Thời lượng gợi ý |
|---|---|---|---|
| 1 | Index là gì? B-Tree, lợi ích, nhược điểm, thử nghiệm có/không index | Lý thuyết + demo | 7 phút |
| 2 | Các loại Index, composite, FULLTEXT, khi nào tạo index | Lý thuyết + demo | 7 phút |
| 3 | Tạo và quản lý Index, EXPLAIN, index hints, tối ưu truy vấn chậm |
Lý thuyết + demo | 10 phút |
| Thực hành bài 10.1 (4 bài) và bài 10.2 (1 bài), gồm chữa bài | Thực hành | 24 + 10 phút | |
| 4 | Transaction: ACID, COMMIT/ROLLBACK/SAVEPOINT, isolation, deadlock |
Lý thuyết + mô phỏng | 14 phút |
| Thực hành bài 10.3 (3 bài, gồm chữa bài) | Thực hành | 26 phút | |
| 5 | Hiệu năng: truy vấn hiệu quả, lỗi thường gặp, monitoring | Lý thuyết | 5 phút |
| 6 | Kết nối và trích xuất dữ liệu SQL từ Excel | Hướng dẫn từng bước | 9 phút |
| 7 | Review toàn khóa và Capstone (bài 10.4, 8 bài; làm cá nhân hoặc theo cặp, đổi chéo chữa bài; bài khó 10.4f, 10.4h có thể hoàn thiện sau giờ học) | Ôn tập + dự án | 8 + 60 phút |
| Tổng | Lý thuyết + demo ~60 phút, thực hành ~120 phút (67%) | 180 phút |
1. Index (chỉ mục) là gì?
1.1. Khái niệm
Index là một cấu trúc dữ liệu phụ được database lưu bên cạnh bảng để tăng tốc tìm kiếm. Ví dụ đời thường: mục lục cuối sách. Muốn tìm "Trái phiếu", bạn tra mục lục (đã sắp thứ tự chữ cái) rồi nhảy thẳng tới trang, thay vì lật từng trang từ đầu.
Không có index, để tìm WHERE account_id = 42 database phải quét toàn bộ bảng (table scan) — đọc mọi dòng. Có index trên account_id, database đi thẳng tới đúng dòng cần (index seek).
Ví dụ nghiệp vụ: bảng
Tradescủa một công ty chứng khoán có thể lên tới hàng chục triệu dòng sau vài năm. Màn hình "tra cứu giao dịch của khách hàng X" mà quét cả bảng sẽ mất hàng chục giây; có index đúng chỉ mất vài mili-giây.
1.2. Index hoạt động thế nào? (B-Tree)
Loại index phổ biến nhất là B-Tree (cây cân bằng) — dùng cho cả SQL Server và MySQL InnoDB. Hình dưới so sánh việc tìm một giá trị khi không có index (đọc lần lượt từng dòng) với khi có index B-Tree (đi từ gốc xuống lá qua vài bước).
Không có index, SQL phải quét toàn bảng (đọc lần lượt từng dòng). Có index, nó đi từ gốc xuống lá: mỗi bước loại bỏ phần lớn dữ liệu, số bước tăng rất chậm theo số dòng. Đổi lại INSERT/UPDATE/DELETE phải cập nhật thêm index.
Cách đọc hình: nút gốc (root) chứa các mốc để rẽ nhánh, nút trung gian thu hẹp tiếp, nút lá (leaf) chứa giá trị khóa đã sắp xếp cùng con trỏ tới dòng dữ liệu. Mỗi bước đi xuống loại bỏ phần lớn dữ liệu còn lại.
- Các khóa được sắp xếp sẵn: bảng 1 triệu dòng thường chỉ cần 3–4 lần đọc trang thay vì đọc cả bảng.
- Nút lá được nối với nhau theo thứ tự nên tìm khoảng (
BETWEEN,>=,<) vàORDER BYtheo cột index cũng nhanh. - Khi
INSERT/UPDATE/DELETE, database phải cập nhật cả cây để giữ thứ tự (đôi khi tách/gộp nút).
1.3. Lợi ích và nhược điểm
| Nội dung | |
|---|---|
| Lợi ích | SELECT ... WHERE, JOIN, ORDER BY, GROUP BY theo cột có index nhanh hơn rất nhiều; index UNIQUE còn bảo đảm không trùng |
| Nhược điểm 1 | Chậm INSERT/UPDATE/DELETE: mỗi thay đổi phải cập nhật bảng và mọi index của bảng đó |
| Nhược điểm 2 | Tốn dung lượng (đĩa và bộ nhớ đệm), đôi khi bằng cả bảng |
| Nhược điểm 3 | Index thừa làm optimizer khó chọn và tăng chi phí bảo trì |
Mẹo: quy tắc nhớ nhanh: index giúp đọc, làm chậm ghi. Bảng tra cứu nhiều (Customers, Securities) thường đáng có nhiều index; bảng ghi cực nhiều (Trades, Orders trong phiên khớp lệnh) phải cân nhắc kỹ từng index.
1.4. Thử nghiệm: có Index và không có Index
Bảng mẫu chỉ có vài chục dòng nên chênh lệch không thấy rõ. Ta tự tạo một bảng lớn hơn (khoảng 310.000 dòng) từ tích chéo các bảng có sẵn (đây là phần thực hành thủ công trên SSMS/Workbench, không chạy thử trên trang).
SQL Server
SELECT t.symbol, t.quantity, t.price,
DATEADD(DAY, o.order_id % 400, '2025-01-01') AS trade_date, o.order_id AS ref_id
INTO BigTrades
FROM Trades t CROSS JOIN Orders o CROSS JOIN Accounts a
WHERE a.account_id <= 30; -- 86 x 120 x 30 = 309.600 dòng
SET STATISTICS IO ON; -- xem số trang phải đọc trong tab Messages
SELECT COUNT(*), SUM(quantity) FROM BigTrades WHERE trade_date = '2025-03-01';
-- Table 'BigTrades'. Scan count 1, logical reads 1745 <- quét cả bảng
CREATE INDEX ix_Big_date ON BigTrades (trade_date) INCLUDE (quantity);
SELECT COUNT(*), SUM(quantity) FROM BigTrades WHERE trade_date = '2025-03-01';
-- Table 'BigTrades'. Scan count 1, logical reads 14 <- chỉ 14 trangMySQL
CREATE TABLE BigTrades (symbol VARCHAR(10), quantity INT, price DECIMAL(12,2), trade_date DATE, ref_id INT);
INSERT INTO BigTrades
SELECT t.symbol, t.quantity, t.price, DATE_ADD('2025-01-01', INTERVAL (o.order_id % 400) DAY), o.order_id
FROM Trades t CROSS JOIN Orders o CROSS JOIN Accounts a WHERE a.account_id <= 30;
EXPLAIN SELECT COUNT(*), SUM(quantity) FROM BigTrades WHERE trade_date = '2025-03-01';
-- type = ALL, key = NULL, rows ~ 309.000 <- quét toàn bộ
CREATE INDEX ix_Big_date ON BigTrades (trade_date);
EXPLAIN SELECT COUNT(*), SUM(quantity) FROM BigTrades WHERE trade_date = '2025-03-01';
-- type = ref, key = ix_Big_date, rows = 2580 <- tra thẳng ~2.580 dòngTóm tắt kết quả đo:
| Không index | Có index | |
|---|---|---|
| SQL Server (logical reads) | 1.745 trang | 14 trang |
MySQL (type / rows) |
ALL / ~309.000 |
ref / 2.580 |
| Kết quả truy vấn | 2580 dòng, tổng khối lượng 2.430.000 | giống hệt |
Kết quả truy vấn vẫn như cũ: index không đổi kết quả, chỉ đổi cách đến kết quả.
Cảnh báo: hai điều rút ra khi thử nghiệm (đã kiểm chứng khi soạn bài). (1) Trên SQL Server, nếu chỉ tạo
INDEX (trade_date)không cóINCLUDE (quantity), optimizer vẫn chọn quét bảng vì mỗi dòng tìm được phải "tra ngược" (key lookup) về bảng để lấyquantity, và 2.580 lần tra ngược tốn hơn cả bảng (1.745 trang). Index chỉ được dùng khi có lợi; index có đủ mọi cột truy vấn cần gọi là covering index (chỉ mục phủ). (2) Với bảng nhỏ (vài trăm dòng) optimizer luôn quét bảng vì rẻ hơn; đừng kết luận "index vô dụng" khi đo trên dữ liệu bé.
Xong nhớ dọn: DROP TABLE BigTrades;. Để xem chi phí ghi: chèn 10.000 dòng vào BigTrades khi có 0 index, 1 index, 3 index rồi so thời gian SET STATISTICS TIME ON; mỗi index thêm vào làm thời gian tăng rõ rệt.
2. Các loại Index
| Loại | Đặc điểm | SQL Server | MySQL (InnoDB) |
|---|---|---|---|
| PRIMARY KEY index | Tự sinh khi khai báo khóa chính; không NULL, không trùng | Mặc định là clustered (sắp xếp vật lý cả bảng theo khóa) | Luôn là clustered index; mọi index phụ đều chứa thêm khóa chính |
| UNIQUE index | Không cho trùng giá trị (kiêm ràng buộc UNIQUE) |
Chỉ cho một NULL; muốn nhiều NULL dùng filtered index (WHERE col IS NOT NULL) |
Cho phép nhiều NULL |
| Non-clustered / index thường | Không ràng buộc; chỉ để tìm nhanh | CREATE INDEX |
CREATE INDEX |
| COMPOSITE index | Nhiều cột, thứ tự cột quan trọng | (a, b) |
(a, b) |
| Covering (INCLUDE) | Kèm cột phụ ở nút lá để khỏi tra ngược | INCLUDE (col) |
Không có INCLUDE, đưa cột vào cuối composite |
| Filtered index | Chỉ index một phần dòng | WHERE ... |
Không có (dùng generated column) |
| FULLTEXT index | Tìm kiếm từ/cụm từ trong văn bản dài | Cần Full-Text Catalog rồi CREATE FULLTEXT INDEX, dùng CONTAINS/FREETEXT |
CREATE FULLTEXT INDEX, dùng MATCH(col) AGAINST('...') |
2.1. Composite index và quy tắc "cột trái nhất" (leftmost prefix)
Composite (account_id, order_date) giống danh bạ sắp xếp theo họ rồi tên:
Điều kiện WHERE |
Dùng được index (account_id, order_date)? |
Vì sao |
|---|---|---|
account_id = 42 |
Có | Cột đầu có mặt |
account_id = 42 AND order_date >= '2026-09-01' |
Có, tốt nhất | Đẳng thức rồi khoảng, đúng thứ tự |
order_date >= '2026-09-01' |
Không (hoặc rất kém) | Thiếu cột đầu, giống tìm theo tên mà không biết họ |
Quy tắc thứ tự cột: cột dùng điều kiện = đứng trước, cột dùng khoảng (>=, BETWEEN) đứng sau; cột chọn lọc cao (ít trùng) ưu tiên trước nếu cùng loại điều kiện.
2.2. FULLTEXT index
LIKE '%Ngan Hang%' (có % ở đầu) không dùng được index B-Tree. Với văn bản dài, dùng full-text.
MySQL
CREATE FULLTEXT INDEX ft_customers_name ON Customers (full_name);
SELECT customer_id, full_name FROM Customers
WHERE MATCH(full_name) AGAINST('Ngan Hang' IN NATURAL LANGUAGE MODE)
ORDER BY customer_id;Kết quả gồm bốn ngân hàng: 31 Ngan Hang TMCP Dau Tu Viet, 32 Ngan Hang Nong Nghiep Phat Trien, 37 Ngan Hang Quan Doi Mien Bac, 40 Ngan Hang Ban Le Sai Gon.
SQL Server cần Full-Text Search (một thành phần cài thêm):
CREATE FULLTEXT CATALOG ft_cat AS DEFAULT;
CREATE FULLTEXT INDEX ON Customers (full_name) KEY INDEX <tên PK>; -- tên PK xem bằng sys.indexes
SELECT customer_id, full_name FROM Customers WHERE CONTAINS(full_name, '"Ngan Hang"');SQL Server vs MySQL: Codelab không chấm phần full-text trên SQL Server; bạn thực hành trên SSMS của công ty nếu đã bật tính năng.
2.3. Khi nào nên tạo Index?
| Nên tạo cho | Cẩn thận / không nên với |
|---|---|
Cột khóa ngoại dùng để JOIN (Accounts.customer_id, Orders.account_id, Trades.order_id) |
Bảng rất nhỏ (optimizer luôn quét bảng) |
Cột thường xuất hiện trong WHERE, ORDER BY, GROUP BY của truy vấn chạy nhiều lần (trade_date, order_date) |
Cột có ít giá trị khác nhau (status 3 giá trị, side BUY/SELL): index ít chọn lọc, optimizer bỏ qua |
| Cột cần bảo đảm duy nhất (email, mã khách hàng) | Bảng ghi nhiều, đọc ít |
| Cột dùng để sắp xếp/phân trang thường xuyên | Cột bị bọc hàm trong WHERE (YEAR(trade_date)): index vô dụng nếu không viết lại truy vấn |
SQL Server vs MySQL: SQL Server không tự tạo index cho khóa ngoại (chỉ tạo cho PK/UNIQUE) nên bạn phải tự tạo; MySQL InnoDB tự tạo index cho khóa ngoại (
SHOW INDEX FROM Orderscho thấy index tênaccount_id,symbol). Đừng tạo quá nhiều index: mỗi index là một cây phải cập nhật.
3. Tạo và quản lý Index
3.1. Cú pháp
-- Index thường
CREATE INDEX ix_Accounts_status ON Accounts (status);
-- Composite (đẳng thức trước, khoảng sau)
CREATE INDEX ix_Orders_symbol_date ON Orders (symbol, order_date);
-- Unique
CREATE UNIQUE INDEX ux_Customers_phone ON Customers (phone); -- MySQL
CREATE UNIQUE INDEX ux_Customers_phone ON Customers (phone) WHERE phone IS NOT NULL; -- SQL Server (cho nhiều NULL)
-- Xóa
DROP INDEX ix_Accounts_status ON Accounts; -- cả SQL Server lẫn MySQLCách khác qua ALTER TABLE:
| Việc | SQL Server | MySQL |
|---|---|---|
| Thêm index thường | (dùng CREATE INDEX) |
ALTER TABLE Accounts ADD INDEX ix_Accounts_status (status); |
| Thêm ràng buộc duy nhất (kèm index) | ALTER TABLE Customers ADD CONSTRAINT uq_x UNIQUE (col); |
ALTER TABLE Customers ADD UNIQUE INDEX uq_x (col); |
| Xóa | ALTER TABLE Customers DROP CONSTRAINT uq_x; hoặc DROP INDEX |
ALTER TABLE Accounts DROP INDEX ix_Accounts_status; |
| Đổi cấu trúc index | CREATE INDEX ... WITH (DROP_EXISTING = ON) |
Xóa rồi tạo lại |
Mẹo: đặt tên tường minh cho index (
ix_<Bảng>_<Cột>,ux_cho unique) thay vì để hệ thống tự sinh: tên tự sinh nhưPK__Orders__4659622983F810DEchứa mã ngẫu nhiên, khác nhau giữa mỗi database, không dùng được trong script. Vì vậy các bài chấm ở Codelab đều yêu cầu đặt đúng tên index rồi kiểm tra theo tên.
Ví dụ tạo index rồi kiểm tra trong danh mục hệ thống (dùng bảng MarketPrices để không trùng bài tập):
SQL Server
CREATE INDEX ix_MarketPrices_date ON MarketPrices (trade_date);
SELECT i.name, i.type_desc, i.is_unique
FROM sys.indexes i
WHERE i.object_id = OBJECT_ID('MarketPrices') AND i.name LIKE 'ix[_]%';MySQL
CREATE INDEX ix_MarketPrices_date ON MarketPrices (trade_date);
SELECT INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX, NON_UNIQUE
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'MarketPrices' AND LEFT(INDEX_NAME, 3) = 'ix_';Cả hai cho thấy index ix_MarketPrices_date (SQL Server: NONCLUSTERED, is_unique = 0; MySQL: NON_UNIQUE = 1).
Composite index: liệt kê từng cột theo thứ tự.
SQL Server
CREATE INDEX ix_Orders_symbol_date ON Orders (symbol, order_date);
SELECT i.name AS index_name, c.name AS column_name, ic.key_ordinal
FROM sys.indexes i
JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
WHERE i.object_id = OBJECT_ID('Orders') AND i.name = 'ix_Orders_symbol_date'
ORDER BY ic.key_ordinal;MySQL
CREATE INDEX ix_Orders_symbol_date ON Orders (symbol, order_date);
SELECT INDEX_NAME AS index_name, COLUMN_NAME AS column_name, SEQ_IN_INDEX AS key_ordinal
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'Orders' AND INDEX_NAME = 'ix_Orders_symbol_date'
ORDER BY SEQ_IN_INDEX;Kết quả: symbol ở vị trí 1, order_date ở vị trí 2. Xóa index và kiểm tra lại:
CREATE INDEX ix_Securities_sector ON Securities (sector);
DROP INDEX ix_Securities_sector ON Securities;
SELECT COUNT(*) AS so_dong_securities FROM Securities;Xóa index không ảnh hưởng dữ liệu bảng (số dòng vẫn nguyên).
3.2. Xem index đang có
SQL Server: trong SSMS, mở Object Explorer, chọn bảng, mở nhánh Indexes; hoặc truy vấn danh mục:
SELECT i.name, i.type_desc, i.is_unique, i.is_primary_key
FROM sys.indexes i
WHERE i.object_id = OBJECT_ID('Customers') ORDER BY i.name;
-- PK__Customer__CD65CB859651DCC5 CLUSTERED 1 1
-- UQ__Customer__6A9E4CB728092520 NONCLUSTERED 1 0 (từ ràng buộc UNIQUE của customer_code)MySQL:
SHOW INDEX FROM Orders;
SELECT INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX, NON_UNIQUE
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'Customers'
ORDER BY INDEX_NAME, SEQ_IN_INDEX;
-- customer_code customer_code 1 0
-- PRIMARY customer_id 1 0
-- referrer_id referrer_id 1 1 (index tự tạo cho khóa ngoại)Thực hành: Vừa học xong cú pháp tạo index thường, composite, unique (kể cả khác biệt NULL giữa hai hệ) và cách kiểm tra trong danh mục hệ thống, hãy làm bài 10.1: tạo index trên khóa ngoại, composite
(account_id, order_date), unique index trên email cho phép nhiều NULL, và tạo/xóa index cho báo cáo giao dịch.
3.3. Phân tích truy vấn: Execution Plan / EXPLAIN
Để biết database thực sự làm gì với truy vấn của bạn, hãy xem kế hoạch thực thi. Phần này không chấm tự động (kết quả phụ thuộc bản cài đặt), hãy làm theo từng bước trên bảng BigTrades ở mục 1.4.
SQL Server Management Studio (SSMS):
| Bước | Thao tác | Bạn sẽ thấy |
|---|---|---|
| 1 | Mở cửa sổ truy vấn, gõ truy vấn cần phân tích | |
| 2 | Bấm Query → Include Actual Execution Plan (Ctrl + M). Estimated plan (Ctrl + L) không chạy truy vấn, chỉ ước lượng | |
| 3 | Chạy truy vấn (F5) | Cạnh tab Results xuất hiện tab Execution Plan |
| 4 | Đọc từ phải sang trái: dữ liệu chảy từ toán tử phải (đọc bảng) sang trái (kết quả) | Chuỗi hộp toán tử nối bằng mũi tên |
| 5 | Di chuột vào mũi tên để xem Actual Rows, vào toán tử xem Estimated Cost | Số dòng thực tế, chi phí |
| 6 | Bật SET STATISTICS IO, TIME ON; rồi xem tab Messages |
Số trang đọc (logical reads), thời gian CPU |
| Toán tử | Ý nghĩa | Đánh giá |
|---|---|---|
| Table Scan / Clustered Index Scan | Đọc toàn bộ bảng | Cảnh báo nếu bảng lớn và chỉ cần vài dòng |
| Index Seek | Đi theo cây B-Tree tới đúng dòng | Tốt |
| Index Scan | Đọc toàn bộ index | Thường chấp nhận được, kém hơn Seek |
| Key Lookup (RID Lookup) | Tra ngược bảng để lấy cột thiếu | Nhiều lần thì cân nhắc covering index (INCLUDE) |
| Nested Loops / Hash Match / Merge Join | Cách JOIN | Hash Match trên bảng lớn có thể tốn bộ nhớ |
| Sort | Sắp xếp | Tốn; index có thứ tự phù hợp sẽ loại bỏ được |
Lưu ý: nếu thấy dấu cảnh báo vàng, hoặc SSMS gợi ý chữ xanh Missing Index, đó là manh mối tạo index. Nhưng đừng tạo mù quáng mọi gợi ý; hãy xét bảng có ghi nhiều không.
MySQL Workbench / dòng lệnh: đặt EXPLAIN trước câu SELECT.
EXPLAIN SELECT * FROM Orders WHERE account_id = 42 AND order_date >= '2026-09-01';Trước khi có composite index, MySQL dùng index tự tạo cho khóa ngoại:
id select_type table type possible_keys key key_len ref rows filtered Extra
1 SIMPLE Orders ref account_id account_id 4 const 5 33.33 Using whereSau CREATE INDEX ix_Orders_account_date ON Orders (account_id, order_date):
1 SIMPLE Orders range ix_Orders_account_date ix_Orders_account_date 7 NULL 3 100.0 Using index conditionCột EXPLAIN |
Ý nghĩa |
|---|---|
type |
Cách truy cập bảng, tốt → tệ: const/eq_ref → ref → range → index → ALL (quét toàn bảng) |
possible_keys / key |
Index có thể dùng / index thực sự được chọn (NULL = không dùng index) |
key_len |
Số byte index được dùng; với composite cho biết dùng được mấy cột đầu |
rows |
Số dòng ước tính phải đọc, càng nhỏ càng tốt |
filtered |
% dòng còn lại sau khi lọc |
Extra |
Using index (covering, rất tốt), Using where, Using filesort / Using temporary (tốn kém) |
EXPLAIN ANALYZE (MySQL 8) chạy thật và in thời gian/số dòng thực tế, ví dụ -> Index range scan on Trades using ix_Trades_trade_date ... (actual time=0.0215..0.024 rows=14 loops=1).
3.4. Index Hints / FORCE INDEX
Optimizer thường chọn kế hoạch tốt nhất dựa trên thống kê dữ liệu; đôi khi thống kê cũ hoặc truy vấn quá phức tạp khiến nó chọn sai. Khi đó bạn có thể ép dùng (hoặc tránh) một index:
| Mục đích | MySQL | SQL Server |
|---|---|---|
| Gợi ý dùng index | FROM BigTrades USE INDEX (ix_Big_date) |
FROM BigTrades WITH (INDEX(ix_Big_date)) |
| Ép buộc dùng index | FROM BigTrades FORCE INDEX (ix_Big_date) |
WITH (FORCESEEK), WITH (INDEX(ix_Big_date)) |
| Bỏ qua index | IGNORE INDEX (ix_Big_date) |
WITH (INDEX(0)) (ép quét bảng/clustered index) |
Cú pháp hint chạy được ngay trên bảng mẫu:
SQL Server
CREATE INDEX ix_MarketPrices_date ON MarketPrices (trade_date);
GO
SELECT COUNT(*) AS so_dong
FROM MarketPrices WITH (INDEX(ix_MarketPrices_date))
WHERE trade_date = '2026-09-25';MySQL
CREATE INDEX ix_MarketPrices_date ON MarketPrices (trade_date);
SELECT COUNT(*) AS so_dong
FROM MarketPrices FORCE INDEX (ix_MarketPrices_date)
WHERE trade_date = '2026-09-25';Kết quả 12 (12 mã có giá ngày 2026-09-25). Hint không đổi kết quả, chỉ đổi cách tìm.
Cảnh báo: chỉ dùng hint khi đã đo (execution plan + thời gian thật) thấy optimizer chọn sai và bạn không sửa được bằng cập nhật thống kê (
UPDATE STATISTICS/ANALYZE TABLE) hoặc viết lại truy vấn. Hint là "băng dán": khi dữ liệu thay đổi, index bạn ép có thể trở thành lựa chọn tệ nhưng optimizer không còn quyền đổi. Nếu index bị xóa, truy vấn có hint sẽ báo lỗi.
3.5. Tối ưu truy vấn chậm: viết lại cho "sargable"
Nguyên nhân chậm phổ biến nhất không phải thiếu index mà là hàm bọc quanh cột trong WHERE: index trên cột đó bị bỏ qua vì database phải tính hàm cho từng dòng. Điều kiện dùng được index gọi là sargable (Search ARGument ABLE). Cách sửa: để cột trần ở một vế, đưa phép tính sang vế còn lại; với ngày, đổi YEAR()/MONTH() thành khoảng ngày.
Ví dụ trên bảng Orders: đếm lệnh tháng 09/2026.
-- Cách chậm: hàm bọc cột order_date
SELECT COUNT(*) AS so_lenh FROM Orders
WHERE YEAR(order_date) = 2026 AND MONTH(order_date) = 9;-- Cách nhanh: khoảng ngày trên cột trần, cùng kết quả
SELECT COUNT(*) AS so_lenh FROM Orders
WHERE order_date >= '2026-09-01' AND order_date < '2026-10-01';Hai truy vấn cho cùng kết quả (72), nhưng chỉ truy vấn thứ hai tận dụng được index trên order_date. Dùng < '2026-10-01' thay vì <= '2026-09-30' để không bỏ sót dữ liệu khi cột có phần giờ.
Quy trình mỗi lần tối ưu:
| Bước | Việc làm |
|---|---|
| 1 | Chạy truy vấn gốc, ghi lại số trang đọc / thời gian (mục 3.3) |
| 2 | Tìm điểm nghẽn: scan, key lookup, sort, hàm bọc cột |
| 3 | Viết lại truy vấn trước (sargable), rồi mới nghĩ tới thêm index |
| 4 | Đo lại, so sánh và ghi chú |
Thực hành: Vừa học xong cách đọc execution plan/
EXPLAINvà viết lại điều kiện thành sargable, hãy làm bài 10.2: chuyển truy vấn báo cáo giao dịch tháng 09/2026 dùngYEAR()/MONTH()thành điều kiện khoảng ngày, kết quả phải giữ nguyên. Sau khi nộp, thử tự bật execution plan /EXPLAINcho hai phiên bản để thấy sự khác biệt.
4. Transaction (giao tác) và toàn vẹn dữ liệu
Transaction là một nhóm câu lệnh được coi là một đơn vị công việc không thể chia nhỏ: hoặc tất cả cùng thành công, hoặc không cái nào có hiệu lực.
Ví dụ nghiệp vụ: chuyển 10.000.000 từ tài khoản A sang tài khoản B gồm hai bước (trừ A, cộng B). Nếu hệ thống sập giữa hai bước mà không có transaction, tiền của khách hàng "bốc hơi".
4.1. Tính chất ACID
| Chữ | Tính chất | Ý nghĩa | Ví dụ chuyển tiền |
|---|---|---|---|
| A | Atomicity (nguyên tử) | Tất cả hoặc không gì cả | Trừ A và cộng B cùng thành công, hoặc cả hai bị hủy |
| C | Consistency (nhất quán) | Dữ liệu luôn thỏa các ràng buộc/quy tắc | Tổng tiền hai tài khoản trước = sau; không vi phạm khóa ngoại, không âm |
| I | Isolation (cô lập) | Các transaction chạy đồng thời không làm hỏng nhau | Người khác không thấy trạng thái "đã trừ A nhưng chưa cộng B" |
| D | Durability (bền vững) | Đã COMMIT thì không mất, kể cả mất điện |
Ghi vào transaction log trước khi báo thành công |
4.2. Quản lý transaction
| Lệnh | SQL Server | MySQL |
|---|---|---|
| Bắt đầu | BEGIN TRANSACTION; |
START TRANSACTION; (hoặc BEGIN;) |
| Xác nhận | COMMIT TRANSACTION; |
COMMIT; |
| Hoàn nguyên | ROLLBACK TRANSACTION; |
ROLLBACK; |
| Điểm lưu | SAVE TRANSACTION tên; |
SAVEPOINT tên; |
| Quay về điểm lưu | ROLLBACK TRANSACTION tên; |
ROLLBACK TO SAVEPOINT tên; |
Hình dưới nhắc lại (và mở rộng bằng SAVEPOINT) vòng đời của một transaction: mở bằng BEGIN, chạy các lệnh, rồi kết thúc bằng COMMIT (giữ mọi thay đổi) hoặc ROLLBACK (hủy mọi thay đổi).
Các lệnh trong transaction hoặc cùng thành công (COMMIT) hoặc cùng bị hủy (ROLLBACK) — đó là tính nguyên tử (Atomicity) của ACID. SAVEPOINT cho phép hủy một phần.
Cách đọc hình: các lệnh nằm giữa BEGIN và điểm kết thúc chỉ là "nháp" đối với người khác cho tới khi COMMIT. SAVEPOINT là một mốc ở giữa: ROLLBACK về savepoint chỉ hủy phần sau mốc, phần trước mốc vẫn giữ và transaction tiếp tục.
Lưu ý: cả hai hệ mặc định autocommit: mỗi câu lệnh đơn lẻ tự thành một transaction rồi tự
COMMIT.BEGIN/START TRANSACTIONtạm tắt chế độ này cho tới khiCOMMIT/ROLLBACK.
Chuyển tiền có kiểm soát lỗi
SQL Server — bọc bằng TRY ... CATCH. Chuyển tiền sang tài khoản không tồn tại sẽ bị hoàn nguyên; câu cuối của khối CATCH cho thấy số dư tài khoản nguồn vẫn nguyên:
BEGIN TRY
BEGIN TRANSACTION;
UPDATE Accounts SET balance = balance - 5000000 WHERE account_id = 1;
UPDATE Accounts SET balance = balance + 5000000 WHERE account_id = 99999; -- tài khoản không tồn tại
IF @@ROWCOUNT = 0 THROW 50001, 'Tai khoan dich khong ton tai', 1;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
SELECT ERROR_NUMBER() AS err_no, ERROR_MESSAGE() AS err_msg,
(SELECT balance FROM Accounts WHERE account_id = 1) AS balance_acc1;
END CATCH;Kết quả: 50001 Tai khoan dich khong ton tai 67000000.00; khoản trừ 5.000.000 đã được hoàn nguyên.
MySQL — dùng EXIT HANDLER trong procedure:
DELIMITER //
CREATE PROCEDURE sp_TransferSafe(IN p_from INT, IN p_to INT, IN p_amount DECIMAL(18,2))
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT 'ROLLED BACK' AS ket_qua,
(SELECT balance FROM Accounts WHERE account_id = p_from) AS balance_from;
END;
START TRANSACTION;
UPDATE Accounts SET balance = balance - p_amount WHERE account_id = p_from;
UPDATE Accounts SET balance = balance + p_amount WHERE account_id = p_to;
IF ROW_COUNT() = 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Tai khoan dich khong ton tai';
END IF;
COMMIT;
SELECT 'COMMITTED' AS ket_qua, (SELECT balance FROM Accounts WHERE account_id = p_from) AS balance_from;
END //
DELIMITER ;
CALL sp_TransferSafe(1, 99999, 5000000);Kết quả: ROLLED BACK 67000000.00.
Ba cách hoàn nguyên đúng:
| Cách | Mô tả |
|---|---|
| 1. Điều kiện ngay từ đầu | ... AND balance >= @amount rồi kiểm tra số dòng bị ảnh hưởng (@@ROWCOUNT / ROW_COUNT()), không cần gây lỗi. Đây là cách của bài 10.3b |
| 2. Gây lỗi có chủ đích | THROW / SIGNAL và để handler ROLLBACK |
3. XACT_ABORT ON (T-SQL) |
Tự rollback cả transaction khi có lỗi runtime |
Cảnh báo: các bẫy thường gặp: quên
COMMIT(transaction "treo", giữ khóa, chặn người khác);ROLLBACKxong không thoát procedure nên các lệnh sau vẫn chạy ngoài transaction; DDL trong MySQL (CREATE/ALTER/DROP) tự động commit; MyISAM không hỗ trợ transaction (dùng InnoDB).
4.3. SAVEPOINT — hoàn nguyên một phần
Đặt "điểm lưu" giữa transaction; có thể quay về đó mà vẫn giữ phần trước và tiếp tục.
SQL Server
BEGIN TRANSACTION;
UPDATE Accounts SET balance = balance - 1000000 WHERE account_id = 1;
SAVE TRANSACTION sp_step1;
UPDATE Accounts SET balance = balance - 2000000 WHERE account_id = 1;
ROLLBACK TRANSACTION sp_step1; -- chỉ hủy khoản trừ 2.000.000
COMMIT TRANSACTION;
SELECT balance FROM Accounts WHERE account_id = 1;MySQL
START TRANSACTION;
UPDATE Accounts SET balance = balance - 1000000 WHERE account_id = 1;
SAVEPOINT sp_step1;
UPDATE Accounts SET balance = balance - 2000000 WHERE account_id = 1;
ROLLBACK TO SAVEPOINT sp_step1; -- chỉ hủy khoản trừ 2.000.000
COMMIT;
SELECT balance FROM Accounts WHERE account_id = 1;Cả hai cho 66000000.00: chỉ còn khoản trừ 1.000.000.
| Bước | Số dư tài khoản 1 (bắt đầu 67.000.000) |
|---|---|
Sau UPDATE thứ nhất |
66.000.000 |
Sau SAVE TRANSACTION / SAVEPOINT |
66.000.000 (đánh dấu mốc) |
Sau UPDATE thứ hai |
64.000.000 |
Sau ROLLBACK về savepoint |
66.000.000 |
Sau COMMIT |
66.000.000 (đã ghi vĩnh viễn) |
Lưu ý:
ROLLBACKkhông kèm tên savepoint trong T-SQL sẽ hủy toàn bộ transaction; savepoint không tự giải phóng khóa của phần đã hoàn nguyên.
Thực hành: Vừa học xong
COMMIT,ROLLBACK,SAVEPOINTvà cách hoàn nguyên khi lỗi, hãy làm bài 10.3: chuyển tiền bằng transaction rồi hoàn nguyên thao tác nhầm, viếtsp_TransferMoneyan toàn (không làm gì khi thiếu tiền/tài khoản sai) và procedure hai khoản chi dùng SAVEPOINT. Phần isolation và deadlock ngay sau đó là hướng dẫn mô phỏng thủ công.
4.4. Isolation level và các xung đột đồng thời
Khi nhiều phiên (session) chạy cùng lúc trên cùng dữ liệu (nhiều nhân viên đặt lệnh, chạy báo cáo), có ba hiện tượng đọc bất thường:
| Hiện tượng | Mô tả (ví dụ) |
|---|---|
| Dirty Read (đọc bẩn) | Phiên B đọc số dư account 1 vừa bị phiên A trừ nhưng chưa COMMIT; sau đó A ROLLBACK nên B đã đọc con số chưa từng tồn tại |
| Non-repeatable Read (đọc không lặp lại) | Trong cùng transaction, B đọc số dư account 1 = 67 triệu; A COMMIT sửa thành 57 triệu; B đọc lại thấy 57 triệu: cùng một dòng, hai giá trị |
| Phantom Read (đọc bóng ma) | B đếm lệnh của account 1 = 3 dòng; A COMMIT chèn thêm 1 lệnh; B đếm lại = 4: xuất hiện dòng mới |
Isolation level quy định mức chống các hiện tượng trên (mức càng cao càng an toàn nhưng càng nhiều khóa, càng dễ chờ nhau/deadlock):
| Mức | Dirty read | Non-repeatable read | Phantom read | Ghi chú |
|---|---|---|---|---|
READ UNCOMMITTED |
Có thể | Có thể | Có thể | Nhanh nhất, kém tin cậy (T-SQL: hint NOLOCK) |
READ COMMITTED |
Không | Có thể | Có thể | Mặc định của SQL Server |
REPEATABLE READ |
Không | Không | Có thể (theo chuẩn) | Mặc định của MySQL InnoDB (InnoDB dùng khóa khoảng nên phần lớn cũng chặn được phantom) |
SERIALIZABLE |
Không | Không | Không | Như chạy tuần tự, chậm và dễ chờ khóa |
SNAPSHOT (chỉ SQL Server) |
Không | Không | Không | Đọc từ "ảnh chụp" phiên bản dữ liệu, không chặn người ghi |
-- Đổi mức cho phiên hiện tại (cú pháp giống nhau ở cả hai hệ)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;Thực hành mô phỏng dirty read (không chấm tự động vì cần hai phiên đồng thời). Mở hai cửa sổ truy vấn trong SSMS (hoặc hai tab trong Workbench), gọi là Phiên A và Phiên B, cùng kết nối tới hnx_securities:
| Bước | Phiên A | Phiên B | Bạn sẽ thấy |
|---|---|---|---|
| 1 | BEGIN TRANSACTION; UPDATE Accounts SET balance = balance - 1000000 WHERE account_id = 1; (chưa commit) |
A chạy xong, giữ khóa dòng | |
| 2 | SELECT balance FROM Accounts WHERE account_id = 1; |
Ở READ COMMITTED câu này bị treo (chờ khóa) |
|
| 3 | Hủy truy vấn treo, chạy SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; rồi chạy lại |
Đọc được số dư đã trừ (dirty read) | |
| 4 | ROLLBACK; |
Đọc lại | Số dư trở về giá trị cũ: B từng thấy một con số "ma" |
Mẹo: nhớ
ROLLBACKở phiên A khi xong để không giữ khóa; nếu phiên treo, đóng tab hoặc chạyKILL <session_id>.
4.5. Lock và Deadlock
Để giữ tính cô lập, database dùng khóa (lock): khi A đang ghi một dòng, B muốn ghi/đọc dòng đó phải chờ tới khi A COMMIT/ROLLBACK. Chờ là bình thường.
Deadlock (khóa chết) xảy ra khi hai transaction chờ lẫn nhau, không bên nào tiến được. Cách mô phỏng: làm đúng bảng dưới trong hai cửa sổ truy vấn, thực hiện t1 ở cả hai phiên rồi t2 ở cả hai; trong vài giây một phiên nhận lỗi deadlock.
| Thời điểm | Phiên A (chuyển tiền 1 → 2) | Phiên B (chuyển tiền 2 → 1) |
|---|---|---|
| t1 | BEGIN TRAN; UPDATE Accounts ... WHERE account_id = 1; (giữ khóa dòng 1) |
BEGIN TRAN; UPDATE Accounts ... WHERE account_id = 2; (giữ khóa dòng 2) |
| t2 | UPDATE Accounts ... WHERE account_id = 2; → chờ B |
UPDATE Accounts ... WHERE account_id = 1; → chờ A |
| t3 | Cả hai chờ nhau mãi → deadlock. Database chọn một nạn nhân và hủy transaction của nó |
| SQL Server | MySQL InnoDB | |
|---|---|---|
| Lỗi nạn nhân nhận | 1205 ("Transaction was deadlocked ... chosen as the deadlock victim") | 1213 ("Deadlock found when trying to get lock; try restarting transaction") |
| Sau đó | Transaction còn lại được chạy tiếp; nạn nhân phải ROLLBACK và thử lại |
Tương tự |
Cách phòng tránh:
- Luôn truy cập tài nguyên theo cùng một thứ tự, ví dụ luôn khóa tài khoản có
account_idnhỏ hơn trước (trongsp_TransferMoney,UPDATEtheo thứ tự id tăng dần bất kể chiều chuyển). Đây là biện pháp hiệu quả nhất. - Giữ transaction ngắn: chuẩn bị dữ liệu và tính toán trước khi
BEGIN, không chờ người dùng nhập liệu khi transaction đang mở. - Có index phù hợp để câu
UPDATE/DELETEchỉ khóa đúng dòng cần (không có index thì khóa nhiều dòng/quét cả bảng). - Chọn isolation level vừa đủ (đừng dùng
SERIALIZABLEnếu không cần); SQL Server cân nhắcREAD_COMMITTED_SNAPSHOT. - Cài cơ chế thử lại (retry) ở ứng dụng khi bắt lỗi 1205/1213.
Theo dõi: SQL Server dùng Activity Monitor, sp_who2, sys.dm_tran_locks, Extended Events xml_deadlock_report (phiên system_health có sẵn); MySQL dùng SHOW ENGINE INNODB STATUS (mục LATEST DETECTED DEADLOCK), performance_schema.data_locks.
5. Hiệu năng (Performance Tuning)
5.1. Viết truy vấn hiệu quả
- Chỉ lấy thứ cần: liệt kê cột thay vì
SELECT *; lọc sớm bằngWHERE; phân trang bằngOFFSET ... FETCH(T-SQL) /LIMIT(MySQL) thay vì kéo cả bảng về ứng dụng. - Điều kiện "sargable" (đã làm ở bài 10.2): để cột trần ở một vế, đưa phép tính sang vế còn lại.
| Không sargable (index bị bỏ qua) | Sargable |
|---|---|
WHERE YEAR(trade_date) = 2026 AND MONTH(trade_date) = 9 |
WHERE trade_date >= '2026-09-01' AND trade_date < '2026-10-01' |
WHERE quantity * 2 > 1000 |
WHERE quantity > 500 |
WHERE LEFT(full_name, 4) = 'Ngan' |
WHERE full_name LIKE 'Ngan%' |
WHERE full_name LIKE '%Hang' (dấu % ở đầu) |
full-text index hoặc thiết kế lại |
WHERE ISNULL(city, '') = 'Ha Noi' |
WHERE city = 'Ha Noi' |
- Tránh chuyển kiểu ngầm: so sánh cột chuỗi với số (
WHERE customer_code = 100007) khiến database chuyển đổi cả cột và mất index. So sánh đúng kiểu dữ liệu. - Dùng JOIN/EXISTS thay cho subquery tương quan chạy lại cho từng dòng; dùng
EXISTSkhi chỉ cần biết "có hay không". - Xử lý theo tập hợp, không theo từng dòng (vòng lặp/cursor). Một
UPDATEtập hợp thường nhanh hơn vòng lặp hàng trăm lần.
5.2. Common Mistakes (lỗi hiệu năng thường gặp)
| Lỗi | Hậu quả | Cách sửa |
|---|---|---|
SELECT * mọi nơi |
Đọc thừa dữ liệu, phá covering index | Liệt kê cột |
Hàm bọc cột trong WHERE |
Không dùng được index | Viết lại dạng khoảng |
Thiếu index cho khóa ngoại dùng JOIN (SQL Server) |
JOIN quét toàn bảng | CREATE INDEX trên khóa ngoại |
UPDATE/DELETE thiếu WHERE |
Sửa/xóa cả bảng | Luôn thử bằng SELECT trước; dùng transaction |
Transaction dài / quên COMMIT |
Chặn người khác, deadlock | Ngắn gọn, luôn có COMMIT/ROLLBACK |
NOT IN (subquery) gặp NULL |
Trả về rỗng bất ngờ | Dùng NOT EXISTS |
5.3. Monitoring & Optimization
| Việc | SQL Server | MySQL |
|---|---|---|
| Xem kế hoạch thực thi | SSMS: Include Actual Execution Plan | EXPLAIN / EXPLAIN ANALYZE |
| Đo số trang đọc / thời gian | SET STATISTICS IO, TIME ON; |
EXPLAIN ANALYZE, SHOW PROFILE |
| Tìm truy vấn chậm | Query Store, sys.dm_exec_query_stats, Activity Monitor |
Slow Query Log, performance_schema |
| Xem phiên đang chạy | sp_who2, sys.dm_exec_requests |
SHOW PROCESSLIST |
| Gợi ý index còn thiếu | sys.dm_db_missing_index_details |
(Workbench Performance Reports) |
| Index ít dùng (đáng xóa) | sys.dm_db_index_usage_stats |
sys.schema_unused_indexes |
| Bảo trì index bị phân mảnh | ALTER INDEX ... REORGANIZE / REBUILD |
OPTIMIZE TABLE |
| Cập nhật thống kê | UPDATE STATISTICS Trades; |
ANALYZE TABLE Trades; |
Mẹo: quy trình tối ưu bài bản: (1) xác định truy vấn chậm bằng số liệu thật, (2) xem execution plan tìm điểm nghẽn (scan, key lookup, sort), (3) sửa: viết lại truy vấn trước, rồi mới thêm index, (4) đo lại và so sánh, (5) ghi chú lại. Tối ưu "theo cảm giác" thường phản tác dụng.
6. Kết nối và trích xuất dữ liệu SQL trực tiếp từ Excel
Nhân viên phân tích thường cần kéo dữ liệu từ database vào Excel để lập bảng tổng hợp/biểu đồ, và muốn bấm Refresh là số liệu tự cập nhật thay vì copy-paste. Codelab không chấm phần này; bạn làm theo các bước dưới đây trên máy tính công ty.
6.1. Tổng quan các phương thức kết nối
| Phương thức | Dùng khi | Ghi chú |
|---|---|---|
| Get Data → From SQL Server Database (connector gốc) | Kết nối SQL Server | Nhanh, ổn định, hỗ trợ Windows Authentication |
| Get Data → From Other Sources → From ODBC | Kết nối MySQL và hầu hết CSDL khác | Cần cài ODBC driver (vd. MySQL Connector/ODBC) và tạo DSN |
| Get Data → From MySQL Database | MySQL (trên bản Excel có connector) | Cần cài MySQL Connector/NET |
| Microsoft Query (cách cũ) | Excel đời cũ | Vẫn dùng được, kém linh hoạt hơn Power Query |
Cả hai đường đi qua Power Query (công cụ nạp và biến đổi dữ liệu trong Excel 2016+/Microsoft 365, nằm trong tab Data).
6.2. Các bước: kết nối SQL Server
| Bước | Thao tác | Bạn sẽ thấy |
|---|---|---|
| 1 | Mở Excel, tab Data → Get Data → From Database → From SQL Server Database | Hộp thoại nhập server |
| 2 | Nhập Server (tên máy chủ hoặc tenmay\INSTANCE) và Database (hnx_securities) |
|
| 3 | Mở Advanced options nếu muốn dán một câu SQL vào ô SQL statement (khuyến khích), ví dụ SELECT customer_id, full_name, status FROM Customers WHERE city = 'Ha Noi', rồi bấm OK |
|
| 4 | Chọn xác thực: Windows (ưu tiên, không phải nhập mật khẩu) hoặc Database (tên đăng nhập/mật khẩu SQL), bấm Connect. Nếu hiện cảnh báo mã hóa, chọn theo chính sách công ty | Cửa sổ Navigator |
| 5 | Tick bảng/view cần lấy. Nếu dùng câu SQL riêng thì kết quả hiện luôn ở bước xem trước | Bảng xem trước bên phải |
| 6 | Chọn Load (nạp thẳng vào sheet dạng Table) hoặc Transform Data (mở Power Query Editor để lọc cột, đổi kiểu, gộp bảng, thêm cột tính, rồi Close & Load) | Dữ liệu xuất hiện trong sheet |
6.3. Các bước: kết nối MySQL (hoặc CSDL khác) bằng ODBC
| Bước | Thao tác |
|---|---|
| 1 | Cài MySQL Connector/ODBC (bản 64-bit khớp với Excel 64-bit) |
| 2 | Mở ODBC Data Sources (64-bit) của Windows, tab User DSN → Add → chọn MySQL ODBC Unicode Driver → nhập tên DSN (vd. hnx_mysql), Server, Port 3306, User, Password, Database hnx_securities → Test → OK |
| 3 | Trong Excel: Data → Get Data → From Other Sources → From ODBC |
| 4 | Chọn DSN vừa tạo; có thể dán câu SQL vào Advanced options → SQL statement; nhập thông tin đăng nhập (tab Default or Custom/Database) → Connect |
| 5 | Chọn bảng, rồi Load hoặc Transform Data như trên |
Trong Power Query Editor có thể xem công thức M sinh ra, ví dụ:
= Sql.Database("tenmay", "hnx_securities", [Query = "SELECT customer_id, full_name FROM Customers"])6.4. Thao tác trích xuất và xử lý
- Lọc / chọn cột / đổi kiểu ngay trong Power Query (mỗi thao tác là một bước ghi lại, có thể sửa/xóa).
- Làm mới dữ liệu: tab Data → Refresh All (Ctrl + Alt + F5). Đặt tự động: Data → Queries & Connections → chuột phải truy vấn → Properties → Refresh every N minutes hoặc Refresh data when opening the file.
- Dùng dữ liệu: chèn PivotTable/PivotChart trên bảng đã nạp; dùng
SUMIFS,XLOOKUP, Slicer; hoặc tạo Connection only rồiLoad To → PivotTable Report. - Xuất kết quả: bảng đã nạp lưu ngay trong file Excel (hoặc xuất CSV).
Mẹo: đẩy phần tính toán nặng vào câu SQL (
GROUP BY,SUM) để Excel chỉ nhận vài trăm dòng tổng hợp thay vì hàng triệu dòng chi tiết.
6.5. Ứng dụng thực tế và lưu ý an toàn
| Nhu cầu | Cách làm |
|---|---|
| Báo cáo tài sản khách hàng hằng ngày | Dán truy vấn "Top 10 khách hàng theo tài sản" (chính là bài 10.4a) vào Power Query → PivotChart → mỗi sáng bấm Refresh |
| Theo dõi giá trị giao dịch theo tháng | Kéo kết quả sp_MonthlyReport hoặc truy vấn 10.4c vào Excel để vẽ biểu đồ đường |
| Báo cáo dùng lại nhiều nơi | Nhờ đội IT chuẩn hóa thành view/stored procedure (có index tối ưu) rồi Excel chỉ gọi tên |
Cảnh báo: dùng tài khoản chỉ đọc (chỉ
SELECT) cho Excel; ưu tiên Windows Authentication; không lưu mật khẩu trong file chia sẻ; dữ liệu khách hàng là dữ liệu nhạy cảm, không gửi file Excel chứa dữ liệu thật ra ngoài công ty; thu hẹp phạm vi dữ liệu bằngWHERE; tránhSELECT *trên bảng lớn.
7. Review và hệ thống hóa toàn bộ khóa học
7.1. Bản đồ 10 buổi
| Buổi | Chủ đề | Từ khóa cần nhớ |
|---|---|---|
| 1 | Giới thiệu SQL, SELECT cơ bản | SELECT, FROM, WHERE, AND/OR, IN, BETWEEN, LIKE, DISTINCT |
| 2 | Sắp xếp và hàm thống kê | ORDER BY, COUNT/SUM/AVG/MIN/MAX, GROUP BY, HAVING |
| 3 | Window Functions | OVER (PARTITION BY ... ORDER BY ...), ROW_NUMBER, RANK, LAG/LEAD, tổng lũy kế |
| 4 | Kiểu dữ liệu và hàm xử lý | CAST/CONVERT, hàm chuỗi, hàm ngày, CASE, COALESCE |
| 5 | CTE, Temp Table, Subquery | WITH ... AS, bảng tạm, subquery vô hướng / nhiều dòng / tương quan, EXISTS |
| 6 | INSERT, UPDATE, DELETE | Thay đổi dữ liệu, WHERE an toàn, transaction |
| 7 | Quan hệ giữa bảng, Normal Form | Khóa chính/ngoại, 1-1 / 1-N / N-N, 1NF–3NF |
| 8 | JOIN | INNER, LEFT, RIGHT, FULL, SELF JOIN |
| 9 | Stored Procedure và Trigger | CREATE PROCEDURE, tham số, IF/WHILE, AFTER trigger, inserted/deleted/NEW/OLD |
| 10 | Index, Transaction, Review | B-Tree, composite, ACID, COMMIT/ROLLBACK/SAVEPOINT, deadlock |
7.2. Sơ đồ ER hệ thống chứng khoán (nhắc lại)
Bộ dữ liệu xuyên suốt khóa gồm 9 bảng; sơ đồ dưới nhắc lại các quan hệ chính để bạn xác định đường JOIN cho Capstone.
PK = khóa chính, FK = khóa ngoài. Đường nối: 1 ở phía bảng 'một', n ở phía bảng 'nhiều'. Đường nét đứt: liên hệ logic (không khai báo FK). Quan hệ n–n giữa Accounts và Securities đi qua bảng Portfolio.
Đường đi từ giao dịch tới khách hàng: Trades → Orders → Accounts → Customers. Thống kê nằm ở CustomerStats (1-1 với Customers), nhật ký ở AuditLog (1-N với Customers).
7.3. Danh sách hàm thường dùng (cheat sheet SQL Server ↔ MySQL)
| Nhóm | SQL Server | MySQL |
|---|---|---|
| Ghép chuỗi | CONCAT, + |
CONCAT |
| Độ dài chuỗi | LEN (bỏ khoảng trắng cuối) |
CHAR_LENGTH |
| Tìm vị trí | CHARINDEX(x, s) |
INSTR(s, x) / LOCATE(x, s) |
| Chuỗi con | SUBSTRING, LEFT, RIGHT |
SUBSTRING, LEFT, RIGHT |
| Ngày: năm/tháng/ngày | YEAR, MONTH, DAY |
YEAR, MONTH, DAY |
| Cộng ngày | DATEADD(DAY, n, d) |
DATE_ADD(d, INTERVAL n DAY) |
| Khoảng cách ngày | DATEDIFF(DAY, a, b) |
DATEDIFF(b, a) |
| Định dạng ngày | FORMAT(d, 'yyyy-MM') |
DATE_FORMAT(d, '%Y-%m') |
| Xử lý NULL | ISNULL, COALESCE |
IFNULL, COALESCE |
| Điều kiện | CASE, IIF |
CASE, IF() |
| Gộp chuỗi nhóm | STRING_AGG |
GROUP_CONCAT |
| Lấy N dòng | TOP n / OFFSET..FETCH |
LIMIT n [OFFSET m] |
| Bảng tạm | #tmp, SELECT ... INTO #tmp |
CREATE TEMPORARY TABLE |
| Làm tròn | ROUND |
ROUND |
7.4. Best Practices và Anti-patterns (tổng kết)
| Nên | Tránh (anti-patterns) |
|---|---|
| Đặt tên rõ ràng, nhất quán; viết hoa từ khóa, thụt lề | SELECT *; hàm bọc cột trong WHERE; NOT IN với cột có NULL |
Luôn có ORDER BY khi cần thứ tự ổn định, thêm khóa phụ khi có thể đồng hạng |
UPDATE/DELETE không WHERE; sửa trực tiếp trên production không backup |
| Liệt kê cột; lọc sớm; xử lý theo tập hợp; dùng CTE cho truy vấn dài | Vòng lặp/cursor thay câu lệnh tập hợp; dồn mọi logic nghiệp vụ vào trigger |
Bọc thay đổi dữ liệu trong transaction; thử bằng SELECT trước khi UPDATE/DELETE |
Tạo index tràn lan; transaction dài; quên COMMIT |
Bọc CAST(ROUND(x, 2) AS DECIMAL(18,2)) khi báo cáo tỷ lệ; nhớ chia số nguyên trên SQL Server |
Chia hai số nguyên rồi tin kết quả thập phân |
Xử lý NULL chủ động (COALESCE, IS NULL); index cho khóa ngoại/cột lọc, đo trước-sau |
Tối ưu theo cảm giác không đo |
7.5. Capstone Project — báo cáo phân tích khách hàng (bài 10.4)
Mini project cho phòng chăm sóc khách hàng. Bạn sẽ ráp các mảnh của cả khóa thành một bộ báo cáo, giống công việc thực tế. Đây là phần thực hành lớn nhất của buổi, làm sau cùng.
| Bài | Nội dung | Kỹ năng ôn lại |
|---|---|---|
| 10.4a | Top 10 khách hàng theo tài sản | JOIN, GROUP BY, CTE, window ROW_NUMBER, TOP/LIMIT |
| 10.4b | Tổng giao dịch theo ngày (tháng 09/2026) | GROUP BY, điều kiện khoảng ngày (sargable) |
| 10.4c | Tổng giao dịch theo tháng, lũy kế trong năm | CTE, hàm ngày, window SUM() OVER (PARTITION BY ...) |
| 10.4d | Tổng giao dịch theo năm và tỷ trọng | CTE, SUM() OVER (), CAST/ROUND |
| 10.4e | Phân loại khách hàng VIP / Thuong / Moi | LEFT JOIN, CASE, COALESCE |
| 10.4f | Top 3 khách hàng giao dịch nhiều nhất mỗi loại | nhiều JOIN, CTE, ROW_NUMBER() OVER (PARTITION BY ...) |
| 10.4g | Stored Procedure báo cáo theo tháng | Procedure, tham số, hàm ngày, sargable |
| 10.4h | Hồ sơ 360 độ khách hàng | Tổng hợp mọi kỹ thuật |
Cách tiếp cận đề xuất:
- Đọc đề, xác định bảng và đường JOIN (
Trades → Orders → Accounts → Customers). - Viết từng CTE nhỏ và kiểm tra kết quả từng bước.
- Ghép lại; kiểm tra trường hợp biên (khách hàng không có tài khoản/giao dịch, đồng hạng,
NULL). - Đóng gói truy vấn dùng lại thành Stored Procedure (bài 10.4g) và thêm index phục vụ báo cáo (như bài 10.1), rồi xem
EXPLAIN/execution plan.
Mẫu ghép báo cáo (dùng chủ đề khác để bạn thấy khung xương: CTE tổng hợp, ROW_NUMBER xếp hạng, lọc top N). Top 5 mã chứng khoán theo giá trị giao dịch:
WITH v AS (
SELECT symbol, COUNT(*) AS trade_count, SUM(quantity * price) AS total_value
FROM Trades
GROUP BY symbol
), r AS (
SELECT symbol, trade_count, total_value,
ROW_NUMBER() OVER (ORDER BY total_value DESC, symbol) AS rk
FROM v
)
SELECT rk, symbol, trade_count, total_value
FROM r
WHERE rk <= 5
ORDER BY rk;Lưu ý: hai quy tắc phân loại dùng thống nhất trong 10.4e và 10.4h:
VIPnếu tổng tài sản ACTIVE từ 1 tỷ trở lên;Moinếucreated_date >= '2026-06-27'(90 ngày tính đến 2026-09-25); còn lạiThuong. Kiểm tra theo thứ tự từ trên xuống: một khách hàng vừa đủ điều kiện VIP vừa mới mở tài khoản vẫn là VIP.
Thực hành: Vừa ôn xong toàn bộ khóa, hãy làm bài 10.4 (Capstone) theo thứ tự a → h: từ Top 10 tài sản, báo cáo giao dịch theo ngày/tháng/năm, phân loại khách hàng, top 3 mỗi loại, procedure báo cáo cho tới hồ sơ 360 độ. Mỗi bài là một mảnh của báo cáo hoàn chỉnh; làm lần lượt và kiểm tra kỹ trường hợp biên trước khi nộp.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
Tạo index trên bảng nhỏ hoặc cột ít giá trị (status, side) |
Optimizer bỏ qua, chỉ tốn dung lượng và làm chậm ghi | Index cột chọn lọc cao, dùng trong WHERE/JOIN |
| Tạo composite với thứ tự cột sai (khoảng trước, đẳng thức sau) | Không tận dụng hết index | Cột = trước, cột khoảng sau; nhớ quy tắc cột trái nhất |
| Không đặt tên index tường minh | Tên tự sinh khác nhau mỗi database, script không tái dùng được | ix_<Bảng>_<Cột>, ux_... |
| Unique index trên cột cho phép NULL (SQL Server) | Lỗi vì dữ liệu đã có nhiều NULL | Filtered index WHERE col IS NOT NULL |
Bọc hàm quanh cột (YEAR(trade_date)) |
Index không được dùng, quét cả bảng | Viết lại thành khoảng ngày |
Quên COMMIT/ROLLBACK |
Transaction treo, giữ khóa, chặn người khác | Luôn kết thúc transaction, giữ ngắn |
Cho phép thao tác âm số dư vì không kiểm tra @@ROWCOUNT/ROW_COUNT() |
Cộng tiền vào tài khoản không tồn tại, tiền "biến mất" | Kiểm tra số dòng ảnh hưởng sau từng UPDATE và ROLLBACK khi 0 |
| Hai phiên khóa tài nguyên theo thứ tự ngược nhau | Deadlock | Luôn UPDATE theo thứ tự id tăng dần, có retry |
| Ép index bằng hint rồi quên | Index bị xóa thì truy vấn báo lỗi; dữ liệu đổi thì kế hoạch tệ | Chỉ hint khi đã đo; ghi chú lý do |
| Dán mật khẩu vào file Excel chia sẻ | Lộ dữ liệu khách hàng | Tài khoản chỉ đọc, Windows Authentication |
Tóm tắt
| Chủ đề | Ghi nhớ |
|---|---|
| Index (B-Tree) | Tăng tốc đọc, làm chậm ghi và tốn dung lượng; index chỉ được dùng khi có lợi |
| Loại index | PK (clustered), UNIQUE, composite (cột trái nhất, = trước khoảng sau), covering (INCLUDE), FULLTEXT |
| Khóa ngoại | SQL Server không tự tạo index cho khóa ngoại; MySQL InnoDB tự tạo |
| Unique + NULL | SQL Server chỉ cho 1 NULL → dùng filtered index; MySQL cho nhiều NULL |
| Quản lý index | CREATE INDEX, DROP INDEX ... ON ..., đặt tên tường minh |
| Phân tích | SSMS Include Actual Execution Plan; MySQL EXPLAIN (tránh type = ALL); hint/FORCE INDEX chỉ khi đã đo |
| Transaction | ACID; BEGIN/START TRANSACTION, COMMIT, ROLLBACK, SAVEPOINT |
| Isolation | Dirty / Non-repeatable / Phantom read; mặc định SQL Server = READ COMMITTED, MySQL = REPEATABLE READ |
| Deadlock | Hai phiên chờ nhau; phòng tránh: cùng thứ tự truy cập, transaction ngắn, có index, retry |
| Hiệu năng | Truy vấn sargable, không SELECT *, xử lý theo tập hợp, đo trước-sau |
| Excel | Data → Get Data → From Database (SQL Server) / From ODBC; Load hoặc Transform; Refresh All |
| Capstone | CTE nhỏ ghép lại, kiểm tra biên, đóng gói procedure, thêm index |
Gợi ý mở rộng
- Thử tạo index cho từng truy vấn 10.4 rồi so sánh execution plan trước/sau trên bảng
BigTrades; ghi lại số trang đọc. - Viết procedure
sp_TransferMoneyphiên bản chống deadlock (luônUPDATEtài khoản có id nhỏ hơn trước). - Tìm hiểu View và indexed view (SQL Server) cho báo cáo dùng lại nhiều lần; partitioning cho bảng giao dịch rất lớn.
- Tìm hiểu bảo mật ở mức database: phân quyền
GRANT EXECUTEtrên procedure, tài khoản chỉ đọc cho Excel/BI. - Tự xây dựng bộ SQL snippet cá nhân từ các bài của khóa để dùng lại trong công việc hằng ngày.