Skip to content

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.