JSON & JSONB — A Document Database Inside a Relational One
PostgreSQL
Chapter 4 · JSON & JSONB — A Document Database Inside a Relational One
postgres1-3 closed by drawing a line: arrays, ranges, and true ENUMs are all still structured and relational. This chapter crosses that line — JSONB is genuinely semi-structured, document-shaped data living inside a relational column, and it's the feature that makes Postgres's own "JSON-heavy hybrid relational/document workloads" entry from postgres1-1's comparison table make real sense.
JSON vs. JSONB
Postgres has two distinct JSON types. json stores an exact textual copy of the input — preserving whitespace, key order, and even duplicate keys — and is re-parsed on every query. jsonb stores a decomposed binary representation instead: no whitespace or key-order preservation, but considerably faster to query, and — critically — indexable.
Practical guidance: use jsonb for nearly everything. json is really only worth reaching for when byte-for-byte preservation of the original document matters, such as an audit log that needs to store exactly what was received.
jsonb, not a case of "MySQL has nothing." The real differentiator isn't JSON support existing at all; it's the depth of Postgres's own JSONB operator and indexing ecosystem, covered next.
JSONB Operators
| Operator | Meaning |
|---|---|
| -> | Get object field or array element, returned as JSON |
| ->> | Get object field or array element, returned as TEXT |
| #> | Get value at a path, returned as JSON |
| #>> | Get value at a path, returned as TEXT |
| @> | Containment — does the left JSONB contain the right as a subset (the same idea as postgres1-3's own array @>) |
| ? | Does this key exist at the top level? |
A concrete example where this genuinely earns its keep: a product catalog where a shirt has size/color and a laptop has ram/storage — wildly different fields per category, impossible to model cleanly with fixed relational columns without either a huge, mostly-empty sparse table or an entity-attribute-value (EAV) design, both worse options in their own way.
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
category TEXT,
attributes JSONB
);
INSERT INTO products (name, category, attributes)
VALUES ('Blue T-Shirt', 'apparel', '{"size": "M", "color": "blue"}'),
('ThinkPad X1', 'laptop', '{"ram_gb": 32, "storage_gb": 1024}');
-- Find every laptop with at least 16GB RAM
SELECT name FROM products
WHERE category = 'laptop' AND (attributes ->> 'ram_gb')::int >= 16;Indexing JSONB — GIN Indexes
A JSONB column can be indexed with a GIN (Generalized Inverted Index), making containment and key-existence queries genuinely fast at real scale:
CREATE INDEX idx_attrs ON products USING GIN (attributes);
This is the concrete payoff distinguishing Postgres's own JSONB support from a plain "store JSON as a text blob" implementation — the database can query into the structure with real index support, not just store and retrieve an opaque document.
Revisiting mongodb1-1's Own Document-vs-Relational Framing
mongodb1-1 introduced the fundamental document-vs-relational distinction, and the question of when MongoDB's own model fits better than MySQL's. JSONB is the point where that clean dividing line gets genuinely blurry: a single Postgres table can have strictly relational columns — foreign keys, indexed scalars with real integrity constraints — alongside one or more JSONB columns behaving like an embedded, schema-flexible sub-document, all in the same row, inside the same ACID-transactional engine.
This doesn't mean Postgres replaces MongoDB — a dedicated document database still has real advantages at massive horizontal scale, and a genuinely schema-less domain still fits MongoDB's own design center better. But for the extremely common real-world case of "mostly relational data, with a few genuinely variable fields," JSONB lets one Postgres database serve both needs at once, without the added complexity of keeping two separate database systems in sync.
NOT NULL, no CHECK constraint, no foreign key reference into or out of a nested JSONB field. Pushing too much of an application's actual core data model into JSONB sacrifices the very data-integrity strength postgres1-1 credited Postgres with in the first place ("data-integrity-critical applications"). JSONB is the right tool for genuinely variable, secondary data — not a substitute for real columns on the data that matters most.
Hands-On Exercises
Explain the difference between json and jsonb, and state this chapter's own practical guidance on which to use by default and why.
📄 View solutionUsing the product catalog example, explain why JSONB is a better fit than either a huge sparse relational table or an EAV pattern for storing per-category product attributes.
📄 View solutionExplain this chapter's own honest nuance about JSONB "blurring the line" with MongoDB — what does JSONB NOT replace, and what's the real trade-off this chapter's own warn-box names about pushing core data into JSONB?
📄 View solutionChapter 4 Quick Reference
- json — exact textual copy, re-parsed each query · jsonb — decomposed binary, faster, indexable — use jsonb by default
- MySQL has had native JSON since 5.7.8 — the real gap is Postgres's operator/indexing depth, not JSON support itself
- Operators:
->/->>(field access),#>/#>>(path access),@>(containment),?(key existence) - GIN indexes make JSONB containment/key queries genuinely fast at scale — the real difference from a plain JSON text blob
- JSONB revisits mongodb1-1's document-vs-relational line — great for "mostly relational, a few variable fields," not a MongoDB replacement
- No NOT NULL/CHECK/foreign-key enforcement inside JSONB — keep core data in real columns
- Next chapter: Advanced Querying — Recursive CTEs & Window Function Extras