Khi làm việc với cơ sở dữ liệu, khả năng truy vấn dữ liệu là kỹ năng cốt lõi mà bất kỳ lập trình viên nào cũng cần nắm vững. Với MongoDB – cơ sở dữ liệu NoSQL phổ biến nhất hiện nay – cú pháp truy vấn khá linh hoạt, mạnh mẽ và gần gũi nhờ được thiết kế dựa trên JSON. Bài viết này sẽ giúp bạn có một cái nhìn rõ ràng hơn về cách truy vấn trong MongoDB.
Đọc bài viết này để hiểu rõ hơn về:
- Các loại truy vấn trong MongoDB
- Một số cách tối ưu truy vấn
Tổng quan về MongoDB
MongoDB là một hệ quản trị cơ sở dữ liệu NoSQL dạng tài liệu (document-oriented database). Nó được thiết kế để xử lý lượng dữ liệu lớn một cách nhanh chóng và linh hoạt.
Thay vì lưu trữ dữ liệu trong các bảng (tables) với hàng và cột cố định như cơ sở dữ liệu quan hệ (RDBMS), MongoDB sử dụng document (tài liệu) làm đơn vị lưu trữ chính. Mỗi document được biểu diễn dưới dạng BSON (Binary JSON) – một định dạng nhị phân mở rộng của JSON, hỗ trợ thêm các kiểu dữ liệu như Date, ObjectId, Binary data, có thể linh hoạt thay đổi về số lượng và kiểu dữ liệu của các trường (fields).
Chính sự linh hoạt này giúp MongoDB đặc biệt phù hợp với các ứng dụng hiện đại khi dữ liệu thay đổi liên tục và khó “ép buộc” vào một schema cứng nhắc.
Đọc chi tiết: MongoDB là gì? Định nghĩa và Hiểu rõ A-Z về MongoDB
Các khái niệm cơ bản trong MongoDB
- Database (Cơ sở dữ liệu): Tập hợp các collections (bộ sưu tập). Một instance MongoDB có thể chứa nhiều databases độc lập với nhau.
- Collection: Có thể hình dung như “bảng” trong SQL, nhưng không bị ràng buộc bởi cấu trúc cứng. Mỗi collection có thể chứa nhiều document với cấu trúc khác nhau.
- Document (Tài liệu): Đơn vị dữ liệu cơ bản trong MongoDB, lưu dưới dạng BSON (hiển thị dưới dạng JSON khi thao tác). Ví dụ:
{ "name": "Alice", "age": 25, "email": "alice@example.com" }
- Field (Trường): Các cặp key-value trong document. Ví dụ:
name: "Alice"chính là một field. - _id: Mỗi document đều có một trường _id duy nhất đóng vai trò là primary key, được MongoDB tự động tạo nếu không chỉ định.
Điểm nổi bật của MongoDB là gì?
- Linh hoạt về schema: Không cần định nghĩa trước, có thể thêm hoặc bớt trường khi dữ liệu thay đổi mà không cần chuyển đổi (migration) phức tạp như RDBMS.
- Mở rộng dễ dàng: Nhờ cơ chế sharding (phân mảnh ngang), dữ liệu có thể chia nhỏ và phân phối trên nhiều máy chủ, đảm bảo hệ thống vận hành tốt ngay cả khi lượng dữ liệu khổng lồ.
Tính sẵn sàng cao: Với replication (cơ chế Replica Set), dữ liệu được sao chép sang nhiều máy chủ, giúp giảm thiểu rủi ro mất mát khi có sự cố.
Truy vấn phong phú: Hỗ trợ từ các câu lệnh đơn giản đến nâng cao như text search, truy vấn địa lý (geospatial queries) hay pipeline tổng hợp (aggregation pipeline) để xử lý dữ liệu phức tạp. - Thân thiện với hệ sinh thái hiện đại: Được sử dụng rộng rãi trong phát triển web, ứng dụng di động (mobile), big data, phân tích dữ liệu thời gian thực (real-time analytics) và cả IoT (Internet of Things).
Các loại truy vấn trong MongoDB
Trước khi tìm hiểu các loại truy vấn, ta cần chuẩn bị sẵn một collection mẫu để dễ thực hành. Giả sử chúng ta đang có một collection users trong database test, dữ liệu ban đầu như sau:
db.test.insertMany([
{ name: "Alice", age: 25, email: "alice@gmail.com", gender: "Female", interests: ["Reading", "Traveling"] },
{ name: "Bob", age: 30, email: "bob@yahoo.com", gender: "Male", interests: ["Gaming", "Cooking"] },
{ name: "Charlie", age: 28, email: "charlie@gmail.com", gender: "Male", interests: ["Sports", "Traveling"] },
{ name: "David", age: 35, email: "david@hotmail.com", gender: "Male", interests: ["Cooking", "Reading"] },
{ name: "Eva", age: 22, email: "eva@gmail.com", gender: "Female", interests: ["Dancing", "Music"] }
])
Truy vấn đơn giản
- Để lấy toàn bộ dữ liệu trong collection ta dùng cú pháp:
db.users.find()
- Để lấy bản ghi đầu tiên ta dùng cú pháp:
db.users.findOne()
Kết quả: Trả về user đầu tiên MongoDB tìm thấy là Alice
Truy vấn với điều kiện
- Tìm bản ghi với điều kiện cụ thể: Ví dụ để tìm user có tên là Alice:
db.users.find({ name: "Alice" })
- Tìm user vừa có tên Alice vừa có tuổi 25:
db.users.find({ name: "Alice", age: 25 })
Kết quả: MongoDB chỉ trả về document của Alice nếu đồng thời thỏa cả 2 điều kiện (đây là toán tử AND ngầm định).
- Truy vấn với nhiều điều kiện (
AND)
Ví dụ để tìm user là nam và có tuổi > 25 ta dùng:
db.users.find({ $and: [ { age: { $gt: 25 } }, { gender: "Male" } ] })
Kết quả sẽ trả về Bob (30), Charlie (28), David (35).
Lưu ý: Trong hầu hết trường hợp, bạn không cần dùng $and một cách tường minh, vì MongoDB mặc định áp dụng AND khi có nhiều điều kiện. Cú pháp đơn giản hơn:
db.users.find({ age: { $gt: 25 }, gender: "Male" })
- Truy vấn với toán tử OR
Ví dụ để tìm user có tuổi < 25 hoặc là nữ ta có cú pháp như sau:
db.users.find({ $or: [ { age: { $lt: 25 } }, { gender: "Female" } ] })
Kết quả sẽ trả về Alice (25, Female) và Eva (22, Female).
Truy vấn với toán tử
| Toán tử | Mô tả | Ví dụ |
$gt | Lớn hơn | db.users.find({ age: { $gt: 25 } }) |
$gte | Lớn hơn hoặc bằng | db.users.find({ age: { $gte: 30 } }) |
$lt | Nhỏ hơn | db.users.find({ age: { $lt: 30 } }) |
$lte | Nhỏ hơn hoặc bằng | db.users.find({ age: { $lte: 25 } }) |
$ne | Khác với | db.users.find({ gender: { $ne: "Male" } }) |
$in | Trong danh sách | db.users.find({ name: { $in: ["Alice", "Eva"] } }) |
$nin | Không nằm trong danh sách | db.users.find({ name: { $nin: ["Alice", "Eva"] } }) |
$regex | Khớp chuỗi | db.users.find({ email: { $regex: /gmail/ } }) |
- Ví dụ để tìm user có tuổi lớn hơn 25:
db.users.find({ age: { $gt: 25 } })
Kết quả: Với câu lệnh trên sẽ trả về Bob (30), Charlie (28), David (35).
- Ví dụ để tìm user có email chứa “gmail”:
db.users.find({ email: { $regex: /gmail/ } })
Kết quả sẽ trả về Alice, Charlie, Eva.
- Ví dụ để tìm user có tên trong danh sách [“Alice”, “Eva”]:
db.users.find({ name: { $in: ["Alice", "Eva"] } })
Kết quả sẽ trả về Alice và Eva.
Một số câu truy vấn trong MongoDB khác
Projection / Field Filtering (chỉ lấy trường cần thiết)
Đôi khi dữ liệu có rất nhiều field, nhưng bạn chỉ muốn xem một vài field quan trọng. Ví dụ để chỉ lấy trường name và email, bỏ qua các field khác thì ta dùng cú pháp:
db.users.find({}, { name: 1, email: 1, _id: 0 })
Kết quả trả về chỉ hiển thị name và email mà không có _id. Trong đó:
1: nghĩa là bao gồm (include)0: nghĩa là loại trừ (exclude)
Lưu ý: Bạn không thể kết hợp inclusion và exclusion trong cùng một truy vấn, ngoại trừ trường hợp đặc biệt với _id.
Sorting (Sắp xếp dữ liệu)
Dùng để sắp xếp kết quả theo một hoặc nhiều trường. Ví dụ để sắp xếp danh sách user theo tuổi giảm dần, ta dùng cú pháp:
db.users.find().sort({ age: -1 })
Kết quả trả về danh sách user từ người lớn tuổi nhất đến trẻ nhất. Trong đó:
-1: là giảm dần1: là tăng dần
Limit (Giới hạn số kết quả)
Ví dụ để chỉ lấy 2 user đầu tiên trong tập kết quả, ta dùng cú pháp:
db.users.find().limit(2)
Kết quả trả về chỉ gồm 2 document đầu tiên của collection.
Skip (Bỏ qua số bản ghi)
Thường kết hợp với limit để phân trang. Ví dụ bỏ qua 2 bản ghi đầu và lấy 2 bản ghi tiếp theo:
db.users.countDocuments({ gender: "Female" })
Hữu ích cho phân trang (pagination). Ví dụ: trang 1 sẽ lấy các bản ghi bằng skip(0).limit(10), còn trang 2 là skip(10).limit(10) – nghĩa là bỏ qua 10 bản ghi đầu tiên và lấy tiếp 10 bản ghi kế tiếp
Count Documents (Đếm số bản ghi thỏa điều kiện)
Ví dụ để đếm số lượng user là nữ, ta dùng cú pháp:
db.users.countDocuments({ gender: "Female" })
Kết quả trả về một con số, ví dụ như 5.
Distinct (Lấy giá trị duy nhất của một field)
Thay vì lấy dữ liệu, chỉ muốn biết có bao nhiêu bản ghi thỏa điều kiện. Ví dụ:
db.users.distinct("gender")
Kết quả sẽ trả về ["Male", "Female"].
Tổng hợp các lệnh truy vấn trong MongoDB
| Lệnh | Chức năng | Cách sử dụng | Ví dụ ứng dụng |
db.test.find() | Lấy toàn bộ document trong collection | Không cần tham số, trả về tất cả dữ liệu | Xem nhanh toàn bộ dữ liệu trong collection để kiểm tra nhập liệu |
db.test.findOne({field: value}) | Lấy 1 document đầu tiên thỏa điều kiện | Truyền vào object điều kiện | Tìm thông tin một người dùng cụ thể theo tên |
db.test.find({field1: value1, field2: value2}) | Truy vấn nhiều trường cùng lúc | Ghi các cặp key-value trong object | Tìm user vừa có name = Enayet và age = 36 |
db.test.find({field: {$exists: true}}) | Kiểm tra sự tồn tại của trường | Dùng $exists: true/false | Lấy tất cả tài liệu có chứa trường age (hoặc không có trường này) |
db.test.find({field: {$eq: value}}) | So sánh bằng | Dùng $eq trong object điều kiện | Tìm user có age = 36 |
db.test.find({field: {$ne: value}}) | So sánh khác | Dùng $ne trong object điều kiện | Tìm tất cả user không phải Male |
db.test.find({field: {$gt: value}}) | Lớn hơn | Dùng $gt trong object điều kiện | Tìm người lớn tuổi hơn 20 |
db.test.find({field: {$gte: value}}) | Lớn hơn hoặc bằng | Dùng $gte trong object điều kiện | Lấy người 20 tuổi trở lên |
db.test.find({field: {$lt: value}}) | Nhỏ hơn | Dùng $lt trong object điều kiện | Lấy trẻ em dưới 5 tuổi |
db.test.find({field: {$lte: value}}) | Nhỏ hơn hoặc bằng | Dùng $lte trong object điều kiện | Lấy trẻ <= 3 tuổi |
db.test.find({field: {$in: [v1, v2]}}) | Thuộc tập giá trị | Dùng $in với array giá trị | Tìm user tên là Enayet hoặc Mariyam |
db.test.find({field: {$nin: [v1, v2]}}) | Không thuộc tập giá trị | Dùng $nin với array giá trị | Lấy người có tuổi khác 1 và 3 |
db.test.find({ $and: [cond1, cond2] }) | Kết hợp nhiều điều kiện (AND tường minh) | Dùng $and với mảng các object | Tìm user age > 20 và gender = Male |
db.test.find({ $or: [cond1, cond2] }) | Thỏa ít nhất 1 điều kiện (OR) | Dùng $or với mảng các object | Tìm người dưới 5 tuổi hoặc giới tính Female |
db.test.find({arrayField: {$all: [v1, v2]}}) | Array chứa tất cả giá trị | Dùng $all với array | Tìm người có cả sở thích Travelling & Cooking |
db.test.find({arrayField: {$elemMatch: {...}}}) | Array object thỏa tất cả điều kiện | Dùng $elemMatch | Tìm người có skill JavaScript ở mức Intermediate |
Làm thế nào để tối ưu truy vấn trong MongoDB?
1. Sử dụng Index (Chỉ mục)
Index giúp MongoDB tìm dữ liệu nhanh hơn, giống như mục lục trong sách. Thay vì phải lật từng trang, MongoDB chỉ cần tra mục lục để đến đúng vị trí. Index đặc biệt quan trọng khi collection có hàng triệu documents.
Ví dụ để tìm user theo email nhanh hơn, ta tạo index cho field email như sau:
// Tạo index trên field "email"
db.users.createIndex({ email: 1 })
// Truy vấn nhanh hơn khi có index
Kết quả: Với index trên, MongoDB sẽ truy cập thẳng đến document có email = alice@example.com thay vì duyệt toàn bộ dữ liệu.
- Các loại index phổ biến:
// Single Field Index
db.users.createIndex({ age: 1 })
// Compound Index (index nhiều trường) - thứ tự quan trọng!
db.users.createIndex({ gender: 1, age: -1 })
// Unique Index (đảm bảo giá trị duy nhất)
db.users.createIndex({ email: 1 }, { unique: true })
// Text Index (cho full-text search)
db.users.createIndex({ name: "text", interests: "text" })
// Multikey Index (tự động cho array fields)
db.users.createIndex({ interests: 1 })
Lưu ý:
- Index tăng tốc độ đọc nhưng làm chậm các thao tác ghi như
insert,update,deletevì dữ liệu phải được cập nhật thêm trong cấu trúc chỉ mục. - Mỗi index chiếm thêm dung lượng lưu trữ, do đó việc tạo quá nhiều index không cần thiết có thể làm giảm hiệu suất tổng thể.
- Cách kiểm tra index hiện có:
db.users.getIndexes()
- Cách xóa index không còn sử dụng:
db.users.dropIndex("index_name")
2. Projection – Chỉ lấy dữ liệu cần thiết
Khi truy vấn, MongoDB mặc định trả về toàn bộ các field trong document. Nhưng nếu chỉ cần vài trường thông tin, việc lấy tất cả sẽ lãng phí tài nguyên, làm truy vấn chậm hơn và tốn băng thông. Điều này càng đáng chú ý khi document chứa trường lớn như danh sách (array) hoặc tài liệu lồng nhau (embedded document).
Vì vậy chỉ lấy những dữ liệu cần thiết để tránh tốn tài nguyên. Ví dụ để chỉ lấy name và email (bỏ qua _id), ta dùng cú pháp:
db.users.find({}, { name: 1, email: 1, _id: 0 })
Projection nâng cao
// Loại trừ một trường cụ thể (exclusion)
db.users.find({}, { interests: 0 })
// Lấy phần tử đầu tiên của array
db.users.find({}, { interests: { $slice: 1 } })
// Lấy 2 phần tử cuối của array
db.users.find({}, { interests: { $slice: -2 } })
3. Sử dụng Explain để phân tích truy vấn
MongoDB hỗ trợ lệnh explain() để phân tích truy vấn đang chạy nhanh hay chậm, có dùng index hay không. Ví dụ để kiểm tra truy vấn tìm user có age > 25, ta dùng cú pháp:
db.users.find({ age: { $gt: 25 } }).explain("executionStats")
Kết quả trả về cho biết MongoDB đã quét bao nhiêu document, có dùng index hay không rồi từ đó tinh chỉnh thêm.
4. Giới hạn (Limit) và phân trang
Nếu bạn chỉ cần một phần dữ liệu (ví dụ: top 10 bản ghi mới nhất), hãy dùng limit() hoặc kết hợp với skip() để phân trang. Tuy nhiên skip() với số lớn rất chậm, nên dùng phương pháp phân trang dựa trên range queries. Ví dụ:
// Lấy 10 user đầu tiên, sắp xếp theo tuổi giảm dần
db.users.find().sort({ age: -1 }).limit(10)
// Lấy trang thứ 2 (bỏ qua 10 bản ghi đầu)
db.users.find().sort({ age: -1 }).skip(10).limit(10)
Phân trang tối ưu hơn (Range-based pagination):
// Trang đầu tiên
db.users.find().sort({ _id: -1 }).limit(10)
// Trang tiếp theo (dùng _id của bản ghi cuối trang trước)
db.users.find({ _id: { $lt: lastSeenId } }).sort({ _id: -1 }).limit(10)
5. Sử dụng Covered Query
Covered Query là loại truy vấn mà toàn bộ các trường được truy vấn và trả về đều nằm trong index. Nhờ đó, MongoDB không cần đọc document gốc trong collection, giúp tăng tốc độ truy vấn.
Ví dụ:
// Tạo compound index
db.users.createIndex({ gender: 1, age: 1, name: 1 })
// Covered query - cực nhanh!
db.users.find(
{ gender: "Male", age: { $gt: 25 } },
{ gender: 1, age: 1, name: 1, _id: 0 }
)
- Trong đó: Cả điều kiện lọc (
gender,age) và các trường trả về (gender,age,name) đều nằm trong index{ gender: 1, age: 1, name: 1 }. Vì vậy, MongoDB có thể trả kết quả trực tiếp từ index mà không cần truy cập document gốc.
Khi kiểm tra bằng lệnh explain(), ta sẽ thấy:
"totalDocsExamined" : 0
Nghĩa là không có document nào được đọc từ đĩa, tất cả dữ liệu đều được lấy từ index
Câu hỏi thường gặp về các truy vấn trong MongoDB
Truy vấn trong MongoDB có khác gì so với SQL không?
Có. MongoDB dùng cú pháp dựa trên JSON thay vì SQL.
Ví dụ, thay vì SELECT * FROM users WHERE age > 25, ta sẽ dùng:
db.users.find({ age: { $gt: 25 } })
MongoDB có hỗ trợ truy vấn phức tạp không?
Có. MongoDB cung cấp nhiều toán tử (operators) như $and, $or, $in, $regex và hệ thống Aggregation Pipeline để xử lý dữ liệu phức tạp như lọc, nhóm, tính toán.
Truy vấn trong MongoDB có phân biệt hoa thường không?
Có. Mặc định MongoDB phân biệt chữ hoa và thường. Tuy nhiên, ta có thể dùng $regex kèm flag i để bỏ qua phân biệt.
db.users.find({ name: { $regex: /^alice$/i } })
Tổng kết
Truy vấn là bước nền tảng để bạn khai thác tối đa sức mạnh của MongoDB. Chỉ cần nắm vững cú pháp cơ bản, hiểu rõ các toán tử và biết cách tối ưu như ITviec đã chia sẻ ở trên, bạn đã có thể tự tin xây dựng những hệ thống xử lý dữ liệu linh hoạt, nhanh chóng và hiệu quả, từ đó dễ dàng tiến tới các kỹ năng nâng cao như Aggregation Pipeline, Indexing hay Data Modeling.

