Documents, Collections, DBs
MongoDB stores BSON documents inside collections inside databases. Documents are flexible-schema JSON-with-types; collections are like tables; databases group collections. Indexing, replication, and sharding live on top.
Database, collection, doc, _id, indexes
EXAMPLE
// 1) Hierarchy
// cluster
// └── database (e.g. 'shop')
// └── collection (e.g. 'orders')
// └── document { _id: ObjectId(...), … }
// 2) Documents — JSON shape, BSON storage
{
_id: ObjectId('6543210...'), // always 12 bytes — timestamp+random+counter
customerId: ObjectId('64999...'),
items: [
{ sku: 'SKU-101', qty: 2, priceCents: 4999 },
{ sku: 'SKU-203', qty: 1, priceCents: 1299 },
],
totalCents: 11297,
status: 'paid',
shippingAddress: { line1: '1 Pitt St', city: 'Sydney', country: 'AU' },
createdAt: ISODate('2024-08-01T03:21:00Z'),
}
// 3) _id — the primary key
// • Auto-generated as ObjectId if you don't supply one
// • Indexed automatically
// • Can be anything serializable: string, integer, UUID — but pick one shape
// • ObjectId encodes timestamp → sort by _id == sort by creation time
import { ObjectId } from 'mongodb';
const id = new ObjectId();
id.getTimestamp(); // when this id was generated
// 4) Connect (Node driver)
import { MongoClient } from 'mongodb';
const client = new MongoClient(process.env.MONGO_URL);
await client.connect();
const db = client.db('shop');
const orders = db.collection('orders');
// 5) CRUD
await orders.insertOne({ totalCents: 4999, status: 'pending', createdAt: new Date() });
await orders.insertMany([{ … }, { … }]);
await orders.findOne({ _id: new ObjectId(id) });
await orders.find({ status: 'paid' }).sort({ createdAt: -1 }).limit(20).toArray();
await orders.updateOne(
{ _id: id },
{ $set: { status: 'shipped', shippedAt: new Date() } },
);
await orders.deleteOne({ _id: id });
// 6) Schema flexibility — both freedom and footgun
// MongoDB does not enforce a schema by default. Use validator OR validation library:
await db.createCollection('orders', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['totalCents', 'status', 'createdAt'],
properties: {
totalCents: { bsonType: 'int', minimum: 0 },
status: { enum: ['pending', 'paid', 'shipped', 'cancelled'] },
createdAt: { bsonType: 'date' },
},
},
},
validationLevel: 'strict',
validationAction: 'error',
});
// In app code: Zod, Mongoose, or Prisma for schema enforcement.
// 7) Indexes — make queries fast
await orders.createIndex({ customerId: 1 });
await orders.createIndex({ status: 1, createdAt: -1 });
await orders.createIndex({ 'shippingAddress.country': 1 });
await orders.createIndex({ items: 'multikey' }); // arrays get multikey indexes
await orders.createIndex({ description: 'text' }); // text search
// Compound index order matters! Sort matches the index direction or it's a scan.
await orders.find({ status: 'paid' }).sort({ createdAt: -1 }); // uses { status:1, createdAt:-1 }
// 8) Embed vs reference
// Embed when:
// • child entities don't exist independently (order line items)
// • you always read parent + children together
// • child count is bounded
// Reference (separate collection) when:
// • children are queried independently
// • cardinality is high or unbounded (comments on a popular post)
// • children have rich queries of their own
// 9) Aggregation pipeline
await orders.aggregate([
{ $match: { status: 'paid', createdAt: { $gte: ISODate('2024-01-01') } } },
{ $group: { _id: '$customerId', revenueCents: { $sum: '$totalCents' }, n: { $sum: 1 } } },
{ $sort: { revenueCents: -1 } },
{ $limit: 10 },
]).toArray();
// 10) Transactions — multi-document atomicity
const session = client.startSession();
try {
session.startTransaction();
await orders.updateOne({ _id }, { $set: { status: 'paid' } }, { session });
await ledger.insertOne({ orderId: _id, amountCents }, { session });
await session.commitTransaction();
} catch (e) {
await session.abortTransaction();
throw e;
} finally {
await session.endSession();
}
// Transactions require a replica set (any modern cluster, Atlas, or local rs).
// 11) Replication + sharding
// • Replica set: 3+ nodes, one primary, others secondaries → HA + read scaling
// • Sharding: horizontal partitioning by a shard key → write scaling, huge data
// Choose a shard key that distributes writes evenly and matches your query patterns.
// 12) Common bugs
// • Storing dates as strings → can't range-query → ALWAYS new Date()
// • Storing money as floats → ROUND ERRORS → store cents as integers (Int32/Int64)
// • No index on common filter → collection scans → slow at scale
// • _id mismatch — passing a string id where ObjectId is expected
// • Unbounded array growth in a doc → 16 MB doc limit; reference instead
// • Missing $ on operators — { status: 'paid' } vs { $match: { status: 'paid' } }
Why it matters
MongoDB’s schema flexibility is a feature for evolving products and a footgun for shipping prod. Enforce shape with a Zod / Mongoose layer in the app, store money as integer cents, dates as Date, and add an index for every filter that touches a real workload.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…