MongoDB lúc nhanh lúc chậm nguyên nhân từ kiến trúc ít người để ý
Trong quá trình làm dự án của tôi, một trong những Case kinh điển nhất hay gặp đó là Database tự nhiên đùng cái chạy rất chậm. Thà rằng cứ chậm ngay từ đầu đi anh em còn biết và dễ xử lý. Hiện tượng này gặp ở đủ loại database luôn, từ những ông không mất tiền như MySQL, PostgreSQL, và rồi cả các ông license rất đắt đỏ như SQL Server, Oracle cũng dính chưởng. Bài viết này tôi muốn đề cập tới cho anh em vấn đề MongoDB cũng không thoát khỏi kiếp nạn.
Dưới đây là một số nội dung chính mà chúng ta cùng tìm hiểu
MongoDB hay bất kỳ Database nào khác (Oracle, PostgreSQL, …) được tạo ra với nhiệm vụ gì.
Chiến lược thực thi của câu lệnh trong MongoDB là gì, xem thế nào ?
MongoDB lựa chọn chiến lược thực thi có phải luôn chính xác không ?
Cách chọn chiến lược thực thi trong MongoDB là gì và tại sao có thể dẫn tới việc lúc nhanh, lúc chậm ?
Bắt đầu luôn nhé.
1. MongoDB hay bất kỳ Database nào khác (Oracle, PostgreSQL…) có nhiệm vụ gì ?
Về mặt bản chất dù là NoSQL hay RDBMS thì nó cũng có mấy nhiệm vụ chính
- Lưu trữ các dữ liệu mà người dùng cần
- Và làm thế nào đó lấy được các dữ liệu từ “cái cục” đã lưu trữ bên trên, trả ra đúng thứ mà ông người dùng yêu cầu.
Ví dụ tôi có 2004 thông tin Orders và dữ liệu 1 Orders định dạng như dưới đây
db.orders.countDocuments();
2004
{
_id: ObjectId('690045896ac3a72c81e8887a'),
order_id: 1971,
customer_id: 107,
status: 'paid',
amount: 846.38,
quantity: 2,
region: 'Ha Noi',
category: 'Cameras',
payment_method: 'credit_card',
created_at: 2025-03-01T17:00:00.000Z,
shipped_at: 2025-03-02T17:00:00.000Z
}
Trên Collection Orders đã có 1 số Index
- Một Index mặc định _id
- Một Index trên trường status
- Một Index trên đồng thời 2 trường (status, customer_id)
- Một Index trên amount
db.orders.getIndexes();
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { status: 1 }, name: 'status_1' },
{
v: 2,
key: { status: 1, customer_id: 1 },
name: 'status_1_customer_id_1'
},
{ v: 2, key: { amount: 1 }, name: 'amount_1' }
]
Bây giờ tôi muốn tìm tất cả các đơn hàng với vùng là Hải Phòng, số lượng (amount) lớn hơn 10, câu lệnh mà ứng dụng sẽ gửi tới MongoDB tương ứng
db.orders.find({
region: "Hai Phong",
amount: { $gt: 10 }
});
MongoDB sẽ nhận yêu cầu này và phải tìm trong hơn 2000 dữ liệu (Documents) của mình, lọc ra những dữ liệu thỏa mãn điều kiện, và nó tìm thấy 64 dữ liệu như vậy
Hiểu một cách đơn giản thì nó phải cân nhắc xem có nên dùng Index không, nếu dùng thì dùng Index gì.
Trong MongoDB, để xem được chiến lược thực thi ấy cụ thể thế nào, anh em có thể dùng cách thức kinh điển sau
db.orders.find({ region: "Hai Phong", amount: { $gt: 10 } }).explain();
2. Chiến lược thực thi của câu lệnh trong MongoDB
Khi tối ưu với bất kỳ loại Database nào, anh em cũng sẽ đều phải biết về chiến lược thực thi.
Nếu né cái kiến thức này thì coi như thành thầy bói mù xem voi luôn.
Phần nội dung dưới đây tôi sẽ giúp anh em có thể dùng được 2 kỹ thuật thực tế
- Kiểm tra được MongoDB dự đoán sẽ sử dụng chiến lược thực thi nào - dùng explain
- Và nếu muốn xem các thông số thực thế khi chạy của chiến lược thực thi thì dùng gì
- Trong lúc nói về 2 cái kỹ thuật ấy, chúng ta sẽ cùng thực hiện Demo luôn cách mà Database MongoDB lựa chọn chiến lược thực thi khi chúng ta bổ sung thêm 1 Index mới cho Collection Orders.
2.1. Sử dụng Explain để xem chiến lược thực thi trong MongoDB
Với việc sử dụng Explain, chúng ta có thể biết ông MongoDB sẽ dự kiến thực hiện câu lệnh của mình như thế nào
Tôi có lời khuyên rằng bất kỳ câu lệnh nào khi làm dự án, anh em cũng nên có thói quen thực hiện explain, cách luyện tập này giúp anh em hiểu và học được ngay từ thực tế.
db.orders.find({ region: "Hai Phong", amount: { $gt: 10 } }).explain();
{
explainVersion: '1',
queryPlanner: {
namespace: 'Wecommit.orders',
parsedQuery: {
'$and': [
{
region: {
'$eq': 'Hai Phong'
}
},
{
amount: {
'$gt': 10
}
}
]
},
indexFilterSet: false,
queryHash: 'E094A2A6',
planCacheShapeHash: 'E094A2A6',
planCacheKey: '01434637',
optimizationTimeMillis: 0,
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
prunedSimilarIndexes: false,
winningPlan: {
isCached: false,
stage: 'FETCH',
filter: {
region: {
'$eq': 'Hai Phong'
}
},
inputStage: {
stage: 'IXSCAN',
keyPattern: {
amount: 1
},
indexName: 'amount_1',
isMultiKey: false,
multiKeyPaths: {
amount: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
amount: [
'(10, inf]'
]
}
}
},
rejectedPlans: []
},
queryShapeHash: '20AE9E363AF34F5D0612697FEDE3DC295582860CBB2F47D3D7F064698329A6EF',
command: {
find: 'orders',
filter: {
region: 'Hai Phong',
amount: {
'$gt': 10
}
},
'$db': 'Wecommit'
},
serverInfo: {
host: 'DESKTOP-U747V0Q',
port: 27017,
version: '8.2.1',
gitVersion: '3312bdcf28aa65f5930005e21c2cb130f648b8c3'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted',
internalQueryPlannerIgnoreIndexWithCollationForRegex: 1
},
ok: 1
}
Dưới đây là một số thông tin giúp anh em đọc và hiểu được chiến lược thực thi:
- MongoDB thực hiện biên dịch lại câu lệnh thành dạng cây điều kiện, từ đây nó hiểu được rằng sẽ cần lọc region = “Hai Phong” và amount > 10
"$and": [
{ "region": { "$eq": "Hai Phong" } },
{ "amount": { "$gt": 10 } }
]
- indexFilterSet: false tham số này nghĩa là câu lệnh từ người dùng không “ép nó phải chọn” một bộ Index nào cả. Để nó thoải mái, tự MongoDB ra quyết định.
- Bản thân MongoDB cần phải biết các câu lệnh đã gửi tới mình là những câu nào, có khác nhau không, do đó nó sẽ cần một số thông tin để chuyển các câu lệnh dài lòng thòng như vậy thành những “mã” ngắn gọn để lưu trữ. Đây chính là ý nghĩa của các tham số queryHash, planCacheShapeHash, planCacheKey
-optimizationTimeMillis: 0 điều này cho thấy thời gian (ms) mà Query Planner tính toán và ra quyết định là cực kỳ nhỏ, ~0 ms (thứ quyết định lựa chọn chiến lược thực thi trong MongoDB)
- Một số cờ (True/False) được sử dụng để MongoDB biết được liên trong lúc lên chiến lược thực thi có bị chạm tới giới hạn nào không. Đây chính là ý nghĩa của maxIndexedOrSolutionsReached, maxIndexedAndSolutionsReached, maxScansToExplodeReached. Phần này anh em cứ tạm bỏ qua, chưa quan trọng để mất thời gian vào nó anh em nhé.
- winningPlan: Đây là phần thông tin mà chúng ta cần tập trung
{
"isCached": false,
"stage": "FETCH",
"filter": { "region": { "$eq": "Hai Phong" } },
"inputStage": {
"stage": "IXSCAN",
"keyPattern": { "amount": 1 },
"indexName": "amount_1",
"indexBounds": { "amount": ["(10, inf]"] }
}
}
Mấy cái thông số trên có nghĩa như sau:
- MongoDB sử dụng Index trên cột amount (tên Index là amount_1) để tìm các document có giá trị amount > 10. Đây chính là bước stage: IXSCAN
- Sau khi lấy được các Document từ Index rồi, MongoDB phải truy cập vào Document gốc để tìm và lấy ra những nội dung thỏa mãn điều kiện region = "Hai Phong”. Điều này thể hiện ở stage "FETCH", sau đó là "filter": { "region": { "$eq": "Hai Phong" } }
- rejectedPlans: [] điều này thể hiện MongoDB chỉ thấy duy nhât 1 chiến lược khả thi, nó không cân loại bất kỳ chiến lược nào khác
- queryShapeHash: MongoDB sử dụng việc hash câu lệnh truy vấn gửi tới, để lưu được “hình dạng” của nó, sau này gặp câu tương tự đỡ phải phân tích.
MongoDB cũng lưu luôn cụ thể câu lệnh truyền vào là gì, điều này thể hiện trong phần
command: {
find: 'orders',
filter: {
region: 'Hai Phong',
amount: {
'$gt': 10
}
}
Phần bên dưới cùng là một số thông tin liên quan tới các tham số tài nguyên của Database trong quá trình chạy câu lệnh.
Okie, như vậy anh em đã biết được đến đây là:
MongoDB sẽ phải có 1 cách ước lượng gì đó để chọn ra chiến lược thực thi (con đường đi cụ thể, quét Index thì quét Index nào, sau đó làm tiếp bước gì để có thể lấy được dữ liệu mong muốn).
Vậy thông số THỰC TẾ khi thực hiện của chiến lược thực thi ra sao, và chiến lược thực thi mà loại Database này lựa chọn có phải luôn là giải pháp tối ưu không ?
Chúng ta sẽ cùng tìm hiểu ở ngay nội dung kế tiếp.
2.2 MongoDB lựa chọn chiến lược thực thi có phải luôn đúng không ?
Tôi trả lời luôn câu này là
MongoDB không phải lúc nào cũng chọn chiến lược thực thi đúng. Anh em thấy thế có cay không ?
Anh em sẽ tự mình thấy qua Demo sau
Tôi sẽ thực hiện cùng 1 câu lệnh tìm kiếm các dữ liệu có Region = Hà Nội trong Orders.
Tại Collection Orders, các đơn hàng có Region Hà Nội chiếm hơn 98% tổng số đơn hàng.
Tôi sẽ thực hiện việc so sánh hiệu năng khi CÓ INDEX và LÚC CHƯA CÓ INDEX.
Để xem MongoDB lựa chọn chiến lược thực thi của câu lệnh như thế nào, và lựa chọn đó sai toét ra sao nhá.
Chúng ta sẽ cần thực hiện đo đạc số liệu thực tế của câu lệnh, việc này sẽ sử dụng explain("executionStats")
Đầu tiên là chứng minh hơn 98% số lượng đơn hàng có Region là Hà Nội nhé. Cụ thể là trong 2004 đơn hàng, chúng ta có 1964 cái tới từ thủ đô
db.orders.countDocuments({ region: "Ha Noi" });
1964
Lưu ý: tại thời điểm này, trên Database MongoDB không hề có index trên trường region.
Thực hiện xem chi tiết chiến lược thực thi THỰC TẾ thực hiện khi tìm kiếm tất cả các bản ghi có Region = Hà Nội
db.orders.find({ region: "Ha Noi" }).explain("executionStats");
Kết quả như sau
{
explainVersion: '1',
queryPlanner: {
namespace: 'Wecommit.orders',
parsedQuery: {
region: {
'$eq': 'Ha Noi'
}
},
indexFilterSet: false,
queryHash: '93AC9708',
planCacheShapeHash: '93AC9708',
planCacheKey: '742D40D1',
optimizationTimeMillis: 0,
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
prunedSimilarIndexes: false,
winningPlan: {
isCached: false,
stage: 'COLLSCAN',
filter: {
region: {
'$eq': 'Ha Noi'
}
},
direction: 'forward'
},
rejectedPlans: []
},
executionStats: {
executionSuccess: true,
nReturned: 1964,
executionTimeMillis: 0,
totalKeysExamined: 0,
totalDocsExamined: 2004,
executionStages: {
isCached: false,
stage: 'COLLSCAN',
filter: {
region: {
'$eq': 'Ha Noi'
}
},
nReturned: 1964,
executionTimeMillisEstimate: 1,
works: 2005,
advanced: 1964,
needTime: 40,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
direction: 'forward',
docsExamined: 2004
}
},
queryShapeHash: '6EEE3EF7BA355FB8E11B7ECD124A43DC875970959DC4780147250C544BCAB3B3',
command: {
find: 'orders',
filter: {
region: 'Ha Noi'
},
'$db': 'Wecommit'
},
serverInfo: {
host: 'DESKTOP-U747V0Q',
port: 27017,
version: '8.2.1',
gitVersion: '3312bdcf28aa65f5930005e21c2cb130f648b8c3'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted',
internalQueryPlannerIgnoreIndexWithCollationForRegex: 1
},
ok: 1
}
Chiến lược thực thi ở đây là COLLSCAN, việc này tương tự với khái nhiệm FULL TABLE SCAN ở trong các RDBMS anh em nhá. Ý nghĩa thì MongoDB phải thực hiện quét tất cả những dữ liệu của Collection Orders (giả sử đống dữ liệu này là 10GB, thì nó quét cả 10GB, nếu là 100GB thì quét 100GB).
Sau khi quét toàn bộ dữ liệu thì MongoDB mới thực hiện lọc xem trong đó có bản ghi nào có giá trị "region" = "Ha Noi”
Tôi đọc phần nội dung trên ở chỗ này anh em nhé
stage: 'COLLSCAN',
filter: {
region: {
'$eq': 'Ha Noi'
}
}
Mục executionStats cho biết các thông số chi tiết khi câu lệnh thực hiện
- totalDocsExamined: 2004 → Tổng số Documents phải đọc là 2004
- nReturned: 1964 → Tổng số Documents thỏa mãn điều kiện region là Hà Nội
- executionTimeMillis: 0 → thời gian cực nhanh
Như vậy đã có các thông số hiệu năng và chiến lược cụ thể khi không có Index trên trường đang tìm kiếm.
Ngay sau đây, tôi sẽ thực hiện tạo Index trên trường Region và kiểm tra lại chiến lược thực thi của câu lệnh
Thực hiện tạo thêm Index trên trường Region
`db.orders.createIndex({ region: 1 });`
`region_1`
Kiểm tra chiến lược thực thi sau khi thêm Index thì kết quả như sau
{
explainVersion: '1',
queryPlanner: {
namespace: 'Wecommit.orders',
parsedQuery: {
region: {
'$eq': 'Ha Noi'
}
},
indexFilterSet: false,
queryHash: '93AC9708',
planCacheShapeHash: '93AC9708',
planCacheKey: '44CFCCFF',
optimizationTimeMillis: 0,
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
prunedSimilarIndexes: false,
winningPlan: {
isCached: false,
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: {
region: 1
},
indexName: 'region_1',
isMultiKey: false,
multiKeyPaths: {
region: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
region: [
'["Ha Noi", "Ha Noi"]'
]
}
}
},
rejectedPlans: []
},
executionStats: {
executionSuccess: true,
nReturned: 1964,
executionTimeMillis: 2,
totalKeysExamined: 1964,
totalDocsExamined: 1964,
executionStages: {
isCached: false,
stage: 'FETCH',
nReturned: 1964,
executionTimeMillisEstimate: 0,
works: 1965,
advanced: 1964,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1964,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1964,
executionTimeMillisEstimate: 0,
works: 1965,
advanced: 1964,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: {
region: 1
},
indexName: 'region_1',
isMultiKey: false,
multiKeyPaths: {
region: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
region: [
'["Ha Noi", "Ha Noi"]'
]
},
keysExamined: 1964,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
},
queryShapeHash: '6EEE3EF7BA355FB8E11B7ECD124A43DC875970959DC4780147250C544BCAB3B3',
command: {
find: 'orders',
filter: {
region: 'Ha Noi'
},
'$db': 'Wecommit'
},
serverInfo: {
host: 'DESKTOP-U747V0Q',
port: 27017,
version: '8.2.1',
gitVersion: '3312bdcf28aa65f5930005e21c2cb130f648b8c3'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted',
internalQueryPlannerIgnoreIndexWithCollationForRegex: 1
},
ok: 1
}
Ngon! Lúc này chiến lược thực thi của MongoDB đã không chọn COLLSCAN nữa, mà thay vào đó sử dụng được Index chúng ta vừa tạo:
- stage: 'IXSCAN’
- indexName: 'region_1'
Tài nguyên sử dụng của câu lệnh:
- nReturned: 1964
- executionTimeMillis: 2,
- totalKeysExamined: 1964,
- totalDocsExamined: 1964
Tại đây tổng số Documents phải thực hiện chỉ còn 1964 (thay vì quét toàn bộ totalDocsExamined: 2004 như COLLSCAN lúc trước). Aha, số lượng quét giảm đi rồi.
TUY NHIÊN
Chúng ta có thể thấy executionTimeMillis khi sử dung Index ở đây là 2 ms, còn lớn hơn cả việc câu lênh khi không sử dụng Index lúc nãy.
Với kinh nghiệm của một người đã làm trực tiếp rất nhiều dự án tối ưu, nếu trên 1 tập dữ liệu mà hơn 90% giá trị thỏa mãn (như trường hợp của câu lệnh này), việc tìm kiếm theo INDEX là một sai lầm lớn.
Việc liên tục truy cập INDEX và rồi lại nhảy vào COLLECTION để tìm, sẽ khiến cho tổng số tài nguyên cần thực hiện lớn hơn, từ đó dẫn tới thời gian phản hồi lâu hơn.
Như vậy, từ cả lý thuyết lẫn thực tế đều cho thấy chiến lược thực thi của MongoDB lựa chọn ở trường hợp này không chính xác
Ghi chú quan trọng: Mặc dù anh em thấy hơi “cùi bắ” khi khoảng thời gian chênh lệch chỉ 2 ms. Nhưng đây chỉ là demo với số lượng nhỏ Documents. Trong các trường hợp thực thế, nếu số lượng Documents lớn hơn, tổng dung lượng Collections lớn (ví dụ vài chục GB trở lên), anh em sẽ thấy hiệu năng khác xa nhau rất nhiều. Bài này với mục tiêu giúp anh em hiểu cốt lõi, không hướng tới mục tiêu làm cho số thật to nghe cho oai, nên tôi không để dữ liệu nhiều.
Okie, vậy đến đây chúng ta đã hiểu được rằng
Thứ nhất: Để thực hiện được câu lệnh, MongoDB phải biết chiến lược thực thi (các bước cụ thể cần thực hiện) là gì.
Thứ hai: Ngay cả khi MongoDB nghĩ rằng 01 chiến lược thực thi là ngon nhất rồi, thực tế vẫn có thể không phải như vậy.
Chỗ này tôi muốn đặt ra câu hỏi và cùng tìm hiểu với anh em.
Vậy Database lựa chọn chiến lược thực thi kiểu gì ?
3. Cách mà Database lựa chọn chiến lược thực thi
3.1. RDBMS và NoSQL khi lựa chọn chiến lược thực thi
Từ thời xa xưa (thực ra là từ đời đầu của các ông Database như Oracle, SQL Server), việc lựa chọn 1 chiến lược thực thi được dựa trên các luật.
Cái này gọi là RBO (Rule based optimizer).
Hiểu nôm thì nó cũng kiểu “AI if else” (thực tế thì nó có khôn hơn, tôi ví dụ thế cho anh em dễ hình dung thôi).
- Tức là sẽ có 1 tập các luật, trường hợp đường có 2 lựa chọn A và B thì lựa chọn A luôn ngon hơn B.
- Ví dụ kinh điển là nếu có Index thì lựa chọn Index sẽ ngon hơn là không dùng.
- Đây là phong cách của RBO
Câu chuyện RBO chắc cũng ít anh em phải xử lý trong khi tối ưu ở các dự án hiện tại, nó là câu chuyện lịch sử rồi.
Phần lớn mọi người bây giờ sẽ cần biết về CBO (Cost Based Optimizer).
Đây là giải thuật chính mà các RDBMS (Oracle, SQL Server, MySQL, PostgreSQL…) đang sử dụng để ra quyết định.
Khi Database có nhiều chiến lược thực thi cần lựa chọn, nó sẽ ước lượng tài nguyên phải thực hiện của các chiến lược thực thi ấy ra 1 CHI PHÍ (dạng con số - và đây chính là COST).
Sau đó nó so sánh các COST ấy với nhau và chọn ra chiến lược thực thi có COST bé nhất.
Ví dụ cho dễ hình dung nhé.
Đây là câu lệnh tìm kiếm trên Database Oracle
select * from users where upvotes = 48
Chiến lược thực thi của câu lệnh này như sau
Plan hash value: 1598083519
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2287 | 602 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| USERS | 2287 | 602 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_UPVOTES | 2287 | 7 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------
Ở câu lệnh trên COST = 602 chính là cái ý CBO mà tôi nói tới
Okie, có lẽ anh em đã hình dung được rồi.
Vậy, dựa vào đâu mà nó ước lượng cái COST này nhỉ.
Tôi giải đáp luôn: nó có nhiều đầu vào, nhưng trong đó có 1 cái gọi là STATISTICS, tức là những thông số thống kê liên quan tới các objects mà câu lệnh đang cần xử lý (Objects ở đây hiểu đơn giản là Table, Index).
Nó cần biết được như
- Table có bao nhiêu cột, bao nhiêu bản ghi
- Độ phân bổ dữ liệu các cột ấy thế nào
Ví dụ bản thân Database Oracle lưu các cột ấ có bao nhiêu giá trị NULL, bao nhiêu giá trị khác nhau
.webp)
Từ các thông số ấy rồi mới quyết định
Nhưng MongoDB không có cố định thiết kế, nó không có cụ thể bao nhiêu “cột”.
Vì thế nó không có được các thông tin thống kê mà mấy ông RDBMS đang lưu.
Vậy nó làm thế nào ?
Chiến lược mà mà MongoDB lựa chọn đó là
Tôi sẽ thử hết các chiến lược thực thi có thể có, chạy thử phát (với 1 khối lượng đủ nhỏ thôi, chứ không phải chạy toàn bộ). Sau đó so sánh kết quả ấy, chọn ra ông thắng trong cuộc đua thử nghiệm này.
Một số suy nghĩ của tôi khi biết điều này:
Có lẽ đây cũng chính là lý do mấy ông Dev của MongoDB thể hiện thông tin chiến ở phần chiến lược thực thi là winningPlan và rejectedPlans
Chúng ta sẽ thử 1 trường hợp nếu như có nhiều Index có thể cùng thỏa mãn điều kiện lọc của MongoDB, để tận mắt thấy những gì Query Planner của MongoDB thực hiện nhé
db.orders.find({ status: "paid" }).explain("executionStats");
Đây là kết quả
explainVersion: '1',
queryPlanner: {
namespace: 'Wecommit.orders',
parsedQuery: {
status: {
'$eq': 'paid'
}
},
indexFilterSet: false,
queryHash: 'F7CB0027',
planCacheShapeHash: 'F7CB0027',
planCacheKey: '9A481F22',
optimizationTimeMillis: 2,
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
prunedSimilarIndexes: false,
winningPlan: {
isCached: false,
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: {
status: 1
},
indexName: 'status_1',
isMultiKey: false,
multiKeyPaths: {
status: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
status: [
'["paid", "paid"]'
]
}
}
},
rejectedPlans: [
{
isCached: false,
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: {
status: 1,
customer_id: 1
},
indexName: 'status_1_customer_id_1',
isMultiKey: false,
multiKeyPaths: {
status: [],
customer_id: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
status: [
'["paid", "paid"]'
],
customer_id: [
'[MinKey, MaxKey]'
]
}
}
}
]
},
executionStats: {
executionSuccess: true,
nReturned: 520,
executionTimeMillis: 2,
totalKeysExamined: 520,
totalDocsExamined: 520,
executionStages: {
isCached: false,
stage: 'FETCH',
nReturned: 520,
executionTimeMillisEstimate: 0,
works: 521,
advanced: 520,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 520,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 520,
executionTimeMillisEstimate: 0,
works: 521,
advanced: 520,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: {
status: 1
},
indexName: 'status_1',
isMultiKey: false,
multiKeyPaths: {
status: []
},
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: {
status: [
'["paid", "paid"]'
]
},
keysExamined: 520,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
},
queryShapeHash: 'CE3BBE8E9B88196DF20DCC554D07A378EB02529BE6474D7131425C543CDF3CEE',
command: {
find: 'orders',
filter: {
status: 'paid'
},
'$db': 'Wecommit'
},
serverInfo: {
host: 'DESKTOP-U747V0Q',
port: 27017,
version: '8.2.1',
gitVersion: '3312bdcf28aa65f5930005e21c2cb130f648b8c3'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted',
internalQueryPlannerIgnoreIndexWithCollationForRegex: 1
},
ok: 1
}
Ở kết quả này chúng ta cần chú ý điều gì ?
- Đầu tiên xem ngay WinningPlan, tại phần này cho biết MongoDB đã sử dụng Index status_1
- Ở RejectPlan, MongoDB cho biết nó đã loại bỏ việc chiến lược thự thi của 1 Index khác tên là status_1_customer_id_1
3.2. Trong cách chọn chiến lược thực thi này, có điều gì MongoDB làm chưa ổn ?
Anh em có còn nhớ cái Demo ở phần trước, khi tôi thử nghiệm tìm kiếm trên trương Region không ?
Trong trường hợp thực thế, dùng Index thì chậm hơn so với việc không dùng Index.
Ngoài ra một chi tiết đang chú ý ở đây.
Khi kiểm tra chiến lược thực thi lúc đó, ta thấy RejectPlan là [].
Tức là MongoDB hoàn toàn không hề cân nhắc đánh giá hiệu năng giữa việc chọn quét Index so với việc quét toàn bộ Collection !!
Bây giờ anh em để ý lên tiếp cái Demo ngay phần trước thôi, RejectPlan cũng chỉ cho ta thấy MongoDB so sánh giữa việc nên lựa chọn Index status_1 hay Index status_1_customer_id_1, nó không hề tính tới việc COLLSCAN luôn.
Đây là điểm dở đầu tiên và cực kỳ quan trọng.
Query Planner của MongoDB đã BỎ SÓT hoàn toàn 1 chiến lược thực thi quét toàn bộ COLLECTION, nếu như trên đó có INDEX
Suy nghĩ của tôi:
Điều này nghe có vẻ các bác Dev của MongoDB ép theo RBO hay sao ấy nhể.
Ngoài vấn đề trên, do mỗi lần thực thi, MongoDB sẽ cần đánh giá thực thế chiến lược thực thi nào sẽ tốt hơn, nhưng chỉ chạy với một mẫu nhỏ, việc này hoàn toàn có rủi ro dẫn tới việc lựa chọn giữa các lần không ổn định.
Chính điều này có thể dẫn tới 2 lần chạy có 2 chiến lược thực thi khác nhau, và hiệu năng hoàn toàn khác nhau.
Uầy, vậy có phải mấy ông RDBMS xịn xò hơn không nhỉ ?
Cũng chẳng phải đâu, cái vụ kiểm soát chiến lược thực thi thật sự là một yếu tố khó.
Mấy ông Oracle, SQL Server dù đã cập nhật rất nhiều chiến lược, nhưng vẫn không thể giải quyết dứt điểm và cho hiệu năng 100% ổn định được.
Tôi cũng có 1 bài viết về SQL Server chạy hiệu năng lệch nhau hàng trăm lần.
Anh em cần có thể xem bài viết đó (cũng có demo luôn) ở đây: Click vào đây
4. Kết luận
Anh em đừng tin cứ sang NoSQL là nhanh, không cần tối ưu.
Hiểu về mặt bản chất anh em sẽ thấy gần như khi dữ liệu lớn lên, database nào cũng phải tối ưu và cũng có thể tối ưu được
Nếu bạn muốn tìm hiểu tối ưu SQL thì nên bắt đầu làm/ học từ đâu, hãy xem một bài viết mà tôi từng chia sẻ: Click vào đây
Nếu bạn muốn thấy Framework tối ưu mà tôi đang trực tiếp áp dụng trong các dự án, bạn có thể xem và đăng ký trải nghiệm, xem tại đây: Xem chi tiết Framework Wecommit 100x





