Tự học index MongoDB qua 7 bài tập thực tế

Tự học index MongoDB qua 7 bài tập thực tế

34 phút đọc Tự học index MongoDB qua các bài tập thực tế: tối ưu truy vấn, cải thiện hiệu năng với dữ liệu mẫu, ví dụ rõ ràng và cách kiểm chứng.
(0 Đánh giá)
Học index MongoDB qua chuỗi bài tập có hướng dẫn: tạo, chọn, và kiểm tra các loại index (single, compound, text, TTL, partial, sparse, geo), phân tích explain, tránh anti-pattern, và đo hiệu năng bằng sample dataset. Phù hợp backend, data engineer, DevOps.
Tự học index MongoDB qua 7 bài tập thực tế

Nếu bạn đang làm việc với MongoDB mà chưa khai thác triệt để index, rất có thể bạn đang để hiệu năng rơi rụng trên sàn. Tin vui: chỉ cần 7 bài tập nhỏ, bạn sẽ tự mình hiểu được cách thiết kế index hiệu quả, đọc explain như một thói quen, và biết cách tránh những cái bẫy thường gặp khiến CPU và I/O đội lên vô nghĩa. Bài viết này là lộ trình tự học thực hành, có dữ liệu mẫu, câu truy vấn cụ thể, chỉ số cần đo và lời giải có lý do phía sau.

Bộ dữ liệu mẫu và thiết lập nhanh

ecommerce dataset, mongodb shell, collections, sample data

Chúng ta sẽ mô phỏng một ứng dụng thương mại điện tử với 3 collection chính: users, products, orders. Bạn có thể dùng MongoDB Atlas hoặc cài mongod cục bộ. Dưới đây là script khởi tạo dữ liệu tối giản (mongosh):

use shop

// Users
for (let i = 1; i <= 50000; i++) {
  db.users.insertOne({
    _id: i,
    name: 'User ' + i,
    email: 'user' + i + '@example.com',
    createdAt: new Date(2022, Math.floor(Math.random()*12), Math.floor(Math.random()*28)+1)
  })
}

// Products
const categories = ['electronics','beauty','books','home','toys','fashion']
const tagsPool = ['new','sale','popular','eco','premium','budget','gift','wireless','portable','classic']
for (let i = 1; i <= 200000; i++) {
  const tags = Array.from({length: Math.floor(Math.random()*4)+1}, () => tagsPool[Math.floor(Math.random()*tagsPool.length)])
  db.products.insertOne({
    _id: i,
    sku: 'SKU' + i,
    name: 'Product ' + i,
    category: categories[Math.floor(Math.random()*categories.length)],
    price: Math.round(Math.random()*100000)/100,
    active: Math.random() < 0.85,
    tags: tags,
    createdAt: new Date(2023, Math.floor(Math.random()*12), Math.floor(Math.random()*28)+1)
  })
}

// Orders
for (let i = 1; i <= 1000000; i++) {
  const cId = Math.floor(Math.random()*50000) + 1
  const statusArr = ['PLACED','PAID','SHIPPED','DELIVERED','CANCELLED']
  db.orders.insertOne({
    _id: i,
    customerId: cId,
    items: [
      { productId: Math.floor(Math.random()*200000)+1, qty: Math.ceil(Math.random()*3), price: Math.round(Math.random()*100000)/100 },
      { productId: Math.floor(Math.random()*200000)+1, qty: Math.ceil(Math.random()*3), price: Math.round(Math.random()*100000)/100 }
    ],
    status: statusArr[Math.floor(Math.random()*statusArr.length)],
    createdAt: new Date(2023, Math.floor(Math.random()*12), Math.floor(Math.random()*28)+1)
  })
}

Nếu dữ liệu quá lớn với máy bạn, hãy giảm số lượng tuỳ ý. Quan trọng là có đủ độ lệch phân phối giữa các trường.

Mục tiêu chung trong 7 bài tập:

  • Đo baseline bằng explain và thời gian chạy, sau đó thêm index để thấy hiệu quả thật.
  • Quan sát planning (IXSCAN vs COLLSCAN), số document được examine, rồi kiểm tra CPU/I/O.
  • Rút ra quy tắc thiết kế index từ chính dữ liệu và truy vấn của bạn.

Những nguyên lý index MongoDB bạn cần nắm (5 phút)

btree index, selectivity, cardinality, query planning

Tóm tắt, đủ dùng cho thực hành:

  • Cấu trúc: Phần lớn index trong MongoDB là B-Tree. Chúng tối ưu duyệt theo thứ tự và tìm kiếm theo khoảng.
  • Tính chọn lọc (selectivity): Trường càng phân biệt tốt (cardinality cao), index càng hữu dụng cho lọc. Index trên trường có 2 giá trị true/false thường kém hiệu quả nếu dùng đơn lẻ.
  • Quy tắc tiền tố trái (left-prefix) cho compound: Với index { a: 1, b: 1, c: -1 }, planner có thể tận dụng cho điều kiện trên a, a+b, a+b+c nhưng không cho b+c nếu thiếu a.
  • Equality trước range trước sort: Trong compound index, hãy đặt các trường so khớp bằng (equality) lên trước, tiếp theo là range, rồi tới trường dùng cho sort. Đây thường là thứ tự tối ưu nhất cho nhiều workload.
  • Covered query: Nếu truy vấn và projection chỉ dùng các trường nằm trong cùng một index, MongoDB có thể trả kết quả mà không cần đọc document (bỏ qua FETCH stage) — rất nhanh và ít I/O.
  • Multi-key index: Khi index trường mảng, mỗi phần tử được index. Cẩn trọng độ phình toàn cục; với mảng rất dài, index trở nên nặng.
  • Index intersection: MongoDB có thể kết hợp nhiều index đơn để trả lời truy vấn, nhưng compound index phù hợp thường vượt trội về hiệu năng và khả năng sort.
  • Partial index vs sparse index: Partial cho phép điều kiện filter linh hoạt hơn (biểu thức), còn sparse chỉ index document có trường đó tồn tại và không null.
  • TTL, text, 2dsphere, hashed: Những loại index chuyên biệt phục vụ case cụ thể (hết hạn, tìm kiếm toàn văn, không gian, băm phục vụ shard key).
  • Chi phí ghi: Mỗi index thêm vào là thêm chi phí update/insert. Chỉ giữ index phục vụ truy vấn thật sự quan trọng.

Với những nguyên lý này, ta bước vào 7 bài tập.

Bài tập 1: Tăng tốc truy vấn khách hàng theo thời gian với compound index

compound index, sort optimization, query performance

Bài toán: Hiển thị 30 đơn hàng gần nhất của một khách hàng, dùng phân trang theo thời gian.

Truy vấn baseline:

const customerId = 1234
const pageSize = 30
const cursor = db.orders.find({ customerId: customerId })
  .sort({ createdAt: -1 })
  .limit(pageSize)

cursor.explain('executionStats')

Kỳ vọng baseline: Nếu chưa có index phù hợp, planner có thể chọn COLLSCAN hoặc dùng index kém hiệu quả, dẫn đến sort in-memory. Dấu hiệu trong explain:

  • stage: COLLSCAN hoặc IXSCAN rồi SORT với memLimitExceeded: false nhưng nMem: lớn.
  • totalDocsExamined: rất cao.

Thiết kế index: Vì customerId là equality và sort theo createdAt giảm dần, index hợp lý là { customerId: 1, createdAt: -1 }.

db.orders.createIndex({ customerId: 1, createdAt: -1 }, { name: 'idx_orders_customer_created_desc' })

Chạy lại explain:

db.orders.find({ customerId: customerId })
  .sort({ createdAt: -1 })
  .limit(pageSize)
  .explain('executionStats')

Kết quả tốt khi thấy:

  • stage: IXSCAN, không có SORT stage.
  • totalDocsExamined xấp xỉ bằng số documents trả về (<= pageSize).
  • executionTimeMillis giảm mạnh.

Mẹo nâng cao:

  • Nếu thêm điều kiện status, xem xét mở rộng thành { customerId: 1, status: 1, createdAt: -1 } nếu tỷ lệ lọc theo status cao. Nhưng hãy cân nhắc tính đa dụng: index càng đặc thù càng khó tái sử dụng.
  • Nếu phân trang vô hạn, sử dụng seek method với createdAt và _id để tránh skip/limit tốn kém.

Sai lầm hay gặp:

  • Đảo thứ tự index thành { createdAt: -1, customerId: 1 } làm hỏng quy tắc left-prefix cho lọc theo customerId, dẫn đến planner khó tối ưu.

Bài tập tự làm: Thêm filter theo khoảng thời gian createdAt từ ngày A đến B, đo tác động đến totalKeysExamined.

Bài tập 2: Covered query — giảm I/O về 0 cho màn hình danh sách

covered query, projection, index only

Bài toán: Trang quản trị cần danh sách đơn hàng với cột _id, status, createdAt. Không cần toàn bộ document.

Truy vấn baseline:

db.orders.find(
  { status: { $in: ['PAID','SHIPPED','DELIVERED'] } },
  { _id: 1, status: 1, createdAt: 1 }
).sort({ createdAt: -1 }).limit(50).explain('executionStats')

Nếu chỉ có index ở bài 1, planner có thể dùng { customerId, createdAt } và phải FETCH để đọc status, vì status không nằm trong index. Giải pháp: tạo index bao phủ đúng bộ trường query, sort, projection.

db.orders.createIndex(
  { status: 1, createdAt: -1, _id: 1 },
  { name: 'idx_orders_status_created_cover' }
)

Lý do thêm _id: Projection có _id, nên đưa _id vào index giúp truly covered; nếu không, MongoDB vẫn có thể lấy _id từ index mặc định, nhưng khi compound khác thứ tự, có thể không cover đầy đủ trường hợp sort.

Kiểm tra explain sau khi thêm index:

  • stage: IXSCAN, không có FETCH.
  • indexOnly: true (tuỳ phiên bản, có thể xem qua winningPlan và absence của FETCH).
  • totalDocsExamined gần bằng số trả về.

Nhận xét thiết kế:

  • Thứ tự status (equality hoặc low-cardinality) trước createdAt (sort) để bỏ được SORT stage.
  • Index này phục vụ tốt dashboard theo trạng thái, nhưng có chi phí ghi. Nếu dashboard ít truy cập, cân nhắc thay bằng pipeline và cache.

Bài tập tự làm: Thử bỏ _id khỏi projection và index để xem tác động; đo chênh lệch thời gian.

Bài tập 3: Partial index cho tập con dữ liệu thật sự quan trọng

partial index, selective filter, performance

Bài toán: Trong products, 85% sản phẩm active: true. Đa số truy vấn frontend chỉ hiển thị sản phẩm đang bán. Tạo index toàn bộ trên active sẽ không chọn lọc tốt. Partial index giúp giảm kích thước và tăng hiệu quả.

Truy vấn điển hình:

db.products.find(
  { active: true, category: 'electronics', price: { $gte: 500, $lte: 1500 } }
).sort({ price: 1 }).limit(20).explain('executionStats')

Thiết kế index với partialFilterExpression chỉ index document active: true.

db.products.createIndex(
  { category: 1, price: 1 },
  { name: 'idx_products_active_category_price', partialFilterExpression: { active: true } }
)

Vì filter luôn có active: true, planner có thể chọn index nhỏ gọn chỉ chứa tập con cần thiết. Sắp xếp theo price tăng dần đồng bộ với index.

Lý do không đưa active vào key của index: Trường nhị phân ít phân biệt, thường không giúp nhiều. Hơn nữa, partial đã định nghĩa tập index rồi.

Kiểm chứng:

  • kích thước index: db.products.stats() và db.products.getIndexes() để so sánh size.
  • executionTimeMillis giảm, totalKeysExamined/DocsExamined giảm.

Sai lầm hay gặp:

  • Quên điều kiện active: true trong truy vấn -> planner không dùng được partial index.
  • Lạm dụng partial khiến nhiều truy vấn khác bị fallback qua COLLSCAN. Hãy đảm bảo pattern sử dụng rõ ràng.

Bài tập tự làm: Thử so sánh với sparse index trên active và phân tích vì sao partial linh hoạt hơn (support biểu thức điều kiện phức tạp như price > 0).

Bài tập 4: Multi-key index trên mảng tags — chọn thứ tự đúng và tránh bẫy

multikey index, array fields, tags filter

Bài toán: Mục lọc sản phẩm theo tags và khoảng giá. Document có trường tags là mảng, ví dụ ['sale','wireless','portable'].

Truy vấn điển hình:

db.products.find(
  { tags: { $all: ['wireless','portable'] }, price: { $lte: 300 } }
).sort({ price: 1 }).limit(30).explain('executionStats')

Thiết kế index multi-key:

db.products.createIndex(
  { tags: 1, price: 1 },
  { name: 'idx_products_tags_price' }
)

Giải thích:

  • Khi index một trường mảng, index trở thành multi-key. MongoDB sẽ tạo nhiều entries cho mỗi phần tử của mảng. Với { tags: 1, price: 1 }, planner có thể dùng index cho filter trên tags và range/sort theo price.
  • Tại sao tags trước price? Vì filter equality trên tags giúp thu hẹp trước, sau đó sort theo price khớp với hướng index.

Kiểm tra explain:

  • Nếu thấy dùng index nhưng vẫn có SORT, cân nhắc hướng index price: 1 hoặc -1 phù hợp với sort.
  • totalKeysExamined có thể cao nếu $all với nhiều giá trị hiếm. Cân nhắc chuyển thành $and với điều kiện riêng hoặc precompute trường tagsKey canonical (ví dụ sắp xếp tag) tuỳ mục tiêu.

Góc nâng cao:

  • Nếu bạn muốn sort theo độ phổ biến hay theo thời gian tạo, hãy lập compound index { tags: 1, createdAt: -1 } tương ứng và xem xét hint khi cần.
  • Với tags có độ dài mảng lớn, index phình nhanh. Theo dõi size và đo chi phí ghi trên insert/update.

Cái bẫy phổ biến:

  • Đặt price trước tags sẽ phá vỡ khả năng dùng index cho sort khi filter tags không chọn lọc tốt; planner có thể phải resort để sort.

Bài tập tự làm: So sánh hiệu năng giữa $all và $in kết hợp pipeline $group để lọc đủ 2 tag, và sự khác nhau khi dùng index.

Bài tập 5: Text index cho tìm kiếm sản phẩm — kiểm soát độ liên quan và gợi ý

text search, relevance scoring, product search

Bài toán: Ô tìm kiếm sản phẩm theo name và mô tả. Muốn sắp xếp theo độ liên quan (textScore) và fallback theo bán chạy nếu điểm ngang nhau.

Chuẩn bị: thêm trường description tối giản (nếu chưa có, bạn có thể update ngẫu nhiên). Tạo text index:

db.products.createIndex(
  { name: 'text', description: 'text' },
  { name: 'idx_products_text_name_desc', default_language: 'english' }
)

Truy vấn:

const q = 'wireless portable speaker'
db.products.find(
  { $text: { $search: q } },
  { score: { $meta: 'textScore' }, name: 1, price: 1 }
).sort({ score: { $meta: 'textScore' } }).limit(20).explain('executionStats')

Quan sát:

  • stage: TEXT_MATCH hoặc IXSCAN trên text index, không có SORT rời vì sort theo $meta: textScore.
  • Nếu kết hợp filter khác như active: true, hãy cân nhắc partial index riêng cho route khác, vì text index không thể compound trực tiếp với trường không text trừ một số hạn chế (MongoDB cho phép 1 text index per collection; có thể kết hợp trường khác nhưng cần cân đối). Ví dụ:
db.products.createIndex(
  { active: 1, _fts: 'text', _ftsx: 1 },
  { name: 'idx_products_text_active', weights: { name: 10, description: 2 } }
)

Lưu ý: Cú pháp hệ thống _fts là internal representation; thực tế, khi khai báo, bạn ghi { active: 1, name: 'text', description: 'text' }. Mongo sẽ chuyển thành cấu trúc đặc biệt. Chỉ nên thêm active nếu bạn luôn filter active.

Tối ưu trải nghiệm:

  • Dùng weights để tăng trọng số name so với description.
  • Dùng phrase search với dấu ngoặc kép trong $search để chính xác hơn, ví dụ '"portable speaker"'.
  • Có thể tạo trường nameNgram để hỗ trợ autocomplete (không phải text index thuần), rồi index { nameNgram: 1 } cho prefix search.

Cái bẫy cần tránh:

  • Text index không hỗ trợ sort tuỳ ý ngoài textScore trừ khi kèm điều kiện match rất chọn lọc và sort trên trường nằm trong index khác — thường kém tin cậy. Phương án thực tế: Sau khi lọc theo $text, sắp xếp secondary bằng application logic nếu cần.

Bài tập tự làm: So sánh two-phase query: $text để lấy _id top 200 theo score, sau đó $lookup hoặc fetch bổ sung để sắp xếp thứ cấp theo soldCount.

Bài tập 6: 2dsphere index và truy vấn địa điểm gần nhất kèm bộ lọc

geospatial index, 2dsphere, nearby search

Bài toán: Tìm cửa hàng gần vị trí người dùng trong bán kính 5km, ưu tiên danh mục cụ thể và sắp xếp theo khoảng cách.

Tạo collection stores:

for (let i = 1; i <= 20000; i++) {
  db.stores.insertOne({
    _id: i,
    name: 'Store ' + i,
    location: { type: 'Point', coordinates: [105.8 + Math.random()*0.5, 21.0 + Math.random()*0.5] },
    category: ['pickup','showroom','service'][Math.floor(Math.random()*3)],
    open: Math.random() < 0.9
  })
}

db.stores.createIndex({ location: '2dsphere', category: 1, open: 1 }, { name: 'idx_stores_geo_category_open' })

Truy vấn gần nhất:

const userLng = 105.85
const userLat = 21.02

db.stores.find({
  location: {
    $near: {
      $geometry: { type: 'Point', coordinates: [userLng, userLat] },
      $maxDistance: 5000
    }
  },
  category: 'pickup',
  open: true
}, { name: 1, location: 1 })
.limit(20)
.explain('executionStats')

Giải thích:

  • 2dsphere index hỗ trợ $near để trả về theo thứ tự khoảng cách, vì vậy không cần sort.
  • Thêm category và open trong compound index giúp lọc sớm. Thứ tự đặt location trước là yêu cầu đặc thù: với 2dsphere compound, location thường phải đứng đầu để $near hoạt động hiệu quả; các trường còn lại hỗ trợ post-filter. Từ MongoDB 2.6+, có thể đặt trường phụ trước sau, nhưng $near sẽ chỉ dùng phần địa lý; hãy kiểm tra explain để xác nhận planning.

Mẹo nâng cao:

  • Khi kết hợp $geoWithin với polygon, index tương tự hoạt động tốt.
  • Chuẩn hoá toạ độ và SRID: 2dsphere dùng WGS84, toạ độ [lng, lat].

Sai lầm phổ biến:

  • Nhầm thứ tự lat/lng.
  • Dùng $near nhưng thiếu index 2dsphere tương ứng.

Bài tập tự làm: Thêm filter theo giờ mở cửa (ví dụ openAt/closeAt), cân nhắc design index và cách precompute trường isOpenNow cho truy vấn thực tế.

Bài tập 7: TTL index cho phiên đăng nhập — dọn dẹp tự động, an toàn

ttl index, session cleanup, expiration

Bài toán: Collection sessions lưu token đăng nhập với thời hạn 30 phút không hoạt động (rolling expiration) hoặc hết hạn cứng sau 7 ngày.

Thiết lập dữ liệu:

for (let i = 1; i <= 200000; i++) {
  db.sessions.insertOne({
    _id: i,
    userId: Math.floor(Math.random()*50000)+1,
    token: 't' + i,
    lastSeenAt: new Date(Date.now() - Math.floor(Math.random()*60*60*1000)),
    createdAt: new Date(Date.now() - Math.floor(Math.random()*7*24*60*60*1000))
  })
}

TTL rolling theo lastSeenAt:

db.sessions.createIndex(
  { lastSeenAt: 1 },
  { name: 'idx_sessions_ttl_lastSeen', expireAfterSeconds: 1800 }
)

Điều này đảm bảo document tự động bị xoá sau 30 phút kể từ lastSeenAt. Lưu ý:

  • TTL không đảm bảo xoá đúng thời điểm tức thì; một nền tảng background chạy mỗi 60s hoặc lâu hơn.
  • TTL chỉ hỗ trợ kiểu Date hoặc TTL trên clustered index meta; không hỗ trợ biểu thức phức tạp.

Hết hạn cứng sau 7 ngày: Bạn có thể chạy job định kỳ, hoặc lưu thêm trường expireAt và dùng TTL theo thời điểm tuyệt đối.

db.sessions.updateMany({}, [ { $set: { expireAt: { $add: ['$createdAt', 7*24*3600*1000] } } } ])

db.sessions.createIndex(
  { expireAt: 1 },
  { name: 'idx_sessions_ttl_absolute', expireAfterSeconds: 0 }
)

Thực hành kiểm chứng:

  • Tạo vài document có lastSeenAt cũ hơn 30 phút, chờ background TTL chạy, xác nhận số lượng giảm.
  • Theo dõi log mongod hoặc db.serverStatus().ttl để thấy tiến trình quét TTL.

Sai lầm thường gặp:

  • Gán expireAfterSeconds trên field không phải Date.
  • Kỳ vọng TTL xóa ngay tức thì; hãy thiết kế ứng dụng để coi document quá hạn là không hợp lệ ngay cả khi chưa bị TTL xoá (check ở tầng app).

Bài tập tự làm: Tối ưu sessions bằng cách chỉ index các trường cần thiết khác như userId để tra cứu nhanh, và đo chi phí ghi khi mỗi request cập nhật lastSeenAt.

Cách đọc explain nhanh: khung tham chiếu và checklist đo lường

explain plan, execution stats, performance metrics

Để mỗi bài tập thực sự đọng lại, hãy quen thuộc với explain. Một checklist ngắn:

  • QueryPlanner.winningPlan: Kiểu scan nào? IXSCAN hay COLLSCAN? Có SORT không?
  • executionStats.totalDocsExamined vs nReturned: Tỷ lệ càng gần 1 càng tốt. Nếu lớn, index chưa chọn lọc hoặc order index chưa khớp sort.
  • executionStats.totalKeysExamined: Với compound index tốt, keys examine thường thấp và gần với docs examine.
  • executionTimeMillis: Chỉ số tổng quan, nhưng dễ nhiễu do cache và tài nguyên máy. Hãy chạy vài lần và lấy median.
  • rejectedPlans: Các kế hoạch bị loại. Nếu có plan khác sát sao, planner có thể flip flop khi dữ liệu thay đổi. Hint khi cần ổn định.

Ví dụ mẫu tóm lược explain tối ưu:

winningPlan: {
  stage: FETCH,
  inputStage: {
    stage: IXSCAN,
    indexName: 'idx_orders_customer_created_desc',
    direction: 'backward',
    indexBounds: { customerId: [1234,1234], createdAt: [MinKey,MaxKey] }
  }
}
executionStats: {
  nReturned: 30,
  totalDocsExamined: 30,
  totalKeysExamined: 30,
  executionTimeMillis: 3
}

Trường hợp xấu cần sửa:

  • Có stage SORT lớn và nYields cao.
  • totalDocsExamined lên hàng trăm nghìn cho kết quả vài chục.

Mẹo tối ưu nâng cao và kiểm lỗi trong thực chiến

performance tuning, indexing tips, production checklist

Đây là phần tổng hợp những kinh nghiệm nhỏ nhưng rất đáng giá khi triển khai ở môi trường thật:

  • Đặt thứ tự key đúng: Equality trước, rồi range, rồi sort. Nếu có nhiều trường equality, hãy ưu tiên trường có tính chọn lọc cao hơn đặt trước để giảm keys examined.
  • Dùng hint có kiểm soát: Khi bạn biết index nào tốt nhất cho một route cụ thể và planner đôi khi chọn sai do thống kê, hint để ổn định. Nhưng đừng lạm dụng; thống kê thay đổi, hãy giám sát.
  • Tái sử dụng index: Mỗi index thêm là mỗi chi phí ghi tăng. Tìm cách thiết kế compound index có thể phục vụ nhiều truy vấn nhờ quy tắc left-prefix.
  • Đo viết (write amplification): Với bảng có tần suất cập nhật cao, hạn chế số index. Ví dụ orders cập nhật status nhiều lần; index trên status chỉ nên tồn tại nếu dashboard thật sự cần.
  • Collation và case-insensitive: Nếu cần tìm kiếm không phân biệt hoa thường, tạo index kèm collation phù hợp. Đảm bảo truy vấn dùng cùng collation.
  • Covered query đúng nghĩa: Projection chỉ các trường nằm trong index. Đừng vô tình thêm một trường thừa, kẻo khiến planner phải FETCH mất lợi thế.
  • Index intersection vs compound: Intersection hữu dụng khi bạn không có compound phù hợp, nhưng thường chậm hơn compound được thiết kế đúng cho sort. Ưu tiên compound cho truy vấn nóng.
  • Theo dõi hệ số cache: Nếu working set (document + index thường dùng) nằm gọn trong RAM, hiệu năng tăng vọt. Sử dụng db.serverStatus() để xem metrics cache (WiredTiger cache dirty, used, eviction).
  • Phân tích log slow query: Bật profiler ở mức phù hợp hoặc kiểm tra system.log để tìm route chậm, từ đó ưu tiên thiết kế index.
  • Quản lý tiến hoá schema: Khi bổ sung tính năng mới, đừng vội tạo index ngay. Hãy đo lượng truy vấn thực tế sau khi phát hành và chỉ tạo index nếu pattern ổn định.

Checklist triển khai index an toàn:

  • Tạo index ở môi trường staging với dữ liệu gần thật, so sánh explain trước/after.
  • Dùng background build (mặc định true ở phiên bản mới) để không chặn ghi.
  • Đặt tên index có ý nghĩa: idx_collection_fields_order_usecase.
  • Gỡ index không dùng: db.collection.getIndexes() và db.collection.dropIndex('name') sau khi xác nhận không còn query nào cần.

Lộ trình luyện tập: cách bạn có thể tự nâng cấp bài tập

learning roadmap, practice tasks, self study

Để vượt qua mức cơ bản và làm chủ index trong hệ thống của bạn, hãy mở rộng 7 bài tập theo hướng sau:

  • Tạo workload sinh dữ liệu và truy vấn theo tỷ lệ gần với thực tế (kịch bản JMeter, k6, hoặc script Node.js), đo P95/P99 thay vì chỉ mean.
  • Thử nhiều biến thể compound: Hoán đổi thứ tự key trong index, đo lại, ghi chép lý do tại sao có chênh lệch. Lặp lại đến khi bạn có trực giác tốt.
  • Tái tạo incident: Cố tình xoá một index quan trọng ở staging và xem chuyện gì xảy ra trong profiler. Học cách phát hiện nhanh.
  • Chuẩn hoá naming và policy index: Yêu cầu mỗi index đi kèm ticket, mô tả truy vấn sử dụng, dashboard giám sát hiệu quả. Tháng sau review lại và dọn dẹp.
  • Đối chiếu với thiết kế shard: Nếu dự định sharding, hãy để mắt tới hashed index hoặc cách đặt shard key sao cho truy vấn dùng được cả index lẫn phân phối dữ liệu hợp lý.

Khi bạn hoàn tất những mở rộng này, bạn sẽ không chỉ biết tạo index, mà còn biết quản trị vòng đời index như một phần của vận hành hệ thống.


Hãy quay lại nhìn 7 bài tập: bạn đã học cách giải quyết các truy vấn phổ biến nhất trong ứng dụng — từ danh sách đơn hàng theo thời gian, màn hình quản trị cần covered query, lọc sản phẩm theo tags và khoảng giá, tìm kiếm toàn văn với độ liên quan hợp lý, bản đồ địa điểm gần bạn, cho tới dọn dẹp phiên đăng nhập bằng TTL. Mỗi bài đặt bạn trước một quyết định thiết kế index khác nhau, và chính explain là người bạn đồng hành trung thực nhất.

Từ đây, điều quan trọng nhất là thói quen: bất kỳ khi nào bạn viết một truy vấn mới, hãy nghĩ về index; bất kỳ khi nào bạn thấy CPU hoặc I/O tăng đột ngột, hãy mở explain và profiler. Sự khác biệt giữa một hệ thống ì ạch và một hệ thống mượt mà đôi khi chỉ là một compound index đúng chỗ, đúng thứ tự. Chúc bạn luyện tập vui vẻ và tối ưu được những phần trăm hiệu năng xứng đáng!

Đánh giá bài viết

Thêm bình luận & đánh giá

Đánh giá của người dùng

Dựa trên 0 đánh giá
5 Star
0
4 Star
0
3 Star
0
2 Star
0
1 Star
0
Thêm bình luận & đánh giá
Chúng tôi sẽ không bao giờ chia sẻ email của bạn với bất kỳ ai khác.