Tối ưu hóa truy vấn truy xuất dữ liệu lớn với MongoDB

Tối ưu hóa truy vấn truy xuất dữ liệu lớn với MongoDB

38 phút đọc Khám phá kỹ thuật tối ưu truy vấn MongoDB cho dữ liệu lớn: indexing, sharding, aggregation, thiết kế schema và best practices tăng tốc, tiết kiệm tài nguyên.
(0 Đánh giá)
Hướng dẫn thực chiến tối ưu truy vấn trên cụm MongoDB ở quy mô lớn: chọn chỉ mục đúng, viết truy vấn sargable, tận dụng aggregation pipeline, phân mảnh dữ liệu hợp lý, dùng explain, giám sát bằng PMM/Atlas, tinh chỉnh connection pool, write concern và cache.
Tối ưu hóa truy vấn truy xuất dữ liệu lớn với MongoDB

Tối ưu hóa truy vấn truy xuất dữ liệu lớn với MongoDB

Khi dữ liệu bùng nổ theo cấp số nhân, câu hỏi không còn là làm sao để lưu, mà là làm sao để lấy về nhanh, đúng, và ổn định. MongoDB, với mô hình tài liệu linh hoạt và hạ tầng phân tán mạnh mẽ, cung cấp nhiều đường đi nước bước cho truy vấn hiệu năng cao. Nhưng chỉ khi hiểu sâu cơ chế thực thi, thiết kế chỉ mục có chủ đích, và sắp xếp pipeline thông minh, bạn mới thực sự khai mở được tốc độ. Bài viết này đưa bạn qua một hành trình từ nền tảng tới kỹ thuật nâng cao, với các ví dụ, mẹo thực thi, và các bẫy hiệu năng cần tránh, nhằm giúp hệ thống của bạn phục vụ truy vấn dữ liệu lớn một cách nhất quán dưới tải cao.

Hiểu cách MongoDB thực thi truy vấn

query planner, execution, index scan, mongodb

Trước khi thêm chỉ mục hay sửa câu lệnh, bạn cần hiểu MongoDB làm gì khi nhận một truy vấn. Ở cấp độ lõi, máy lập kế hoạch truy vấn sẽ tạo ra các kế hoạch khả dĩ, chấm điểm, chọn winning plan và thực thi theo pipeline các stage như COLLSCAN, IXSCAN, FETCH, SORT, LIMIT, SHARDING_FILTER, v.v.

Những điểm mấu chốt:

  • Query planner đánh giá chi phí dựa trên selectivity (tính chọn lọc) của bộ lọc, độ phù hợp chỉ mục, và cần sắp xếp hay không. Kế hoạch tốt nhất là kế hoạch giảm tối đa số tài liệu quét và tránh SORT trên bộ nhớ.
  • IXSCAN là mục tiêu. Khi thấy COLLSCAN trên production dữ liệu lớn, hãy coi đó là mùi cần xử lý ngay.
  • Khi cần sắp xếp, nếu không có chỉ mục hỗ trợ thứ tự, MongoDB sẽ phải SORT trong bộ nhớ (bị giới hạn), hoặc đẩy xuống đĩa khi aggregation cho phép dùng allowDiskUse. Điều này tăng độ trễ và dùng I/O.
  • Trên cụm sharded, truy vấn có thể bị scatter-gather nếu thiếu bộ lọc shard key. Mục tiêu là targeted query: định tuyến đến một hoặc vài shard thay vì tất cả.

Ví dụ một truy vấn khả dĩ kém hiệu năng:

// Truy vấn log theo khoảng thời gian và sắp xếp mới nhất
// Xấu khi thiếu chỉ mục phù hợp
const cursor = db.logs.find({
  ts: { $gte: ISODate('2024-01-01'), $lt: ISODate('2024-02-01') },
  level: 'error'
}).sort({ ts: -1 }).limit(200);

Nếu chỉ có index { level: 1 }, MongoDB có thể phải SORT nhiều hoặc thậm chí là COLLSCAN. Thiết kế chỉ mục đúng sẽ thay đổi hoàn toàn cục diện. Xem phần chỉ mục bên dưới.

Đo lường: explain, profiler và các con số biết nói

dashboard, metrics, profiler, explain

Không đo lường, không tối ưu. Hai vũ khí cơ bản:

  • explain với chế độ executionStats hoặc allPlansExecution
  • Database Profiler và slow query logs

Mẹo thực tế với explain:

  • Tập trung vào các chỉ số: nReturned, totalDocsExamined, totalKeysExamined, executionTimeMillis.
  • Mục tiêu: totalDocsExamined và totalKeysExamined xấp xỉ nReturned. Nếu hai con số này vượt xa nReturned, bạn đang quét thừa.
  • Kiểm tra stage tree: tìm IXSCAN thay vì COLLSCAN; nhìn vào input stages của SORT để xem có dùng index sort hay không.

Ví dụ sử dụng explain để bắt bệnh nhanh:

// Quan tâm executionStats để thấy số lượng quét và thời gian
const plan = db.logs.find({
  level: 'error',
  ts: { $gte: start, $lt: end }
}).sort({ ts: -1 }).limit(200).explain('executionStats');
printjson(plan.executionStats.totalDocsExamined);
printjson(plan.executionStats.totalKeysExamined);
printjson(plan.queryPlanner.winningPlan);

Profiler và slow query logs:

  • Bật profiler ở mức cần thiết trong một khoảng thời gian ngắn để thu thập truy vấn chậm, tránh overhead kéo dài.
  • Trên Atlas, sử dụng Performance Advisor: đề xuất chỉ mục dựa trên workload thực tế, cực hữu ích cho điểm bắt đầu.
  • Thiết lập ngưỡng slowms hợp lý. Với workload throughput cao, 100ms có thể đã là chậm.

Checklist đánh giá nhanh:

  • totalDocsExamined lớn hơn 1 triệu cho mỗi truy vấn: báo động
  • ratio totalDocsExamined / nReturned > 100: phản ánh chỉ mục chưa tốt
  • SORT stage xuất hiện thường xuyên: cân nhắc bổ sung chỉ mục phù hợp thứ tự

Thiết kế chỉ mục chiến lược: từ nguyên tắc đến thực chiến

index design, btree, compound index, performance

B-tree indexes là trái tim của truy vấn nhanh. Nhưng chỉ mục chỉ hiệu quả khi nó khớp với mẫu truy vấn thực tế. Ba quy tắc vàng:

  • Equality — Sort — Range: trong index compound, đặt các trường equality trước, tiếp đến là các trường phục vụ sort, cuối cùng là range.
  • Prefix rule: truy vấn chỉ dùng được prefix liên tục từ đầu compound index.
  • High selectivity trước: trường nào lọc hẹp dữ liệu nên đứng sớm để giảm quét.

Ví dụ bài bản:

Mẫu truy vấn phổ biến cho logs:

  • Lọc theo appId (equality)
  • Khoảng thời gian ts (range)
  • Sắp xếp ts giảm dần
  • Thêm bộ lọc level hoặc category

Chỉ mục đề xuất:

// Equality trước, rồi đến sort và range
// 1. Nếu luôn lọc appId và ts, và sort theo ts desc
db.logs.createIndex({ appId: 1, ts: -1, level: 1 });

// 2. Nếu level không ổn định, có thể để sau
// Vì sort theo ts cần nằm trước range ts? Quan trọng: chỉ có 1 ts.
// Trường hợp này, do ts vừa sort vừa range: index { appId: 1, ts: -1, level: 1 } hỗ trợ tốt.

Lưu ý:

  • Không có chuyện đặt ts vừa sort vừa range ở hai vị trí khác nhau; bản chất là một trường. Trật tự -1 hay 1 phải phù hợp với sort.
  • Với sắp xếp theo nhiều khóa, phải bảo đảm index order đúng hướng; nếu không, MongoDB phải re-sort.
  • Đừng lạm dụng index. Mỗi index thêm chi phí ghi và dung lượng. Chỉ nên tạo index cho truy vấn nóng.

Partial và sparse index:

  • partialFilterExpression cho phép tạo index trên một phần dữ liệu có điều kiện, tăng selectivity và giảm kích cỡ.
  • sparse index bỏ qua tài liệu thiếu trường. Tiện cho dữ liệu không đồng nhất, nhưng cẩn trọng với semantics khi truy vấn giá trị null.

Ví dụ partial index cho lỗi nghiêm trọng:

db.logs.createIndex(
  { appId: 1, ts: -1 },
  { partialFilterExpression: { level: { $in: ['error', 'fatal'] } } }
);

Wildcard index cho tài liệu linh hoạt:

// Khi trường động và cần tìm kiếm nhiều khóa lồng nhau
// Chỉ dùng khi có lý do rõ ràng vì wildcard index có thể lớn
db.events.createIndex({ 'attributes.$**': 1 });

Hidden index để thử nghiệm an toàn:

// Tạo index ở chế độ ẩn, đánh giá explain với hint trước khi public
db.logs.createIndex({ appId: 1, ts: -1 }, { hidden: true });
// Dùng hint để so sánh kế hoạch
const p = db.logs.find({ appId: 'a1' }).hint({ appId: 1, ts: -1 }).explain('executionStats');

Truy vấn trên mảng: multikey và cạm bẫy ít ai nói

array indexing, multikey, elemMatch, performance

Khi index trên trường mảng, index trở thành multikey. Điều này cho phép truy vấn nhanh trên từng phần tử mảng, nhưng kéo theo chi phí:

  • Key explosion: mảng dài tạo ra rất nhiều index entries, làm phình index và giảm hiệu năng write.
  • Kết hợp nhiều trường mảng trong cùng một compound index thường bị hạn chế (trước 6.x, không thể có nhiều trường multikey trong một compound trừ một số điều kiện). Hãy kiểm tra version và docs cụ thể.

Sử dụng $elemMatch đúng cách:

// Dữ liệu: tags là mảng, metrics là mảng object
// Tìm tài liệu có phần tử metrics thỏa đồng thời cả hai điều kiện
// Nếu không dùng $elemMatch, các điều kiện có thể khớp khác phần tử nhau
const cursor = db.events.find({
  metrics: { $elemMatch: { name: 'latency', value: { $gt: 200 } } }
});

// Index phù hợp
db.events.createIndex({ 'metrics.name': 1, 'metrics.value': 1 });

Mẹo hạn chế key explosion:

  • Cắt mảng dài bằng bucketing hoặc chỉ lưu top-K phần tử cần truy vấn.
  • Tạo chỉ mục trên trường mảng có tính chọn lọc cao, tránh index trên các mảng include nhiều giá trị ít chọn lọc như tags phổ biến.
  • Khi lọc tags, cân nhắc normalizing thành set tần suất thấp và dùng partial index.

Tránh full scan: bộ lọc gọn và projection thông minh

covered query, projection, keysOnly

Projection không chỉ giảm payload mạng, mà còn mở đường cho covered query: MongoDB chỉ cần đọc index mà không cần truy cập tài liệu.

Nguyên tắc:

  • Truy vấn + sort + projection chỉ chứa các trường có mặt trong index để thành covered query.
  • Sử dụng projection loại bỏ trường lớn như description, blob, comments khi không cần.

Ví dụ:

// Index phù hợp
db.users.createIndex({ email: 1, status: 1 });

// Truy vấn covered: chỉ đọc từ index, không FETCH tài liệu
const u = db.users.find(
  { email: 'a@x.com', status: 'active' },
  { _id: 0, email: 1, status: 1 }
);

Áp dụng projection có chủ đích ở aggregation:

// Đặt $project sớm để bỏ trường nặng trước khi $group
const pipeline = [
  { $match: { appId: 'a1', ts: { $gte: start, $lt: end } } },
  { $project: { appId: 1, ts: 1, level: 1 } },
  { $group: { _id: '$level', cnt: { $sum: 1 } } },
  { $sort: { cnt: -1 } }
];

Tối ưu hoá sắp xếp và phân trang: đừng dùng skip bừa bãi

pagination, sorting, seek method, cursor

skip trở nên đắt đỏ khi offset lớn vì hệ thống vẫn phải đi qua n bản ghi để bỏ qua. Dưới dữ liệu lớn, đây là công thức rắc rối.

Các mẫu phân trang tốt:

  • Seek method dựa trên giá trị khóa có thứ tự (ví dụ ts hoặc _id). Thay vì skip, dùng điều kiện ts < lastTs của trang trước.
  • Sử dụng index phục vụ sort để loại bỏ SORT stage.
  • Tránh sort không ổn định. Với giá trị trùng, thêm tiebreaker như _id để sort ổn định.

Ví dụ seek pagination:

// Index
db.logs.createIndex({ appId: 1, ts: -1, _id: -1 });

// Trang đầu
const page1 = db.logs.find({ appId: 'a1' })
  .sort({ ts: -1, _id: -1 })
  .limit(200)
  .toArray();

// Trang tiếp theo
const last = page1[page1.length - 1];
const page2 = db.logs.find({
  appId: 'a1',
  $or: [
    { ts: { $lt: last.ts } },
    { ts: last.ts, _id: { $lt: last._id } }
  ]
}).sort({ ts: -1, _id: -1 }).limit(200);

Nếu cần phân trang theo nhiều chiều, cân nhắc khóa tổng hợp ổn định và luôn có chỉ mục phù hợp hướng sort.

Tối ưu Aggregation Pipeline: đặt $match đúng chỗ và giữ pipeline gọn nhẹ

aggregation pipeline, match early, project, group

Pipeline mạnh mẽ nhưng cũng dễ trở nên nặng nề. Lời khuyên:

  • $match càng sớm càng tốt, đặc biệt là các điều kiện sử dụng index.
  • $project sớm để loại bỏ trường không cần, giảm lưu lượng qua các stage sau.
  • $group sau khi đã lọc và tối giản trường, tránh giữ trạng thái lớn trong bộ nhớ.
  • allowDiskUse chỉ là cứu cánh tạm thời, không thay thế cho thiết kế tối ưu.

Ví dụ pipeline chuẩn mực cho analytics sự kiện:

// Mục tiêu: đếm số sự kiện lỗi theo giờ trong một ngày
// Index đề xuất: { appId: 1, ts: 1, level: 1 }
const pipeline = [
  { $match: {
      appId: 'a1',
      ts: { $gte: ISODate('2024-05-01'), $lt: ISODate('2024-05-02') },
      level: 'error'
  }},
  { $project: {
      hour: { $dateTrunc: { date: '$ts', unit: 'hour' } }
  }},
  { $group: { _id: '$hour', cnt: { $sum: 1 } } },
  { $sort: { _id: 1 } }
];

Tối ưu $lookup:

  • Đảm bảo cả localField và foreignField đều có index phù hợp. Với pipeline $lookup nâng cao, các bộ lọc trong pipeline phía foreign cũng phải được index.
  • Hạn chế fan-out lớn. Cân nhắc pre-join hoặc denormalize khi truy vấn thường xuyên.

Ví dụ $lookup có index:

// orders: { userId, ts, total }
// users: { _id, email, segment }
// Index: orders { userId: 1, ts: -1 }, users { segment: 1 }
const pipeline = [
  { $match: { ts: { $gte: start, $lt: end } } },
  { $lookup: {
      from: 'users',
      localField: 'userId',
      foreignField: '_id',
      as: 'u'
  }},
  { $unwind: '$u' },
  { $match: { 'u.segment': 'vip' } },
  { $project: { ts: 1, total: 1, 'u.email': 1 } },
  { $sort: { ts: -1 } }
];

Tối ưu thêm:

  • Đưa $match trên trường u.segment vào trong một $lookup dạng pipeline để đẩy điều kiện xuống sớm:
{ $lookup: {
    from: 'users',
    let: { uid: '$userId' },
    pipeline: [
      { $match: { $expr: { $eq: ['$_id', '$$uid'] }, segment: 'vip' } },
      { $project: { email: 1, segment: 1 } }
    ],
    as: 'u'
}}

Sharding cho dữ liệu lớn: chọn shard key để đọc nhanh, không chỉ ghi nhanh

sharding, cluster, shard key, distribution

Sharding mở rộng quy mô, nhưng shard key là quyết định một lần cho rất lâu. Bạn cần một shard key:

  • Cardinality cao và phân phối đều để tránh hotspot.
  • Bao phủ các mẫu truy vấn chính, để truy vấn có thể targeted đến một shard. Nếu truy vấn không chứa shard key, mongos phải gửi đến tất cả shard, gây scatter-gather.

So sánh nhanh:

  • Hashed shard key: phân phối tốt cho ghi đều, nhưng đọc theo range không hiệu quả (range timestamp sẽ scatter). Phù hợp cho workload đọc theo _id cụ thể.
  • Ranged shard key: tốt cho truy vấn theo khoảng (ví dụ ts), nhưng cẩn thận hotspot theo thời gian hiện tại. Khắc phục bằng cách prefix với trường có độ phân tán hoặc dùng zone sharding.

Mẫu phổ biến cho logs đa tenant:

  • shard key compound: { appId: 1, ts: 1 }
  • Truy vấn theo appId và ts range sẽ targeted đến một số ít shard.
  • Dùng zone sharding để gán appId lớn vào nhóm shard riêng.

Ví dụ cấu hình sharded collection:

sh.enableSharding('analytics');
sh.shardCollection('analytics.logs', { appId: 1, ts: 1 });
// Định nghĩa tags và zone nếu cần
sh.addShardTag('shard0000', 'zoneA');
sh.addShardTag('shard0001', 'zoneB');
sh.updateZoneKeyRange('analytics.logs', { appId: 'a1', ts: MinKey }, { appId: 'a1', ts: MaxKey }, 'zoneA');

Tránh scatter-gather:

  • Luôn cố gắng đưa shard key (hoặc prefix của nó) vào $match sớm nhất.
  • Trên pipeline aggregation, dùng $match sớm để mongos route đến shard mục tiêu.

Theo dõi balancing:

  • Chunk migrations trong giờ cao điểm có thể ảnh hưởng latency. Lên lịch balancing ngoài giờ hoặc giảm tần suất.

Đọc tại quy mô: read preference, read concern và độ trễ

consistency, replicas, hedged reads, latency

Replica set cho phép mở rộng đọc và cân bằng giữa độ trễ và nhất quán.

Read preference:

  • primary: nhất quán mạnh nhất (theo write concern đã dùng), độ trễ cao hơn nếu primary bận.
  • secondary hoặc nearest: giảm tải primary, có thể stale một chút. Trên Atlas có hedged reads để cắt đuôi tail latency bằng cách gửi song song đến nhiều node gần.
  • Khi đọc analytic, cân nhắc secondaryPreferred + tag sets để điều hướng đến node chuyên đọc.

Read concern:

  • local: nhanh nhất, có thể đọc dữ liệu chưa replicate đủ.
  • majority: đảm bảo dữ liệu đã được đa số node ghi nhận.
  • snapshot: nhất quán theo thời điểm, cần transaction (MongoDB 4.0+), hữu ích cho báo cáo nhất quán.

Kết hợp thực tiễn:

  • Truy vấn dịch vụ người dùng: primary + majority cho đường giao dịch quan trọng.
  • Báo cáo và dashboard: secondaryPreferred + majority hoặc local tùy mức chấp nhận trễ; đặt maxStalenessSeconds hợp lý.

Quản trị tài nguyên: working set, cache và I/O

memory cache, wiredtiger, io, performance

Hiệu năng đọc phụ thuộc lớn vào khả năng giữ working set trong cache.

WiredTiger cache size:

  • Mặc định ~50% RAM. Điều chỉnh dựa trên bộ nhớ ứng dụng và hệ điều hành.
  • Nếu working set lớn hơn cache đáng kể, page faults sẽ tăng, latency đuôi dài.

Nén và block storage:

  • Snappy hoặc zstd giảm I/O, tăng CPU. Trên workload CPU rỗi, zstd thường có lợi.
  • Bật compression cho cả collection và index (tùy phiên bản và cấu hình) để giảm footprint.

Quan sát chỉ số:

  • Page faults, cache eviction, iowait. Trên Atlas có biểu đồ WiredTiger Cache, opcounters, và read latency percentiles.

Thực tiễn:

  • Tránh SELECT * kiểu tài liệu: loại bỏ trường nặng để giảm cache churn.
  • Preload dữ liệu nóng bằng chạy warm-up queries sau deploy.
  • Cân nhắc thêm read replicas dành riêng cho analytics, giảm cạnh tranh cache với đường giao dịch.

Mẫu thiết kế schema tối ưu cho truy xuất

schema design, document model, denormalization, timeseries

Schema document không có chuẩn duy nhất, mà là thỏa hiệp giữa truy vấn, cập nhật và kích cỡ.

Embed vs reference:

  • Embed khi dữ liệu đi kèm được truy xuất cùng nhau thường xuyên và có kích cỡ giới hạn. Lợi ích: tránh $lookup, giảm round-trip.
  • Reference khi quan hệ nhiều-nhiều, dữ liệu lớn hoặc được dùng độc lập. Cân nhắc pre-join một phần cho đường đọc nóng.

Bucketing theo thời gian:

  • Thay vì lưu mỗi event thành một document riêng lẻ, có thể gom theo bucket thời gian (ví dụ mỗi 5 phút) nhưng cần cân đối kích thước document (16MB limit) và mẫu truy vấn. Thích hợp cho counts, histograms.
  • Sử dụng time series collections (MongoDB 5.0+) cho dữ liệu đo đạc theo thời gian. Engine tối ưu lưu trữ và chỉ mục cho time series giúp truy vấn theo thời gian nhanh hơn và tiết kiệm dung lượng.

Tiền tổng hợp (pre-aggregation):

  • Tạo bảng số liệu đã tổng hợp theo chiều phổ biến (theo giờ, ngày, appId). Đọc nhanh hơn hàng bậc độ lớn.
  • Dùng $merge để cập nhật incrementally:
// Pipeline tính tổng số log lỗi theo giờ và ghi vào collection aggregates
const pipeline = [
  { $match: { ts: { $gte: start, $lt: end }, level: 'error' } },
  { $project: { appId: 1, hour: { $dateTrunc: { date: '$ts', unit: 'hour' } } } },
  { $group: { _id: { appId: '$appId', hour: '$hour' }, cnt: { $sum: 1 } } },
  { $merge: {
      into: 'error_hourly',
      on: ['_id.appId', '_id.hour'],
      whenMatched: 'replace',
      whenNotMatched: 'insert'
  } }
];

Kỹ thuật flatten dữ liệu phức tạp:

  • Tạo trường denormalized như keyword_normalized hoặc email_lower để tăng khả năng dùng index với collation đơn giản.
  • Tránh dùng collation case-insensitive tùy biến trên truy vấn nóng nếu chưa có index tương ứng; thay vào đó tạo trường chuẩn hóa, index trực tiếp.

Chỉ số và bộ lọc nâng cao: partial, sparse, hint và index intersection

partial index, sparse, hint, intersection

Partial index đã đề cập giúp tập trung vào phần dữ liệu cần thiết. Bổ sung:

  • sparse index phù hợp khi đa số tài liệu thiếu trường. Tránh bẫy null: tìm null khác với thiếu trường.

Index intersection:

  • MongoDB có thể kết hợp nhiều index để phục vụ truy vấn (index intersection). Nhưng không phải lúc nào cũng nhanh hơn một compound index tốt; hãy đo lường. Trong nhiều trường hợp, tạo compound index đúng vẫn tối ưu hơn.

Hint đúng lúc:

  • Dùng hint khi planner chọn sai trong trường hợp đặc thù. Nhưng hint không nên là nạng vĩnh viễn; nó che khuất vấn đề thiết kế index.

Ví dụ intersection vs compound:

// Giả sử có index { appId: 1 } và { ts: -1 }
// Truy vấn { appId: 'a1', ts: { $gte: start } } có thể dùng intersection
// Nhưng index compound { appId: 1, ts: -1 } thường nhanh và ổn định hơn

Những mùi truy vấn và cách khử

anti-patterns, debugging, performance smell

Nhận diện mùi giúp bạn tiết kiệm hàng giờ debug.

Danh sách mùi phổ biến:

  • Regex không anchor đầu chuỗi, ví dụ { name: { $regex: 'abc' } } khiến index ít hữu ích. Khử: dùng ^abc hoặc tạo trường n-gram/normalized.
  • $ne, $nin trên dữ liệu lớn: khó dùng index hiệu quả, thường dẫn đến quét nhiều. Khử: diễn đạt lại điều kiện hoặc chuyển logic sang whitelist.
  • $or với nhiều mệnh đề không cùng một index prefix dẫn đến nhiều kế hoạch và hợp nhất. Khử: cấu trúc lại index hoặc tách truy vấn, hoặc dùng $in trên trường phù hợp index.
  • So sánh kiểu dữ liệu không nhất quán (string vs number) khiến index bỏ lỡ. Chuẩn hóa kiểu trường.
  • Truy vấn sort khác hướng index hoặc sort trên trường không có trong prefix dẫn đến SORT tốn kém.
  • Lạm dụng $lookup cho đường đọc nóng tốc độ cao. Khử: denormalize hoặc pre-join.
  • Dùng skip lớn để phân trang sâu. Khử: seek method.
  • Không có bộ lọc shard key trên sharded cluster, gây scatter-gather.
  • Truy vấn với $exists, $type câu rộng trên collection lớn: cần thận trọng, cân nhắc partial index hoặc job nền để gắn cờ.

Ví dụ rewrite regex:

// Thay vì
{ name: { $regex: 'smith', $options: 'i' } }
// Hãy chuẩn hóa
{ name_normalized: { $regex: '^smith' } }
// Và có index { name_normalized: 1 }

Công cụ giám sát và quy trình tối ưu hoá bền vững

monitoring tools, observability, atlas metrics, profiling

Tối ưu không phải dự án một lần, mà là quy trình lặp lại dựa trên quan sát.

Công cụ nên có:

  • mongostat, mongotop: nhịp đập hệ thống theo giây.
  • Database Profiler: bắt truy vấn chậm, xem chi tiết filter, sort, index used.
  • Slow query logs: theo dõi theo thời gian, kết hợp với centralized logging.
  • Atlas Performance Advisor và Query Profiler: đề xuất index và hiển thị plan đồ họa.
  • APM ở ứng dụng: đo p50, p95, p99 latency cho endpoint gắn với truy vấn MongoDB.

Quy trình khuyến nghị:

  • Đặt SLO cho truy vấn chính (ví dụ p95 < 120ms ở mức QPS x). Dùng SLO để ưu tiên công việc.
  • Lấy mẫu truy vấn chậm theo giờ, gom nhóm theo mẫu (fingerprint) để tránh tối ưu lặt vặt.
  • Triển khai index mới dưới dạng hidden, so sánh explain, sau đó unhide.
  • Kiểm tra tác động write amplification khi thêm index.
  • Tự động kiểm tra hồi quy hiệu năng sau khi thay đổi schema hoặc driver.

Tình huống thực chiến: từ 2 giây xuống 80ms

case study, optimization, before after

Bối cảnh: collection logs 1,2 tỉ tài liệu, tăng 15 triệu mỗi ngày. Truy vấn chính: hiển thị bảng sự cố cho một app trong 1 ngày, kèm sắp xếp mới nhất.

Ban đầu:

  • Index: { level: 1 }, { ts: -1 }
  • Truy vấn: match appId, level in error,fatal; ts range; sort ts -1; limit 100
  • explain: totalDocsExamined 12 triệu, totalKeysExamined 14 triệu, executionTimeMillis ~ 2000

Điều chỉnh:

  • Tạo partial index: { appId: 1, ts: -1, _id: -1 } với partialFilterExpression level in error,fatal
  • Sửa truy vấn sang seek pagination cho trang tiếp theo
  • Dùng projection loại bỏ payload message lớn
  • Đặt $match theo appId và ts sớm, rồi $project, sau đó mới $sort nếu dùng aggregation

Kết quả:

  • totalDocsExamined ~ 1200, totalKeysExamined ~ 1200
  • executionTimeMillis ~ 80 ở p95
  • Tải CPU giảm, cache hit tăng, băng thông mạng giảm 60%

Checklist hành động nhanh

checklist, action plan, quick wins
  • Dùng explain với executionStats cho truy vấn nóng, đặt mục tiêu docsExamined gần nReturned.
  • Tạo compound index theo quy tắc Equality — Sort — Range; đảm bảo hướng sort khớp index.
  • Thay skip bằng seek pagination dựa trên khóa có thứ tự.
  • Đặt $match và $project sớm trong pipeline; index hóa các điều kiện trong $lookup.
  • Dùng partial index cho subset dữ liệu truy vấn thường xuyên.
  • Kiểm tra mùi regex không anchor, $ne, $nin, $or phân tán.
  • Trên sharded cluster, bảo đảm truy vấn chứa shard key prefix để targeted.
  • Tối ưu read preference cho workload analytics sang secondary và dùng hedged reads khi phù hợp.
  • Giảm payload bằng projection; nhắm tới covered query nếu có thể.
  • Theo dõi profiler, slow query, và p95/p99 latency; thiết lập SLO rõ ràng.

Mẹo tinh chỉnh nâng cao ít người để ý

pro tips, advanced, tuning
  • Cân nhắc batchSize hợp lý cho cursor lớn để cân bằng độ trễ và băng thông; quá nhỏ gây nhiều round-trip, quá lớn tăng memory.
  • Đặt maxTimeMS cho truy vấn phân tích nặng để bảo vệ hệ thống khỏi runaway queries.
  • Với aggregation nặng cần disk spill, cân nhắc phân mảnh logic theo appId và thời gian rồi hợp nhất kết quả ở tầng ứng dụng.
  • Tận dụng hidden index để A/B test kế hoạch trước khi chính thức.
  • Theo dõi sự khác biệt collation giữa index và truy vấn; mismatch khiến index bị bỏ qua.
  • Khi buộc phải sắp xếp theo trường không index, cân nhắc precompute trường sortKey được index hóa.
  • Sử dụng $setWindowFields cho rolling metrics nhưng đảm bảo có index hỗ trợ partition và sort key.
  • Với mảng lớn, cân nhắc lưu summary fields (count, min, max) giúp loại nhanh tài liệu không phù hợp trước khi đào sâu.

Một hệ thống truy vấn nhanh không đến từ một mẹo duy nhất, mà là tổng hòa của thiết kế dữ liệu, chỉ mục đúng nghĩa, pipeline gọn nhẹ và giám sát liên tục. Khi bạn nhìn một explain và gần như đoán được các con số trước khi chạy, đó là lúc bạn đã điều khiển được MongoDB thay vì bị nó điều khiển.

Bạn không cần biến mọi truy vấn thành đường đua công thức 1. Hãy tối ưu những con đường bận rộn nhất trước: xác định truy vấn nóng, đo lường, thiết kế lại index, kiểm tra tác động, và lặp lại. Sự khác biệt về tốc độ không chỉ làm mượt trải nghiệm người dùng, mà còn tiết kiệm hạ tầng, mở rộng biên độ tăng trưởng cho sản phẩm của bạn.

Đá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.