NoSQL Foundations and MongoDB
Before you start
You need: programming basics and JSON familiarity. SQL (Track 06) helps for the comparisons but is not required.
You need installed: MongoDB, plus MongoDB Compass (the GUI) and mongosh (the shell).
Time: about 45 minutes, plus the practice.
Learning objective
Explain what a document database stores, how it differs from a relational one, and judge which suits a given problem.
Topics
- What NoSQL means
- SQL versus document databases
- Databases, collections and documents
- JSON and BSON
- Installing MongoDB
- Compass and mongosh
- The
_idfield - When to choose which
What NoSQL means
"NoSQL" covers several unrelated database families. The one this track teaches is the document store.
| Family | Stores | Example |
|---|---|---|
| Document | JSON-like documents | MongoDB |
| Key-value | Values by key | Redis |
| Column-family | Wide sparse rows | Cassandra |
| Graph | Nodes and edges | Neo4j |
The name is misleading — MongoDB has a rich query language, and modern SQL databases store JSON. The real distinction is the data model: fixed rows and columns versus flexible documents.
This track assumes SQL Server first. Relational modelling, joins, constraints and transactions are the reference point, and MongoDB is easier to judge once you know what you are trading away.
SQL versus document
-- Relational: a student across three tables
Student (Id, SchoolId, Name, RollNumber, ClassName, Section)
Address (Id, StudentId, Line1, City, PinCode)
Subject (Id, StudentId, Name, Marks)
// Document: one student, one document
{
_id: ObjectId("6650a1b2c3d4e5f6a7b8c9d0"),
schoolId: 1,
name: "Ravi Kumar",
rollNumber: "NCA-2024-0012",
className: "10th",
section: "A",
address: {
line1: "12 MG Road",
city: "Hyderabad",
pinCode: "500001"
},
subjects: [
{ name: "Mathematics", marks: 87 },
{ name: "Science", marks: 72 }
]
}
Reading a student with their address and subjects is three joins in SQL and one document read in MongoDB. That is the core trade.
| SQL | Document | |
|---|---|---|
| Structure | Fixed schema, enforced | Flexible per document |
| Related data | Normalised, joined | Often embedded |
| Reading one entity | Several joins | One read |
| Reporting across entities | Natural | Harder |
| Constraints | Foreign keys, CHECK, UNIQUE | Unique indexes and validation rules only |
| Transactions | Mature, everywhere | Supported, less commonly used |
| Schema changes | A migration | Write the new shape |
Terminology
| SQL | MongoDB |
|---|---|
| Database | Database |
| Table | Collection |
| Row | Document |
| Column | Field |
| Primary key | _id |
| Index | Index |
| Join | $lookup (or embedding) |
SELECT | find |
INSERT | insertOne / insertMany |
UPDATE | updateOne / updateMany |
DELETE | deleteOne / deleteMany |
Flexible schema
Two documents in one collection need not match:
{ _id: 1, name: "Ravi Kumar", className: "10th" }
{ _id: 2, name: "Priya Sharma", className: "10th", section: "A", scholarship: true }
That flexibility is genuinely useful and genuinely dangerous.
Useful: adding a field needs no migration, and optional data costs nothing.
Dangerous: nothing stops className: "10th" in one document and class_name: 10 in another. A query on className silently misses half the collection, and no error is raised.
Flexible schema does not mean no schema. It means the schema lives in your application code and your discipline rather than in the database — so a code review is the only thing enforcing it, unless you add validation rules.
Databases, collections and documents
MongoDB deployment
└── Database nexcoding_school
└── Collection students
└── Document { _id: ..., name: "Ravi Kumar", ... }
└── Field name, rollNumber, className
Databases and collections are created lazily — on first write, not by a CREATE statement:
use nexcoding_school // does not exist yet
db.students.insertOne({ name: "Ravi Kumar" }) // now both exist
That is convenient and it is why a typo in a collection name creates a second, empty collection rather than failing. db.studnets.find() returns nothing and reports no error.
Check show collections when a query returns nothing unexpectedly.
JSON and BSON
MongoDB stores BSON — binary JSON — which adds types JSON lacks.
| BSON type | Notes |
|---|---|
ObjectId | The default _id — 12 bytes, includes a timestamp |
Date | A real date, not a string |
Int32 / Int64 | Distinct integer widths |
Double | Floating point |
Decimal128 | Exact decimal — use for money |
Boolean, String, Array, Object, Null | As in JSON |
Binary | Binary data |
{
_id: ObjectId("6650a1b2c3d4e5f6a7b8c9d0"),
admittedOn: ISODate("2024-06-15T00:00:00Z"),
totalFees: NumberDecimal("50000.00"),
marks: NumberInt(87),
isActive: true
}
Use NumberDecimal for money. The default numeric type is Double, which carries the same binary rounding error as float everywhere else — fee totals drift and receipts stop reconciling.
Store dates as ISODate, not strings. A string date cannot be compared with $gt, cannot be sorted correctly across formats, and cannot use date operators. This is the most common modelling mistake.
// Wrong
{ admittedOn: "2024-06-15" }
// Right
{ admittedOn: ISODate("2024-06-15T00:00:00Z") }
Type mismatches are silent:
db.students.insertOne({ rollNumber: "NCA-2024-0012" })
db.students.insertOne({ rollNumber: 20240012 }) // a number
db.students.find({ rollNumber: "NCA-2024-0012" }) // finds only the first
MongoDB does not coerce. A field stored as a string and queried as a number matches nothing, with no error — the single most common cause of "my query returns no results".
The _id field
Every document has _id. MongoDB generates an ObjectId when you do not supply one.
ObjectId("6650a1b2c3d4e5f6a7b8c9d0")
// └──────┘└────┘└──┘└────┘
// timestamp machine pid counter
| Property | Detail |
|---|---|
| Unique | Within the collection |
| Immutable | Cannot be changed after insert |
| Indexed | Automatically, and the index cannot be dropped |
| Sortable by time | The first four bytes are a timestamp |
ObjectId("6650a1b2c3d4e5f6a7b8c9d0").getTimestamp()
Sorting by _id is approximately sorting by creation time, which is often enough to avoid a separate createdAt field.
You may supply your own:
db.students.insertOne({ _id: "NCA-2024-0012", name: "Ravi Kumar" })
Use a natural key as _id only when it genuinely never changes. A roll number that gets renumbered means every reference to it must change, and _id is immutable — so the fix is deleting and reinserting the document.
An ObjectId is not a string. This is a constant source of confusion:
db.students.find({ _id: "6650a1b2c3d4e5f6a7b8c9d0" }) // finds nothing
db.students.find({ _id: ObjectId("6650a1b2c3d4e5f6a7b8c9d0") }) // correct
An id arriving from a URL or a JSON body is a string and must be converted before querying.
Installing
Local, for development:
Download MongoDB Community Server from mongodb.com and install it as a service. Install MongoDB Compass — the GUI — and mongosh, the shell.
Atlas, the hosted option, has a free tier and is the fastest way to start. It requires adding your IP to an access list; forgetting to is why a connection times out with no useful message.
Docker, for a disposable local instance:
docker run -d --name mongo -p 27017:27017 mongo:7
mongodb://localhost:27017
mongodb://user:password@localhost:27017/nexcoding_school
mongodb+srv://user:password@cluster.mongodb.net/nexcoding_school
mongodb+srv:// is the Atlas form — it resolves the cluster's hosts from DNS.
Never commit a connection string. It contains the password, and Git history is permanent.
Compass and mongosh
Compass — connect, browse collections, edit documents, build queries visually, view indexes and explain plans. The Schema tab samples a collection and reports which fields exist, their types and how often each appears. That is the fastest way to find inconsistent data in a flexible-schema collection.
mongosh — the shell, and a full JavaScript environment:
mongosh "mongodb://localhost:27017"
show dbs
use nexcoding_school
show collections
db.students.countDocuments()
db.students.findOne()
db.students.find().limit(5)
db.students.find().pretty()
db.students.drop()
db.dropDatabase()
use x switches database and creates it lazily. db is the current database.
db.students.insertMany([
{ schoolId: 1, name: "Ravi Kumar", rollNumber: "NCA-2024-0012",
className: "10th", section: "A", marks: NumberInt(87) },
{ schoolId: 1, name: "Priya Sharma", rollNumber: "NCA-2024-0018",
className: "10th", section: "A", marks: NumberInt(91) },
{ schoolId: 1, name: "Arjun Reddy", rollNumber: "NCA-2024-0031",
className: "9th", section: "B", marks: NumberInt(65) }
])
Because mongosh is JavaScript, scripting works:
db.students.find({ className: "10th" }).forEach(s => print(s.name))
const total = db.students.find({ className: "10th" })
.toArray()
.reduce((sum, s) => sum + s.marks, 0)
mongosh "mongodb://localhost:27017/nexcoding_school" --file seed.js
mongosh --file runs a script — the equivalent of sqlcmd -i, and the right way to seed a development database reproducibly.
When to choose which
Document databases suit:
- Data naturally read as one unit — a student with their address and subjects
- Shapes that vary between records
- Rapidly changing requirements early in a project
- Catalogues, content, event logs, session data
Relational databases suit:
- Data queried across entities in many combinations
- Reporting and ad-hoc analysis
- Rules that must be guaranteed — foreign keys, uniqueness,
CHECKconstraints - Anything financial
For the School Management System, SQL Server is the better fit, and it is worth being explicit about why:
- Fee payments need transactions and exact decimal arithmetic
- Exam results are queried across students, subjects and exams in every combination
RollNumbermust be unique per school, and aCHECKmust prevent an absent student having marks- Reporting is the main workload
A document database can do all of that, with more application code and fewer guarantees.
Where MongoDB would fit in the same system: an audit log, notification records, uploaded document metadata, or a flexible "additional details" area whose shape differs per school. Using both is a normal and sensible design.
Do not choose a database because it is newer. Choose it because the data model matches how the data is written and read.
Errors you will hit
| Message | Cause | Fix |
|---|---|---|
MongoNetworkError: connect ECONNREFUSED | Service not running, or wrong port | Start MongoDB; default port is 27017 |
Authentication failed | Wrong credentials or auth database | Check the connection string |
| A query returns nothing but the data is there | Wrong database or collection selected | use <db>; show collections |
TypeError: db.students.find is not a function | Typo in the collection name | Names are case-sensitive |
Numbers come back as Double unexpectedly | Inserted without a type | Use NumberDecimal for money |
Collection names are case-sensitive and created on first write. A typo does not error — it silently makes a new, empty collection.
Common mistakes
- Treating flexible schema as no schema
- Inconsistent field names across documents
- The same field stored as a string in some documents and a number in others
- Storing dates as strings
- Using
Doublefor money instead ofDecimal128 - Querying
_idwith a string instead of anObjectId - A typo creating a new empty collection silently
- A natural key as
_idwhen it can change - Committing a connection string
- Choosing MongoDB for data that needs transactions and constraints
Practice
The course exercise is create a document collection.
- Install MongoDB locally or create an Atlas free-tier cluster. Connect with Compass and mongosh.
- Create
nexcoding_schooland insert five students withinsertMany. - Run
show dbsbefore and after the first insert. Confirm lazy creation. - Query a misspelled collection name. Confirm no error and no results, then run
show collections. - Insert one student with
rollNumberas a string and one as a number. Query for the string and confirm only one matches. - Insert
totalFeesas a plain number and asNumberDecimal. Add each to itself repeatedly and compare. - Store
admittedOnas a string and asISODate. Try$gtagainst a date on both. - Find a document, copy its
_id, and query with the raw string. Confirm nothing is found, then wrap it inObjectId(). - Call
.getTimestamp()on anObjectIdand confirm it matches the insert time. - Insert a document with your own string
_id. Try to change it withupdateOne. - Insert two documents with different field names for the same concept. Open the Schema tab in Compass and observe the report.
- Write a
seed.jsand run it withmongosh --file. - Write down, for the School Management System, which parts you would keep in SQL Server and which you would model as documents. Justify each.
Exercises 5, 7 and 8 correspond to the three most common "my query returns nothing" causes.
You can now
- Explain what a document database stores
- Connect with Compass and mongosh
- Say how collections and documents map to tables and rows
- Select the right database and list its collections
- Say why money needs
NumberDecimal
Review questions
- What is the practical difference between a flexible schema and no schema?
- Why must money use
Decimal128rather than the default number type? - Why does querying
_idwith a string return nothing? - Name two parts of a school system that suit SQL and two that suit documents.
Next: CRUD operations