mdashikjs/blog
All posts
PostgreSQL JSONB: When to Use It and When to Normalize
Backend

PostgreSQL JSONB: When to Use It and When to Normalize

Backend5 min

PostgreSQL JSONB: When to Use It and When to Normalize

JSONB is powerful but it is not a replacement for proper schema design. After using it in three production systems, I have a clear mental model for when it helps and when it hurts.

PostgreSQLDatabaseJSONBSchema Design
Share:

When JSONB is the right call

1. Truly dynamic attributes. A product catalog where different categories have different fields. A t-shirt has size and color. A laptop has RAM and storage. Normalizing this into an EAV table is worse than JSONB in every way.

2. External API responses. Store the raw webhook payload alongside your parsed version. When the provider changes their schema (and they will), you have the original data to re-parse.

3. User preferences and settings. Shape varies per user, changes frequently during development, and is always read as a whole. Perfect JSONB use case.

When to normalize instead

1. You query the field frequently. JSONB indexes work, but they are slower and larger than B-tree indexes on regular columns. If you filter by data->>'status' on every request, that should be a column.

2. You need referential integrity. JSONB cannot enforce foreign keys. If your JSON contains user IDs, nothing stops them from pointing to deleted users.

3. You aggregate on the field. SUM(data->>'amount') requires casting and cannot use regular indexes efficiently.

Indexing JSONB correctly

The GIN index covers the entire document:

CREATE INDEX idx_products_attrs ON products USING GIN (attributes);

This supports @> (contains) queries well:

SELECT * FROM products WHERE attributes @> '{"color": "red"}';

For specific key lookups, an expression index is smaller and faster:

CREATE INDEX idx_products_color ON products ((attributes->>'color'));

The hybrid approach

My go-to pattern: normalize the fields you query, keep JSONB for the rest. A product has name, price, category as columns and an attributes JSONB column for everything else. Best of both worlds.

Migrations with JSONB

The biggest risk: schema drift. Without a schema definition, different parts of the codebase write different shapes. I use Zod to validate JSONB content at the application layer before every write. If the shape is wrong, it fails loudly instead of silently corrupting data.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.