Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn là BA tại một sàn thương mại điện tử như Tiki. Một sáng thứ Hai, bạn viết một câu query tưởng chừng rất đơn giản: "Lấy tất cả đơn hàng của khách hàng có email là nguyen.van.a@gmail.com trong tháng vừa rồi". Bạn nhấn Run, rồi... chờ. Một phút trôi qua. Hai phút. Cốc cà phê đã nguội mà kết quả vẫn chưa trả về. Trong khi đó, đồng nghiệp ngồi bên cạnh chạy một query phức tạp hơn nhiều lại trả kết quả trong nửa giây.
Sự khác biệt thường không nằm ở câu SQL bạn viết, mà nằm ở index — thứ quyết định cơ sở dữ liệu phải "lật" qua bao nhiêu dòng để tìm ra đáp án.
Là một BA, bạn có thể nghĩ: "Index là việc của DBA hay data engineer, đâu phải việc của tôi". Đúng là bạn hiếm khi tự tay tạo index trên production. Nhưng đây là điểm mấu chốt: bạn là người viết query, bạn là người chờ kết quả, và bạn là người đi xin đội kỹ thuật tối ưu hệ thống. Nếu không hiểu index, bạn sẽ viết những query "vô tình" làm chậm cả hệ thống, hoặc đi hỏi DBA những câu mơ hồ kiểu "query của em chạy chậm quá, anh xem giúp với". Còn nếu bạn hiểu index, bạn sẽ hỏi đúng: "Cột email trong bảng customers có index chưa? Query của em đang full scan 8 triệu dòng, mình thêm index được không?". Một câu hỏi như vậy giúp DBA xử lý trong 5 phút thay vì cả buổi điều tra.
Bài này không biến bạn thành DBA. Mục tiêu là giúp bạn hiểu đủ sâu để hỏi đúng, đọc đúng, và tránh những sai lầm khiến report của bạn chạy ì ạch.
Khái niệm cốt lõi
Index là gì? Hãy nghĩ về mục lục cuốn sách
Hình dung bạn cầm một cuốn giáo trình SQL dày 800 trang và muốn tìm phần nói về "GROUP BY". Bạn có hai cách:
- Lật từng trang từ đầu đến cuối cho đến khi gặp chữ "GROUP BY". Với 800 trang, đây là cực hình.
- Mở mục lục (index) ở cuối sách, tra chữ "GROUP BY", thấy ghi "trang 342", rồi lật thẳng tới đó.
Index không phải là dữ liệu chính của bạn. Nó là một bản sao đã được sắp xếp của một (hoặc vài) cột, kèm theo "con trỏ" chỉ về vị trí dòng dữ liệu thật. Đa số database dùng một cấu trúc tên là B-tree (cây cân bằng) cho index — bạn không cần nhớ chi tiết kỹ thuật, chỉ cần biết: nhờ cấu trúc cây này, database tìm một giá trị trong 8 triệu dòng chỉ mất vài bước nhảy, thay vì 8 triệu lần so sánh.
Vì sao không có index lại chậm khủng khiếp?
Giả sử bảng orders của bạn có 10 triệu dòng. Bạn chạy:
SELECT * FROM orders WHERE customer_email = 'nguyen.van.a@gmail.com';
Nếu cột customer_email không có index, database buộc phải đọc lần lượt cả 10 triệu dòng, so sánh từng dòng xem email có khớp không. Đó là full table scan — chậm, ngốn CPU và đĩa cứng.
Nếu cột customer_email có index, database tra index (đã sắp xếp theo email), nhảy thẳng tới vài dòng khớp, trả kết quả gần như tức thì. Khác biệt có thể là 2 giây với 2 phút — gấp 60 lần.
Cái giá của index: không có gì miễn phí
Nếu index nhanh như vậy, sao không index mọi cột? Vì index có cái giá của nó:
- Tốn dung lượng lưu trữ. Mỗi index là một bản sao dữ liệu được sắp xếp, chiếm thêm ổ đĩa.
- Làm chậm thao tác ghi (INSERT/UPDATE/DELETE). Mỗi khi bạn thêm hoặc sửa một dòng, database phải cập nhật lại tất cả index liên quan. Bảng có 10 index sẽ ghi chậm hơn nhiều so với bảng có 2 index.
- Index sai còn tệ hơn không có. Index trên cột mà query không bao giờ lọc theo là dung lượng và chi phí ghi bị phí phạm hoàn toàn.
Những cột nào nên được index?
Quy tắc thực dụng cho BA — index thường có ích nhất trên các cột mà bạn thường xuyên dùng để:
- Lọc (WHERE): ví dụ
customer_email,status,created_date. - Nối bảng (JOIN): các cột khóa như
customer_id,product_id. Đây thường là index quan trọng nhất. - Sắp xếp (ORDER BY): cột bạn hay dùng để sắp, ví dụ
order_date.
gender chỉ có 2-3 giá trị, hay cột is_active chỉ có true/false — thường không đáng index, vì lọc theo nó vẫn trả về một nửa số dòng, index gần như vô dụng. Khái niệm này gọi là cardinality (độ đa dạng của giá trị): cardinality càng cao (nhiều giá trị khác nhau, như email), index càng hiệu quả.Composite index — index trên nhiều cột
Bạn có thể tạo index trên nhiều cột cùng lúc, gọi là composite index (index tổ hợp), ví dụ index trên (customer_id, order_date). Điều quan trọng BA cần biết: thứ tự cột trong composite index có ý nghĩa. Index (customer_id, order_date) giống như danh bạ điện thoại sắp theo họ trước, tên sau. Nó giúp ích khi bạn lọc theo customer_id, hoặc theo cả hai, nhưng không giúp ích nếu bạn chỉ lọc theo mình order_date mà bỏ qua customer_id — giống như tìm người theo tên trong danh bạ sắp theo họ. Đây là nguyên tắc "leftmost prefix" (tiền tố trái nhất), một chi tiết khiến nhiều người bối rối khi query vẫn chậm dù "đã có index".
Tình huống thực tế
Tình huống 1: Report doanh thu của Tiki chạy 4 phút mỗi sáng
Một BA tên Linh tại bộ phận Category của Tiki xây một report Metabase tổng hợp doanh thu theo ngành hàng, chạy trên bảng order_items có khoảng 40 triệu dòng. Query lọc theo order_date BETWEEN '2026-06-01' AND '2026-06-30'. Report mất gần 4 phút để load — mỗi sáng cả team họp đều phải chờ.
Linh nhờ DBA xem, và phát hiện cột order_date chưa có index. Mỗi lần chạy, database quét toàn bộ 40 triệu dòng chỉ để giữ lại dữ liệu của một tháng. Sau khi DBA thêm index trên order_date, query chỉ còn quét đúng phần dữ liệu trong khoảng ngày đó. Thời gian load giảm từ 4 phút xuống khoảng 6 giây.
Bài học: Cột ngày tháng dùng để lọc khoảng thời gian (BETWEEN, >=, <=) là ứng viên index hàng đầu trong các report phân tích. Nếu report của bạn lọc theo ngày mà chạy chậm, câu hỏi đầu tiên nên là: "Cột ngày này có index chưa?".
Tình huống 2: Câu WHERE "bọc hàm" làm index vô hiệu — fintech MoMo
Tại một ví điện tử kiểu MoMo, một BA viết query tìm giao dịch trong tháng 6:
SELECT * FROM transactions
WHERE MONTH(created_at) = 6 AND YEAR(created_at) = 2026;
Cột created_at đã có index, nhưng query vẫn chậm như rùa. Lý do: khi bạn bọc cột trong một hàm như MONTH(created_at), database không thể dùng index nữa — vì index được sắp theo giá trị gốc created_at, chứ không phải theo kết quả của hàm MONTH(). Database buộc phải tính MONTH() cho từng dòng, nghĩa là full scan trở lại.
Cách viết đúng để index vẫn hoạt động:
SELECT * FROM transactions
WHERE created_at >= '2026-06-01' AND created_at < '2026-07-01';
Cách này so sánh trực tiếp trên giá trị gốc của cột, nên index phát huy tác dụng. Kết quả: query nhanh trở lại như kỳ vọng.
Bài học: Có index thôi chưa đủ — cách bạn viết WHERE quyết định index có được dùng hay không. Bọc cột trong hàm (MONTH(), DATE(), UPPER(), CAST()...) là một trong những cách phổ biến nhất khiến index "bị mù". Đây là lỗi mà chính BA gây ra qua câu query của mình, không phải lỗi hệ thống.
Tình huống 3: Ride-hailing Be — index thừa làm chậm việc ghi dữ liệu
Tại một hãng gọi xe kiểu Be, đội phân tích từng đề xuất tạo index trên gần như mọi cột của bảng trips để "report nào cũng nhanh". Ban đầu các report đọc nhanh thật. Nhưng vài tuần sau, đội vận hành phàn nàn rằng việc ghi chuyến đi mới vào hệ thống bị chậm dần, đặc biệt vào giờ cao điểm khi hàng chục nghìn chuyến được tạo mỗi phút. Mỗi chuyến mới phải cập nhật cả chục index, tạo nút thắt cổ chai.
Sau khi rà soát, đội kỹ thuật xóa bớt các index trên những cột gần như không ai lọc theo (ví dụ index trên cột ghi chú tài xế), giữ lại các index thực sự quan trọng cho driver_id, customer_id và trip_date. Tốc độ ghi phục hồi, mà report vẫn nhanh.
Bài học: Index không miễn phí. Trong hệ thống có lượng ghi lớn (giao dịch, chuyến đi, log), index thừa gây hại thật sự. BA cần đề xuất index một cách có chọn lọc, dựa trên query thực tế hay dùng, chứ không phải "rải" index cho yên tâm.
Hướng dẫn từng bước
Khi report hay query của bạn chạy chậm, đây là quy trình suy nghĩ từng bước:
Bước 1 — Xác định query nào chậm. Nếu một dashboard có 5 widget, hãy tìm đúng widget chậm và lấy ra câu SQL của nó. Trong Metabase/Looker bạn thường xem được SQL gốc. Đừng đoán mò.
Bước 2 — Đọc câu WHERE và JOIN. Hỏi: query đang lọc theo cột nào? Nối bảng theo cột nào? Sắp xếp theo cột nào? Đây chính là những cột mà index sẽ tác động.
Bước 3 — Kiểm tra xem có "bọc hàm" làm hỏng index không. Tìm các biểu thức như MONTH(cột), DATE(cột), UPPER(cột), cột + 0, hay phép nối chuỗi trong WHERE. Nếu có, thử viết lại để so sánh trực tiếp trên cột gốc (như tình huống MoMo).
Bước 4 — Ước lượng độ chọn lọc. Cột bạn lọc có cardinality cao không? Lọc theo email (rất chọn lọc) khác hẳn lọc theo status = 'active' (có thể là 90% số dòng). Index chỉ giúp nhiều khi nó loại được phần lớn dữ liệu.
Bước 5 — Trao đổi với DBA bằng ngôn ngữ cụ thể. Thay vì "query chậm quá", hãy nói: "Query này lọc theo customer_email và order_date trên bảng orders 10 triệu dòng. Em nghi cột customer_email chưa có index. Anh kiểm tra và cân nhắc thêm index giúp em được không?". (Ở bài tiếp theo về EXPLAIN plan, bạn sẽ học cách tự kiểm chứng nghi ngờ này bằng số liệu thay vì chỉ phỏng đoán.)
Bước 6 — Đo lại sau khi tối ưu. Luôn ghi lại thời gian chạy trước và sau. "Giảm từ 4 phút xuống 6 giây" là một con số thuyết phục để báo cáo và để biện minh cho việc thêm index.
Lỗi thường gặp & mẹo
Lỗi: Tưởng "có index là tự động nhanh". Index chỉ giúp nếu query được viết để tận dụng nó. Bọc cột trong hàm, hoặc dùng LIKE '%abc' (ký tự % ở đầu) đều khiến index không dùng được. Mẹo: với LIKE, chỉ LIKE 'abc%' (wildcard ở cuối) mới tận dụng được index.
Lỗi: SELECT * trên bảng lớn. Kéo về tất cả cột buộc database đọc thêm nhiều dữ liệu không cần. Mẹo: chỉ chọn đúng cột cần dùng — vừa nhanh hơn, vừa rõ ràng hơn.
Lỗi: Đề xuất index cột cardinality thấp. Index trên cột gender hay is_active thường vô ích. Mẹo: ưu tiên index cột có nhiều giá trị phân biệt và hay xuất hiện trong WHERE/JOIN.
Lỗi: Quên thứ tự cột trong composite index. Query lọc theo order_date mà index lại là (customer_id, order_date) thì index không giúp được. Mẹo: cột lọc thường xuyên nhất nên đứng đầu trong composite index.
Lỗi: "Index mọi thứ cho chắc". Như tình huống Be, index thừa làm chậm ghi và tốn dung lượng. Mẹo: index theo nhu cầu query thực tế, rà soát định kỳ để bỏ index không dùng.
Mẹo vàng cho BA: Bạn không cần tự tạo index trên production — đó là việc của DBA và cần kiểm soát. Vai trò của bạn là phát hiện vấn đề, mô tả chính xác, và đề xuất hợp lý. Một BA hiểu index là một BA mà đội kỹ thuật thích làm việc cùng.
Bài tập thực hành
- Nhận diện cột nên index. Cho bảng
orders(order_id, customer_id, order_date, status, total_amount, shipping_note)với 20 triệu dòng. Một report lọc theocustomer_idvàorder_date, sắp xếp theoorder_date. Hãy liệt kê những cột bạn sẽ đề xuất index và giải thích vì sao. Cộtshipping_notecó nên index không? Vì sao không?
- Sửa query làm hỏng index. Viết lại câu sau để index trên
created_atcó thể được dùng:
SELECT * FROM transactions WHERE DATE(created_at) = '2026-06-15';
- Đánh giá đánh đổi. Một đồng nghiệp đề xuất thêm 8 index vào bảng
tripscủa hệ thống gọi xe đang ghi 50.000 chuyến/phút vào giờ cao điểm. Hãy nêu 2 rủi ro của đề xuất này và 2 câu hỏi bạn sẽ đặt ra trước khi đồng ý.
- Soạn câu hỏi cho DBA. Report doanh thu theo tháng của bạn chạy 3 phút, lọc theo
order_datetrên bảng 40 triệu dòng. Hãy viết một tin nhắn ngắn, cụ thể gửi DBA để nhờ kiểm tra index — đúng kiểu "hỏi đúng" mà bài này nhấn mạnh.
Tóm tắt
Index là "mục lục" của database: một cấu trúc phụ đã sắp xếp giúp tìm dòng dữ liệu nhanh, thay vì phải quét cả bảng (full table scan). Với bảng hàng triệu dòng, khác biệt giữa có và không có index có thể là vài giây so với vài phút.
Nhưng index không miễn phí — nó tốn dung lượng và làm chậm việc ghi, nên cần dùng có chọn lọc: ưu tiên các cột hay dùng trong WHERE, JOIN, ORDER BY và có cardinality cao. Cột ít giá trị phân biệt thường không đáng index.
Quan trọng với BA: có index thôi chưa đủ — cách bạn viết query quyết định index có được dùng hay không. Bọc cột trong hàm hay đặt % đầu chuỗi LIKE sẽ làm index "bị mù". Và với composite index, thứ tự cột rất quan trọng.
Bạn không cần tự quản lý index, nhưng hiểu nó giúp bạn viết query không vô tình làm chậm hệ thống, và đặc biệt là hỏi đội kỹ thuật đúng câu. Đó chính là khác biệt giữa một BA bị động chờ DBA "đoán bệnh" và một BA chủ động chỉ ra đúng vấn đề. Ở bài kế tiếp, bạn sẽ học cách đọc EXPLAIN plan để tự mình kiểm chứng xem một query có thực sự dùng index hay không.