PostgreSQL JSONB Query Examples
Ghazi · July 25, 2026
JSONB is useful when one row owns a flexible document and you still need PostgreSQL to inspect it. The operators are compact, but they are easier to remember when every example uses the same table.
Start with -> for JSON, ->> for text, @> for containment, and ? for key existence. Add a GIN index only for the operators your real queries use.
Example table
create table products (
id bigint generated always as identity primary key,
attributes jsonb not null default '{}'::jsonb
);
insert into products (attributes) values
('{"name":"Desk lamp","color":"green","price":49,"tags":["home","lighting"]}'),
('{"name":"Travel mug","color":"black","price":28,"tags":["kitchen"]}');Extract a value
The single-arrow operator returns JSONB. The double-arrow operator returns text, which is usually what you want for display or a text comparison. Cast text before numeric or date comparisons.
Read and cast
select
attributes->>'name' as name,
(attributes->>'price')::numeric as price
from products
where (attributes->>'price')::numeric < 40;Match part of a document
Containment asks whether the left JSONB value contains the structure on the right. It is often clearer than extracting several keys and can use a GIN index.
Containment with @>
select *
from products
where attributes @> '{"color":"green"}';Check keys and array contents
The ? operator checks whether a top-level object key exists. On an array, it checks for a matching string element. Keep the level of the document in mind; nested keys need an extraction step or jsonpath.
Existence checks
select * from products
where attributes ? 'price';
select * from products
where attributes->'tags' ? 'lighting';Update one path without replacing the document
jsonb_set returns a new document with one path changed. The final true argument allows PostgreSQL to create the key when it is missing.
Change one value
update products
set attributes = jsonb_set(
attributes,
'{price}',
to_jsonb(45),
true
)
where id = 1;Index the query you actually run
The default GIN operator class supports containment, key existence, and jsonpath matches. A jsonb_path_ops index is smaller and specialized for containment and jsonpath, but it does not support the ? family of key operators.
Expression indexes are often better when one extracted value behaves like a normal column. If price drives filtering and sorting on every screen, making it a numeric column may be clearer than maintaining a cast expression.
Two index shapes
create index products_attributes_gin
on products using gin (attributes);
create index products_price_idx
on products (((attributes->>'price')::numeric));Use JSONB at the edge of a relational model
JSONB works well for provider payloads, optional product attributes, event metadata, and settings that vary by type. It is a poor hiding place for required fields, foreign keys, money, and status values that need constraints.
The useful question is not whether PostgreSQL can query the document. It can. Ask whether the database should know and enforce the shape of that data.