Repository at github.com/dbu/postgres-json
and github.com/dbu/postgres-json/pull/2 for MySQL
CREATE TABLE auction_jsonb ( id SERIAL NOT NULL, title VARCHAR(255) NOT NULL, current_price INT DEFAULT NULL, item JSONB NOT NULL, PRIMARY KEY(id) );
JSON: Untyped, stored raw | JSONB: Stored structured |
---|---|
|
|
=> Use JSONB unless you rely on non-standard behaviour.
Result | Postgres | MySQL |
---|---|---|
'"David"' | item->'author' | item->'$.author' |
'David' | item->>'author' | item->>'$.author' |
Postgres: ->>
is 2.5 times faster
MySQL: Query with ->
need to CAST('"David"' AS JSON)
> The JSON information |
>> Extract primitive type |
SELECT COUNT(*) FROM auction_jsonb WHERE item->>'author' = 'David'; # WHERE item->>'$.author' = 'David';
Rows | json | jsonb | MySQL |
---|---|---|---|
10k | 136 | 31 | 21 |
100k | 319 | 46 | 148 |
1M | 1'280 | 157 | 1'225 |
10M | 15'505 | 2'768 | 11'758 |
Query time in milliseconds
CREATE INDEX json_author ON auction_json_indexed ((item->>'author')); # ON auction_idx ((CAST(item->>'$.author' AS CHAR(255)));
Rows | json | json indexed | jsonb | jsonb indexed | mysql | mysql indexed |
---|---|---|---|---|---|---|
10k | 136 | 25 | 31 | 20 | 21 | 9 |
100k | 319 | 12 | 46 | 13 | 148 | 15 |
1M | 1'280 | 3 | 157 | 3 | 1225 | 146 |
10M | 15'505 | 7 | 2'768 | 6 | 11'758 | 1'276 |
Query time in milliseconds
# mysql needs to be queried with cast to use index CAST(item->>'$.author' AS CHAR(255)) = ...
CREATE TABLE auction_generated_fields ( id SERIAL NOT NULL, author VARCHAR(255) generated always as (item->>'author') stored, # as (item->>'$.author') stored, item JSON NOT NULL, PRIMARY KEY(id));
Rows | jsonb indexed | gen | gen indexed | mysql gen | mysql gen indexed |
---|---|---|---|---|---|
10k | 20 | 21 | 18 | 15 | 11 |
100k | 13 | 30 | 10 | 55 | 8 |
1M | 3 | 88 | 3 | 741 | 20 |
10M | 6 | 786 | 4 | 7'097 | 21 |
Query time in milliseconds
Use | Postgres | MySQL |
---|---|---|
Contains | item @> '{"author": "David"}' | JSON_CONTAINS(item, '{"author": "David"}') |
Path Exists | item ? 'author jsonb_exists(item, 'author') |
JSON_CONTAINS_PATH(item, 'one', '$.author') |
Any key |
item ?| array['author','foo'] jsonb_exists_any(item, array[...] |
JSON_CONTAINS_PATH(item, 'one', 'a..', 'f..') |
All keys | item ?& array['author','title'] jsonb_exists_all(item, array[...] |
JSON_CONTAINS_PATH(item, 'all', 'a..', 'f..') |
CREATE INDEX auction_json_gin_idx ON auction_jsonb_gin USING GIN (item); SELECT COUNT(*) FROM auction_jsonb_gin WHERE item @> '{"author": "David"}';
Rows | ->> indexed | @> no index | @> with index | @> with GIN | MySQL |
---|---|---|---|---|---|
10k | 20 | 5 | 5 | 5 | 29 |
100k | 13 | 20 | 19 | 5 | 117 |
1M | 3 | 144 | 149 | 13 | 1'478 |
10M | 6 | 1'239 | 1'235 | 76 | 13'866 |
Query time in milliseconds
Speed up arbitrary JSON contains queries, but not other queries
No equivalent in MySQL
Data distribution of the indexed column makes a huge difference for Postgres
Variants | json | json idx | jsonb | jsonb idx | GIN >> | GIN @ | gen | gen idx |
---|---|---|---|---|---|---|---|---|
10 | 12'372 | 784 | 1'895 | 676 | 1'882 | 1'364 | 1'030 | 44 |
1'000 | 12'424 | 6 | 1'583 | 6 | 1'260 | 58 | 931 | 3 |
100'000 | 13'197 | 3 | 1'710 | 3 | 1'501 | 5 | 838 | 3 |
Response times in milliseconds, table with 10 million rows
Jsonb, indexes and generated fields do not come for free
Rows | json idx | jsonb idx | GIN | gen idx | MySQL | MySQL idx | MySQL gen idx |
---|---|---|---|---|---|---|---|
10k | 90 | 120 | 130 | 143 | 240 | 330 | 480 |
100k | 740 | 990 | 2'110 | 1'000 | 1'600 | 2'610 | 2'840 |
1M | 8'500 | 11'340 | 24'000 | 11'773 | 15'410 | 27'110 | 28'650 |
10M | 96'000 | 115'000 | 256'000 | 116'000 | 216'480 | 275'060 | 297'660 |
Inserting dummy rows, duration in milliseconds
Manipulate JSON instead of replacing it completely
-- update, ignore if not exists jsonb_set(item, '{author}', '"value"', false) -- update or create jsonb_set(item, '{author}', 'value', true) -- create, ignore if already exists jsonb_insert(item, '{author}', 'value') -- optional insert_after flag to append elements into array
Postgres is replacing the whole JSON, query is not faster
Manipulate JSON instead of replacing it completely
-- update, ignore if not exists JSON_REPLACE(item, '$.author', '"value"') -- update or create JSON_SET(item, '$.author', '"value"') -- create, ignore if already exists JSON_INSERT(item, '$.author', '"value"')
MySQL doc claims engine can optimize to not rebuild whole JSON object, but I was not able to observe velocity difference
-- delete attribute item - 'key' -- delete attribute alternate jsonb_set_lax(item, '{author}', null, true, 'delete_key') -- delete multiple item - array['key1', 'key2'] -- delete in MySQL (takes any number of path parameters) JSON_REMOVE(item, '$.author', '$.other')
raise_exception | Fail if value is null |
use_json_null | Set JSON field to null (default) |
delete_key | Delete the key on null value |
return_target | Leave previous value when new is null |
set_jsonb_lax(item, '{author}', null, true, 'return_target');
pg_jsonschema
/ postgres-json-schema
)
JSON_SCHEMA_VALID
function, can be defined as CHECK
Postgres | MySQL | Use |
---|---|---|
|| |
JSON_MERGE_PATCH() |
concat array / merge objects |
jsonb_build_object() |
JSON_OBJECT() | Alternate key and value |
jsonb(...) | CAST('...' as JSON) | Parse from string |
jsonb_serialize() | JSON_PRETTY or CAST | JSON to string |
jsonb_each | Not supported | Expand top-level object key-value into rows |
json_each(item)
jsonb_each(item)
SELECT *
FROM jsonb_each
((SELECT item FROM auction_jsonb WHERE id=1));
item->author
JSON_QUERY(item, '$.author')
item #> '{author}'
item['author']
No sharing of indexes.
=> Decide which construct you use