JSONB in PostgreSQL
JSONB is PostgreSQL's binary JSON type — it lets you store, query, and index JSON documents inside a relational database, giving Postgres much of the flexibility of a document store like MongoDB without leaving SQL. You can keep semi-structured or schema-flexible data in a single column, query deep into it, and index it for fast lookups, all alongside your normal relational tables and transactions.
JSONB is a big reason "just use Postgres" holds up against reaching for a separate NoSQL database. When part of your data is genuinely schema-flexible — user preferences, event payloads, third-party API responses, product attributes that vary by category — JSONB stores it naturally while the rest of your schema stays relational. The skill is knowing when that flexibility helps and when a normal column is the better choice.
TL;DR
- JSONB stores JSON in a decomposed binary format — slightly slower to write than
JSON, but far faster to query and indexable. Use JSONB, notJSON, almost always. - Query into documents with operators:
->/->>(get field),#>/#>>(get by path), and@>(containment). - GIN indexes make JSONB containment and key lookups fast — without one, queries scan every row.
- JSONB gives Postgres document-store flexibility for schema-variable data while keeping SQL, joins, and transactions.
- Use JSONB for genuinely flexible/sparse data; use normal columns for data with a stable shape you filter, join, and constrain on.
- Don't turn JSONB into an excuse to avoid schema design — over-nesting everything into one blob loses the relational strengths you chose Postgres for.
Quick Example
Store a flexible document, then query and filter into it with JSONB operators and a GIN index for speed:
The @> containment query, backed by a GIN index, finds matching documents without scanning the table.
Core Concepts
JSONB vs JSON
Postgres has two JSON types, and the choice is almost always JSONB:
Use json only in the rare case you must preserve the exact input text byte-for-byte. For everything you'll query or index, use JSONB.
Accessing Data
JSONB has a family of operators for reaching into documents:
->returns the value as jsonb;->>returns it as text.attrs->'ram_gb'(jsonb) vsattrs->>'ram_gb'(text). Cast text to the type you need for comparisons/math.#>and#>>extract by a path:attrs #>> '{address,city}'.@>(containment) asks "does the left JSONB contain the right?" — the workhorse for filtering and the operator GIN indexes accelerate.?,?|,?&test for key existence.- JSON path expressions (
jsonb_path_query,@?,@@) support richer querying for complex needs.
Indexing With GIN
Without an index, a JSONB query scans and inspects every row. A GIN (Generalized Inverted Index) makes containment (@>) and key-existence queries fast by indexing the document's keys and values:
jsonb_path_ops is a more compact GIN variant optimized specifically for @> containment. For a single frequently-filtered field, a plain expression index on the extracted value (CREATE INDEX ON products ((attrs->>'cpu'))) can be smaller and faster than a whole-document GIN index.
When to Use JSONB (and When Not To)
JSONB is powerful and easy to overuse. A guide:
Use JSONB when:
- The data is genuinely schema-flexible or sparse — attributes that vary by row/category, optional metadata, user-defined fields.
- You're storing external payloads whose shape you don't control (webhook bodies, API responses, event data).
- The shape changes often and you don't want a migration for every field.
Prefer normal columns when:
- The data has a stable, known shape.
- You filter, join, sort, or aggregate on the values heavily — relational columns with regular indexes and constraints are faster and cleaner.
- You need constraints (NOT NULL, foreign keys, CHECK, uniqueness) — these are first-class on columns, awkward inside JSONB.
💡 Tip: A good hybrid pattern: promote the fields you query and constrain on into real columns, and keep the long tail of variable attributes in a JSONB column. You get relational rigor where it matters and flexibility where you need it.
Best Practices
Default to JSONB, Reserve JSON for Exact-Text Cases
Unless you specifically need to preserve the original text (whitespace, key order, duplicate keys), use JSONB — it's queryable and indexable. Choosing json for a column you'll filter on is a common early misstep.
Index the Access Pattern, Not the Whole Column Reflexively
Match the index to how you query. Use a GIN index (or jsonb_path_ops) for containment queries across the document; use an expression index for a single hot field. Indexing everything wastes space; indexing nothing scans every row.
Don't Abandon Schema Design
JSONB is a tool for the flexible parts of your data, not a license to store everything as one giant blob. Over-nesting relational data into JSONB loses joins, constraints, and query clarity — the reasons you picked Postgres. Model stable data relationally.
Cast Explicitly for Comparisons
->> returns text; comparing or doing math needs a cast ((attrs->>'ram_gb')::int). Forgetting the cast leads to string comparisons ("100" < "9") and subtle bugs.
Validate Shape at the Edges
Postgres won't enforce a JSONB document's structure by default. If a document must have certain fields/types, validate in the application or with a CHECK constraint / trigger, so garbage doesn't accumulate in the column.
Common Mistakes
Using json Instead of jsonb
Querying JSONB Without an Index
Storing Relational Data as a Blob
Dumping structured, frequently-joined data into one JSONB column throws away foreign keys, constraints, and efficient joins. Keep well-structured data in columns; use JSONB for the variable remainder.
FAQ
Should I use json or jsonb?
Almost always jsonb. It stores JSON in a binary form that's fast to query and can be indexed with GIN, at the cost of slightly slower writes and not preserving the exact input text (whitespace, key order, duplicate keys). Use plain json only when you must retain the original text byte-for-byte and won't query into it.
How do I make JSONB queries fast?
Index them. For containment (@>) and key-existence queries across a document, add a GIN index (USING gin (col), or the smaller jsonb_path_ops variant for @>-only). For a single field you filter on constantly, an expression index on the extracted value (((col->>'field'))) is often smaller and faster. Without an index, JSONB queries scan every row.
What's the difference between -> and ->>?
-> returns the extracted value as JSONB (so you can chain further into it); ->> returns it as text. Use -> when navigating deeper into nested JSON, and ->> when you want the scalar value out — remembering to cast it ((col->>'n')::int) for numeric comparisons or math.
Does JSONB replace a NoSQL document database?
For many use cases, yes — JSONB gives Postgres document-style storage, deep querying, and indexing while keeping SQL, joins, transactions, and constraints on the rest of your data. A dedicated document database may still win at extreme document-oriented scale or specific features, but JSONB lets a single Postgres serve both relational and flexible data, avoiding a second system.
When should I NOT use JSONB?
When the data has a stable, known shape and you filter, join, sort, or constrain on it heavily — normal columns are faster, clearer, and support first-class constraints (foreign keys, NOT NULL, uniqueness). JSONB is for genuinely flexible, sparse, or externally-shaped data. Don't use it to avoid schema design for structured data.
Related Topics
- PostgreSQL — The database JSONB is part of
- SQL Fundamentals — The query language JSONB operators extend
- MongoDB — The document database JSONB competes with
- Database Indexing — GIN and expression indexing fundamentals
- PostgreSQL Performance Tuning — Reading plans for JSONB queries
- PostgreSQL Full-Text Search — Another semi-structured Postgres capability
- Data Modeling — Choosing relational vs document shapes