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

MongoDB - Quy tắc ESR cho INDEX - P1: Khi Filter và Sort cùng tồn tại trong truy vấn

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 viết trước về MongoDB - Giảm 99% lượng truy vấn đến Database nhờ Caching & Distributed Locking trên Redis. Cùng thử nhé!, chúng ta đã cùng nhau thực nghiệm sử dụng Distributed Lock trên bảng Users với 1 triệu documents. Hôm nay, chúng ta sẽ quay trở lại với INDEX trong MongoDB nhé.

1. ESR Index là gì?

Như chúng ta đã biết, MongoDB chia INDEX thành 2 loại chính xét về cách đánh INDEX là: Single INDEX và Compound INDEX. Trong Compound INDEX, ta sẽ có thể chọn nhiều field để hợp thành 1 INDEX. Tuy nhiên, đã bao giờ bạn thắc mắc, thứ tự các field đó có quan trọng hay không chưa? Giả sử mình muốn đánh INDEX trên 2 trường A và B, ta sẽ có các trường hợp như:

// TH1: {A: 1, B: 1}
// TH2: {B: 1, A: 1}
// TH3: {A: 1, B: -1}
// TH4: {B: 1, B: -1}
... và nhiều trường hợp khác nữa

Lúc này, để chọn được cách tạo INDEX phù hợp, MongoDB đã khuyến khích chúng ta tạo INDEX theo 1 tiêu chuẩn gọi là ESR. Trong đó:

  • E: (Equality): Đầu tiên là thêm các trường được sử dụng trong điều kiện so sánh bằng (=), tức là những trường truy vấn theo giá trị chính xác.
  • S: (Sort): Tiếp theo, thêm các trường được dùng để sắp xếp kết quả truy vấn (sort).
  • R (Range): thêm các trường được sử dụng cho các điều kiện lọc theo khoảng giá trị (range), như $gt, $lt, $gte, $lte.

Với tiêu chuẩn này, chúng ta sẽ có thể truy vấn nhanh hơn nhờ INDEX hoạt động hiệu quả. Lý thuyết thôi thì chưa đủ đúng không nào, nên giờ chúng ta sẽ cùng thực nghiệm nhé. Mình sẽ chia nội dung này thành nhiều phần, để các bạn có thể dễ nắm được từng ví dụ, và dễ nhớ hơn nha

2. Bài toán

Ví dụ, mình có 1 bảng Orders với schema đơn giản hóa như sau:

{ "_id": { "$oid": "68302946f185602f01445b52" }, "ordered_by": { "$oid": "682e9881cc777592fda2bab5" }, "product": "Practical Cotton Cheese", "price": 75, "quantity": 1, "status": "completed", "createdAt": { "$date": "2025-05-23T07:52:38.042Z" }, "updatedAt": { "$date": "2025-05-23T07:52:38.042Z" }
}

Và chúng ta cần thực hiện truy vấn theo yêu cầu:

Lấy những đơn hàng có trạng thái (status) là "X" và sắp xếp các đơn hàng đó theo thứ tự tăng dần của số lượng (quantity)

Okay, bài toán cũng không có gì lạ phải không? Lúc này, câu truy vấn của chúng ta sẽ gồm 2 phần:

  • E: Lấy chính xác đơn hàng với status là X
  • S: Sort kết quả theo thứ tự tăng dần của quantity

Ở bài trước, chúng ta đã có file user_seed.js để tạo 1 triệu User documents. Trước khi vào thực nghiệm, bạn tạo thêm 1 file là order_seed.js để thêm dữ liệu cho bảng Orders nha:

const { MongoClient, ObjectId } = require("mongodb");
const faker = require("faker"); const uri = "mongodb://localhost:27017";
const dbName = "demo_caching";
const usersCollection = "Users";
const ordersCollection = "Orders";
const BATCH_SIZE = 1000; function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min;
} function generateOrder(userId) { return { ordered_by: userId, product: faker.commerce.productName(), price: parseFloat(faker.commerce.price()), quantity: getRandomInt(1, 10), status: faker.random.arrayElement(["pending", "completed", "cancelled"]), createdAt: new Date(), updatedAt: new Date(), };
} async function seedOrders() { const client = new MongoClient(uri); try { await client.connect(); const db = client.db(dbName); const users = await db .collection(usersCollection) .find({}, { projection: { _id: 1 } }) .toArray(); const ordersCol = db.collection(ordersCollection); await ordersCol.deleteMany({}); console.log("Cleared Orders collection"); let totalOrders = 0; let batch = []; for (const user of users) { const numOrders = getRandomInt(0, 50); for (let i = 0; i < numOrders; i++) { batch.push(generateOrder(user._id)); if (batch.length >= BATCH_SIZE) { await ordersCol.insertMany(batch); totalOrders += batch.length; batch = []; } } } // Insert any remaining orders if (batch.length > 0) { await ordersCol.insertMany(batch); totalOrders += batch.length; } console.log(`Inserted ${totalOrders} orders for ${users.length} users.`); } catch (err) { console.error(err); } finally { await client.close(); }
} seedOrders(); 

Với file này, ta sẽ random mỗi User sẽ có từ 0 đến 50 đơn hàng nha. Sau khi chạy xong, thì mình có khoảng 25 triệu documents cho bảng Orders (Con số này có thể khác ở phía bạn vì chúng ta đang random). Quá trình chạy file có thể mất vài phút, nên bạn cố chờ nhé:

node order_seed.js

image.png

Có dữ liệu rồi, giờ là câu truy vấn sẽ được dùng xuyên suốt bài này nha. Mình giả sử status cần tìm là completed nhé:

[ { $match: { status: "completed" } }, { $sort: { quantity: 1 } },
]

3. Thử nghiệm với Single INDEX

Ủa, chúng ta đang nhắc đến Compund Index, sao giờ lại thử nghiệm trên Single Index nữa? À thì cũng đâu có sao, thử xem nếu chỉ dùng Single Index thì sao nhé

3.1. Single Index trên trường status (Filter)

Rồi, đầu tiên, tạo giúp mình Index nhé: image.png

Và đây là kết quả khi thực thi nè:

image.png

Các bạn có thể thấy:

  • Chúng ta cần đến khoảng 13s để hoàn thành truy vấn. Và 8_333_739 kết quả được trả về.
  • Việc thực hiện truy vấn được chia làm 3 giai đoạn:
    • IXSCAN trên index status mà chúng ta đã tạo. Giai đoạn này chỉ mất khoảng 1.5s
    • FETCH dữ liệu: Số lượng dữ liệu khá lớn với hơn 8 triệu documents, do đó, thời gian cho nó khoảng 4.1s
    • Cuối cùng, SORT dữ liệu: Công đoạn này mất nhiều thời gian nhất với 6.8s để sắp xếp toàn bộ 8 triệu documents đó

Vậy là với phương án này, dù INDEX để SCAN khá oke, nhưng lại phải tốn chi phí cho việc SORT khá nhiều vì INDEX của chúng ta chỉ đánh trên trường status, tức là dành cho Filter mà thôi. MongoDB sẽ phải dùng memory để sắp xếp dữ liệu nên tổng thời gian của chúng ta cũng khá lớn.

3.2. Single Index trên trường quantity (Sort)

Rồi, xóa giúp mình Index trên trường status trước đó đã nhé. Sau đó, tạo mới Index cho trường quantity nha. Chúng ta đang SORT dữ liệu theo thứ tự tăng dàn của quantity, nên ta sẽ chọn INDEX tăng dần - 1 (asc) nhé:

image.png

Chạy lại EXPLAIN và xem kết quả nào:

image.png

Ồ, nhìn vào là thấy ngay giờ truy vấn chỉ cần 2 giai đoạn là hoàn thành thôi. Ta đã bỏ được giai đoạn SORT rồi nè. Nhưng mà.... sao thời gian truy vấn lại tăng lên 17ms thế kia?

Ta sẽ dễ nhận ra ngay, là giai đoạn FETCH đang chiếm đến 11.8s. Nguyên do là vì ta đánh INDEX trên trường quantity, dó đó việc SCAN sẽ thực hiện trên đó. Thời gian SCAN cũng chiếm nhiều hơn do do nó phải SCAN toàn bộ bảng với hơn 25 triệu documents, dù cho chiến lược thực thi là IXSCAN trên field quantity. Với số lượng documents lớn thế, sau khi scan hoàn tất, MongoDB sẽ phải lọc lại những documents với statuscompleted. Rõ ràng là thời gian lọc này sẽ rất nhiều so với cách trước đó phải không nào? Vì nếu chúng ta đánh INDEX trên trường status, MongoDB đã dựa theo INDEX và lọc ra đúng khoảng 8 triệu documents thôi, thay vì 25 triệu như bây giờ.

Vậy là phương án đánh INDEX trên trường quantity này không tốt bằng đánh INDEX trên trường status rồi.

4. Compound Index

Rồi, đến phần chính rồi đây. Giờ chúng ta sẽ thử cả 2 phương án nhé:

4.1. Tuân thủ ESR

Như đã phân tích, bài toán này sẽ chỉ có E và S, không có R. Dù cho vậy đi nữa, E vẫn được ưu tiên hơn S, do đó, Index mà chúng ta cần tạo theo đúng chuẩn sẽ là status trước, sau đó đến quantity.

image.png

Ròi, EXPLAIN lần nữa nhé: image.png

Ồ, tốt hơn hẳn luôn nè:

  • IXSCAN đã chọn INDEX mà chúng ta tạo, và chỉ quét đúng 8 triệu documents mong muốn mà thôi. Đó là nhờ phần E của chúng ta đã được xác định là status nhé
  • Tiếp theo, chúng ta cũng không thấy giai đoạn SORT đâu nữa. Đó là nhờ ta đã kết hợp phần S cho quantity.
  • Thời gian thực thi nhờ đó đã giảm xuống chỉ còn khoảng **7.2s** mà thôi cho 8 triệu documents

4.2. Không tuân thủ ESR

Good rồi đấy, tuân thủ quy tắc là lựa chọn đúng đắn phải không nào, nhất là khi quy tắc đó được tạo ra bởi chính "nhà sản xuất" MongoDB. Nhưng nếu bạn chưa phục, thì thử đảo ngược quy tắc xem sao nhé. Giờ ta sẽ cho S (quantity) lên trước E (status) nhé

image.png

Kết quả nè: image.png

Ôi trời, các bạn thấy chưa? Trái quy tắc còn thảm họa hơn cả Single Index. Đó là lý do vì sao mình thực nghiệm trên Single Index ở trước đó nhé. Là để so sánh với cái này nè!

Các bạn nhìn vào số liệu, sẽ thấy vì sao nó cần đến gần 22s cho câu truy vấn rồi ha. IXSCAN giờ sẽ quét full bảng với 25 triệu documents, sau đó lại FETCH dữ liệu dựa theo INDEX trái với quy tắc nên cần đến 16.3s để hoàn thành.

5. Kết luận

Như vậy, qua phần 1 này, ta đã thực nghiệm với 2 phần trong quy tắc ESR là E và S. Bài học thì chắc chắn là chúng ta phải tuân thủ quy tắc rồi, nhưng hy vọng với những thực nghiệm trên, các bạn sẽ dễ hiểu hơn nhé.

Sẽ còn nhiều ví dụ nữa để chúng ta có thể kiểm nghiệm. Cùng chờ đón những phần tiếp theo nha.

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:

Xin chào và hẹn gặp lại

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