MongoDB
The most popular document database. MongoDB stores flexible JSON-like documents in collections — no fixed schema, ideal for event data, scraped content, and ML feature stores that evolve quickly.
Table of Contents
Installation / Setup
docker run -d -p 27017:27017 mongo:7
pip install pymongo # or: uv add pymongo
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017")
db = client["analytics"] # database
col = db["events"] # collection
MongoDB vocabulary ↔ SQL: database ↔ database, collection ↔ table, document ↔ row, field ↔ column, embedded doc ↔ JOIN (often).
Core Syntax
CRUD (PyMongo)
# Create
col.insert_one({"user_id": 42, "action": "click", "meta": {"page": "pricing"}, "ts": datetime.utcnow()})
col.insert_many([{...}, {...}])
# Read — find with query filters
doc = col.find_one({"user_id": 42})
docs = col.find({"action": "click", "meta.page": "pricing"})
docs = col.find({"ts": {"$gte": start}}, {"_id": 0, "user_id": 1, "action": 1}) # projection
# Update
col.update_one({"user_id": 42}, {"$set": {"action": "purchase"}, "$inc": {"n": 1}})
col.update_many({"action": "click"}, {"$set": {"processed": True}})
# Delete
col.delete_one({"user_id": 42})
col.delete_many({"ts": {"$lt": cutoff}})
Query operators
| Operator |
Meaning |
SQL equivalent |
$eq, $ne, $gt, $gte, $lt, $lte |
comparisons |
=, <>, >, ... |
$in, $nin |
membership |
IN |
$exists |
field presence |
IS NOT NULL |
$regex |
pattern match |
LIKE |
$and, $or, $not |
boolean logic |
AND OR NOT |
$elemMatch |
array element condition |
— |
$text |
text search (needs text index) |
MATCH |
col.find({
"$and": [
{"action": "click"},
{"$or": [{"meta.page": "pricing"}, {"meta.page": "signup"}]},
{"meta.device": {"$regex": "mobile"}},
]
})
Common Operations
Aggregation pipeline (MongoDB's GROUP BY + more)
pipeline = [
{"$match": {"ts": {"$gte": start}}}, # WHERE
{"$group": { # GROUP BY + agg
"_id": {"page": "$meta.page", "day": {"$dateToString": {"format": "%Y-%m-%d", "date": "$ts"}}},
"count": {"$sum": 1},
"unique_users": {"$addToSet": "$user_id"}, # set accumulator
}},
{"$project": { # SELECT/reshape
"_id": 0, "page": "$_id.page", "day": "$_id.day",
"count": 1, "unique_users": {"$size": "$unique_users"},
}},
{"$sort": {"day": 1, "count": -1}}, # ORDER BY
{"$limit": 100}, # LIMIT
]
results = list(col.aggregate(pipeline))
| Stage |
Purpose |
$match |
filter documents (early = fast) |
$group |
aggregate with _id as the key |
$project |
select/rename/compute fields |
$sort, $limit, $skip |
order & paginate |
$unwind |
flatten arrays to one doc per element |
$lookup |
left-join to another collection |
$addFields |
add computed fields |
$set / $unset |
add / drop fields |
$count |
count documents |
Indexes
col.create_index([("user_id", 1)]) # ascending
col.create_index([("ts", -1)]) # descending
col.create_index([("user_id", 1), ("ts", -1)]) # compound
col.create_index([("meta.page", 1)]) # on nested field
col.create_index([("$**", "text")]) # text index
col.drop_index("user_id_1")
Practical Examples
1. Daily active users per page (aggregation)
pipeline = [
{"$match": {"ts": {"$gte": start}, "action": "view"}},
{"$group": {
"_id": {"page": "$meta.page", "day": {"$dateToString": {"format": "%Y-%m-%d", "date": "$ts"}}},
"dau": {"$addToSet": "$user_id"},
}},
{"$project": {"_id": 0, "page": "$_id.page", "day": "$_id.day",
"dau": {"$size": "$dau"}}},
{"$sort": {"day": 1}},
]
df = pd.DataFrame(list(col.aggregate(pipeline)))
2. Flatten nested events for pandas / ML features
pipeline = [
{"$match": {"user_id": {"$in": target_users}}},
{"$unwind": "$items"}, # arrays -> rows
{"$project": {
"_id": 0, "user_id": 1,
"item_id": "$items.id",
"qty": "$items.qty",
"ts": 1,
}},
]
df = pd.DataFrame(list(col.aggregate(pipeline)))
3. LEFT JOIN with $lookup
pipeline = [
{"$lookup": {
"from": "users", # other collection
"localField": "user_id",
"foreignField": "_id",
"as": "user",
}},
{"$unwind": {"path": "$user", "preserveNullAndEmptyArrays": True}},
{"$project": {"_id": 0, "action": 1, "user.name": "$user.name"}},
]
Common Patterns
# Pagination with skip/limit
col.find({}).sort("ts", -1).skip(100).limit(50)
# Atomic upsert
col.update_one({"user_id": 42}, {"$set": {"last_seen": now},
"$inc": {"sessions": 1}}, upsert=True)
# Find one random-ish doc (sampling for EDA)
import random
n = col.estimated_document_count()
sample = col.find().skip(random.randrange(n)).limit(1)
# Distinct values of a field
col.distinct("meta.page")
Best Practices
- Put
$match and $project early in pipelines — cut data before grouping.
- Model for your queries: embed what you always read together; reference what you join rarely.
- Index everything you filter/sort — unindexed queries on big collections scan everything.
- Use
_id wisely: it's a unique index already; consider meaningful ids for lookups.
- Never trust a schema — documents vary; always handle missing fields (
$exists, defaults).
- Prefer
$set with upsert for idempotent writes.
- Export for analysis: pull aggregates into pandas with
pd.DataFrame(list(...)).
- Reach for aggregation over loading-and-filtering in Python whenever the collection is large.
Common Mistakes
| Mistake |
Consequence |
Fix |
Filtering in Python instead of $match |
Slow; pulls everything |
Push filters into the query/pipeline |
== None for missing fields |
Misses documents without the field |
{"field": None} or $exists: False |
Ignoring _id projection |
Unwanted _id in DataFrames |
{"_id": 0} in $project |
$unwind without preserveNullAndEmptyArrays |
Drops docs with empty arrays |
Decide which semantics you need |
| Forgetting indexes before scale |
Sudden slow queries |
create_index on hot fields |
| Treating documents like fixed tables |
Confusion when schema drifts |
Version fields; document assumptions |
Deep nested updates without $set |
Whole-document rewrites |
Use $set on dot paths ("meta.page") |
Troubleshooting
| Symptom |
Cause |
Fix |
OperationFailure: E11000 duplicate key |
Unique index conflict |
Handle upsert or dedupe first |
| Aggregation returns nothing |
$match field path wrong |
Check nested paths ("meta.page") |
| Slow aggregation |
No index on $match/$sort keys |
Add supporting indexes |
BSON size errors |
Oversized documents/pipeline |
Slim projections; allowDiskUse=True |
| Connection refused |
Server not running |
Start mongod; check port 27017 |
$lookup on unindexed foreign field |
Slow join |
Index foreignField |
Official Documentation
Last Updated
2026-09-06 — Initial version.