json, nebo jsonb?
Oba typy přijmou jen platný JSON text. Rozdíl je v tom, co si
databáze uloží. Typ json drží vstup znak po znaku. Typ jsonb ho při
vložení rozloží, takže zahodí bílé znaky, seřadí klíče podle vlastního pořadí a z
duplicitních klíčů nechá poslední hodnotu:
SELECT '{"b": 1, "a": 2, "a": 3}'::json AS json,
'{"b": 1, "a": 2, "a": 3}'::jsonb AS jsonb;
json | jsonb
---------------------------+------------------
{"b": 1, "a": 2, "a": 3} | {"a": 3, "b": 1} | Vlastnost | json | jsonb |
|---|---|---|
| Uložení | přesná kopie vstupního textu | rozložená binární reprezentace |
| Bílé znaky a pořadí klíčů | zachová | nezachová |
| Duplicitní klíče | zachová všechny (funkce berou poslední) | ponechá jen poslední |
| Vložení | rychlejší (jen kontrola syntaxe) | pomalejší (převod) |
| Dotazy | při každém dotazu znovu parsuje | bez opakovaného parsování |
| Operátory @>, ?, @?, @@ a GIN index | ne | ano |
json má smysl tam, kde musíš uchovat přesný přijatý text, například pro audit
nebo kontrolu podpisu. Pro data, nad kterými se dotazuješ, použij jsonb.
Čísla ukládá jsonb jako numeric. Zachová tedy počet desetinných míst,
ale zápis normalizuje:
SELECT '{"cena": 199.90, "velke": 1e2}'::jsonb;
-- {"cena": 199.90, "velke": 100}
Escapovaný zápis "ž" a přímé "ž" jsou v JSON stejný řetězec a
jsonb je uloží stejně. Escapování diakritiky na straně aplikace tak nemá vliv na
obsah sloupce, jen na velikost přenášeného textu. Viz řetězce v JSON.
Ukázková tabulka
Další příklady pracují s touto tabulkou. Kontrola CHECK zajistí, že sloupec obsahuje
objekt, ne libovolnou JSON hodnotu. Povinné klíče a typy jejich hodnot ale kontroluje až
aplikace nebo JSON Schema.
CREATE TABLE objednavka (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
data jsonb NOT NULL,
CONSTRAINT data_je_objekt CHECK (jsonb_typeof(data) = 'object')
);
INSERT INTO objednavka (data) VALUES
('{"zakaznik":{"jmeno":"Jana","mesto":"Brno"},"stav":"nova","stitky":["expres","firma"],
"polozky":[{"kod":"A1","ks":2,"cena":199.9},{"kod":"B7","ks":1,"cena":49}]}'),
('{"zakaznik":{"jmeno":"Petr","mesto":"Praha"},"stav":"odeslana","stitky":["firma"],
"polozky":[{"kod":"A1","ks":1,"cena":199.9}]}'),
('{"zakaznik":{"jmeno":"Eva","mesto":"Brno"},"stav":"nova","polozky":[]}'); Čtení hodnot: operátory
| Operátor | Vrací | Význam |
|---|---|---|
| -> | jsonb | položka objektu podle klíče nebo prvek pole podle indexu (od 0) |
| ->> | text | totéž jako text; JSON null dá SQL NULL |
| #> / #>> | jsonb / text | položka podle cesty, např. '{polozky,0,kod}' |
| @> | boolean | levá hodnota obsahuje pravou (podstrukturu) |
| ? | boolean | klíč nebo řetězcový prvek pole existuje na nejvyšší úrovni |
| ?| / ?& | boolean | existuje některý / všechny z klíčů |
| @? | boolean | jsonpath vrátí aspoň jednu položku |
| @@ | boolean | výsledek predikátu jsonpath |
| || / - / #- | jsonb | sloučení na nejvyšší úrovni / odstranění klíče / odstranění podle cesty |
Rozhodující je rozdíl mezi -> a ->>. První vrací
jsonb a hodí se pro další zanořování. Druhý vrací text pro porovnání,
výstup a přetypování, například (data->>'cena')::numeric.
SELECT id,
data->'zakaznik' AS objekt, -- jsonb
data->'zakaznik'->>'jmeno' AS jmeno, -- text
data#>>'{polozky,0,kod}' AS prvni_kod -- text podle cesty
FROM objednavka ORDER BY id;
id | objekt | jmeno | prvni_kod
----+-------------------------------------+-------+-----------
1 | {"jmeno": "Jana", "mesto": "Brno"} | Jana | A1
2 | {"jmeno": "Petr", "mesto": "Praha"} | Petr | A1
3 | {"jmeno": "Eva", "mesto": "Brno"} | Eva | -- obsahuje tuto podstrukturu? → 1, 3
SELECT id FROM objednavka WHERE data @> '{"stav":"nova","zakaznik":{"mesto":"Brno"}}';
-- existuje klíč na nejvyšší úrovni? → 1, 2
SELECT id FROM objednavka WHERE data ? 'stitky';
-- obsahuje pole řetězec? → 1
SELECT id FROM objednavka WHERE data->'stitky' ? 'expres';
-- vrátí jsonpath aspoň jednu položku? → 1, 2
SELECT id FROM objednavka WHERE data @? '$.polozky[*] ? (@.cena > 100)';
-- je predikát jsonpath pravdivý? → 3
SELECT id FROM objednavka WHERE data @@ '$.polozky.size() == 0';
Operátor ? hledá jen na nejvyšší úrovni hodnoty, na kterou ho použiješ. Proto se
prvek pole hledá výrazem data->'stitky' ? 'expres'. Výraz
@> porovnává strukturu: '{"zakaznik":{"mesto":"Brno"}}' odpovídá
každé objednávce, jejíž zákazník má toto město, bez ohledu na další klíče.
Jazyk cest jsonpath v PostgreSQL vychází ze standardu SQL/JSON. Není to
JSONPath podle RFC 9535, i když začíná stejným znakem $.
Filtr se zapisuje ? (@.cena > 100), ne [?@.cena > 100].
JSON_TABLE: pole jako řádky
Od PostgreSQL 17 převede JSON_TABLE pole objektů na řádky s typovanými sloupci.
Ve starších verzích stejnou práci udělá jsonb_array_elements v LATERAL
spojení.
SELECT o.id, p.kod, p.ks, p.cena
FROM objednavka o,
JSON_TABLE(o.data, '$.polozky[*]' COLUMNS (
kod text PATH '$.kod',
ks integer PATH '$.ks',
cena numeric PATH '$.cena')) AS p
ORDER BY o.id, p.kod;
id | kod | ks | cena
----+-----+----+-------
1 | A1 | 2 | 199.9
1 | B7 | 1 | 49
2 | A1 | 1 | 199.9
Objednávka 3 má prázdné pole položek, a proto ve výsledku chybí. Pokud ji potřebuješ, použij
LEFT JOIN JSON_TABLE(…) ON true.
Generování JSON v SQL dotazu
Server může vrátit hotovou strukturu místo řádků, které by aplikace teprve skládala.
jsonb_build_object vytvoří objekt z dvojic klíč–hodnota a jsonb_agg
spojí řádky skupiny do pole:
SELECT data->'zakaznik'->>'mesto' AS mesto,
jsonb_agg(jsonb_build_object('id', id, 'stav', data->>'stav') ORDER BY id) AS objednavky
FROM objednavka
GROUP BY 1 ORDER BY 1;
mesto | objednavky
-------+--------------------------------------------------------
Brno | [{"id": 1, "stav": "nova"}, {"id": 3, "stav": "nova"}]
Praha | [{"id": 2, "stav": "odeslana"}]
Funkce json_build_object a json_agg dělají totéž, ale vrací
json, takže zachovají pořadí klíčů tak, jak je zapíšeš. Od verze 16 existují i
standardní konstruktory JSON_OBJECT a JSON_ARRAYAGG.
Agregace nad prázdnou množinou vrací SQL NULL, ne prázdné pole:
SELECT jsonb_agg(x) FROM (SELECT 1 AS a WHERE false) x; -- NULL
SELECT coalesce(jsonb_agg(x), '[]') FROM (SELECT 1 AS a WHERE false) x; -- [] Úpravy dokumentu
UPDATE objednavka SET data = jsonb_set(data, '{stav}', '"zaplacena"') WHERE id = 1;
UPDATE objednavka SET data = data || '{"poznamka":"volat předem"}' WHERE id = 2;
UPDATE objednavka SET data = data - 'poznamka' WHERE id = 2;
Každý UPDATE zapíše novou verzi celého řádku, i když změníš jediný klíč. U
velkých dokumentů, které se často mění po částech, proto zvaž, jestli ty části nepatří do
samostatných sloupců. Operátor || slučuje jen nejvyšší úroveň, vnořený objekt
nahradí celý.
Indexy: GIN a B-strom nad výrazem
GIN index nad jsonb indexuje klíče a hodnoty dokumentu. Zrychlí dotazy na obsah
(@>), existenci klíče (?) a jsonpath (@?,
@@). Není to fulltextový index: nehledá slova uvnitř řetězců. Fulltext dělá
typ tsvector, například přes to_tsvector('simple', data->>'popis').
-- univerzální: @>, ?, ?|, ?&, @?, @@
CREATE INDEX udalost_data_gin ON udalost USING GIN (data);
-- menší, ale jen @>, @?, @@ (bez operátorů ?)
CREATE INDEX udalost_data_path ON udalost USING GIN (data jsonb_path_ops);
-- jeden klíč porovnávaný na rovnost nebo rozsah: běžný B-strom nad výrazem
CREATE INDEX udalost_uzivatel ON udalost ((data->>'uzivatel')); Index se použije, jen když dotaz obsahuje stejný operátor nebo stejný výraz jako index. To ukazují plány z tabulky s 200 000 řádky:
EXPLAIN (COSTS OFF) SELECT count(*) FROM udalost WHERE data->>'uzivatel' = 'u42';
-> Bitmap Index Scan on udalost_uzivatel
Index Cond: ((data ->> 'uzivatel'::text) = 'u42'::text)
EXPLAIN (COSTS OFF) SELECT count(*) FROM udalost WHERE data->'uzivatel' = '"u42"';
-> Parallel Seq Scan on udalost
Filter: ((data -> 'uzivatel'::text) = '"u42"'::jsonb)
-- jen s indexem jsonb_path_ops:
EXPLAIN (COSTS OFF) SELECT count(*) FROM udalost WHERE data ? 'reklamace';
-> Seq Scan on udalost
Filter: (data ? 'reklamace'::text)
Výraz data->'uzivatel' = '"u42"' vrací stejné řádky jako
data->>'uzivatel' = 'u42', ale index nad (data->>'uzivatel')
nepoužije. Na této tabulce měl GIN index s jsonb_path_ops 1,6 MB, výchozí
jsonb_ops 2,3 MB a B-strom nad jedním klíčem 1,4 MB. Velikosti závisí na datech;
výběr ověř pomocí EXPLAIN ANALYZE na svých dotazech.
Chybějící klíč, JSON null a SQL NULL
->> vrací SQL NULL pro chybějící klíč i pro hodnotu
null. Pokud ten rozdíl potřebuješ, ptej se na klíč zvlášť:
SELECT '{"a":1}'::jsonb->>'b' IS NULL AS chybi, -- t
'{"b":null}'::jsonb->>'b' IS NULL AS null_hodnota, -- t
'{"b":null}'::jsonb ? 'b' AS klic_existuje; -- t
Stejný rozdíl musíš ošetřit i v aplikaci, viz chybějící
hodnoty v Go. data->'b' vrací pro {"b":null} hodnotu
'null'::jsonb, která není SQL NULL.
Dotazy z aplikace: placeholder ?
Rozhraní s otazníkovými placeholdery (PHP PDO, JDBC) považují operátory ?,
?| a ?& za parametry. PDO od PHP 7.4 i ovladač pgJDBC proto
přijímají zdvojený ??, který se do databáze odešle jako jeden otazník:
// Chybně: PDO považuje ? za placeholder
$st = $pdo->prepare("SELECT id FROM objednavka WHERE data ? 'stitky' AND data->>'stav' = ?");
// PDOException: syntax error at or near "$1"
// Správně: ?? se odešle jako operátor ?
$st = $pdo->prepare("SELECT id FROM objednavka WHERE data ?? 'stitky' AND data->>'stav' = ?");
$st->execute(['odeslana']);
// JSON parametr vždy přes json_encode, nikdy skládáním řetězce
$st = $pdo->prepare("SELECT id FROM objednavka WHERE data @> ?::jsonb");
$st->execute([json_encode(['zakaznik' => ['mesto' => 'Praha']], JSON_THROW_ON_ERROR)]);
Operátor @> otazník neobsahuje a GIN index použije také, takže je často
jednodušší alternativou. Laravel where('data->stav', …) sestaví stejné dotazy
za tebe, viz JSON sloupce v Laravelu.
XML v PostgreSQL
Typ xml při vložení zkontroluje, že jde o well-formed obsah. Nevaliduje proti DTD
ani XSD. Ve výchozím nastavení (xmloption = content) přijme i fragment s více
kořeny, takže pro celé dokumenty kontroluj IS DOCUMENT. Typ je dostupný jen v
sestavení s libxml2, což běžné balíčky splňují.
SELECT xml '<a>1</a><b/>' IS DOCUMENT; -- f: obsah (CONTENT), ne dokument
SELECT xml_is_well_formed_document('<a><b></a>'); -- f
SELECT '<a><b></a>'::xml;
-- ERROR: invalid XML content
-- DETAIL: line 1: Opening and ending tag mismatch: b line 1 and a
Funkce xpath() používá XPath 1.0. Elementy ve
výchozím jmenném prostoru proto najde jen přes prefix, který
předáš třetím argumentem. Podrobně to popisuje stránka
XPath nic nevrací.
INSERT INTO feed VALUES (1, '<feed xmlns="http://www.w3.org/2005/Atom">
<entry><title>První</title><updated>2026-10-01T08:00:00Z</updated></entry>
<entry><title>Druhý</title><updated>2026-10-03T08:00:00Z</updated></entry>
</feed>');
SELECT xpath('//entry/title/text()', doc) FROM feed; -- {}
SELECT xpath('//a:entry/a:title/text()', doc,
ARRAY[ARRAY['a', 'http://www.w3.org/2005/Atom']])
FROM feed; -- {První,Druhý} Pro převod opakujících se elementů na řádky slouží XMLTABLE (od verze 10):
SELECT x.titulek, x.zmeneno
FROM feed,
XMLTABLE(XMLNAMESPACES('http://www.w3.org/2005/Atom' AS a),
'/a:feed/a:entry' PASSING doc
COLUMNS titulek text PATH 'a:title',
zmeneno timestamptz PATH 'a:updated') AS x;
titulek | zmeneno
---------+------------------------
První | 2026-10-01 11:00:00+03
Druhý | 2026-10-03 11:00:00+03 Funkce xmlelement a xmlattributes vytvoří XML a samy escapují text:
SELECT xmlelement(name objednavka, xmlattributes(42 AS id),
xmlelement(name zakaznik, 'Jana & syn'));
-- <objednavka id="42"><zakaznik>Jana & syn</zakaznik></objednavka>
Typ xml nemá obdobu GIN indexu, jaký má jsonb, a neumí ani operátor
rovnosti. Hodnoty, podle kterých často filtruješ, vytáhni do běžného sloupce nebo do indexu nad
výrazem, například ((xpath('/a/@id', doc))[1]::text).
Časté chyby
- Typ json pro data, nad kterými se dotazuje. Nefungují operátory
@>,?ani GIN index a každý dotaz text znovu parsuje. - Index, který dotaz nepoužije. GIN s
jsonb_path_opsnezrychlí?; index nad(data->>'klic')nezrychlí@>anidata->'klic'. - Vše do jednoho sloupce. Cizí klíče, unikátnost a povinné hodnoty patří do běžných sloupců s omezeními. JSON se hodí pro proměnlivou nebo méně dotazovanou část dat.
- Skládání JSON v SQL řetězcem. Hodnotu z aplikace předávej jako parametr a
přetypuj
?::jsonb, nebo ji vytvoř přesjsonb_build_object. - Záměna jsonpath a JSONPath. Syntaxe filtrů se liší, dotaz z JSONPath testeru nelze vložit beze změny.
Verze
Příklady jsou ověřené na PostgreSQL 18.4, aktuální stabilní řadě k 4. 10. 2026 (verze 19 je
ve fázi beta). Ostatní příklady kromě JSON_TABLE fungují ve všech
podporovaných verzích (14 až 18).
| Funkce | Od verze |
|---|---|
| json | 9.2 |
| jsonb, GIN indexy jsonb_ops a jsonb_path_ops | 9.4 |
| typ jsonpath, operátory @? a @@ | 12 |
| IS JSON, JSON_OBJECT, JSON_ARRAY, JSON_ARRAYAGG, JSON_OBJECTAGG | 16 |
| JSON_TABLE, JSON_EXISTS, JSON_QUERY, JSON_VALUE | 17 |
| XMLTABLE | 10 |
Zdroje: JSON Types, JSON Functions and Operators, XML Type a XML Functions.