MySQL và InnoDB đo thật: từ clustered index tới replication
Sê-ri mổ xẻ MySQL 8 và storage engine InnoDB bằng đo thật trong Docker: clustered index, buffer pool, B+tree, MVCC qua undo log, khoá và mức cô lập, redo log, covering index, partitioning, replication. Mỗi bài một phép đo có con số.
9/12 phần đã đăng
Lập trình
1
Kiến trúc InnoDB đo thật: bảng CHÍNH LÀ clustered index — tra theo khóa chính chạm đúng 1 dòng trong 0,015 ms, còn lọc theo cột không index phải duyệt cả 1.000.001 dòng lá mất 161 ms
Bài mở đầu sê-ri MySQL và InnoDB: vì sao trong InnoDB, bảng không nằm cạnh index mà bảng CHÍNH LÀ clustered index — một cây B+ khóa theo khóa chính, lá chứa trọn hàng. Đo thật trong container mysql:8 trên bảng một triệu dòng: tra theo khóa chính là một lần đi xuống cây (Handler_read_key=1, type=const, 0,015 ms), còn lọc theo cột không index phải duyệt sạch 1.000.001 dòng lá (Handler_read_rnd_next=1.000.001, 161 ms) vì không có heap riêng để nhảy tới. Hàng luôn nằm vật lý theo thứ tự khóa chính: bảng chèn khóa ngẫu nhiên vẫn trả về theo thứ tự PK, và quét một dải PK là đi tuần tự trên lá.
20/10/2026
· 7 phút đọc
2
Buffer pool đo thật: cùng một truy vấn quét bảng 131 MB chạm đĩa 577 trang mỗi lần (185 ms) khi pool 128 MB không đủ chứa, rồi tụt về 0 trang chạm đĩa (128 ms) ngay khi nới pool lên 256 MB để bảng vừa khít
Phần 2 sê-ri MySQL và InnoDB: buffer pool là vùng RAM InnoDB giữ các trang 16 KB của bảng và index, và nó quyết định một truy vấn chạm đĩa bao nhiêu lần. Đo thật trong mysql:8: bảng 46 MB vừa pool thì quét ấm chạm đĩa 0 trang (hit ratio ~100%); bảng 131 MB lớn hơn pool 128 MB thì mọi lần quét vẫn chạm đĩa 577 trang và mất 185 ms, nhưng nới innodb_buffer_pool_size lên 256 MB để bảng vừa khít thì lần quét sau chạm đĩa 0 trang, còn 128 ms. Phân biệt đọc logic (read_requests) và đọc vật lý (reads), hit ratio ~99,9%, và vì sao một lần quét lớn không đẩy tập dữ liệu nóng khỏi pool nhờ LRU hai vùng.
21/10/2026
· 7 phút đọc
3
B+tree đo thật: tra một khóa trên bảng 10 triệu dòng chỉ đọc 3 trang, vì cây cao đúng 3 tầng — và từ 1.000 dòng lên 10 triệu dòng (gấp 10.000 lần) chiều cao cây chỉ tăng từ 2 lên 3
Phần 3 sê-ri MySQL và InnoDB: vì sao một index trên bảng hàng triệu dòng chỉ sâu vài tầng, nên tra một khóa chỉ đọc vài trang. Đo thật chiều cao cây B+ trong mysql:8 bằng số trang một lần tra khóa phải đọc: bảng 1.000 dòng cao 2 tầng, 100.000 dòng vẫn 2 tầng, 1 triệu dòng lên 3 tầng, 10 triệu dòng vẫn 3 tầng. Gấp 10.000 lần số dòng chỉ thêm đúng một tầng, vì mỗi nút là một trang 16 KB chứa cả nghìn khóa định tuyến (fanout lớn): cây cao 2 tầng chứa tới ~110 nghìn dòng, cao 3 tầng chứa tới ~110 triệu dòng. Nhờ vậy tra điểm theo khóa chính đọc 2–3 trang bất kể bảng to cỡ nào.
22/10/2026
· 7 phút đọc
4
Secondary index đo thật: lấy 100 dòng qua index phụ nhưng cần cột ngoài index phải tra ngược về khóa chính 100 lần, chạm 410 trang — còn truy vấn phủ chỉ chạm 10 trang, và quá ~10% số dòng thì MySQL bỏ index quét cả bảng
Phần 4 sê-ri MySQL và InnoDB: secondary index lưu giá trị khóa chính chứ không lưu địa chỉ hàng, nên tra theo index phụ mà cần cột ngoài index phải đi hai cây — index phụ rồi tra ngược về clustered index từng dòng (bookmark lookup). Đo thật trong mysql:8: lấy 100 dòng theo cust=5000 có kèm payload chạm 410 trang, còn truy vấn phủ chỉ cần id thì Using index, không tra ngược, chỉ chạm 10 trang — ít hơn 41 lần. Vì mỗi dòng thêm một lần tra ngược, qua ngưỡng ~10–15% số dòng MySQL bỏ hẳn index mà quét cả bảng: cust<1000 vẫn dùng index, cust<1500 chuyển full scan.
23/10/2026
· 7 phút đọc
5
EXPLAIN ANALYZE đo thật: cùng một truy vấn trả về đúng 100 dòng, dùng index chỉ quét 100 dòng (0,013 ms) còn bỏ index quét cả 1.000.000 dòng (100 ms) — số dòng quét mới quyết định nhanh chậm, không phải số dòng trả về
Phần 5 sê-ri MySQL và InnoDB: đọc EXPLAIN và EXPLAIN ANALYZE, và vì sao con số rows examined (số dòng quét) mới quyết định một truy vấn nhanh hay chậm, chứ không phải số dòng trả về. Đo thật trong mysql:8: cùng một truy vấn trả về đúng 100 dòng, bản dùng index quét 100 dòng và mất 0,013 ms, bản bỏ index quét cả 1.000.000 dòng và mất 100 ms — gấp 7.700 lần dù kết quả y hệt. Một truy vấn trả về 0 dòng vẫn quét 1.000.001 dòng khi lọc cột không index. EXPLAIN cho ước tính (type, key, rows, filtered), EXPLAIN ANALYZE cho số thật; ước tính lệch số thật là dấu hiệu thống kê cũ.
24/10/2026
· 7 phút đọc
6
MVCC đo thật: giao dịch A vẫn đọc số dư 1000 sau khi B đã sửa thành 2000 và commit, nhờ dựng lại phiên bản cũ từ undo log — và vì đọc thường không khóa, B cập nhật xong trong 55 ms thay vì chờ 2035 ms như khi A khóa hàng
Phần 6 sê-ri MySQL và InnoDB: MVCC cho nhiều giao dịch đọc cùng một hàng mà không chặn nhau, bằng cách giữ các phiên bản cũ trong undo log. Đo thật trong mysql:8 ở mức REPEATABLE READ: giao dịch A đọc số dư 1000, giao dịch B sửa thành 2000 rồi commit, A đọc lại vẫn thấy 1000 (ảnh chụp nhất quán dựng từ undo log), chỉ sau khi A commit mới thấy 2000. Và vì đọc thường không lấy khóa, B cập nhật xong trong 55 ms khi A đang đọc thường, nhưng phải chờ 2035 ms khi A đọc bằng SELECT FOR UPDATE. Mỗi hàng mang khóa giao dịch và con trỏ undo để dựng lại phiên bản đúng cho từng ảnh chụp.
25/10/2026
· 7 phút đọc
7
Khoá và mức cô lập đo thật: đọc lại trong REPEATABLE READ vẫn ra 1000 còn READ COMMITTED ra 3000, gap lock chặn chèn vào khe 2035 ms, và một deadlock thật bị InnoDB phá trong tích tắc với lỗi 1213
Phần 7 sê-ri MySQL và InnoDB: mức cô lập quyết định một giao dịch thấy gì, còn khoá quyết định ai phải chờ ai. Đo thật trong mysql:8: cùng một kịch bản, đọc lại trong REPEATABLE READ vẫn ra 1000 (ảnh chụp cả giao dịch) còn READ COMMITTED ra 3000 (ảnh chụp mới mỗi câu). Gap lock của REPEATABLE READ chặn một INSERT vào khe trong khoảng đang bị khoá mất 2035 ms, trong khi READ COMMITTED cho chèn ngay sau 57 ms. Và một deadlock thật — hai giao dịch khoá hai hàng theo thứ tự ngược nhau — bị InnoDB phát hiện tức thì, cuộn lại một nạn nhân với lỗi 1213, đọc được trong SHOW ENGINE INNODB STATUS.
26/10/2026
· 7 phút đọc
8
Redo log đo thật: với innodb_flush_log_at_trx_commit=1, mười nghìn commit gọi fsync 10.209 lần (7.680 commit/s); đổi sang 2 chỉ còn 4 lần fsync và nhanh hơn, còn group commit gộp commit đồng thời xuống 3.688 lần fsync cho gấp đôi throughput vẫn bền
Phần 8 sê-ri MySQL và InnoDB: redo log cho giao dịch commit xong là bền dù máy tắt đột ngột, và innodb_flush_log_at_trx_commit quyết định đánh đổi giữa độ bền và throughput. Đo thật trong mysql:8 bằng số lần gọi fsync (Innodb_os_log_fsyncs): mười nghìn commit riêng lẻ ở mức 1 gọi fsync 10.209 lần (7.680 commit/s, bền từng commit), ở mức 2 chỉ 4 lần (10.384 commit/s), ở mức 0 chỉ 1 lần (17.889 commit/s). Group commit gộp các commit đồng thời: cùng mức 1, chạy qua 8 kết nối song song thì mười nghìn commit chỉ cần 3.688 lần fsync và đạt 17.990 commit/s — gấp đôi mà độ bền không đổi.
27/10/2026
· 7 phút đọc
9
Composite index đo thật: index (cust, created) tra thẳng cột đầu chỉ 1 lần seek (0,018 ms), nhưng lọc theo cột thứ hai một mình phải skip scan 20.001 lần seek (10 ms); thêm đúng cột vào index cho Using index, khớp thứ tự cho tránh filesort
Phần 9 sê-ri MySQL và InnoDB: index composite nhiều cột và covering index. Đo thật trong mysql:8 trên index (cust, created): tra theo cột đầu (leftmost prefix) chỉ 1 lần seek và 0,018 ms, nhưng lọc theo cột thứ hai created một mình không seek thẳng được — MySQL phải index skip scan qua mọi tiền tố cust, tốn 20.001 lần seek và 10 ms để tìm 10 dòng. Chọn SELECT đúng cột có trong index cho Using index (phủ, không tra ngược clustered). Và ORDER BY khớp thứ tự cột index thì Using index không filesort, lệch thứ tự thì Using filesort. Thứ tự cột trong index quyết định nó phục vụ được truy vấn nào.
28/10/2026
· 7 phút đọc
Còn 3 phần nữa sẽ lần lượt được đăng.