zalo-icon
facebook-icon
phone-icon
Index và explain trong MongoDB: Chọn compound index và đọc kế hoạch thực thi | Bài 5
Học index và explain trong MongoDB qua lab 20.000 đơn: chọn compound index, đọc executionStats, hiểu ESR, covered query và chi phí duy trì.

Chuỗi MongoDB · Bài 5/6. Ở bài 4 về Aggregation Pipeline, chúng ta đã tổng hợp đúng số đơn và tiền hàng. Bài này giải quyết câu hỏi tiếp theo: khi dữ liệu tăng, làm sao biết một truy vấn đang làm quá nhiều việc?

Màn hình lịch sử mua hàng chỉ cần 20 đơn mới nhất của một khách. Khi collection còn vài trăm đơn, mọi thứ có vẻ nhanh. Sau nhiều tháng, cùng màn hình ấy bắt đầu chậm. Một người đề nghị thêm index cho tất cả trường; người khác nhìn thấy IXSCAN trong explain và kết luận truy vấn đã được tối ưu.

Cả hai cách đều thiếu bước kiểm chứng. Một truy vấn có thể dùng index nhưng vẫn xét rất nhiều khóa, đọc nhiều document rồi sắp xếp thêm. Mục tiêu của bài là giúp bạn chọn index từ câu truy vấn, đọc được việc MongoDB đã làm và so sánh trước sau mà không nhầm kết quả đúng với kết quả nhanh.

1. Mục tiêu, điều kiện đầu vào và phạm vi bài lab

Bạn cần biết find(), điều kiện lọc, sort và projection. Nếu mới bắt đầu, đọc MongoDB và CRUD cho người mới. Sau bài này, bạn sẽ phân biệt COLLSCAN, IXSCAN, FETCH và SORT; hiểu thứ tự trường trong compound index; dùng executionStats để đánh giá; nhận biết chi phí khi thêm index.

Các lệnh dùng mongosh với cú pháp tương thích MongoDB 7.0/8.0. Kiểm tra phiên bản bằng db.version(). Lab giả định collection thông thường, không sharding, collation mặc định, các trường lọc và sort là scalar, không có query settings hoặc index filter tùy chỉnh. Tên stage và cấu trúc explain có thể khác giữa phiên bản và execution engine [1][2].

Chúng ta tạo database riêng techdata_mongo5_lab gồm 20.000 document giả lập. Đây là dữ liệu mới để luyện index, không sửa collection ở bài 4. Các số lượng và mã đơn được suy ra từ bộ sinh dữ liệu; thời gian chạy và số khóa thực tế phải lấy từ môi trường của bạn. Bài không đưa con số mili giây giả làm benchmark.

2. Index là một cấu trúc dữ liệu phụ, không phải nút tăng tốc mọi truy vấn

Hãy hình dung collection là kho chứa các phiếu đơn hàng. Nếu không có đường tìm phù hợp, nhân viên phải xem lần lượt từng phiếu để tìm một khách. Index giống một danh mục đã sắp xếp, chứa giá trị khóa và đường dẫn tới document. MongoDB có thể tìm vùng liên quan trong danh mục trước rồi đọc những document cần thiết [3].

Index không phải bản sao đầy đủ của collection. Index trên customerId giúp tìm đơn theo khách; nó không tự chứa mọi sản phẩm trong đơn để trả về. Với projection hoặc điều kiện chưa được index đáp ứng, MongoDB có thể phải đọc document gốc. Bạn sẽ thấy sự khác biệt giữa quét khóa index và đọc document trong explain.

MongoDB thường tạo sẵn index _id_ cho collection thông thường. Index này hữu ích khi tìm theo _id, nhưng không tự đáp ứng câu hỏi “20 đơn paid mới nhất của C042”. Trước khi thử nghiệm, luôn xem getIndexes() để biết mình đang có gì [4].

db = db.getSiblingDB("techdata_mongo5_lab");
db.version();

Sau khi nạp dữ liệu ở phần tiếp theo, chạy db.orders.getIndexes() để xem index hiện có. Không cần xóa index của một hệ thống đang hoạt động để làm bài lab này.

3. Tạo dữ liệu có quy luật để tự kiểm tra kết quả

Chạy đoạn sau một lần trong database lab mới. Mỗi lô chỉ có 1.000 document để đoạn sinh dữ liệu dễ theo dõi. Với mỗi 500 đơn, có một cụm 100 đơn paid; nhờ đó trạng thái không bị gắn cố định vào một mã khách. Mỗi khách có cả đơn paid và pending.

db = db.getSiblingDB("techdata_mongo5_lab");
const baseTime = Date.parse("2026-11-01T00:00:00Z");
for (let batchStart = 0; batchStart < 20000; batchStart += 1000) {
  const batch = [];
  for (let i = batchStart; i < batchStart + 1000; i++) {
    batch.push({
      _id: i,
      customerId: "C" + String(i % 100).padStart(3, "0"),
      status: Math.floor(i / 100) % 5 === 0 ? "paid" : "pending",
      createdAt: new Date(baseTime + i * 60000),
      total: 100000 + (i % 20) * 10000,
      note: "Dữ liệu giả lập cho bài học index"
    });
  }
  db.orders.insertMany(batch);
}
db.orders.countDocuments({}); // 20000

Nếu chạy lại trên collection đã nạp, _id sẽ bị trùng. Hãy dùng database lab mới hoặc chủ động làm lại dữ liệu mẫu trong môi trường học; không sao chép thao tác dọn dữ liệu sang production. createdAt là Date; total là số nguyên đồng, không phải chuỗi.

3.1. Viết yêu cầu nghiệp vụ trước khi thiết kế index

Màn hình cần toàn bộ document của 20 đơn paid mới nhất của C042 trong khoảng UTC từ 01/11 đến trước 15/11/2026. Khi hai đơn trùng createdAt, _id lớn hơn đứng trước. Khoảng thời gian trong bài cố ý dùng UTC; nếu báo cáo theo giờ Việt Nam, đổi cận thời gian như đã học ở bài 4.

const q = {
  customerId: "C042",
  status: "paid",
  createdAt: {
    $gte: ISODate("2026-11-01T00:00:00Z"),
    $lt: ISODate("2026-11-15T00:00:00Z")
  }
};
const orderBy = { createdAt: -1, _id: -1 };
db.orders.countDocuments(q); // 40

db.orders.find(q).sort(orderBy).limit(20);

Giữ cùng phiên mongosh để dùng lại q và orderBy ở các bước sau. Nếu mở phiên mới, chọn lại database và khai báo lại hai biến. Một query cursor sẽ được tạo mới ở mỗi phép đo; không dùng lại cursor đã được đọc hết.

Mốc đối chiếuGiá trị của bộ mẫu
Toàn collection20.000 document
Đơn của C042, mọi trạng thái200 document
Đơn khớp toàn bộ q40 document
Kết quả sau sort và limit20 document
Mã đơn đầu tiên19542
Mã đơn thứ 2010042

Index chỉ được phép đổi cách tìm dữ liệu, không được đổi yêu cầu nghiệp vụ. Khi so sánh hai phương án, q, sort, limit và projection phải giống nhau. Nếu thêm projection ở một bên nhưng không thêm ở bên còn lại, bạn đang so sánh hai công việc khác nhau.

4. Lấy mốc ban đầu bằng explain("executionStats")

queryPlanner mô tả kế hoạch; executionStats còn chạy truy vấn để lấy thống kê thực thi. Vì vậy, không coi explain là thao tác luôn miễn phí trên dữ liệu lớn. Trong lab nhỏ, ta dùng executionStats để thấy lượng công việc thay vì chỉ nhìn tên index [5].

const before = db.orders.find(q)
  .sort(orderBy)
  .limit(20)
  .hint({ $natural: 1 })
  .explain("executionStats");

printjson(before.queryPlanner.winningPlan);
printjson(before.executionStats);

Ở đây hint({ $natural: 1 }) chủ động yêu cầu quét collection theo chiều tiến, giúp giữ mốc so sánh ngay cả khi bạn đã thêm index ở bước sau. Đây là lựa chọn phục vụ thí nghiệm, không phải đề nghị gắn hint này vào ứng dụng [6].

Với dữ liệu lab, truy vấn cần xét collection để tìm các đơn phù hợp rồi lấy top 20 theo sort. Hãy tìm COLLSCAN và bước SORT trong kế hoạch hoặc phần kế hoạch con tương ứng. Sort kết hợp limit có thể dùng tối ưu top-k; điều đó không có nghĩa nó bỏ qua được việc tìm các ứng viên từ nguồn không có thứ tự cần thiết.

Ba câu hỏi khi đọc explain: số trả về, số đã xét và cách thực thi
Đọc số liệu cùng cây kế hoạch; một tên stage riêng lẻ không đủ để kết luận.

5. Thử single-field index: thu hẹp tìm kiếm nhưng chưa giải quyết hết

db.orders.createIndex(
  { customerId: 1 },
  { name: "customer_only" }
);

const single = db.orders.find(q)
  .sort(orderBy)
  .limit(20)
  .hint("customer_only")
  .explain("executionStats");

printjson(single.queryPlanner.winningPlan);
printjson(single.executionStats);

Index này giúp đi tới những đơn của C042 thay vì tìm trên toàn collection. Nhưng trong vùng khách đó vẫn có nhiều trạng thái; createdAt cũng chưa được sắp theo nhu cầu màn hình. Với dữ liệu mẫu có 200 đơn của C042, ta dự kiến còn phải đọc các ứng viên để lọc rồi sắp 40 đơn phù hợp, trước khi trả 20 đơn.

Bạn có thể thấy IXSCAN đi cùng FETCH và SORT. Đây không phải lỗi của MongoDB: index chỉ được thiết kế cho một phần yêu cầu. Bước tiếp theo là xem có thể đưa cả bộ lọc và thứ tự mong muốn vào cùng cấu trúc tìm kiếm hay không.

6. Chọn compound index từ Equality, Sort và Range

Compound index chứa nhiều trường theo một thứ tự xác định. Với q, customerId và status đều là điều kiện bằng. createdAt vừa có khoảng thời gian vừa đứng đầu yêu cầu sắp xếp; _id là khóa sắp xếp phụ. Một ứng viên phù hợp là:

db.orders.createIndex(
  { customerId: 1, status: 1, createdAt: -1, _id: -1 },
  { name: "customer_status_newest" }
);

const compound = db.orders.find(q)
  .sort(orderBy)
  .limit(20)
  .hint("customer_status_newest")
  .explain("executionStats");

printjson(compound.queryPlanner.winningPlan);
printjson(compound.executionStats);

Hình dung index chia theo khách, trong mỗi khách chia theo trạng thái, rồi sắp các đơn theo createdAt giảm dần và _id giảm dần. Khi hai trường đầu đã cố định bằng C042 và paid, MongoDB có thể duyệt vùng thời gian theo thứ tự cần dùng. Index không phải đọc mọi khách để trả lịch sử của một khách [7].

Compound index cho customerId, status, createdAt và id theo nhu cầu tìm đơn mới nhất
Thiết kế theo bộ lọc và sort thực tế, không theo thứ tự các trường xuất hiện trong document.

6.1. ESR là hướng dẫn lựa chọn, không phải công thức cứng

ESR viết tắt của Equality, Sort, Range: thường đặt điều kiện bằng trước, sau đó ưu tiên trường sắp xếp và trường khoảng. Các trường equality không bắt buộc có một thứ tự duy nhất giữa chúng; lựa chọn còn phụ thuộc các truy vấn khác cần dùng prefix. Nếu range rất chọn lọc, thứ tự Equality, Range, Sort có thể đáng thử dù cần sort thêm [8].

Ví dụ chính có một trường createdAt đóng cả vai trò sort lẫn range. Nếu bổ sung điều kiện total: { $gte: 250000 }, total là một trường range khác. Đặt total sau các khóa sort có thể giữ khả năng trả theo thứ tự; đặt total sớm hơn có thể giảm số ứng viên phải xét. Hãy đo trên phân bố tiền và thời gian đại diện, không mặc định một phương án luôn thắng.

6.2. Bỏ hint để xem lựa chọn tự nhiên của optimizer

const automatic = db.orders.find(q)
  .sort(orderBy)
  .limit(20)
  .explain("executionStats");

printjson(automatic.queryPlanner.winningPlan);

Các hint trước đó giúp so sánh từng phương án có kiểm soát. Khi không hint, optimizer được quyền chọn kế hoạch. Nếu nó không chọn index bạn mong đợi, xem lại độ chọn lọc, sort, projection và các index hiện có. Không gắn hint vĩnh viễn chỉ vì nó tốt trong một bộ mẫu; dữ liệu và workload sẽ thay đổi [6].

7. Đọc executionStats: hiểu công việc trước khi nhìn mili giây

Thông tinCách đọc trong bài lab
nReturnedSố document trả về; cả ba cách phải trả 20
totalDocsExaminedSố lượt document được xét; không phải số document trả về
totalKeysExaminedSố entry index được xét; thấp không tự chứng minh nhanh
executionTimeMillisThời gian server của lần explain; không phải độ trễ end-to-end
winningPlanKế hoạch được chọn; xem cả cây và kế hoạch con

COLLSCAN là quét collection; IXSCAN là duyệt khóa index; FETCH là đọc document; SORT là một bước sắp xếp. FETCH là bình thường khi màn hình cần các trường không nằm trong index. Một số truy vấn hoặc phiên bản có stage khác, nên đừng viết công cụ kiểm tra bằng giả định mọi kế hoạch đều có đúng một đường inputStage.inputStage [2].

Có thể in bảng tóm tắt từ ba kết quả vừa đo:

function summary(label, plan) {
  const s = plan.executionStats;
  return {
    label,
    nReturned: s.nReturned,
    totalDocsExamined: s.totalDocsExamined,
    totalKeysExamined: s.totalKeysExamined,
    executionTimeMillis: s.executionTimeMillis
  };
}
printjson([
  summary("collection scan", before),
  summary("customer_only", single),
  summary("compound", compound)
]);
Phương ánĐiều cần quan sát, không phải benchmark đo sẵn
Quét collectionXét toàn bộ 20.000 document; trả 20; có bước sort
Index customer_onlyThu hẹp về khách C042; vẫn cần lọc và sort
Index compoundDuyệt vùng khách + trạng thái theo thứ tự; lượng đọc có thể gần số cần trả

Ở phương án compound của bộ mẫu, kỳ vọng không cần một bước SORT riêng và số document xét giảm mạnh. Số khóa xét có thể khác số trả về vì kiểm tra biên hoặc chi tiết thực thi. Đừng viết assertion cứng rằng keysExamined luôn bằng 20 trên mọi phiên bản [1].

7.1. Kiểm tra hai phương án có trả đúng cùng 20 đơn không

const expectedIds = Array.from({ length: 20 }, (_, k) => 19542 - k * 500);
const idsByScan = db.orders.find(q)
  .sort(orderBy).limit(20).hint({ $natural: 1 })
  .toArray().map(doc => doc._id);
const idsByIndex = db.orders.find(q)
  .sort(orderBy).limit(20).hint("customer_status_newest")
  .toArray().map(doc => doc._id);

printjson({ expectedIds, idsByScan, idsByIndex });
if (JSON.stringify(idsByScan) !== JSON.stringify(expectedIds) ||
    JSON.stringify(idsByIndex) !== JSON.stringify(expectedIds)) {
  throw new Error("Kết quả không khớp bộ dữ liệu mẫu");
}

Điều kiện đúng trước, hiệu năng sau. Khóa phụ _id giúp hai lần đọc có thứ tự xác định ngay cả khi createdAt trùng nhau; trong bộ sinh hiện tại mỗi thời điểm là duy nhất. Mã kiểm tra này cố định cho dữ liệu lab, không áp dụng nguyên xi nếu bạn đã thêm hoặc thay đổi đơn.

7.2. Vì sao không kết luận bằng một lần chạy 0 ms?

Dữ liệu nhỏ, cache đã nóng, tải máy và độ phân giải mili giây có thể làm nhiều phương án cùng hiện 0 ms. Hơn nữa, explain không sử dụng và không tạo entry plan cache như một truy vấn thông thường [5]. Hãy xem lượng dữ liệu xét trước; sau đó đo nhiều lần bằng ứng dụng với tải và dữ liệu đại diện. Thời gian server không bao gồm mọi chi phí mạng, giải mã và render màn hình.

8. Prefix và hướng sort: những chỗ dễ hiểu nhầm

Prefix là đoạn trường liên tiếp bắt đầu từ đầu index. Với index hiện tại, các prefix là customerId; customerId + status; customerId + status + createdAt; và toàn bộ bốn trường. Một bộ lọc chỉ có status không bắt đầu bằng prefix đầu tiên. MongoDB vẫn có thể cân nhắc quét index, nhưng đó không phải đường tìm thu hẹp tương đương truy vấn bắt đầu bằng customerId [7].

Truy vấnIndex hiện tại đáp ứng thế nào?
customerId = C042Có prefix; chưa tự bảo đảm sort toàn khách theo createdAt khi thiếu status
customerId = C042 và status = paidCố định hai trường đầu; phù hợp sort createdAt, _id
Chỉ status = paidThiếu trường đầu; nên đánh giá index khác theo workload
Cố định customerId, status; sort createdAt tăng, _id tăngCó thể duyệt ngược index để đáp ứng thứ tự đảo toàn bộ
Cố định customerId, status; sort createdAt giảm, _id tăngKhông cùng hướng hoặc đảo toàn bộ hai khóa sort của index

Một index có thể hỗ trợ sort theo hướng khai báo hoặc hướng đảo ngược toàn bộ các khóa sort liên quan. Với nhiều trường, đảo một trường nhưng giữ trường kia không tương đương đảo toàn bộ. Đồng thời, nếu muốn sort bằng phần sau của index, điều kiện trên các trường đứng trước phải đáp ứng yêu cầu thích hợp, như equality trong ví dụ này [9].

9. Covered query: lúc nào không cần đọc document gốc?

Giả sử màn hình danh sách chỉ cần _id, customerId, status và createdAt. Tất cả các trường đó đã có trong compound index. Với lab scalar, không sharding và không điều kiện null, truy vấn dưới là một ứng viên covered query: MongoDB có thể trả kết quả từ index mà không đọc document gốc [3].

const covered = db.orders.find(q, {
  _id: 1, customerId: 1, status: 1, createdAt: 1
})
  .sort(orderBy)
  .limit(20)
  .hint("customer_status_newest")
  .explain("executionStats");

printjson(covered.queryPlanner.winningPlan);
printjson(covered.executionStats);

Kiểm tra totalDocsExamined bằng 0 cùng kế hoạch không cần FETCH cho truy vấn có kết quả. Nếu thêm total: 1 vào projection, index này thiếu total nên thường cần đọc document. Đừng lẫn chỉ tiêu này với trường hợp truy vấn không tìm thấy document nào.

Trong ví dụ này _id đã được đưa rõ vào compound index. Nếu dùng index khác không có _id và muốn cover, nhớ projection mặc định vẫn trả _id, nên có thể cần _id: 0. Không phải cứ dùng projection là được cover, và cũng không nên thêm mọi trường vào index chỉ để cố đạt con số 0 document đọc [3].

Covered query làm thay đổi dữ liệu trả về so với ví dụ lấy toàn document. Vì vậy hãy đo và so sánh nó như một thiết kế màn hình riêng, không lấy nó làm minh chứng trực tiếp rằng index ban đầu tăng tốc cùng một truy vấn bao nhiêu lần.

10. Mở rộng vừa đủ: multikey, unique và partial index

10.1. Multikey cho mảng: index không sửa ý nghĩa điều kiện

Nếu tạo index trên một trường chứa mảng, MongoDB tự xử lý nó như multikey; một document có thể tạo nhiều entry index. Ví dụ collection đơn ở bài 3 có thể cân nhắc index { "items.sku": 1 } cho việc tìm đơn chứa một SKU [10].

Tuy nhiên, index không thay thế $elemMatch khi nhiều điều kiện phải đúng trên cùng một phần tử mảng. Trước tiên viết điều kiện đúng như bài 3 về truy vấn mảng, sau đó mới kiểm tra index bounds. Đừng suy luận số khóa xét phải bằng số đơn khi index chứa nhiều phần tử trên một đơn.

10.2. Unique bảo vệ tính duy nhất của khóa

Với ứng dụng nhiều cửa hàng, mã đơn chỉ cần duy nhất trong từng cửa hàng, có thể dùng unique compound index trên shopId và orderCode. Đây là quy tắc toàn vẹn dữ liệu, không chỉ tối ưu đọc. Index bảo vệ cặp giá trị, không bắt từng orderCode duy nhất trên tất cả cửa hàng [4].

db.order_registry.createIndex(
  { shopId: 1, orderCode: 1 },
  { name: "unique_shop_order", unique: true }
);

Ví dụ này tạo collection registry riêng trong lab nếu chưa có. Với dữ liệu có sẵn, kiểm tra trùng trước khi tạo; unique index không tự dọn dữ liệu trùng. Ứng dụng cần xử lý lỗi duplicate key, thay vì chỉ kiểm tra “đã có chưa” rồi insert trong hai bước độc lập.

10.3. Partial index chỉ chứa một phần document

Nếu màn hình chỉ phục vụ đơn paid, một phương án khác là index một phần dữ liệu với partialFilterExpression. Đoạn dưới là bài thử thêm sau khi bạn đã hoàn thành so sánh chính:

db.orders.createIndex(
  { customerId: 1, createdAt: -1, _id: -1 },
  {
    name: "paid_customer_newest",
    partialFilterExpression: { status: "paid" }
  }
);

Index chỉ chứa document paid. Truy vấn phải có điều kiện khiến MongoDB xác định kết quả nằm trọn trong phạm vi index; q có status paid nên phù hợp để thử. Một truy vấn lấy cả paid lẫn pending không được dùng partial index này để thay thế toàn bộ tập kết quả. Đừng ép hint chỉ để nhìn thấy tên index [11].

Trong bộ mẫu, chỉ 4.000 trên 20.000 đơn là paid, nên đây là ví dụ dễ nhìn về phạm vi index. Lợi ích dung lượng thực tế còn phụ thuộc độ dài khóa và overhead. Nếu trả cả status trong projection, không tự khẳng định truy vấn sẽ cover vì status không nằm trong key pattern partial này; hãy kiểm tra explain riêng.

11. Chi phí vận hành và lỗi thường gặp

Cân bằng lợi ích đọc, dung lượng bộ nhớ và chi phí cập nhật index
Một index đáng giữ khi lợi ích cho workload quan trọng phù hợp chi phí duy trì.

Mỗi index dùng thêm dung lượng và góp vào working set. Khi insert hoặc xóa document, MongoDB phải duy trì các index liên quan; update làm thay đổi trường được index cũng cần cập nhật khóa. Vì vậy nhiều index có thể giúp đọc nhưng làm ghi tốn công hơn. Hãy đo cả đường đọc lẫn đường ghi quan trọng [12].

Các index được tạo trong bài là ứng viên học tập, không phải danh sách phải giữ nguyên trong mọi hệ thống. Trong production, xem truy vấn phổ biến, tần suất, p95 độ trễ và tốc độ ghi trước khi quyết định. Việc build index trên collection lớn cần được lên kế hoạch theo tài nguyên, topology và phiên bản; không suy ra nó nhẹ chỉ vì lab chạy nhanh.

Dấu hiệuKiểm tra và hướng xử lý
Thấy IXSCAN nhưng vẫn chậmĐọc keys/docs examined, FETCH, SORT và số kết quả; kiểm tra độ chọn lọc
Có compound index nhưng vẫn SORTĐối chiếu prefix equality, thứ tự và hướng sort; kiểm tra collation
Số liệu trước sau không cùng kết quảGiữ nguyên filter, sort, limit, projection; kiểm tra dữ liệu có đổi giữa hai lần
Hai lần đều 0 msTăng dữ liệu đại diện, xem lượng việc và đo nhiều lần trong ứng dụng
Ghi chậm hơn sau khi thêm nhiều indexĐánh giá chi phí duy trì và nhu cầu từng index
hint báo lỗiKiểm tra tên, tồn tại và khả năng sử dụng của index; không đoán tên

Đừng xóa index chỉ dựa trên một câu truy vấn hoặc một khoảng quan sát ngắn. Một index có thể phục vụ báo cáo ít chạy nhưng quan trọng, hoặc thực thi unique constraint. Đánh giá toàn workload trước khi thay đổi; bài lab không yêu cầu bạn xóa bất kỳ index production nào.

12. Bài tập tự luyện và đáp án định hướng

Bài 1: đổi yêu cầu thành “đơn paid mới nhất của toàn cửa hàng”

Bỏ customerId khỏi q và giữ sort createdAt giảm, _id giảm. Hãy đề xuất một index mới. Gợi ý: câu hỏi không còn cố định trường đầu của index hiện tại. Ứng viên là { status: 1, createdAt: -1, _id: -1 }. Kiểm tra bằng explain với truy vấn mới và số lượng kết quả cần trả; không chỉ nhìn việc index cũ có được chọn hay không.

Bài 2: sort một chiều khác và kiểm chứng

Với customerId và status cố định, thử sort { createdAt: 1, _id: 1 }, sau đó thử { createdAt: -1, _id: 1 }. Gợi ý: trường hợp đầu đảo toàn bộ phần sort nên có thể duyệt ngược index; trường hợp sau đổi quan hệ hướng giữa hai khóa nên không được index hiện tại bảo đảm trực tiếp. Dù bộ mẫu không có ngày trùng, hãy xem kế hoạch theo cấu trúc sort, không chỉ thấy dữ liệu tình cờ đúng thứ tự.

Bài 3: thêm total vào projection covered

Sao chép truy vấn covered rồi thêm total. Dự đoán trước khi chạy: thường cần FETCH vì total không có trong index. Đáp án cần nêu totalDocsExamined, các stage và kết quả có còn đúng 20 đơn không. Nếu muốn thử index có total, ghi lại chi phí kích thước và nhu cầu màn hình; không mặc định nên giữ index rộng hơn.

Bài 4: đánh giá partial index

Dùng q với hint paid_customer_newest rồi chạy lại cùng q không hint. Ghi kế hoạch, số khóa và document xét. Sau đó giải thích bằng lời vì sao partial index không đại diện cho truy vấn lấy tất cả trạng thái. Không ép truy vấn ngoài phạm vi chỉ để tạo một kết quả có vẻ nhanh.

13. Tổng kết: chọn index bằng truy vấn và chứng minh bằng số liệu

Quy trình có thể lặp lại là: chốt filter, sort, limit và projection; lấy mốc; tạo một ứng viên; so sánh lượng việc và kế hoạch; kiểm tra kết quả vẫn đúng; cuối cùng đánh giá tải ghi và dung lượng. Một index hợp lý phục vụ nhu cầu cụ thể, không phải mọi trường trong document.

Trong bài này, single-field index mới thu hẹp theo khách; compound index còn đưa trạng thái và thứ tự thời gian vào đường tìm. Bạn cũng đã thấy covered query là một thiết kế projection riêng, partial index có phạm vi riêng và hint là công cụ thí nghiệm cần dùng có lý do.

Bài 6 tiếp theo: Atomicity và Transaction trong MongoDB: xử lý tồn kho, đơn hàng và retry an toàn. Khi truy vấn đã đúng và được đo, chúng ta sẽ chuyển sang bảo vệ tính đúng đắn khi nhiều thao tác ghi diễn ra đồng thời.

14. Tài liệu tham khảo

Đối chiếu tài liệu chính thức ngày 12/11/2026. Các trang manual có thể cập nhật; chọn nhánh tài liệu khớp server khi triển khai. Dữ liệu và yêu cầu cửa hàng trong bài là ví dụ giảng dạy tự xây dựng, không phải benchmark của MongoDB.

  1. Interpret Explain Plan Results
  2. Explain Results
  3. Query Optimization và covered queries
  4. Unique Indexes
  5. cursor.explain()
  6. cursor.hint()
  7. Compound Indexes
  8. Equality, Sort, Range Guideline
  9. Sort Results with Indexes
  10. Multikey Indexes
  11. Partial Indexes
  12. Write Operation Performance

Học cùng TechData.AI: Muốn luyện truy vấn, index và kiểm tra hiệu năng trong project Data Engineering có hướng dẫn? Xem các chương trình đào tạo Data & AI để chọn lộ trình phù hợp với nền tảng hiện tại.

Comments are closed!

Scroll to Top