Même EWikipedia nous propose qu'une définition négative:
«A database that focuses on data that is modeled in means other than tabular relations»
Repository at github.com/dbu/postgres-json
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: Non typé, stoqué verbatim | JSONB: Analysé et optimisé |
---|---|
|
|
=> Préfèrez le JSONB sauf si vous avez absolument besoin des comportements non-standard.
item->'author'
= '"David"'
ou, 2.5 fois plus vite
item->>'author'
= 'David'
Similaire avec MySQL
item->>'$.author'
> L'info JSON |
>> Extrair les données native |
SELECT COUNT(*) FROM auction_jsonb WHERE item->>'author' = 'David';
Lignes | json | jsonb | MySQL |
---|---|---|---|
10k | 136 | 31 | 21 |
100k | 319 | 46 | 148 |
1M | 1'280 | 157 | 1'225 |
10M | 15'505 | 2'768 | 11'758 |
Temps de requête en milliseconds
CREATE INDEX json_author ON auction_json_indexed ((item->>'author'));
Lignes | json | json indexé | jsonb | jsonb indexé | MySQL | MySQL indexé |
---|---|---|---|---|---|---|
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 |
Temps de requête en milliseconds
A 10M lignes, postgres ne voulait plus utiliser l'index, et a décidé d'executer un full table scan
-- Value <= 1.15 makes postgres use the index ALTER TABLESPACE pg_default SET (random_page_cost=1.1);
Merci à Frank Heikens sur stackoverflow pour proposer random_page_cost
CREATE TABLE auction_generated_fields ( id SERIAL NOT NULL, author VARCHAR(255) generated always as (item->>'author') stored, item JSONB NOT NULL, PRIMARY KEY(id));
Lignes | jsonb idx | generated | generated idx | MySQL gen | MySQL gen idx |
---|---|---|---|---|---|
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 |
Temps de requête en milliseconds
CREATE FUNCTION text_to_timestamp(text) RETURNS TIMESTAMP LANGUAGE sql IMMUTABLE AS $$ SELECT CASE WHEN $1 ~ '^\d{4}-\d{2}-\d{2}[ |T]\d{2}:\d{2}:\d{2}(\.\d+)?(\+00:00)?$' THEN CAST($1 AS timestamp without time zone) END $$; CREATE TABLE auction_generated_fields ( id SERIAL NOT NULL, start_date TIMESTAMP(0) generated always AS (text_to_timestamp(item->>'startDate')) stored, item JSONB NOT NULL, PRIMARY KEY(id));
item->author
JSON_QUERY(item, '$.author')
item #> '{author}'
item['author']
Tous ces constructions ne partagent pas leurs indexes.
=> Choisissez lequel de ces construction vous voulez utiliser
contient
item @> '{"author": "David"}'
clé existe
item ? 'author'
jsonb_exists(item, 'author')
au moins un des clés existe
item ?| array['author','foo']
jsonb_exists_any(item, array['a..', 'f..']
tous les clés existent
item ?& array['author','title']
  jsonb_exists_all(item, array['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"}';
Lignes | ->> indexé | @> non indé | @> indexé | @> avec 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 |
    Temps de requête en milliseconds
Accelère tous les recherches "JSON contient", mais pas d'autres requêtes
La nature des données dans une colonne indexé peut mener à des differences important
Variants | json | json idx | jsonb | jsonb idx | GIN @ | gen | gen idx |
---|---|---|---|---|---|---|---|
10 | 12'372 | 784 | 1'895 | 676 | 1'364 | 1'030 | 44 |
1'000 | 12'424 | 6 | 1'583 | 6 | 58 | 931 | 3 |
100'000 | 13'197 | 3 | 1'710 | 3 | 5 | 838 | 3 |
Temps de requête en milliseconds, table de 10 million lignes
Jsonb, indexes et champs générées tous demandent leurs prix
Lignes | json | json idx | jsonb | jsonb idx | GIN | gen | gen idx |
---|---|---|---|---|---|---|---|
10k | 60 | 90 | 90 | 120 | 130 | 94 | 143 |
100k | 550 | 740 | 820 | 990 | 2'110 | 890 | 1'000 |
1M | 5'150 | 8'500 | 7'670 | 11'340 | 24'000 | 8'440 | 11'773 |
10M | 55'000 | 96'000 | 97'000 | 115'000 | 256'000 | 84'000 | 116'000 |
Mésuré avec des lignes synthetiques, duré en milliseconds
Manipuler le JSON au lieu de le remplacer complètement
-- update, ignore if not exists UPDATE auction_jsonb SET item = jsonb_set(item, '{author}', '"value"', false) WHERE id=42; -- update or create UPDATE auction_jsonb SET item = jsonb_set(item, '{author}', 'value', true) WHERE id=42;
Postgres remplace tout le JSON, la requête n'est pas accéleré
Avec MySQL, on peut aussi manipuler le JSON, les fonctions sont appelé differament
-- 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"')
La doc MySQL postule que le moteur de MySQL peut optimiser pour éviter de recréer tout l'object JSON, mais j'ai pas réussi à mesurer une difference
-- delete attribute UPDATE auction_jsonb SET item = item - 'key' WHERE ID=42; -- delete attribute alternate UPDATE auction_jsonb SET item = jsonb_set_lax(item, '{author}', null, true, 'delete_key') WHERE ID=42; -- delete multiple item - array['key1', 'key2']
raise_exception | Erreur si valeur est null |
use_json_null | Mettre le valeur du champs JSON Ã null (defaut) |
delete_key | Supprimer la clé si le valeur est null |
return_target | Ne pas changer le valeur existant si le nouveau est null |
set_jsonb_lax(item, '{author}', null, true, 'return_target');
pg_jsonschema
/ postgres-json-schema
)
JSON_SCHEMA_VALID
, à spécifier dans la partie CHECK
||
concatener array / merger des objectsjsonb_build_object('key', 'value', 'k2', 'v2')
#[ORM\Column(type: Types::JSON)] private array $item; #[ORM\Column(type: Types::JSON, options: ['jsonb' => true])] private array $item;
Doctrine ne fournit pas des outils à définir des indices sur JSON. On les ajoute directement dans un scripte de migration.
$this->addSql('CREATE INDEX json_author ON auction_json_indexed ((item->>\'author\'))');
#[ORM\Column( length: 255, nullable: true, insertable: false, updatable: false, columnDefinition: "VARCHAR(255) generated always as (item->>'author') stored", generated: "ALWAYS" )] private ?string $author = null;
Grace à insertable: false, updatable: false
, Doctrine sait qu'il faut jamais écrire ce champs dans la bd.
Attention: columnDefinition
s'écrit en SQL simple, pas DQL
Vous pouvez utiliser Doctrine DBAL pour écrire des requêtes SQL native
Ou installer scienta/doctrine-json-functions
# config/packages/doctrine.yaml doctrine: orm: dql: string_functions: JSON_GET_TEXT: Scienta\...\Postgresql\JsonGetText JSONB_CONTAINS: Scienta\...\Postgresql\JsonbContains
Fonctions boolean sont à régistrer comme fonctions string
,
  et on doit comparer avec =true
pour du DQL accepté
$qb = $this->createQueryBuilder('a'); $qb->select('COUNT(a)') // Need to qualify with a. in front of item ->where("JSON_GET_TEXT(a.item, 'author') = :author") ->setParameter('author', $authorName) ; return $qb->getQuery()->getSingleScalarResult();
$qb = $this->createQueryBuilder('a'); $qb->select('COUNT(a)') ->where('JSONB_CONTAINS(a.item, :criteria) = true') ->setParameter('criteria', ['author' => $authorName], Types::JSON) ; return $qb->getQuery()->getSingleScalarResult();
Lignes | jsonb | dbal | dbal, batch 1k | entities |
---|---|---|---|---|
10k | 60 | 4519 | 163 | 6'290 |
100k | 550 | 49'964 | 1'587 | 53'823 |
See pgdash.io
json_each(item)
jsonb_each(item)
SELECT *
FROM jsonb_each
((SELECT item FROM auction_jsonb WHERE id=1));