Losing PostgreSQL Gains? Blame Inline JSONB!!
The Hidden Cost of JSONB (Inline Storage)
PostgreSQL stores table rows in 8KB pages, packing as many tuples as possible. For a typical row with 10โ12 columns, and small text/integers, 40โ100 rows can easily fit per page. Typically row count = Page Size(8kb) / row size + row metadata (30-50 bytes approx.)
But the game changes when you add jsonb.
Example
CREATE TABLE events (
id serial PRIMARY KEY,
user_id int,
action text,
metadata jsonb
);
Suppose metadata which is a jsonb column contains:
{
"ip": "127.0.0.1",
"device": "Android",
"country": "IN"
}
This JSON might be just 100โ500 bytes, so PostgreSQL stores it in-line inside the same page (no TOASTing).
Result
- Each row size jumps from ~80 bytes โ ~200โ400 bytes
- Row count per page drops from 100 โ 20โ40
- Index scan still needs to read each page for matching rows
- More pages = more I/O, slower performance
Real Benchmark Insight
Performance comparison: Even with a GIN or B-tree index on the JSONB column, PostgreSQL still needs to scan all matching pages to retrieve the full tuple.
Why Index Doesn't Save You
Say you index a JSONB key like:
CREATE INDEX ON events ((metadata->>'ip'));
And query:
SELECT * FROM events WHERE metadata->>'ip' = '127.0.0.1';
PostgreSQL will:
- Use the index to find matching tuples
- Still need to fetch the row from disk
- Because JSONB is in-line, many pages are touched
- More page fetches = more IO = slower queries
What You Can Do
- Force TOAST: Add padding to make JSONB exceed 2KB:
UPDATE events SET metadata = metadata || jsonb_build_object('padding', repeat('x', 2000)); - Split into separate table: If JSONB is rarely queried
- Stick to well defined schema and avoid using
jsonbunless absolutely necessary
TL;DR
- JSONB under ~2KB is stored inline
- That bloats each row and reduces rows per page
- More pages scanned = slower indexed reads
- Even efficient indexes can't avoid this penalty
- Force TOAST or redesign if performance matters
Final Thought
Indexes reduce logical lookup cost. But if rows are bloated due to in-line JSONB, you're paying a high physical I/O cost - and that's where PostgreSQL performance dies quietly.
Source Code
You can find the source code and diagram files on GitLab:
Comments
No comments yet. Start the discussion.