- vừa được xem lúc

MongoDB - Quy tắc ESR cho INDEX - P3: Index khi có toán từ $in

0 0 2

Người đăng: Đức Phúc

Theo Viblo Asia

Xin chào, lại là mình - Đức Phúc, anh chàng hơn 6 năm trong nghề vẫn nghèo technical nhưng thích viết Blog để chia sẻ kiến thức bản thân học được trong quá trình “cơm áo gạo tiền” đây. Các bạn có thể theo dõi mình thêm qua một số nền tảng bên dưới nhé:

Nếu bài này hữu ích, đừng quên tặng mình một upvote, bookmark để ủng hộ các bạn nhé

Ở bài trước, chúng ta đã làm quen với đầy đủ 3 thành phần ESR, tức là Khi Filter chính xác, theo range và Sort cùng xuất hiện. Ở bài này, ta sẽ tìm hiểu về 1 trường hợp đặc biệt trong Filter, đó là toán từ $in. Vậy nó thuộc thành phần nào trong ESR?

1. Bài toán

Trước tiên, ta vẫn sẽ làm quen với bài toán đã nhé. Chúng ta đã có bảng Orders rồi. Bây giờ, giả sử mỗi Order sẽ chứa 1 field mới, mình gọi là items nha. Field này 1 mảng các số nguyên, lưu những thông tin nào đó. Schema của nó sẽ như thế này:

{ "_id": { "$oid": "68302946f185602f01445b09" }, "ordered_by": { "$oid": "682e9881cc777592fda2bab0" }, "product": "Handcrafted Concrete Ball", "price": 915, "quantity": 1, "status": "pending", "createdAt": { "$date": "2025-05-23T07:52:38.042Z" }, "updatedAt": { "$date": "2025-05-23T07:52:38.042Z" }, "items": [ 62, 526, 521, 980, 88 ]
}

Yêu cầu của bài toán, là ta sẽ query những documents mà trong items của nó chứa 1 hoặc nhiều con số được xác định trước. Sau đó, sắp xếp theo thứ tự tăng dần của quantity. Ở bài này, để giảm thời gian chờ đợi, ta sẽ giới hạn số document trả về là 1 thôi nhé

[ { $match: { items: {$in: [5, 10, 15, 20]} } }, { $sort: { quantity: 1 } }, { $limit: 100 }
]

Vậy là đã rõ bài toán, giờ ta tiến hành setup dữ liệu nha. Các bạn tạo thêm giúp mình 1 file là add_or_remove_order_items_from_orders.js, nội dung file này như sau:

const { MongoClient, ObjectId } = require("mongodb");
const faker = require("faker"); const uri = "mongodb://localhost:27017";
const dbName = "demo_caching";
const collectionName = "Orders";
const BATCH_SIZE = 1000; // Helper to generate array of random numbers
function generateItems(size) { const items = []; for (let i = 0; i < size; i++) { items.push(Math.floor(Math.random() * 1000)); // Random number between 0-999 } return items;
} async function processOrders() { const operation = process.argv[2]; // 'add' or 'remove' const sizeFlag = process.argv[3]; // 'small' or 'large' (for add operation) if (!operation || (operation !== "add" && operation !== "remove")) { console.error( "Usage: node add_or_remove_order_items_from_orders.js <add|remove> [small|large]" ); process.exit(1); } if ( operation === "add" && (!sizeFlag || (sizeFlag !== "small" && sizeFlag !== "large")) ) { console.error( "Usage for add operation: node add_or_remove_order_items_from_orders.js add <small|large>" ); process.exit(1); } const client = new MongoClient(uri); try { await client.connect(); const db = client.db(dbName); const collection = db.collection(collectionName); console.log("Connected to MongoDB"); let totalProcessed = 0; let batch = []; const cursor = collection.find({}); for await (const doc of cursor) { let updateOperation; if (operation === "add") { const arraySize = sizeFlag === "small" ? Math.floor(Math.random() * 200) : Math.floor(Math.random() * 800) + 200; const itemsArray = generateItems(arraySize); updateOperation = { $set: { items: itemsArray }, }; } else { // remove operation updateOperation = { $unset: { items: "" }, }; } batch.push({ updateOne: { filter: { _id: doc._id }, update: updateOperation, }, }); if (batch.length >= BATCH_SIZE) { const result = await collection.bulkWrite(batch); totalProcessed += result.modifiedCount; console.log(`Processed ${totalProcessed} documents...`); batch = []; } } // Process any remaining documents in the batch if (batch.length > 0) { const result = await collection.bulkWrite(batch); totalProcessed += result.modifiedCount; console.log(`Processed ${totalProcessed} documents...`); } console.log( `${ operation === "add" ? "Added" : "Removed" } items field for ${totalProcessed} orders.` ); } catch (err) { console.error("Error:", err); } finally { if (client) { // Ensure client is connected before closing await client.close(); } }
} processOrders(); 

Mình giải thích thêm chút về nội dùng file này nha:

  • File sẽ có nhiệm vụ tạo random field items vào toàn bộ 25 triệu documents của chúng ta trong bảng Orders.
  • Có 2 tùy chọn cho số lượng Number trong mỗi mảng itemssmalllarge. Với small, số lượng sẽ < 200, và large là ngược lại. Mình chọn con số 200 này là ngẫu nhiên thôi nhé.
  • Ngoài ra, ta cũng có 2 tùy chọn là addremove field items. Để các bạn tiện test tùy vào bài toán nhé

Lúc này, câu lệnh để chạy file như sau:

node add_or_remove_order_items_from_orders.js add [size_option]
// Example:
node add_or_remove_order_items_from_orders.js add small // ta sẽ chạy phương án này trước nhé
// or
node add_or_remove_order_items_from_orders.js add large

Với remove, ta chỉ cần

node add_or_remove_order_items_from_orders.js remove

Lưu ý nhỏ: Ở bài này, các Index của chúng ta khá lớn, nên nếu các bạn muốn thực nghiệm, thì sau khi đánh INDEX, ta sẽ phải chờ thời gian khá lâu để MongoDB thực hiện đánh Index xong. Khoảng 30 phút. Do đó, các bạn chịu khó đợi nhé. Còn nếu không muốn thực nghiệm, thì mình đã để kết quả đầy đủ bên dưới rồi nha

2. Single Index cho field items

Rồi, vẫn như cũ thôi image.png

Và đây là kết quả: Screenshot 2025-05-26 at 16.30.55.png

Nhìn cái đúng ngỡ ngàng phải không nào.:

  • Thời gian thực thi: Hơn 11 phút dù chỉ trả về đúng 1 documents
  • IXSCAN tốn khoảng 12.6s, với khoảng 7.7 triệu documents được trả về
  • Quá trình FETCH của chúng ta chiếm đến 10.6m, thời gian rất lớn. Có thể nói là không chấp nhận được. Lý do là sau khi scan được 7.7 triệu documents, MongoDB sẽ phải fetch hết chúng vào memory, rồi mới thực hiện việc SORT. Với việc xuất hiện field items, size của mỗi document cũng lớn hơn trước đó, do đó, việc fetch sẽ cực kì chậm
  • Và cuối cùng, SORT chiếm thấp nhất với chỉ 1.5s

3. Compound Index

Như thường lệ, ta vẫn sẽ kiểm tra trên cả 2 trường hợp nhé. Nhưng trước tiên, cùng mình tim hiểu về cách mà MongoDB xử lý Compound Index khi có toán từ $in nha

3.1. MongoDB đánh Index cho toán tủ $in như thế nào?

  • Khi $in có < 200 phần tử, MongoDB coi như là nhiều điều kiện bằng ($eq) ⇒ vẫn theo nguyên tắc ESR ⇒ có thể dùng index tốt, kể cả với .sort()
  • Khi $in có ≥ 200 phần tử, MongoDB coi nó như một range operator, giống như $gt, $lt, $regex ⇒ chỉ có thể áp dụng nguyên tắc ESR nếu $in nằm sau cùng trong index.

Con số 200 này, là con số mà MongoDB quy ước tùy theo phiên bản. Nhưng hiện tại, mình chưa tìm thấy bất kì offical document nào nói chính xác về con số này. Do đó, để biết được thật sự Filter theo items của chúng ta là E hay R, ta nên kiểm nghiệm ở cả 2 phương án nhé:

3.2. Xem $in là E

Theo như câu truy vấn của chúng ta, số lượng items cần truy vấn của ta trong toán từ $in là 4 ([5, 10, 15, 20]). Do đó, ta sẽ coi nó như là E vì nó < 200.

Rồi, ta tạo INDEX thôi: image.png

Và đây là kết quả: Screenshot 2025-05-26 at 20.05.31.png

Ồ, thời gian thực thi bây giờ chỉ là 4ms, thay vì hơn 11 phút như trước đó. Đó chính là nhờ ESR đấy:

  • Quá trình IXSCAN của chúng ta trải qua khá nhiều phần. Vì sao lại có nhiều phần như vậy? À, nếu để ý, bạn sẽ thấy nó có 4 phần, tương ứng chính là 4 điều kiện nằm trong $in. Với mỗi phần từ trong mảng items, khi ta đánh INDEX, mongoDB sẽ coi nó là multikey field. Tức là với INDEX như trên, và ta có mảng items ví dụ như: [1, 2, 3], MongoDB sẽ chuyển thành:

    • { items: 1, quantity: X },
    • { items: 2, quantity: X }
    • { items: 3, quantity: X }
    • { items: 1, quantity: Y }
    • { items: 2, quantity: Y }
    • { items: 3, quantity: Y }
    • ... cứ thế cho mỗi cặp itemsquantity
  • Bạn có thể tăng, giảm số lượng phần từ trong $in để kiểm nghiệm nhé. Hoặc thử 2 giá trị trong $in trùng nhau, xem thế nào nha

  • Sau quá trình IXSCAN, MongoDB tiến hành SORT_MERGE chúng lại, quá trình này gần như bằng 0ms. Tương tự cho quá trình FETCHLIMIT, vì ta đã SORT theo quantity rồi, nên quá trình này là siêu nhanh, MongoDB sẽ không cần FETCH hết rồi SORT lại nữa

3.3. Xem $in là R

Giờ ta thử xem $in là R xem sao nhé, lúc đó, Index ta sẽ thay đổi thành.

image.png

Và đây là kết quả: Screenshot 2025-05-26 at 19.09.58.png

  • IXSCAN của chúng ta cũng khá nhanh, tốn 14ms, tuy nhiên, nó cần phải quét đến **12590** documents
  • Sau đó, mới FETCH chúng về, và tiến hành LIMIT
  • Toàn bộ quá trình mất khoảng 6s

4. Kết luận

Như vậy, với toán từ $in, ta nên kiểm nghiệm cả 2 phương án, xem xét nó trên cả vai trò là E và R, để xem phương án nào tối ưu hơn các bạn nhé. Các bạn cũng có thể tăng số lượng documents trả về để xem hiệu suất mà mỗi phương án mang lại nữa nhé.

Ngoài ra, như đã đề cập, các bạn sẽ thấy. việc MongoDB tách mảng items thành multikey field, khiến cho chúng ta sẽ cần bộ nhớ khá lớn cho việc lưu trữ INDEX, như ví dụ này này khoảng 12.6GB nha

image.png

Các bạn cũng có thể dùng optionslarge ở mẫu code trên, để tăng số lượng phần từ trong items, nếu muốn test thêm nhé. Lưu ý là quá trình đánh INDEX sẽ tốn kha khá thời gian nha

Tất nhiên, bài này chỉ là ví dụ cho toán từ $in. Trên thực tế, mình đã hướng dẫn các bạn một cách thiết kế schema khác, với hiệu suất cao hơn, tốt hơn cho việc mở rộng ở bài: MongoDB - Dynamic Fields: Câu truy vấn chậm đi hơn 200% vì một sai lầm - Bạn biết chưa?. Các bạn có thể tham khảo nhé.

Nếu bài này hữu ích, đừng quên tặng mình một upvote, bookmark để ủng hộ các bạn nhé. Nếu có thắc mắc gì hay ý kiến gì trao đổi, bạn có thể để lại comment nha.

Ngoài ra, các bạn có thể kết nối với mình qua:

Hẹn gặp lại các bạn ở những bài tiếp theo

Bình luận

Bài viết tương tự

- vừa được xem lúc

TÌM HIỂU VỀ MONGODB

. 1. Định nghĩa về MongoDB. . MongoDB là một cơ sở dữ liệu mã nguồn mở và là cơ sở dữ liệu NoSQL(*) hàng đầu, được hàng triệu người sử dụng.

0 0 55

- vừa được xem lúc

Mongo DB cho người mới bắt đầu !

Lời nói đầu. Gần đây, mình mới bắt đầu nghiên cứu và sử dụng mongo db nên có chút kiến thức cơ bản về Mongo muốn share và note ra đây coi như để nhở (Biết đâu sẽ có ích cho ai đó).

0 0 41

- vừa được xem lúc

Áp dụng kiến trúc 3 Layer Architecture vào project NodeJS

The problem encountered. Các framework nodejs phổ biết như Express cho phép chúng ta dễ dàng tạo ra Resful API xử lí các request từ phía client một cách nhanh chóng và linh hoạt.

0 0 89

- vừa được xem lúc

Mongo DB cho người mới bắt đầu ! (P2)

Lời nói đầu. Gần đây, mình mới bắt đầu nghiên cứu và sử dụng mongo db nên có chút kiến thức cơ bản về Mongo muốn share và note ra đây coi như để nhở (Biết đâu sẽ có ích cho ai đó).

0 0 187

- vừa được xem lúc

Xây dựng CRUD RESTful API sử dụng Node, Express, MongoDB.

Introduction. Trong phạm vi bài viết này chúng ta sẽ cùng tìm hiểu về cách tạo restful api với Node, Express và MongoDB. . Xử lý các hoạt động crud.

0 0 232

- vừa được xem lúc

MongoDB là gì? Cơ sở dữ liệu phi quan hệ

Bài viết này mình sẽ giúp các bạn có cái nhìn tổng quan về MongoDB. Chúng ta không lạ gì với cơ sở dữ liệu quan hệ, còn với cơ sở dữ liệu phi quan hệ thì sao? MEAN stack (MongoDB, Express, AngularJS,

0 0 62