What you'll learn
Quick Answer
find() is SELECT, the first argument is WHERE and the second chooses columns. Operators like $gt and $in replace SQL comparison syntax. The real difference is that documents can be nested, so related data is often embedded rather than joined.
The vocabulary maps almost directly
- Table becomes collection
- Row becomes document
- Column becomes field
- Primary key becomes _id, generated automatically if you do not supply one
A document is JSON-like, so a student record can hold an array of marks and a nested address object directly — no separate tables required. That single capability drives most of the design differences.
{
_id: ObjectId("..."),
name: "Asha",
stream: "Science",
marks: 91,
address: { city: "Pune", pin: "411001" }
}
There is also no enforced schema by default. Two documents in one collection can have different fields, which is flexible and dangerous in equal measure — see the last section.
find() is SELECT
// SELECT * FROM student
db.student.find({})
// SELECT * FROM student WHERE stream = 'Science'
db.student.find({ stream: "Science" })
// SELECT name, marks FROM student WHERE stream = 'Science'
db.student.find({ stream: "Science" }, { name: 1, marks: 1, _id: 0 })
The first argument is the filter, the second chooses which fields to return. In that second object, 1 includes and 0 excludes — and _id is returned unless you explicitly switch it off, which surprises people the first time.
Add ordering and limits by chaining:
db.student.find({}).sort({ marks: -1 }).limit(2)
// ORDER BY marks DESC LIMIT 2
-1 is descending, 1 ascending.
Operators replace comparison syntax
Since the filter is an object, comparisons need named operators:
db.student.find({ marks: { $gt: 80 } }) // marks > 80
db.student.find({ marks: { $gte: 80, $lt: 95 } }) // BETWEEN-ish
db.student.find({ stream: { $in: ["Science", "Arts"] } }) // IN
db.student.find({ stream: { $ne: "Commerce" } }) // !=
Combining conditions: listing several fields in one object is an implicit AND. For OR you need it explicitly:
db.student.find({ $or: [ { marks: { $gt: 90 } },
{ stream: "Arts" } ] })
Querying inside nested documents and arrays uses dot notation, and it works for both:
db.student.find({ "address.city": "Pune" })
If marks were an array, { marks: 91 } matches any document whose array contains 91. That behaviour is convenient and occasionally surprising.
Aggregation is GROUP BY
The aggregation pipeline runs documents through stages, each transforming the output of the last:
db.student.aggregate([
{ $match: { marks: { $gt: 60 } } },
{ $group: { _id: "$stream", avg: { $avg: "$marks" } } },
{ $sort: { avg: -1 } }
])
That is WHERE, then GROUP BY, then ORDER BY. In $group, _id is what you are grouping by, and $stream with the dollar prefix means "the value of the stream field".
Put $match as early as possible. Filtering before grouping means later stages handle fewer documents, and it lets an index do the work — the same reasoning as indexing in SQL.
What is genuinely different
Embedding versus joining. In SQL you normalize and join. In MongoDB you often embed related data inside the parent document, so reading it is a single lookup. That is faster to read and duplicates data, which brings back the update problem normalization exists to prevent. The usual guidance is to embed data that is read together and owned by the parent, and reference data that is shared or grows without limit.
No enforced schema by default. Two documents can have different fields, so an application bug can write mark instead of marks and nothing complains. Queries then silently miss those documents. Use schema validation, or a layer like Mongoose, to get that safety back.
Joins exist but are secondary. $lookup performs a join in an aggregation pipeline. It works, but if your data model needs it everywhere, that is a signal a relational database may suit the problem better.
