Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 23 — Indexes & Performance Tuning

Data Modeling and ERD Mastery Bài 23/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn cầm một cuốn sách giáo trình dày 800 trang và muốn tìm phần nói về "chuẩn hóa dữ liệu". Nếu cuốn sách không có mục lục, không có chỉ mục cuối sách, bạn buộc phải lật từng trang một cho đến khi gặp đúng nội dung. Với 800 trang, có thể bạn mất cả buổi chiều. Nhưng nếu lật ra trang chỉ mục (index) ở cuối sách, bạn tra chữ "chuẩn hóa", thấy ghi "trang 247", và nhảy thẳng đến đó trong vài giây.

Index trong cơ sở dữ liệu hoạt động đúng theo nguyên lý đó. Đây là một trong những kiến thức mà nhiều Business Analyst (BA) thường bỏ qua vì nghĩ rằng "đó là việc của DBA hoặc developer". Nhưng thực tế, khi bạn thiết kế mô hình dữ liệu, chính những quyết định của bạn về việc truy vấn nào sẽ chạy thường xuyên, bảng nào sẽ phình to theo thời gian, sẽ ảnh hưởng trực tiếp đến hiệu năng (performance) của cả hệ thống. Một BA hiểu về index sẽ đưa ra được những khuyến nghị thiết kế tốt hơn, đặt câu hỏi đúng với đội kỹ thuật, và tránh được những "quả bom hẹn giờ" về hiệu năng khi hệ thống có nhiều dữ liệu.

Trong bài này, chúng ta sẽ cùng tìm hiểu index là gì, vì sao nó tăng tốc truy vấn, đánh đổi gì khi dùng nó, và một BA nên tư duy thế nào về hiệu năng ngay từ giai đoạn thiết kế mô hình dữ liệu.

Khái niệm cốt lõi

Index là gì?

Index (chỉ mục) là một cấu trúc tra cứu (lookup structure) được xây dựng riêng để giúp cơ sở dữ liệu tìm dòng dữ liệu nhanh hơn, mà không phải đọc toàn bộ bảng. Bản chất, nó giống hệt phần chỉ mục cuối một cuốn sách: thay vì lưu nội dung, nó lưu "giá trị → vị trí của dòng chứa giá trị đó".

Khi bạn chưa có index, để tìm một dòng, cơ sở dữ liệu phải thực hiện cái gọi là full table scan (quét toàn bảng) — đọc tuần tự từng dòng từ đầu đến cuối để kiểm tra dòng nào thỏa điều kiện. Với bảng vài trăm dòng thì không sao, nhưng với bảng vài triệu dòng, đây là thảm họa.

Khi có index, cơ sở dữ liệu dùng cấu trúc tra cứu đã được sắp xếp sẵn để nhảy thẳng đến vị trí cần tìm. Hầu hết hệ quản trị cơ sở dữ liệu (DBMS) như MySQL, PostgreSQL, SQL Server đều dùng cấu trúc B-Tree (cây cân bằng) cho index. Bạn không cần thuộc lòng thuật toán, chỉ cần hiểu đặc tính của nó: dữ liệu trong B-Tree được sắp xếp và phân tầng, nên việc tìm một giá trị chỉ mất số bước tỉ lệ với logarit của số dòng. Nói cách khác, để tìm trong 1 triệu dòng, B-Tree chỉ cần khoảng 20 bước, trong khi full scan cần tới 1 triệu bước.

Các loại index thường gặp

Primary index (chỉ mục khóa chính): Mỗi khi bạn khai báo PRIMARY KEY cho một bảng, DBMS tự động tạo một index trên cột đó. Đây là lý do tra cứu theo khóa chính luôn rất nhanh.

Unique index (chỉ mục duy nhất): Vừa tăng tốc tra cứu, vừa đảm bảo không có giá trị trùng. Ví dụ cột so_cccd (số căn cước công dân) nên có unique index.

Composite index (chỉ mục tổ hợp): Index xây trên nhiều cột cùng lúc, ví dụ (tinh_thanh, quan_huyen). Thứ tự cột trong composite index cực kỳ quan trọng — sẽ nói rõ ở phần dưới.

Non-clustered vs Clustered: Clustered index quyết định thứ tự vật lý lưu dữ liệu trên đĩa (mỗi bảng chỉ có một). Non-clustered index là cấu trúc tách rời, trỏ về dòng dữ liệu. BA không cần đi sâu, nhưng nên biết khái niệm tồn tại.

Cái giá phải trả của index

Đây là phần quan trọng nhất mà người mới hay quên: index không miễn phí. Nó đánh đổi:

  • Tốn dung lượng lưu trữ: Mỗi index là một bản sao dữ liệu đã sắp xếp, chiếm thêm ổ đĩa.
  • Làm chậm thao tác ghi (INSERT, UPDATE, DELETE): Mỗi lần thêm/sửa/xóa dòng, DBMS phải cập nhật lại tất cả các index liên quan. Bảng có 8 index thì mỗi lần INSERT phải cập nhật 8 cấu trúc.
  • Index thừa gây hại: Index không bao giờ được dùng vẫn tốn chi phí bảo trì mà không mang lại lợi ích.
Vì vậy, nguyên tắc vàng là: index tăng tốc đọc nhưng làm chậm ghi. Bạn chỉ tạo index cho những cột thực sự được dùng để tìm kiếm, lọc (WHERE), nối bảng (JOIN), hoặc sắp xếp (ORDER BY) thường xuyên.

Tình huống thực tế

Tình huống 1 — Sàn thương mại điện tử Tiki và bảng đơn hàng phình to

Một đội kỹ thuật làm việc trên hệ thống tương tự sàn thương mại điện tử như Tiki gặp tình huống sau. Bảng don_hang lúc đầu chỉ vài chục nghìn dòng, mọi truy vấn chạy nhanh nên không ai để ý đến index. Sau 18 tháng vận hành, bảng đạt 12 triệu dòng. Màn hình "Lịch sử đơn hàng của tôi" của khách dùng câu truy vấn lọc theo customer_id. Vì cột này không có index, mỗi lần khách bấm xem, hệ thống phải quét toàn bộ 12 triệu dòng để lọc ra khoảng 20 đơn của riêng họ.

Kết quả: trang load mất 8–9 giây vào giờ cao điểm, khách bỏ giỏ hàng, tỉ lệ thoát tăng vọt. Sau khi đội kỹ thuật thêm một index trên cột customer_id, thời gian truy vấn rớt xuống dưới 50 mili-giây.

Bài học rút ra: Một mô hình dữ liệu chạy tốt khi ít dữ liệu chưa chắc chạy tốt khi dữ liệu lớn. Là BA, khi bạn viết yêu cầu cho một màn hình truy vấn theo một cột nào đó, hãy ghi chú rõ "bảng này dự kiến tăng tới hàng triệu dòng, cột lọc cần được đánh index". Đó là một câu rất giá trị trong tài liệu nghiệp vụ.

Tình huống 2 — Ngân hàng và composite index sai thứ tự cột

Một ngân hàng tại Việt Nam xây hệ thống tra cứu giao dịch. Bảng giao_dich có cột so_tai_khoanngay_giao_dich. Màn hình sao kê luôn lọc theo cả hai: "các giao dịch của tài khoản X trong khoảng ngày từ A đến B".

Đội kỹ thuật tạo composite index theo thứ tự (ngay_giao_dich, so_tai_khoan). Nghe có vẻ ổn, nhưng truy vấn vẫn chậm. Vì sao? Vì trong composite index, DBMS sắp xếp theo cột đầu tiên trước. Truy vấn lọc theo so_tai_khoan cụ thể nhưng ngay_giao_dich lại là một khoảng rộng, nên index gần như vô dụng — cột phân biệt mạnh nhất (so_tai_khoan) bị xếp sau.

Khi đảo lại thành (so_tai_khoan, ngay_giao_dich), DBMS có thể nhảy thẳng đến nhóm dòng của tài khoản đó, rồi quét tiếp theo khoảng ngày. Truy vấn từ 4 giây xuống còn 30 mili-giây.

Bài học rút ra: Thứ tự cột trong composite index phải khớp với cách truy vấn thực tế lọc dữ liệu. Quy tắc thực dụng: đặt cột dùng cho điều kiện so sánh bằng (=) lên trước, cột dùng cho điều kiện khoảng (>, <, BETWEEN) ra sau. Là BA, khi mô tả tiêu chí tìm kiếm trên một màn hình, bạn nên ghi rõ cột nào lọc bằng giá trị chính xác, cột nào lọc theo khoảng — thông tin này giúp đội kỹ thuật chọn đúng thứ tự index.

Tình huống 3 — Startup giao đồ ăn và bệnh "đánh index cho mọi cột"

Một startup giao đồ ăn ở TP.HCM gặp vấn đề ngược lại. Một lập trình viên cẩn thận quá mức đã tạo index cho gần như mọi cột trong bảng tai_xe — gồm 14 index, kể cả những cột như ghi_chu hay trang_thai (chỉ có 3 giá trị: rảnh, đang giao, nghỉ).

Vấn đề: hệ thống cập nhật vị trí tài xế liên tục, mỗi tài xế gửi tọa độ mỗi vài giây, tạo ra lượng UPDATE khổng lồ. Mỗi lần UPDATE, DBMS phải cập nhật 14 index. Hiệu năng ghi sụp đổ, CPU của máy chủ cơ sở dữ liệu thường xuyên chạm 95%.

Sau khi rà soát, đội ngũ xóa bỏ các index thừa, chỉ giữ lại 4 index thực sự cần. Cột trang_thai chỉ có 3 giá trị nên độ phân biệt (selectivity) thấp — index trên nó gần như vô ích vì DBMS vẫn phải đọc rất nhiều dòng. Tải ghi giảm mạnh, CPU về mức ổn định 40%.

Bài học rút ra: Nhiều index không có nghĩa là nhanh hơn. Index trên cột có ít giá trị phân biệt (low cardinality) như giới tính, trạng thái thường không đáng tạo. Với bảng có lượng ghi rất lớn, càng phải dè dặt với index.

Hướng dẫn từng bước

Dưới đây là quy trình tư duy mà một BA (hoặc data analyst) có thể áp dụng khi xem xét hiệu năng của một mô hình dữ liệu:

Bước 1 — Xác định các truy vấn quan trọng. Liệt kê những truy vấn chạy thường xuyên nhất hoặc quan trọng nhất với nghiệp vụ: màn hình nào load mỗi giây? báo cáo nào chạy mỗi sáng? Đây là những "đường nóng" cần tối ưu.

Bước 2 — Tìm các cột trong mệnh đề WHERE, JOIN, ORDER BY. Với mỗi truy vấn quan trọng, xác định cột nào dùng để lọc, nối bảng, sắp xếp. Đây chính là những ứng viên cho index.

Bước 3 — Đánh giá độ phân biệt (selectivity) của cột. Cột có nhiều giá trị khác nhau (như email, so_dien_thoai, customer_id) là ứng viên tốt. Cột chỉ có vài giá trị (gioi_tinh, trang_thai) thường không đáng đánh index riêng lẻ.

Bước 4 — Cân nhắc composite index cho truy vấn lọc nhiều cột. Nếu một truy vấn luôn lọc theo nhiều cột cùng lúc, một composite index thường tốt hơn nhiều index riêng lẻ. Nhớ quy tắc: cột so sánh bằng đặt trước, cột so sánh khoảng đặt sau.

Bước 5 — Cân nhắc khối lượng ghi. Hỏi: bảng này ghi nhiều hay đọc nhiều? Bảng log, bảng tracking ghi rất nhiều thì hạn chế index. Bảng danh mục đọc nhiều ghi ít thì thoải mái index hơn.

Bước 6 — Đo trước, tối ưu sau. Đừng đoán. Dùng công cụ EXPLAIN (trong MySQL/PostgreSQL) hoặc execution plan (trong SQL Server) để xem DBMS có thực sự dùng index không. Nếu kế hoạch thực thi vẫn báo "full table scan" dù đã có index, nghĩa là có gì đó sai — có thể truy vấn viết theo cách khiến index không dùng được.

Bước 7 — Rà soát định kỳ. Theo thời gian, một số index trở nên thừa, một số truy vấn mới cần index mới. Hệ thống nên được rà soát index mỗi vài tháng.

Lỗi thường gặp & mẹo

Lỗi 1 — Bọc cột trong hàm khiến index vô hiệu. Nếu bạn viết WHERE YEAR(ngay_tao) = 2026, DBMS phải tính YEAR() cho từng dòng nên không dùng được index trên ngay_tao. Mẹo: viết lại thành WHERE ngay_tao >= '2026-01-01' AND ngay_tao < '2027-01-01' để index hoạt động.

Lỗi 2 — Tìm kiếm với dấu % ở đầu. WHERE ten LIKE '%nguyen%' không dùng được index B-Tree, vì index sắp xếp từ trái sang. WHERE ten LIKE 'nguyen%' thì dùng được. Với tìm kiếm văn bản kiểu chứa-bất-kỳ-vị-trí, cần dùng full-text index thay vì B-Tree thông thường.

Lỗi 3 — Quên rằng khóa ngoại (foreign key) nên có index. Cột FK dùng để JOIN rất thường xuyên. Nhiều DBMS không tự động đánh index cho FK (khác với PK). Đây là nguyên nhân phổ biến khiến JOIN chậm.

Lỗi 4 — Đánh index cho cột low cardinality. Như đã nói, index trên gioi_tinh (2-3 giá trị) thường vô ích.

Mẹo vàng cho BA: Bạn không cần tự tạo index, nhưng bạn nên nói được câu này với đội kỹ thuật: "Màn hình này lọc theo cột X, dự kiến bảng tăng lên hàng triệu dòng, nhờ team kiểm tra index và execution plan giúp." Một câu nói đúng chỗ tiết kiệm cho dự án hàng tuần debug về sau.

Bài tập thực hành

  • Phân tích truy vấn: Cho bảng hoc_vien(id, ho_ten, email, so_dien_thoai, tinh_thanh, ngay_dang_ky) với 5 triệu dòng. Màn hình admin tìm học viên theo email. Cột nào nên đánh index? Vì sao?
  • Composite index: Một báo cáo luôn lọc "học viên ở tinh_thanh = 'Hà Nội' đăng ký trong khoảng ngay_dang_ky từ A đến B". Hãy đề xuất composite index với thứ tự cột đúng và giải thích.
  • Phát hiện lỗi: Truy vấn WHERE MONTH(ngay_dang_ky) = 6 chạy chậm dù ngay_dang_ky đã có index. Hãy chỉ ra nguyên nhân và viết lại điều kiện để index hoạt động.
  • Đánh đổi đọc/ghi: Bảng log_truy_cap ghi 2.000 dòng mỗi phút, hầu như chỉ đọc khi điều tra sự cố. Bạn có nên đánh nhiều index cho bảng này không? Lập luận.
  • Tư duy BA: Viết một câu ghi chú (note) trong tài liệu yêu cầu cho màn hình "Lịch sử giao dịch", thể hiện sự hiểu biết về index và hiệu năng để gửi cho đội kỹ thuật.

Tóm tắt

Index là cấu trúc tra cứu giúp cơ sở dữ liệu tìm dữ liệu nhanh, giống như chỉ mục cuối một cuốn sách. Không có index, DBMS phải quét toàn bảng — chậm khủng khiếp khi dữ liệu lớn. Hầu hết index dùng cấu trúc B-Tree, cho phép tìm trong hàng triệu dòng chỉ với vài chục bước.

Nhưng index không miễn phí: nó tốn dung lượng và làm chậm thao tác ghi. Vì vậy chỉ đánh index cho cột thực sự dùng để lọc, nối, sắp xếp thường xuyên, ưu tiên cột có độ phân biệt cao, và tránh index thừa. Với composite index, thứ tự cột phải khớp cách truy vấn lọc — cột so sánh bằng đặt trước, cột so sánh khoảng đặt sau.

Là một BA, bạn không cần viết câu lệnh tạo index, nhưng cần tư duy về hiệu năng ngay từ khi thiết kế mô hình: dự đoán bảng nào sẽ phình to, truy vấn nào sẽ chạy thường xuyên, và ghi chú rõ ràng cho đội kỹ thuật. Chính sự hiểu biết đó phân biệt một BA giỏi với một người chỉ vẽ sơ đồ. Hãy nhớ nguyên tắc cuối cùng: đo trước bằng EXPLAIN, đừng đoán, và rà soát index định kỳ khi hệ thống lớn lên.