Nguyên lý hoạt động của Index trong MongoDB
Một Index trong MongoDB hoạt động giống như chỉ mục trong sách giáo khoa, cho phép trình đọc tìm kiếm thông tin nhanh chóng mà không cần đọc toàn bộ cuốn sách.
Khi không có index, MongoDB thực hiện thao tác COLLSCAN (Collection Scan), quét từng tài liệu trong collection để tìm kiếm, gây tốn tài nguyên CPU và IO.
Khi có index, MongoDB sử dụng cấu trúc dữ liệu B-Tree để thực hiện IXSCAN (Index Scan), nhảy trực tiếp đến vị trí chứa dữ liệu cần tìm, giảm độ phức tạp từ O(n) xuống O(log n).
Trade-off: Việc tạo index làm tăng tốc độ đọc (Read) nhưng làm chậm tốc độ ghi (Write) vì mỗi lần insert/update/delete, MongoDB phải cập nhật cả index.
Tạo Index đơn và Index composite
Chúng ta sẽ sử dụng collection orders từ phần trước để minh họa. Giả sử collection này chứa đơn hàng với các trường: customer_id, status, created_at, amount.
Tạo Index đơn (Single Field Index)
Mục đích: Tối ưu hóa các truy vấn tìm kiếm dựa trên một trường duy nhất, ví dụ: tìm đơn hàng theo trạng thái status.
Thao tác: Sử dụng lệnh createIndex với tên trường và thứ tự sắp xếp (1 cho tăng dần, -1 cho giảm dần).
use ecommerce_db
db.orders.createIndex({ status: 1 }, { name: "status_asc" })
Kết quả mong đợi: MongoDB trả về đối tượng chứa created ok: true và name của index là status_asc.
Tạo Index composite (Compound Index)
Mục đích: Tối ưu hóa truy vấn lọc theo nhiều trường cùng lúc. Ví dụ: tìm đơn hàng của một khách hàng cụ thể (customer_id) có trạng thái là pending.
Nguyên tắc quan trọng: Trường được lọc bằng toán tử so sánh (=, >, <) phải đứng trước trường được dùng để sắp xếp (sort) trong định nghĩa index để tận dụng tối đa.
db.orders.createIndex({ customer_id: 1, status: 1 }, { name: "customer_status_idx" })
Kết quả mong đợi: Trả về created ok: true với index name customer_status_idx.
Verify kết quả tạo Index
Sử dụng lệnh getIndexes để liệt kê tất cả index hiện có trong collection.
db.orders.getIndexes()
Kết quả mong đợi: Một mảng (array) chứa các object, bao gồm index mặc định _id_ và các index chúng ta vừa tạo (status_asc, customer_status_idx).
Sử dụng explain() để phân tích Query Plan
Lệnh explain cho phép bạn xem MongoDB thực hiện truy vấn như thế nào, bao gồm loại scan, số lượng tài liệu kiểm tra và thời gian thực thi.
Phân tích Query Plan cơ bản
Mục đích: Kiểm tra xem index vừa tạo có được sử dụng hay không.
Thao tác: Gắn .explain("executionStats") vào cuối câu lệnh find. Chế độ này cung cấp thống kê chi tiết nhất về hiệu năng.
db.orders.find({ status: "pending" }).explain("executionStats")
Kết quả mong đợi: Một object JSON lớn. Quan trọng nhất là trường stage trong queryPlanner. Nếu thấy IXSCAN thì index đã được dùng. Nếu thấy COLLSCAN thì index không được dùng.
Các trường quan trọng trong kết quả explain
- queryPlanner.winningPlan.stage: Loại thao tác chính (IXSCAN hoặc COLLSCAN).
- executionStats.totalDocsExamined: Số lượng tài liệu MongoDB đã phải đọc từ đĩa để trả về kết quả.
- executionStats.nReturned: Số lượng tài liệu thực sự trả về cho client.
- executionStats.executionTimeMillis: Thời gian thực thi tính bằng mili giây.
- executionStats.totalDocsExamined / nReturned: Tỷ lệ này càng gần 1 càng tốt. Nếu tỷ lệ cao (ví dụ: đọc 10000 tài liệu để trả về 10), truy vấn chưa tối ưu.
Tối ưu hóa truy vấn dựa trên kết quả explain()
Giả sử chúng ta chạy một truy vấn phức tạp mà kết quả explain cho thấy COLLSCAN.
Kịch bản 1: Truy vấn không dùng được Index composite
Vấn đề: Index composite { customer_id: 1, status: 1 } được tạo. Nhưng chúng ta chỉ query theo status.
Nguyên lý: MongoDB chỉ sử dụng index composite nếu query sử dụng trường đứng đầu tiên (leftmost prefix). Query chỉ dùng status sẽ bỏ qua index composite này và quay lại COLLSCAN nếu không có index đơn cho status.
Hành động: Đảm bảo đã tạo index đơn cho trường status (đã làm ở phần trước) hoặc sửa query để bao gồm customer_id.
db.orders.find({ status: "shipped" }).explain("executionStats")
Kết quả mong đợi: Nếu index đơn status_asc tồn tại, trường stage sẽ chuyển sang IXSCAN và totalDocsExamined giảm mạnh so với nReturned.
Kịch bản 2: Tối ưu hóa sắp xếp (Sort)
Vấn đề: Query cần sắp xếp kết quả theo created_at nhưng không có index hỗ trợ sắp xếp, dẫn đến MongoDB phải lấy dữ liệu về và sắp xếp trong bộ nhớ (SORT stage).
Hành động: Tạo index composite bao gồm cả trường lọc và trường sắp xếp.
db.orders.createIndex({ status: 1, created_at: -1 }, { name: "status_date_desc" })
Chạy lại query với sort:
db.orders.find({ status: "pending" }).sort({ created_at: -1 }).explain("executionStats")
Kết quả mong đợi: Trong queryPlanner, stage của IXSCAN sẽ có thuộc tính indexName: "status_date_desc" và không còn stage SORT nào ngay sau IXSCAN. Điều này nghĩa là dữ liệu đã được trả về đúng thứ tự từ index.
Xóa Index không cần thiết và kiểm tra hiệu năng
Index dư thừa làm chậm quá trình ghi (write) và tốn dung lượng RAM. Cần xóa các index không được sử dụng hoặc index bị tạo sai cấu trúc.
Xóa Index theo tên
Mục đích: Xóa một index cụ thể đã xác định là không cần thiết.
Thao tác: Dùng lệnh dropIndex với tên index.
db.orders.dropIndex("status_date_desc")
Kết quả mong đợi: Trả về { "ok" : 1 } nếu xóa thành công.
Xóa Index theo định nghĩa (Pattern)
Mục đích: Khi không nhớ tên index nhưng biết cấu trúc trường (key pattern).
Thao tác: Truyền object định nghĩa index vào dropIndex.
db.orders.dropIndex({ status: 1, created_at: -1 })
Kết quả mong đợi: Trả về { "ok" : 1 }.
Verify hiệu năng sau khi xóa
Bước 1: Kiểm tra lại danh sách index còn lại.
db.orders.getIndexes()
Kết quả mong đợi: Index status_date_desc đã biến mất khỏi danh sách.
Bước 2: Chạy lại query từng phần để xem ảnh hưởng.
db.orders.find({ status: "pending" }).sort({ created_at: -1 }).explain("executionStats")
Kết quả mong đợi:
- Nếu không còn index phù hợp cho việc sort: stage trong queryPlanner sẽ xuất hiện thêm stage SORT (chỉ rõ MongoDB phải sắp xếp lại dữ liệu).
- executionTimeMillis có thể tăng lên so với khi có index.
- Điều này xác nhận việc xóa index ảnh hưởng trực tiếp đến hiệu năng của query đó.
Điều hướng series:
Mục lục: Series: Triển khai Database Document với MongoDB và Ubuntu 24.04
« Phần 4: Quản lý dữ liệu, schema và thao tác CRUD cơ bản
Phần 6: Sao lưu, khôi phục dữ liệu và xử lý sự cố (Troubleshooting) »