Skip to main content

JSONB query builder

Write fast, correct PostgreSQL JSONB queries with AI-powered assistance

For people who store JSON or JSONB data in PostgreSQL and need help writing queries — extracting fields, filtering nested documents, indexing JSONB columns, or choosing between operators and jsonpath expressions.

About this tool

PostgreSQL's JSONB type is one of the most powerful features in any relational database, giving you the flexibility of a document store with the reliability and query power of SQL. But the sheer number of JSONB operators and functions — ->>, ->, @>, ?, ?|, ?&, #>, #>>, jsonb_path_query, jsonb_each, jsonb_array_elements, jsonb_set, jsonb_insert, jsonb_strip_nulls, and more — makes writing correct, efficient queries a real challenge, even for experienced developers. Choosing the wrong operator or missing an index can mean the difference between a query that runs in milliseconds and one that triggers a full table scan.

This tool helps you build JSONB queries by understanding what you need and generating the right combination of operators, functions, and indexing strategies. Describe your data shape and the result you want, and get back production-ready SQL with explanations of why each approach was chosen. Whether you are extracting a single field from a flat document, filtering rows by deeply nested values, unnesting arrays for aggregation, or performing batch updates on specific keys, the tool selects the optimal approach for your PostgreSQL version and data model.

Beyond simple key extraction, the tool handles the patterns that trip people up most: querying deeply nested structures with path operators (#> and #>>), unnesting arrays with jsonb_array_elements and re-aggregating results without accidentally multiplying rows, using containment (@>) for efficient indexed lookups instead of slower per-key extraction, writing jsonpath expressions for complex filtering (PostgreSQL 12+), and leveraging the subscript syntax introduced in PostgreSQL 14 for cleaner reads and writes. It also handles JSONB mutation patterns — updating nested keys with jsonb_set, merging documents with ||, removing keys with - and #-, and inserting into arrays with jsonb_insert — with proper null handling and safe defaults.

Performance is a first-class concern. Every query suggestion considers indexing implications. The tool explains when a GIN index with jsonb_ops is appropriate versus jsonb_path_ops, when an expression index on a specific JSONB path outperforms a full-column GIN index, and when you should extract a frequently queried field into a generated column instead. It also covers the critical distinction between JSONB and JSON — JSONB is parsed on write and stored in a decomposed binary format, making reads faster and indexing possible, while JSON preserves formatting and duplicate keys but cannot be indexed. For write-heavy workloads, the tool helps you evaluate whether JSONB column updates (which rewrite the entire TOAST tuple) are acceptable or whether a normalized schema would perform better.

Common anti-patterns are flagged automatically: using ->> in intermediate path steps (which breaks operator chaining), casting JSONB text extraction results without null guards (which causes runtime errors), creating GIN indexes on columns that are only queried by a single known path (where an expression index would be far smaller and faster), and filtering arrays by unnesting when a containment or jsonpath query would be both simpler and indexable.

For teams using JSONB as their primary data model — storing events, configurations, API payloads, or multi-tenant schemas — this tool provides the query patterns and indexing strategies that make the difference between a system that scales and one that grinds to a halt as data grows. It bridges the gap between PostgreSQL's extensive JSONB documentation and the practical patterns you actually need in production.

Examples

-- Data shape: orders table with a JSONB 'details' column containing:
-- {"customer": {"name": "Acme", "tier": "enterprise"},
-- "items": [{"sku": "W-100", "qty": 5, "price": 29.99}, ...],
-- "tags": ["urgent", "international"],
-- "metadata": {"source": "api", "version": 2}}

-- 1. Extract nested scalar values
select
id,
details ->> 'status' as status,
details -> 'customer' ->> 'name' as customer_name,
details -> 'customer' ->> 'tier' as tier,
details #>> '{metadata,source}' as source
from orders
where details -> 'customer' ->> 'tier' = 'enterprise';

-- 2. Containment query (uses GIN index)
select id, details
from orders
where details @> '{"customer": {"tier": "enterprise"}}';

-- 3. Existence operators
select id from orders where details ? 'tags';
select id from orders where details -> 'tags' ?| array['urgent', 'priority'];

-- 4. Unnest array elements and aggregate
select
o.id,
sum((item ->> 'qty')::int * (item ->> 'price')::numeric) as total
from orders o,
jsonb_array_elements(o.details -> 'items') as item
group by o.id;

-- 5. Jsonpath expression (PostgreSQL 12+)
select id, details
from orders
where details @? '$.items[*] ? (@.price > 100)';

-- 6. Update a nested key
update orders
set details = jsonb_set(details, '{customer,tier}', '"premium"')
where id = 42;

A comprehensive set of JSONB query patterns against a realistic orders table. Demonstrates extraction operators, containment for indexed lookups, existence checks, array unnesting with aggregation, jsonpath filtering, and safe in-place updates with jsonb_set.

-- GIN index for containment and existence queries
create index idx_orders_details_gin
on orders using gin (details);

-- jsonb_path_ops: smaller, faster for containment (@>) only
create index idx_orders_details_pathops
on orders using gin (details jsonb_path_ops);

-- Expression index for a frequently filtered scalar path
create index idx_orders_customer_tier
on orders ((details -> 'customer' ->> 'tier'));

-- Expression index for cast numeric path (range queries)
create index idx_orders_total
on orders (((details ->> 'total')::numeric));

-- Partial GIN index to limit index size
create index idx_orders_enterprise_gin
on orders using gin (details)
where details -> 'customer' ->> 'tier' = 'enterprise';

Indexing strategies for JSONB columns. The default GIN index with jsonb_ops supports containment, existence, and key/value queries. The jsonb_path_ops variant is smaller and faster but only supports containment (@>). Expression indexes on specific paths are best when you repeatedly filter on one known key. Partial GIN indexes reduce size when you only need to index a subset of rows.

Inputs and outputs

What you provide

  • Description of the JSONB document structure or a sample JSON document
  • The query goal — what data to extract, filter, aggregate, or modify
  • PostgreSQL version (for jsonpath and subscript syntax compatibility)

What you get

  • Production-ready JSONB queries with proper operators and type casting
  • GIN or expression index recommendations with CREATE INDEX statements
  • Explanation of operator choices, version requirements, and performance trade-offs

Use cases

  • Extracting nested fields from JSONB documents for reporting or API responses
  • Filtering rows by values deep inside JSONB columns using containment, existence, or jsonpath operators
  • Building GIN indexes on JSONB columns and choosing between jsonb_ops and jsonb_path_ops for your access patterns
  • Updating specific keys within JSONB documents using jsonb_set or jsonb_insert without rewriting the entire value
  • Unnesting JSONB arrays with jsonb_array_elements and joining or aggregating the results back into rows
  • Migrating from JSON to JSONB or from EAV schemas to document-style JSONB storage

Features

  • Generates JSONB queries using the optimal operator for each task — extraction (->>, ->), containment (@>), existence (?, ?|, ?&), and path navigation (#>, #>>)
  • Recommends GIN index strategies with concrete CREATE INDEX statements, including jsonb_ops vs jsonb_path_ops trade-offs
  • Produces jsonpath expressions for complex filtering (PostgreSQL 12+) and explains the @? and @@ operators
  • Handles array operations — jsonb_array_elements, jsonb_array_length, array containment queries, and re-aggregation patterns
  • Advises on JSONB mutation functions (jsonb_set, jsonb_insert, jsonb_strip_nulls, || concatenation) with safe update patterns
  • Explains when to use expression indexes on extracted JSONB paths versus full-column GIN indexes for better selectivity and smaller index size

Frequently asked questions

What is the difference between -> and ->> operators in PostgreSQL JSONB?

The -> operator extracts a JSON value and returns it as jsonb, preserving its type — an object stays an object, an array stays an array, a string stays a JSON string (with quotes). The ->> operator extracts a value and returns it as text, which is what you want when comparing against SQL strings, casting to other types, or displaying results. For example, details -> 'customer' returns the nested object as jsonb so you can chain further operators like -> 'name', while details -> 'customer' ->> 'name' returns the name as a plain text value. A common mistake is using ->> in intermediate steps of a path traversal — details ->> 'customer' -> 'name' fails because ->> returns text, and text does not support the -> operator. Use -> for all intermediate steps and ->> only on the final extraction. For deep paths, the #> and #>> operators are cleaner: details #>> '{customer,name}' is equivalent to details -> 'customer' ->> 'name'. In PostgreSQL 14+, you can also use subscript syntax: details['customer']['name'], which returns jsonb (similar to ->).

When should I use a GIN index with jsonb_ops vs jsonb_path_ops for JSONB?

The default GIN operator class jsonb_ops creates index entries for every key and value in the JSONB document. It supports containment (@>), existence (?, ?|, ?&), and key/value matching. The alternative jsonb_path_ops creates index entries only for full paths from root to each value, resulting in a significantly smaller index (often 2-3x smaller) and faster containment lookups. However, jsonb_path_ops *only* supports the containment operator @> — it cannot answer existence queries like ? 'key' or ?| array['a','b']. Choose jsonb_path_ops when all your queries use containment patterns like WHERE data @> '{"status": "active"}' and you want the smallest, fastest index. Choose jsonb_ops when you need existence checks or when your query patterns are varied. In many production systems, the best approach is neither — instead, create expression indexes on the specific paths you query most. An expression index like CREATE INDEX ON t ((data ->> 'status')) is far smaller than either GIN variant and supports equality, range queries, and sorting, which GIN indexes cannot do.

How do I query and filter elements inside a JSONB array in PostgreSQL?

There are several approaches depending on what you need. For simple containment checks — "does this array contain this value?" — use the @> operator: WHERE data -> 'tags' @> '["urgent"]'. This is the fastest approach because it uses GIN indexes. For checking if any of several values exist, use the ?| operator: WHERE data -> 'tags' ?| array['urgent', 'critical']. For complex filtering on array elements that are objects, you have two choices. The traditional approach uses jsonb_array_elements: SELECT * FROM orders o, jsonb_array_elements(o.data -> 'items') AS item WHERE (item ->> 'price')::numeric > 100. This unnests the array into rows and filters, but it prevents index usage on the array contents. The modern approach (PostgreSQL 12+) uses jsonpath: WHERE data @? '$.items[*] ? (@.price > 100)'. Jsonpath expressions are more concise and can use GIN indexes in some cases. For aggregation over array elements — such as summing prices — you must use jsonb_array_elements with a lateral join pattern and GROUP BY to collapse results back to one row per document. Always be aware that unnesting large arrays multiplies your row count and can cause performance surprises; check EXPLAIN ANALYZE to verify the planner's estimates.

What is the difference between JSONB and JSON types in PostgreSQL, and which should I use?

The json type stores the input text verbatim — it preserves whitespace, key ordering, and duplicate keys. It validates JSON syntax on input but does no further processing. The jsonb type parses the input on write and stores it in a decomposed binary format: whitespace is discarded, key order is normalized, and duplicate keys are deduplicated (last value wins). This difference has major practical implications. Reading individual keys from jsonb is O(log n) because the binary format supports direct key lookup, while json requires reparsing the entire text on every access — O(n). More critically, jsonb supports GIN indexes, containment (@>), existence (?), and jsonpath operators, while json supports almost none of these — you cannot create a GIN index on a json column or use @>. The write cost for jsonb is slightly higher because of the parsing and binary conversion, and it typically uses slightly more storage than json for small documents (due to binary overhead), though for large documents with many keys jsonb can actually be smaller because of deduplication. Use jsonb for virtually all cases. The only reasons to choose json are if you must preserve the exact formatting of the input (such as logging raw API payloads for audit compliance) or if the column is write-heavy and never queried beyond full-document retrieval.

Related tools

Ready to try it?

Use this tool for free — powered by PostgresAI.