DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on Originally published at stacknotice.com

MongoDB vs PostgreSQL (2026): When Document Storage Actually Beats Relational

The MongoDB vs PostgreSQL debate has a specific shape: people who chose MongoDB early often wish they had PostgreSQL for complex queries, and people who chose PostgreSQL often reach for JSONB when they need flexible schemas. Both communities have a point.

MongoDB is a document database — data lives in JSON-like documents, collections replace tables, and there's no enforced schema by default. PostgreSQL is a relational database with a strict schema, ACID transactions, and JSONB support that lets you store and query document-shaped data inside a relational model.

The question isn't which is universally better. It's which fits your access patterns.

The Data Model

MongoDB: Documents in Collections

// A MongoDB document — arbitrary nesting, no fixed schema
{
  _id: ObjectId("64abc123"),
  name: "Alice Chen",
  email: "alice@example.com",
  profile: {
    bio: "Full-stack developer",
    location: { city: "Berlin", country: "DE" },
    skills: ["TypeScript", "React", "Node.js"]
  },
  preferences: {
    theme: "dark",
    notifications: { email: true, push: false }
  },
  createdAt: ISODate("2026-01-15T10:30:00Z")
}
Enter fullscreen mode Exit fullscreen mode

No migration needed to add a preferences field to some documents and not others. This is the actual value of schema flexibility — not that schemas are bad, but that evolving a schema during early product development costs zero friction.

PostgreSQL: Structured Rows with Optional JSONB

CREATE TABLE users (
  id          UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name        TEXT NOT NULL,
  email       TEXT UNIQUE NOT NULL,
  city        TEXT,
  country     CHAR(2),
  skills      TEXT[] DEFAULT '{}',
  preferences JSONB DEFAULT '{}',
  metadata    JSONB DEFAULT '{}',
  created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Index into JSONB fields
CREATE INDEX users_preferences_theme_idx
  ON users ((preferences->>'theme'));
Enter fullscreen mode Exit fullscreen mode

PostgreSQL doesn't force you to choose between structured and flexible — you can use both in the same table.

Queries: Where PostgreSQL Wins for Complex Data

The same query in both — users in Germany with TypeScript skill, ordered by post count:

// MongoDB aggregation pipeline
db.users.aggregate([
  { $match: { "profile.location.country": "DE", "profile.skills": "TypeScript" }},
  { $lookup: { from: "posts", localField: "_id", foreignField: "authorId", as: "posts" }},
  { $addFields: { postCount: { $size: "$posts" } }},
  { $sort: { postCount: -1 } },
  { $limit: 20 },
  { $project: { name: 1, email: 1, "profile.location.city": 1, postCount: 1 }}
])
Enter fullscreen mode Exit fullscreen mode
-- PostgreSQL — standard SQL
SELECT u.id, u.name, u.email, u.city, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.author_id = u.id
WHERE u.country = 'DE' AND 'TypeScript' = ANY(u.skills)
GROUP BY u.id, u.name, u.email, u.city
ORDER BY post_count DESC
LIMIT 20;
Enter fullscreen mode Exit fullscreen mode

The SQL is shorter, more readable, and the query planner optimizes it with standard indexes.

JSONB Queries in PostgreSQL

-- Find users who prefer dark theme AND have email notifications enabled
SELECT name, email, preferences
FROM users
WHERE
  preferences->>'theme' = 'dark'
  AND (preferences->'notifications'->>'email')::boolean = true;

-- Update a nested key without replacing the whole document
UPDATE users
SET preferences = jsonb_set(preferences, '{notifications, push}', 'true')
WHERE id = $1;

-- Aggregate over JSONB array elements
SELECT skill, COUNT(*) as developer_count
FROM users, jsonb_array_elements_text(metadata->'skills') AS skill
GROUP BY skill
ORDER BY developer_count DESC;
Enter fullscreen mode Exit fullscreen mode

This is the nuance most comparisons miss: PostgreSQL with JSONB is not a pure relational database. It handles document-shaped data well when you need it.

Transactions

MongoDB added multi-document ACID transactions in v4.0, but they're not the default and come with overhead. The document model encourages embedding related data to avoid transactions entirely.

// MongoDB: embed to avoid multi-document transactions
await db.orders.insertOne({
  customerId: ObjectId("..."),
  status: "pending",
  items: [  // embedded — one atomic write
    { productId: ObjectId("..."), name: "Keyboard", price: 89.99, qty: 1 },
    { productId: ObjectId("..."), name: "Mouse", price: 49.99, qty: 2 }
  ],
  total: 189.97,
  createdAt: new Date()
})
Enter fullscreen mode Exit fullscreen mode
-- PostgreSQL: transactions are the default, always available
BEGIN;
INSERT INTO orders (customer_id, status, total) VALUES ($1, 'pending', $2) RETURNING id INTO order_id;
INSERT INTO order_items (order_id, product_id, price, quantity) VALUES (order_id, $3, $4, $5);
UPDATE inventory SET quantity = quantity - $qty WHERE product_id = ANY($product_ids) AND quantity >= $qty;
COMMIT;
Enter fullscreen mode Exit fullscreen mode

TypeScript Integration

Both have mature TypeScript support.

// MongoDB with Mongoose
const userSchema = new Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  skills: [String]
})
type User = InferSchemaType<typeof userSchema>

// PostgreSQL with Drizzle
const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow()
})
// Fully typed — schema IS the type
Enter fullscreen mode Exit fullscreen mode

Drizzle and Prisma both support MongoDB, so the TypeScript ergonomics are comparable.

Decision Framework

Choose MongoDB if:

  • Early product with rapidly changing data model
  • Data is genuinely document-shaped (deeply nested, no natural relational structure)
  • You need horizontal sharding at scale
  • Content management, product catalogs, or event logs with heterogeneous schemas

Choose PostgreSQL if:

  • Data has clear relational structure with known relationships
  • You need reliable multi-document transactions
  • Complex queries with multiple joins are core
  • Full-text search, geospatial queries, or analytical aggregations matter
  • You want one database that handles both structured and semi-structured data (JSONB)

The honest assessment: For most web applications built in 2026, PostgreSQL is the safer default. Its JSONB support handles most use cases where MongoDB's flexibility would have been the argument. MongoDB remains the right choice for genuinely document-shaped domains.


Full article at stacknotice.com/blog/mongodb-vs-postgresql-2026

Top comments (0)