Prompt
I'm building an app with Postgres and lots of JSON fields, any pitfalls?
Latest observation
Yes—Postgres + lots of JSON/JSONB can work very well, but there are some common pitfalls to watch for.
Main pitfalls
1) Using JSON for data that should be relational
If you find yourself frequently:
- joining on values inside JSON,
- enforcing relationships,
- filtering/sorting heavily on JSON keys,
then those fields probably belong in proper columns or related tables.
Why it matters: relational data is harder to query, index, validate, and maintain when buried in JSON.
2) Weak schema validation
JSONB is flexible, but that flexibility can turn into inconsistent data:
- missing keys
- wrong types
- inconsistent naming
- varying nested shapes
Mitigation:
- validate at the application layer
- consider database constraints or triggers for critical fields
- use generated columns for important extracted values
3) Indexing mistakes
JSONB is powerful, but querying it without the right indexes can get slow.
Common issue:
WHERE data->>'status' = 'active'
This may scan a lot of rows unless indexed properly.
Mitigation options:
- GIN indexes for containment queries:
CREATE INDEX ON mytable USING gin (data); - expression indexes for specific keys:
CREATE INDEX ON mytable ((data->>'status'));
Note: GIN helps for @> containment and some JSONB operators, but not every query pattern.
4) Updating JSON can be expensive
Modifying a single nested key often rewrites the whole JSONB value internally.
Problem: frequent partial updates on large JSON blobs can increase write amplification and bloat.
Mitigation:
- keep JSON documents reasonably small
- separate hot-updated fields into normal columns
- avoid storing huge mutable documents if only a few fields change often
5) Harder querying and maintenance
Complex JSON queries can become:
- verbose
- harder to read
- harder to optimize
- harder for teammates to understand
Example: deeply nested path queries can become messy fast.
Mitigation:
- extract common fields into columns
- use views or CTEs to simplify
- keep JSON for variable/rarely queried parts
6) Type issues and casting surprises
JSON stores everything as text-like values until you cast them:
- numbers might be strings
- booleans may need explicit handling
- comparisons can behave unexpectedly
Example:
WHERE (data->>'age')::int > 18
If values are malformed, this can error.
Mitigation: validate data and cast carefully.
7) Poor selectivity estimates / query planning issues
Sometimes Postgres can’t estimate JSON-related predicates well, especially for complex nested filters, leading to suboptimal plans.
Mitigation:
- create targeted indexes
- consider generated columns for important predicates
- run
ANALYZE - inspect plans with
EXPLAIN (ANALYZE, BUFFERS)
8) Overusing JSON instead of “schema evolution”
Some teams use JSON just to avoid migrations. That can seem convenient at first, but long term it often creates:
- inconsistent data
- hidden coupling
- hard-to-find bugs
A better pattern is often:
- relational columns for stable/core data
- JSONB for optional, varying, or provider-specific data
Good use cases for JSONB
JSONB is a good fit when:
- the shape varies a lot between rows
- you store metadata or optional attributes
- you ingest third-party payloads
- you want to preserve semi-structured data
- you rarely query most of the fields
Examples:
- event payloads
- user settings/preferences
- webhook bodies
- vendor-specific metadata
- form submissions with variable fields
Recommended pattern
A common safe design is:
-
columns for:
- ids
- foreign keys
- timestamps
- status
- frequently queried/sorted fields
-
JSONB for:
- optional config
- rarely queried extras
- raw payloads
- dynamic attributes
This gives you flexibility without giving up performance and integrity.
Practical tips
- Prefer JSONB, not JSON, unless you specifically need text preservation.
- Add expression indexes for frequently queried keys.
- Don’t store massive documents if only a small part changes often.
- Validate JSON structure before inserting.
- Promote frequently queried JSON fields to real columns.
- Use
EXPLAIN ANALYZEearly. - Consider generated columns for hot keys:
ALTER TABLE mytable ADD COLUMN status_text text GENERATED ALWAYS AS (data->>'status') STORED;
If you want, I can also give you:
- a “when to use JSONB vs columns” decision checklist, or
- indexing patterns for common JSONB query types.