Dev & EngARTICLE

jsonb Can Be Larger and Slower Than json in PostgreSQL, Benchmark Shows

A benchmark published on Planet PostgreSQL measured disk size and speed of json and jsonb in PostgreSQL 17.11 and 18.6. The result dismantles the rule of thumb that jsonb is always the right choice.

jsonb Can Be Larger and Slower Than json in PostgreSQL, Benchmark Shows
Image: Roberto Diniz
Article · Dev & Eng
Reading
8 min
Tables
1

A benchmark published on Planet PostgreSQL measured disk size and speed of json and jsonb in PostgreSQL 17.11 and 18.6. The result dismantles the rule of thumb that jsonb is always the right choice.

A benchmark published on Planet PostgreSQL measured disk size and speed of json and jsonb in PostgreSQL 17.11 and 18.6. The result dismantles the rule of thumb that jsonb is always the right choice.

What's at Stake, and for Whom

Anyone modeling a PostgreSQL table with a semi-structured document column decides early between json and jsonb. The textbook answer is straightforward: use jsonb, because it indexes, compares, and groups. A benchmark published on Planet PostgreSQL, with measurements taken between November 3 and 6, 2026 in disposable containers running PostgreSQL 17.11 and 18.6, complicates that answer.

The team behind the benchmark measured disk size, read speed, COPY cost, key update, and GIN index across six document formats, two input formats (compact and indented), and two compression methods (pglz and lz4). The result: the size difference ranged from 22.6% smaller to 40.9% larger depending on the document, and there's no single answer.

This angle matters to whoever architects the schema, not just to whoever writes the query. The choice between the two types changes the storage plan, VACUUM behavior, index size, and even whether equality between two documents exists at all. It applies to those who already have jsonb in production and never measured it, and to those designing a new schema who follow the rule of thumb.

Comparison Criteria

Criterionjsonjsonb
Disk size, small compact docs (1M rows, ~200 bytes)baseline+17.8%
Disk size, small docs with indentationbaseline−3.6%
Disk size, large compact docs (5,000 rows, ~20 KB)baseline+33.0% (pglz) / +22.2% (lz4)
Reading a key with ->>, no indexbaseline (slower)2.7 to 10.6x faster
Containment index (@>)not supported, requires castnative GIN: jsonb_ops or jsonb_path_ops
Updating a keyno json_set; requires cast there and backnative jsonb_set, 1.3 to 3.4x faster than the cast
Equality, GROUP BY, ORDER BY, unique keynot supportedsupported
Preservation of original text (spaces, duplicate keys, order)keeps the exact textrewrites, discards duplicates, reorders
Load cost (COPY)faster1.08 to 4.6x slower

Analysis by Criterion

Disk size. The first layer, before any compression, is always larger in jsonb: the type stores a header per key and value, ordered keys, and numbers as numeric. For small documents this meant 38.5 extra bytes per row, 18.5% overhead, and since these documents don't cross the TOAST compression threshold (around 2 KB, per the PostgreSQL documentation), the difference went straight to disk: tables 17.8% larger.

Above that threshold, the math flips. The benchmark's average document, an order with a customer, an address, and seven items, weighed 1,521.9 bytes as compact json, below the threshold, and stayed uncompressed. As jsonb the same document weighed 2,071.9 bytes, crossed the threshold, got compressed with pglz down to 1,149.7 bytes, and the table ended up 16.6% smaller than the json one. A type 36% larger before compression became smaller after it.

This behavior explains why two numbers circulating in the community on the same topic point in opposite directions. Slides from a 2015 PGConf.Russia talk by Alexander Korotkov, Oleg Bartunov, and Teodor Sigaev record 1,374 MB in jsonb versus 1,322 MB in json for 1.25 million records, a difference under 4%, within the range the current benchmark also measured. An article republished on BestHub in September 2026, meanwhile, reports 888 MB versus 1,200 MB for one million rows, 26% smaller.

The team ran the generator published in that second article, with json.dumps called without arguments exactly as in the original text, and got the opposite result: jsonb 6.7% larger, 653.5 versus 612.6 MB. The reason is that json.dumps without arguments writes a space after every comma and colon, and that space disappears in jsonb. The original article's 26% gain didn't reproduce with its own script; the difference was in the input format, not the column type.

Reading a key. Here there's no ambiguity: jsonb won on every row measured, from 2.7 to 10.6 times faster at extracting a value with ->>, without an index, summing sum(length(doc->>'first')) over the whole table. The reason is structural: json stores text and has to re-parse the string on every read; jsonb already stores the parsed tree.

When the document is compressed, the compression method matters as much as the type. Reading a key from the average documents took 310 ms with pglz and 96 ms with lz4; for the "flat" documents, with 780 keys holding random strings, 149 ms versus 42 ms. Before applying ->>, PostgreSQL has to decompress the entire value, and lz4 decompresses faster, only taking up more space: 24.8% to 45.1% more than pglz on the large documents and on the integer array.

GIN index and containment. Only jsonb has a containment index. Trying CREATE INDEX ... USING gin (j) on a json column fails with the error data type json has no default operator class for access method "gin", and the @> operator doesn't even exist for json; the workaround is casting in the query, which took 6.5 to 12.3 times longer than querying an already-indexed jsonb column.

With the index on jsonb, searching for a rare value, like {"tag":"r042"} among one million rows, dropped from 236.4 ms without an index to 0.74 ms with jsonb_path_ops. For a common value, present in six out of every ten rows, the index didn't help on PostgreSQL 17, which stuck with a sequential scan. The index isn't free either: jsonb_ops ended up larger than the table itself on the small collection, 407.1 MB versus 292.9 MB, and jsonb_path_ops was smaller and about twice as fast to build, but doesn't support the key-existence operator ?.

Updating. json has no json_set; changing a key requires casting to jsonb, applying jsonb_set, and casting back to json, rewriting the text in jsonb's format regardless. This path took 1.8 times longer than jsonb_set directly on small documents, and the gap grew to as much as 3.4 times with lz4 on large documents. Both types rewrite the entire document on every UPDATE: changing a status in the large document generated 7,947 bytes of WAL per row, versus 231 bytes when the same value lives in a separate text column.

Semantic behavior. json stores the text exactly as it arrived; jsonb rewrites it. A document with a duplicate key, {"a":1,"a":2}, keeps both in json (json_object_keys returns a twice) and becomes {"a": 2} in jsonb, silently discarding the first one. json also has no equality operator: =, GROUP BY, DISTINCT, ORDER BY, UNION, IN, min(), and even a UNIQUE constraint fail with errors like operator does not exist: json = json. In jsonb, {"a":1}, {"a": 1}, and {"a":1.0} form a single group.

Verdict by Use Case

Webhook or signature-verified payload. If the application needs to return or validate the text exactly as it arrived, json is the right choice: it preserves spaces, key order, and duplicates, something jsonb irreversibly erases by rewriting the document.

Catalog or record queried by field. If the application filters, groups, or does a JOIN on a value inside the document, jsonb with a GIN index is the way to go: the difference of 0.74 ms versus 236.4 ms in a tag search, in the benchmark, has no reasonable counterpart in json, which doesn't even index containment.

Log or append-only table with no content filtering. For small documents (under 2 KB, compact) written once and read in full, with no key-based search, compact json is cheaper to load and, before compression, smaller on disk. jsonb's key-read speed gain doesn't pay off if the application never reads an isolated key.

Small team with no budget to measure. jsonb remains the safer default option, because it covers indexing, equality, and key reading without workarounds. The benchmark's caveat is to choose jsonb_path_ops over jsonb_ops when the ? operator isn't needed: a smaller index that's faster to build.

A key that changes frequently or is read on every query. Neither json nor jsonb won here: the benchmark's comparison with a separate text column showed 0.49 ms versus 146 ms to read, and 231 bytes of WAL versus 7,947 to update, on large documents. When a specific field dominates access, pulling it out of the document and into its own column beat both JSON types.

What Can't Be Claimed Yet

The benchmark didn't measure the cost of returning the whole document to a client, where json just passes the text through and jsonb has to render it back; also left out were jsonpath queries, SQL/JSON functions, B-tree expression indexes on a key, documents above roughly 40 KB, concurrent writers, and cold cache.

All the measurements came from a single machine, a Ryzen 7 3700X with 62 GiB of memory and shared_buffers set to 2 GB, which ensured every queried table stayed in shared memory. The team itself recommends reading the numbers as relative to one another, not absolute, and treating differences under 10% as a tie.

Also not measured was jsonb's behavior with value types outside the six generated formats: long text, floating-point numbers with many decimal places, or deep nesting. The benchmark's own integer array already behaved very differently from the strings: 164.3% larger in jsonb before compression, versus 7.4% to 36.1% in the other formats, which suggests the internal value type matters more than the document's shape.

This piece doesn't include an embedded image or video because the material received didn't come with any official video URL or iMasters image-bank asset.

Source

Planet PostgreSQL: "json vs jsonb in PostgreSQL: size and speed, measured," published on the Now-Next blog. Available at: https://postgr.es/p/9Bl.

Translated from the Brazilian Portuguese original · Read the original