MongoDB: Index trên dữ liệu ngày tháng không hoạt động, phải làm thế nào đây?

0 0 0

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é:

Ở bài viết trước, chúng ta đã tìm hiểu về sai lầm trong việc lưu trữ dữ liệu ngày tháng theo dạng Dynamic Field, và cách mà chúng ta đã khắc phục nó rồi đúng không nào? Nếu bạn nào chưa đọc bài viết đó, các bạn có thể xem bài viết đó tại đây nha:

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?

Sau khi đọc xong, bạn đã sẵn sàng để đọc tiếp bài này rồi đấy, bắt đầu thôi

1. Bài toán

Trước tiên, cùng mình nhìn lại schema cuối cùng mà chúng ta đã có sau khi kết thúc bài trước ở bảng analyst_optimized nhé:

{ "_id": { "$oid": "6827eff09b43f525fcf9c3b8" }, "userId": 2, "date": "2024-01-01", "value": 81
}

Bây giờ, bài toán mới của chúng ta là:

Viết API (Query) hỗ trợ Admin có thể truy vấn thông tin theo ngày/tháng/năm tùy chỉnh. Lưu ý rằng, Admin có thể chỉ chọn 1 hoặc nhiều điều kiện trong 3 loại:

  • Ngày
  • Tháng
  • Năm
    để truy vấn. Ví dụ:
  • Truy vấn tất cả những bản ghi trong ngày 20 và tháng 01. Không quan tâm đến năm
  • Truy vấn tất cả những bản ghi trong tháng 02. Không quan tâm đến ngày và năm

Okay, nhìn vào yêu cầu bài toán, chúng ta sẽ biết được mình sẽ cần truy vấn dựa trên field nào rồi đúng không nào? Đó chính là field date

2. Hiệu chỉnh dữ liệu

Như các bạn thấy, với schema ở bài trước, ta đang định nghĩa trường date có kiểu dữ liệu là String. Tuy nhiên, thực tế, để tiện cho quá trình truy vấn mà không phải dùng đến những phép toán phức tạp, ta nên lưu chúng dưới dạng Date. Từ giờ các bạn có thể hiểu rằng, trường date của chúng ta sẽ có dữ liệu dạng Date nhé

Với những ai quan tâm đến phần thực nghiệm ở bài trước, các bạn có thể xem phần Thực nghiệm của bài này ở phía dưới, mình sẽ cung cấp code để các bạn thực hiện việc chuyển đổi nha

3. Thử truy vấn và nhìn nhận vấn đề

Ở bài trước, chúng ta đã tạo 1 INDEX mới là: userId_1_date_1 vì truy vấn của chúng ta dựa trên 2 trường userIddate. Tuy nhiên, bây giờ ta chỉ cần truy vấn trên trường date mà thôi. Do đó, mình sẽ tạo thêm 1 INDEX cho trường date này nhé:

image.png

Ví dụ, để tìm những documents trong tháng 02, năm 2024, truy vấn của chúng ta sẽ là:

{ $expr: { $and: [ { $eq: [{ $year: "$date" }, 2024] }, { $eq: [{ $month: "$date" }, 2] } ] }
}

Thử chạy truy vấn cùng EXPLAIN xem kết quả ra sao nha:

Phân tích chút nào:

  • Số documents thỏa mãn điều kiện: 2_064_465 (chiếm khoảng 15% tổng số documents (13_750_254))
  • INDEX được sử dụng: 0, dẫn đến câu truy vấn trên đã phải quét qua toàn bộ 13_750_254 documents của bảng
  • Thời gian thực thi: Khoảng 11.8s

Lạ nhỉ, rõ ràng chúng ta đã đánh INDEX cho trường date rồi mà? Truy vấn cũng không hể có bất kì điều kiện nào khác.

Vâng, lý do là vì MongoDB phải tính toán giá trị như $year$month trong câu truy vấn của chúng ta tại thời điểm thực thi, tức là trên từng document một, vì thế không thể sử dụng INDEX được.

Hmm, thế có cách nào khác không nhỉ? À, mình có thể truy vấn so sánh trực tiếp trên trường date luôn cho ví dụ này:

{ date: { $gte: ISODate("2024-02-01T00:00:00.000Z"), $lt: ISODate("2024-03-01T00:00:00.000Z") }
}

Thử EXPLAIN câu truy vấn này xem sao nhé: image.png

Oh NICE!!! Dữ liệu trả về đúng rồi, và điều đặc biệt là chúng ta đã dùng được INDEX trên trường date, nhờ vậy mà thời gian thực thi chỉ còn khoảng 2s, số lượng documents được quét qua để kiểm tra cũng giảm hẳn 85% rồi nè

Tuy nhiên, đừng vội mừng, giờ thử với yêu cầu mới như sau nha:

Tìm những document có tháng là 02. Không quan tâm ngàynăm

Với trường hợp này, chúng ta không thể dùng truy vấn so sánh ở trên nữa, mà buộc phải dùng toán từ $month để truy vấn:

{ $expr: { $and: [ { $eq: [{ $month: "$date" }, 2] } ] }
}

Và như đã nói, INDEX trên trường date của chúng ta vô dụng trong trường hợp này

4. Giải quyết vấn đề

Để giải quyết vấn đề, điều cần làm là suy nghĩ làm thế nào để tạo được INDEX và tận dụng được nó cho những truy vấn dạng filter đặc biệt như này. Và với bài toán kinh điển này, ta có 1 cách giải quyết vô cùng đơn giản nhưng cực kỳ hiệu quả, đó chính là:

Tách riêng ngày/tháng/năm cho mỗi document và tạo INDEX dựa trên các trường đó. Từ đó, khi thực hiện truy vấn, ta có thể tận dụng được INDEX dù cho có bỏ qua 1 hay nhiều điều kiện bên trên

Hãy thử nhé, schema lúc này của chúng ta sẽ trông như thế này:

{ "_id": { "$oid": "6827eff09b43f525fcf9c3f4" }, "userId": 4, "date": { "$date": "2024-02-01T00:00:00.000Z" }, "value": 74, "day": 1, "month": 2, "year": 2024
}

Ta sẽ đánh INDEX trên cả 3 field này nhé: image.png

Rồi, giờ cùng nhìn lại 2 ví dụ ở trên nhé:

Ví dụ 1: Tìm những documents trong tháng 02, năm 2024

{month: 2, year: 2024}

và đây là kết quả: image.png

Ví dụ 2: Tìm những document có tháng là 02. Không quan tâm ngàynăm

{month: 2}

Tương tự, xem chiến lược nào:

image.png

Các bạn có thể thấy, ta đã tận dụng được INDEX rồi, các bạn cũng có thể thử khi truy vấn với từng điều kiện kết hợp giữa ngày/tháng/năm nhé

5. Thực nghiệm

Trước tiên, ta cần chuyển đổi dữ liệu của trường date từ String sang Date. Các bạn có thể tạo thêm 1 file, mình đặt tên là convert.js để ta chuyển đổi kiểu dữ liệu cho trường date và lưu vào 1 bảng mới tên là analyst_date nhé:

const { MongoClient } = require("mongodb"); const uri = "mongodb://localhost:27017";
const dbName = "demo_dynamic_fields";
const sourceCollectionName = "analyst_optimized";
const targetCollectionName = "analyst_date";
const BATCH_SIZE = 1000; async function convert() { const client = new MongoClient(uri); try { await client.connect(); const db = client.db(dbName); const sourceCollection = db.collection(sourceCollectionName); const targetCollection = db.collection(targetCollectionName); console.log("Connected to MongoDB"); // Optional: clear target collection first await targetCollection.deleteMany({}); console.log("Cleared target collection"); let totalInserted = 0; let batch = []; const cursor = sourceCollection.find({}); for await (const doc of cursor) { const convertedDoc = { ...doc, date: new Date(doc.date), }; batch.push(convertedDoc); if (batch.length >= BATCH_SIZE) { await targetCollection.insertMany(batch); totalInserted += batch.length; console.log(`Inserted ${totalInserted} documents`); batch = []; } } if (batch.length > 0) { await targetCollection.insertMany(batch); totalInserted += batch.length; console.log(`Inserted ${totalInserted} documents`); } console.log("Conversion complete!"); } catch (err) { console.error(err); } finally { await client.close(); }
} convert(); 

Chạy lệnh node convert.js để chạy file trên nha.

Tiếp theo, mình tạo thêm 1 file split_date.js để tách riêng các trường day, month, year từ field date nhé. File này viết 2 chiều nha, bao gồm cả việc xóa 3 field này khỏi schema nếu các bạn muốn hủy nó

const { MongoClient } = require("mongodb"); const uri = "mongodb://localhost:27017";
const dbName = "demo_dynamic_fields";
const collectionName = "analyst_date";
const BATCH_SIZE = 1000; const option = process.argv[2]; // 'add' or 'remove' if (!option || (option !== "add" && option !== "remove")) { console.error('Please provide an option: "add" or "remove"'); process.exit(1);
} async function splitDate() { 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 totalUpdated = 0; let batch = []; const cursor = collection.find({}); for await (const doc of cursor) { let updateDoc; if (option === "add") { const date = doc.date; updateDoc = { $set: { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), }, }; } else { updateDoc = { $unset: { year: "", month: "", day: "", }, }; } batch.push({ updateOne: { filter: { _id: doc._id }, update: updateDoc, }, }); if (batch.length >= BATCH_SIZE) { await collection.bulkWrite(batch); totalUpdated += batch.length; console.log(`Updated ${totalUpdated} documents`); batch = []; } } if (batch.length > 0) { await collection.bulkWrite(batch); totalUpdated += batch.length; console.log(`Updated ${totalUpdated} documents`); } console.log("Operation complete!"); } catch (err) { console.error(err); } finally { await client.close(); }
} splitDate();

Bây giờ, để thêm 3 fields trên vào schema, ta chạy lệnh:

node split_date.js add

Để remove 3 fields đó sau khi test, bạn chạy lệnh:

node split_date.js remove

Được rồi, vậy là xong setup, nhiệm vụ tiếp theo là đánh INDEX cho 3 fields day, month, year trong bảng analyst_date và thực nghiệm nội dung bài này thôi nhé

6. Cái giá phải trả

À thì, như bài trước, chúng ta sẽ phải hy sinh 1 chút về memory cho phương án này nhé. Còn đánh đổi hay không thì mình đã nêu lý do ở bài trước rồi nha.

7. After-credit

Bí mật về INDEX trong MongoDB chưa hết đâu, đừng vội "rời rạp" nhé. Hẹn gặp lại các bạn ở bài viết tiếp theo

Trong thời gian đó, đừng quên upvote/bookmark hoặc để lại ý kiến nếu bài này hữu ích với các bạn nhé. Nếu muốn connect với mình, bạn có thể connect qua:

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 53

- 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 61