The Two Giants
PostgreSQL and MongoDB are the most popular open-source databases in their respective categories. PostgreSQL is the most advanced open-source relational database, while MongoDB is the leading document database. Both are mature, well-supported, and capable of handling production workloads.
Architecture Comparison
| Feature | PostgreSQL | MongoDB |
|---------|-----------|---------|
| Data model | Relational (tables, rows) | Document (JSON-like BSON) |
| Schema | Fixed with constraints | Flexible, schema-less |
| Query language | SQL | MQL (MongoDB Query Language) |
| Storage engine | Custom (multiple options) | WiredTiger |
| Index types | B-tree, Hash, GiST, GIN, BRIN | B-tree, Text, Geospatial, TTL |
| Replication | Streaming (WAL) | Replica set (oplog) |
| Sharding | No (3rd party Citus) | Native (config servers + mongos) |
Performance Comparison
Read Performance
-- PostgreSQL: indexed read
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com';
-- Index Only Scan: ~0.3ms (with index)
// MongoDB: indexed read
db.users.find({ email: "alice@example.com" }).explain("executionStats");
// IXSCAN: ~0.3ms (with index)
For simple point lookups, both databases perform similarly. For complex analytical queries, PostgreSQL's query optimizer generally outperforms MongoDB's aggregation pipeline.
Write Performance
MongoDB typically has a write throughput advantage due to its document model and eventual consistency options:
// MongoDB: high-throughput writes
const bulkOps = [];
for (const event of eventBatch) {
bulkOps.push({ insertOne: { document: event } });
}
db.events.bulkWrite(bulkOps, { ordered: false });
// ~100,000 inserts/second on modest hardware
-- PostgreSQL: batched inserts
INSERT INTO events (type, data, created_at)
SELECT * FROM unnest($1::text[], $2::jsonb[], $3::timestamptz[]);
-- ~20,000 inserts/second (with WAL)
Feature Comparison
PostgreSQL Advantages
**Advanced Indexing**:
-- Partial index: only index active users
CREATE INDEX idx_active_users ON users(email) WHERE status = 'active';
-- Expression index: index a function result
CREATE INDEX idx_users_name_lower ON users(LOWER(name));
-- Covering index: index-only scans
CREATE INDEX idx_users_email ON users(email) INCLUDE (name, avatar_url);
**Full-Text Search**:
-- Built-in full-text search (no Elasticsearch needed for basic cases)
SELECT title, ts_rank(to_tsvector('english', body), query) AS rank
FROM articles, to_tsquery('english', 'database & optimization') query
WHERE to_tsvector('english', body) @@ query
ORDER BY rank DESC
LIMIT 10;
**JSON Support**:
-- PostgreSQL has excellent JSON support
CREATE TABLE events (
id SERIAL PRIMARY KEY,
data JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index JSON fields
CREATE INDEX idx_events_type ON events((data->>'type'));
CREATE INDEX idx_events_gin ON events USING GIN(data jsonb_path_ops);
-- Query JSON
SELECT * FROM events WHERE data @> '{"type": "page_view", "user_id": "123"}';
MongoDB Advantages
**Native Document Model**:
// Read an entire document tree in one query
const order = db.orders.findOne({ _id: "order_123" });
// order contains customer info, items, shipping, payment — all in one document
// No JOINs needed
// Update nested fields
db.orders.updateOne(
{ _id: "order_123" },
{ $set: { "shipping_address.city": "New York" } }
);
**Aggregation Pipeline**:
// Powerful data processing pipeline
db.orders.aggregate([
{ $match: { status: "completed", created_at: { $gte: startDate } } },
{ $unwind: "$items" },
{ $group: {
_id: "$items.category",
total_revenue: { $sum: { $multiply: ["$items.price", "$items.quantity"] } },
total_orders: { $addToSet: "$_id" }
}},
{ $project: {
category: "$_id",
revenue: "$total_revenue",
order_count: { $size: "$total_orders" }
}},
{ $sort: { revenue: -1 } }
]);
**Geo Queries**:
// Find nearby places
db.places.createIndex({ location: "2dsphere" });
db.places.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [-73.97, 40.77] },
$maxDistance: 1000 // meters
}
}
});
Operational Considerations
| Aspect | PostgreSQL | MongoDB |
|--------|-----------|---------|
| Setup complexity | Simple | Moderate (sharding) |
| Monitoring | pg_stat_statements, pgBadger | Ops Manager, Atlas |
| Backup | pg_dump, pgBackRest | mongodump, Ops Manager |
| High availability | Patroni, repmgr | Replica set (automatic failover) |
| Cloud managed | RDS, Cloud SQL, Supabase | Atlas, Cosmos DB, MongoDB on AWS |
Migration Between Them
PostgreSQL to MongoDB
// Denormalize relational data into documents
// Source (PostgreSQL):
// users: id, name, email
// addresses: id, user_id, street, city
//
// Target (MongoDB):
{
_id: ObjectId(),
name: "Alice",
email: "alice@example.com",
addresses: [
{ street: "123 Main St", city: "San Francisco" }
]
}
MongoDB to PostgreSQL
# Extract nested documents to relational tables
for doc in mongo_collection.find():
pg_cursor.execute(
"INSERT INTO users (id, name, email) VALUES (%s, %s, %s)",
[doc['_id'], doc['name'], doc['email']]
)
for addr in doc.get('addresses', []):
pg_cursor.execute(
"INSERT INTO addresses (user_id, street, city) VALUES (%s, %s, %s)",
[doc['_id'], addr['street'], addr['city']]
)
When to Choose Each
| Scenario | Choose |
|----------|--------|
| Complex queries and reporting | PostgreSQL |
| Strict data integrity | PostgreSQL |
| Flexible schema | MongoDB |
| Hierarchical data | MongoDB |
| ACID transactions across tables | PostgreSQL |
| High-volume writes | MongoDB |
| Full-text search | PostgreSQL (basic), Elasticsearch (advanced) |
| Geospatial queries | Both (good in both) |
| Polyglot data types | PostgreSQL |
Summary
PostgreSQL is the better default choice for most applications due to its superior query optimizer, data integrity features, and advanced indexing. MongoDB excels when you need flexible schemas, high write throughput, and naturally hierarchical data. PostgreSQL has narrowed the gap considerably with its JSONB support, while MongoDB has added ACID transactions. Choose based on your specific data model and query patterns rather than generic popularity.