Franck PachotDatabase Developer Advocate Minibook 23 · Database field guides All minibooks

Database field guide · 23

Document Data Modeling

JSONB, BSON, nesting, indexes, and consistency

Model document data deliberately across MongoDB and PostgreSQL, from aggregate boundaries and embedded arrays to indexing and update amplification.

Franck Pachot7 chaptersMongoDB · JSONB · BSON · document model

Build the mental model before choosing the mechanism.

This minibook was AI-generated from Franck Pachot's archived blog posts. Links to the original articles are included for source context and verification.

01

A document is an aggregate boundary

A useful document groups values that are read, validated, and changed together. Embedding is a transactional and locality decision, not simply a way to avoid schema design.

  • Embed data with the same lifecycle and ownership.
  • Reference independently growing or shared entities.
  • Design from access and update patterns, not source-table shape.

02

Nesting carries semantics

Objects and arrays preserve containment, order, and one-to-many structure. Flattening nested values into columns or key-value rows can lose the distinction between elements that belonged together.

  • Arrays are values, not hidden child tables.
  • Repeated field names remain scoped by their path.
  • Absent, null, and empty are separate states.

03

JSONB and BSON optimize different boundaries

PostgreSQL JSONB stores a binary representation inside a relational tuple while MongoDB BSON is native to the document engine and wire protocol. Both expose typed values, but storage and execution paths differ.

  • JSONB participates in tuple visibility and TOAST.
  • BSON includes document-oriented scalar types.
  • Wire format, in-memory format, and durable format need not match.

04

Indexes select paths and array semantics

A document index must identify paths, value types, and whether array elements produce multiple keys. General inverted indexes broaden coverage while targeted indexes preserve ordering and selectivity.

  • Compound key order still governs equality, sort, and range.
  • Multikey expansion affects coverage and key combinations.
  • An index on a document is not an index for every expression.

05

Large updates amplify physical work

Changing one field can rewrite a large JSONB tuple, create new MVCC versions, and prevent HOT updates when indexed expressions change. Native document engines also pay allocation and index-maintenance costs.

  • Measure bytes written per logical field update.
  • Separate frequently changing values from cold large payloads.
  • Compression can reduce storage while increasing rewrite CPU.

06

Cross-document rules need explicit coordination

Single-document atomicity makes embedded invariants straightforward. Constraints spanning documents require transactions, unique indexes, validation, or application protocols with clearly stated isolation assumptions.

  • Denormalization duplicates both data and repair obligations.
  • Write skew survives when concurrent transactions protect different records.
  • Foreign-key absence does not remove referential semantics.

07

Field manual

Concrete mechanics, diagnostic evidence, and executable patterns to carry into a real system.

01

Choose an aggregate boundary

Write down which values must be atomically consistent, loaded together, and deleted together. Embed that owned state; reference independently shared or unbounded state.

Order {
  _id, customerId, status,
  lines: [{ productId, quantity, priceAtOrder }],
  shippingAddress: { ... }
}
// Product catalog and customer remain referenced aggregates.
02

Index a concrete JSON path

In PostgreSQL, a targeted expression index supports a known scalar predicate and ordering more efficiently than assuming one broad GIN index covers every operation.

CREATE INDEX orders_customer_created_idx
ON orders ((payload->>'customerId'), created_at DESC);

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE payload->>'customerId' = $1
ORDER BY created_at DESC LIMIT 20;
03

Preserve same-element array meaning

When multiple predicates must match one array element, keep them in one element expression. Independent path predicates can accidentally match values from different elements.

db.orders.find({
  lines: { $elemMatch: {
    productId: 'P42', quantity: { $gte: 10 }
  }}
})
04

Measure update amplification

Compare one logical field change with tuple versions, WAL, table growth, and index writes. Large hot JSON payloads often deserve decomposition even when reads remain document-shaped.

EXPLAIN (ANALYZE, WAL, BUFFERS)
UPDATE profile
SET document = jsonb_set(document, '{lastSeen}', to_jsonb(now()))
WHERE id = $1;
-- Check n_tup_hot_upd, WAL bytes, and relation growth over a workload.

08

Source articles

Optional deep dives with the complete experiments and product-version context behind this guide.