← You Don't Need MongoDB

Appendix A: MongoDB to PostgreSQL Mapping

The Waiter of Gold Lapel · Published Apr 19, 2026 · 7 min

The translation dictionary. The first reference a developer reaches for mid-migration, organised so that you may keep it open in another tab and answer one question at a time. Every concept is named in MongoDB's idiom on the left and given its PostgreSQL equivalent on the right, with a short note where the equivalence is approximate rather than exact.

MongoDB is excellent software. The shape of this appendix is a translation, not a critique. Where the two systems differ in a way worth understanding, the difference is named. Where they meet at the same place by different roads, the meeting is recorded.

Storage and Identity

MongoDBPostgreSQLNotes
DatabaseDatabaseOne-to-one. A PostgreSQL database holds collections (tables), schemas, and extensions.
CollectionTableOne collection becomes one table with a data JSONB column. Auto-created by doc_insert.
DocumentJSONB rowThe full document lives in data JSONB. _id and created_at sit alongside as columns.
FieldJSONB keyAccessed by data->'field' (jsonb) or data->>'field' (text).
Embedded documentNested JSONB objectdata->'address'->>'city'. Dot notation in filters expands automatically.
Embedded arrayJSONB arraydata->'tags'. Unnested via jsonb_array_elements or jsonb_array_elements_text.
_id (ObjectId)_id UUID DEFAULT gen_random_uuid()Outside the document as a first-class column; B-tree indexed and joinable.
ObjectId (12-byte)UUID (16-byte)Mint fresh UUIDs at migration time; keep the 24-char hex as legacy_id text with a unique index.
BSONJSONBBinary JSON with sorted keys, offset table, and direct field access. Available since PostgreSQL 9.4 (2014).

Extended JSON Types

MongoDB Extended JSON v2 (relaxed mode) wraps types JSON cannot represent. The five wrappers your loader will encounter, and their PostgreSQL homes:

Extended JSONExamplePostgreSQL equivalentLoader action
$oid{"$oid": "6512..."}text (legacy_id) and a fresh uuid for _idUnwrap the hex string into legacy_id; mint a UUID for the new _id.
$date{"$date": "2025-09-27T10:30:00Z"}timestamptzPostgreSQL parses ISO-8601 directly. MongoDB stores ms; PostgreSQL stores µs — the migration is lossless, the round-trip is not.
$numberLong{"$numberLong": "9007199254740993"}bigintUnwrap the string and cast. JavaScript's Number loses precision above 2^53; PostgreSQL does not.
$numberDecimal{"$numberDecimal": "42.50"}numericUnwrap as a string, store as numeric for exact arithmetic. Never round-trip through JS Number.
$binary{"$binary": {"base64": "...", "subType": "00"}}byteaDecode the base64 string into bytea; carry the subType in a sibling column if the application uses it.

In relaxed mode, ordinary 32-bit integers and finite floats arrive as plain JSON numbers and need no special handling. The five wrappers above are the entire migration vocabulary.

Indexes

MongoDBPostgreSQLNotes
Single-field {email: 1}B-tree expression index on ((data->>'email'))Or on a generated STORED column.
Single-field unique {email: 1} uniqueCREATE UNIQUE INDEX ON users ((data->>'email'))Same semantics.
Compound {tenant_id: 1, created_at: -1}Composite expression B-tree, column order matches the query((data->>'tenant_id'), ((data->>'created_at')::timestamptz) DESC).
Multikey (array fields)GIN on data jsonb_path_opsjsonb_path_ops is smaller and faster for @>, @?, @@; choose jsonb_ops if you need ?, ?|, ?&.
TextGIN on to_tsvector('english', data->>'body')Configurable per language.
2dsphere (geo)GiST on a geography generated columnRequires the PostGIS extension.
HashedHash index, or B-tree on md5(data->>'field')Hash indexes are crash-safe since PostgreSQL 10.
TTL {expireAfterSeconds: N}Expression index plus scheduled DELETE (pg_cron), or partition dropPostgreSQL has no native TTL thread. Gold Lapel ships doc_create_ttl_index.
Partial {partialFilterExpression: ...}CREATE INDEX ... WHERE <expr>Smaller, faster index over the rows you actually query.
SparsePartial index WHERE data ? 'field'Same intent expressed as a partial index.
Wildcard {"$**": 1}GIN on data (default jsonb_ops)Indexes every key/value pair in every document.
createIndex(...)CREATE INDEX (or CREATE INDEX CONCURRENTLY)Gold Lapel exposes both via doc_create_index; the proxy auto-creates GIN indexes from observed query patterns.

Operations and Topology

MongoDBPostgreSQLNotes
Replica setStreaming replication (primary + standbys)Same mental model. Synchronous replicas via synchronous_commit = remote_apply.
Primary / SecondaryPrimary / ReplicaRead replicas via --replica routing in Gold Lapel. Read-after-write protection built in.
OplogWrite-Ahead Log (WAL)Both are append-only change journals. WAL drives streaming replication and logical replication.
Sharding (mongos, config servers, balancer)Citus (distributed tables, coordinator + workers)Citus is excellent for most sharded workloads; native MongoDB sharding is more mature past 50 nodes.
Shard keyDistribution columnSELECT create_distributed_table('events', 'tenant_id');
Zone shardingCitus reference tables + tenant routingApproximate. Geographic placement requires more orchestration in PostgreSQL.
Change streamsRow trigger + LISTEN/NOTIFYGold Lapel exposes doc_watch / doc_unwatch. Same event shape, different transport.
GridFSObject storage (S3, GCS, etc.) + JSONB metadata rowCleaner separation; PostgreSQL is not the right home for binary blobs at scale.
AtlasManaged PostgreSQL providerRDS, Cloud SQL, Azure Database for PostgreSQL, Crunchy Bridge, Supabase, Neon, Aiven.
Compasspsql, DBeaver, pgAdmin, TablePlusCompass remains the finest GUI for inspecting MongoDB data; the PostgreSQL ecosystem has many capable equivalents.
Atlas ChartsMetabase, Superset, Grafana, LookerCharts is excellent in its niche; PostgreSQL connects to every BI tool in existence.
Atlas Searchtsvector + GIN, pg_trgm, pgvector, fuzzystrmatchTreated in full in You Don't Need Elasticsearch.

Transactions and Consistency

MongoDBPostgreSQLNotes
Single-document atomic writeSingle-row UPDATE (atomic under all isolation levels)Identical semantics.
Multi-document transaction (session.startTransaction)BEGIN / COMMIT (since the project's earliest releases; MVCC since 6.5, 1999)PostgreSQL: every statement runs in a transaction. MongoDB: 4.0 (replica sets), 4.2 (sharded). 60-second default transactionLifetimeLimitSeconds.
findAndModifyUPDATE ... RETURNING in a CTE with FOR UPDATEAtomic select-update-return. SKIP LOCKED for work queues.
Read concern local / majorityRead committed (default) / serializablePostgreSQL also offers repeatable read; serializable uses SSI (Serializable Snapshot Isolation).
Write concern w: 1 / w: "majority"synchronous_commit = off / on / remote_applyThe trade between throughput and durability lives at this knob.
Causal consistencyStrict consistency on the primary; replica lag bounded by streaming replicationPostgreSQL primaries are linearizable by default.
Optimistic concurrencyMVCC with snapshot isolationPostgreSQL never reads a partially-committed row.

Backup and Recovery

MongoDBPostgreSQLNotes
mongodump / mongorestorepg_dump / pg_restoreLogical backup; portable across versions.
Filesystem snapshotpg_basebackupPhysical backup; faster restore, version-locked.
Point-in-time recovery (Atlas)PITR via WAL archive (every managed provider supports it)Same capability, different tooling.
Oplog tail for incrementalWAL archiving + log shipping / streamingSame mechanism, named differently.

Drivers and Languages

The Gold Lapel SDKs ship doc_* methods in every supported language. Method casing follows each language's convention; behaviour is identical across all seven.

LanguageMongoDB driverGold Lapel SDKMethod casing
Pythonpymongo, motorgoldlapel, goldlapel.asynciosnake_case (doc_find)
Node.jsmongodbgoldlapelcamelCase (docFind)
Rubymongogoldlapelsnake_case (doc_find)
Javamongodb-driver-sync, mongodb-driver-reactivestreamsgoldlapel-jdbc, goldlapel-spring-bootcamelCase (docFind)
PHPmongodb/mongodbgoldlapel/goldlapelcamelCase (docFind)
Gogo.mongodb.org/mongo-drivergithub.com/goldlapel/goldlapel-goPascalCase (DocFind)
.NETMongoDB.DriverGoldLapelPascalCase (DocFind)

Aggregation Pipeline

A complete one-line summary of every supported pipeline stage and accumulator. Appendix E gives full syntax, generated SQL, and examples.

MongoDB stagePostgreSQL equivalent
$matchWHERE (or HAVING after $group)
$groupGROUP BY with aggregate functions
$sortORDER BY
$limitLIMIT
$skipOFFSET (use keyset pagination for deep pages)
$projectSELECT with jsonb_build_object
$unwindCROSS JOIN LATERAL jsonb_array_elements(...)
$lookupCorrelated subquery with jsonb_agg, or true SQL JOIN
MongoDB accumulatorPostgreSQL equivalent
$sumsum(expr) ($sum: 1 short-circuits to count(*))
$avgavg(expr)
$min / $maxmin(expr) / max(expr)
$pushjsonb_agg(expr)
$addToSetjsonb_agg(DISTINCT expr)
$countcount(*)
$first / $last(array_agg(expr ORDER BY ...))[1] / window functions

Filter Operators

Appendix D gives the full syntax, generated SQL, and index used. The summary:

MongoDBPostgreSQL
{field: value}data @> '{`{"field": value}`}' (GIN-indexed)
$eq, $nedata @> ... / NOT (data @> ...)
$gt, $gte, $lt, $lte(data->>'field')::T > v (cast-aware)
$in / $nindata->>'field' IN (...) / NOT IN (...) with IS NULL guard
$existsdata ? 'field' (top-level) or data #> '{path}' IS NOT NULL (nested)
$regex~ or ~* (case-insensitive)
$or / $and / $notSQL boolean composition
$elemMatchEXISTS (SELECT 1 FROM jsonb_array_elements(data->'arr') WHERE ...)
$textto_tsvector(...) @@ plainto_tsquery(...)

Update Operators

MongoDBPostgreSQL
$setjsonb_set(data, path, value, true) (or data || '{...}'::jsonb for shallow merge)
$unsetdata #- '{path}'
$incjsonb_set(data, path, (coalesce((data->>'f')::int, 0) + n)::text::jsonb)
$mulSame shape as $inc with multiplication
$renamejsonb_set(data #- old, new, data->old)
$pushjsonb_set(data, path, coalesce(data->path, '[]'::jsonb) || value)
$pullSubquery over jsonb_array_elements filtered and re-aggregated
$addToSetCASE on @> containment, then jsonb_set
upsert: trueINSERT ... ON CONFLICT (_id) DO UPDATE

Operational Features

MongoDBPostgreSQLGold Lapel API
Change streamsRow trigger + LISTEN/NOTIFYdoc_watch / doc_unwatch
TTL indexExpression index + sweep trigger (or pg_cron DELETE)doc_create_ttl_index / doc_remove_ttl_index
Capped collectionRow-count trigger + ordering indexdoc_create_capped / doc_remove_cap
$jsonSchema validatorCHECK (jsonb_matches_schema(...)) via pg_jsonschema, or hand-rolled CHECK constraintsdoc_create_collection(validator=...)
Server-side functionsPL/pgSQL, PL/Python, PL/V8Native PostgreSQL.
Materialized viewsNative (CREATE MATERIALIZED VIEW, since 9.3)doc_aggregate caches behind a matview keyed by pipeline hash.

Where to Go Next

  • Appendix B — the 21 methods, full reference per language.
  • Appendix C — migration checklist, in the order the work happens.
  • Appendix D — filter operators in full.
  • Appendix E — aggregation pipeline in full.
  • Glossary — for any term in the table above that wants definition.

If a row sends you to a chapter, the chapter contains the full treatment. The appendix is the index, not the argument.