Back to Blog
PostgreSQLJSONDatabaseSQL

PostgreSQL JSON Queries and Operations

Master PostgreSQL JSON operations. From basic queries to indexing to advanced transformations and aggregations.

B
Bootspring Team
Engineering
December 22, 2021
6 min read

PostgreSQL's JSON support enables flexible data modeling. Here's how to query and manipulate JSON data effectively.

JSON vs JSONB#

1-- JSON: Stores exact copy, preserves whitespace and order 2-- JSONB: Binary format, faster queries, supports indexing 3 4CREATE TABLE users ( 5 id SERIAL PRIMARY KEY, 6 profile JSON, -- Use for exact preservation 7 settings JSONB -- Use for querying (recommended) 8); 9 10-- JSONB is generally preferred 11CREATE TABLE products ( 12 id SERIAL PRIMARY KEY, 13 name VARCHAR(255), 14 attributes JSONB DEFAULT '{}'::jsonb, 15 metadata JSONB 16); 17 18-- Insert JSON data 19INSERT INTO products (name, attributes, metadata) 20VALUES ( 21 'Laptop', 22 '{"brand": "Apple", "specs": {"ram": 16, "storage": 512}}', 23 '{"tags": ["electronics", "computer"], "rating": 4.5}' 24);

Basic JSON Queries#

1-- Access JSON field (returns JSON) 2SELECT attributes->'brand' FROM products; 3-- Result: "Apple" (with quotes) 4 5-- Access JSON field as text 6SELECT attributes->>'brand' FROM products; 7-- Result: Apple (without quotes) 8 9-- Nested access 10SELECT attributes->'specs'->'ram' FROM products; 11SELECT attributes->'specs'->>'ram' FROM products; 12 13-- Path access 14SELECT attributes #> '{specs, ram}' FROM products; 15SELECT attributes #>> '{specs, ram}' FROM products; 16 17-- Filter by JSON value 18SELECT * FROM products 19WHERE attributes->>'brand' = 'Apple'; 20 21-- Filter by nested value 22SELECT * FROM products 23WHERE (attributes->'specs'->>'ram')::int >= 16; 24 25-- Check if key exists 26SELECT * FROM products 27WHERE attributes ? 'brand'; 28 29-- Check if any key exists 30SELECT * FROM products 31WHERE attributes ?| array['brand', 'model']; 32 33-- Check if all keys exist 34SELECT * FROM products 35WHERE attributes ?& array['brand', 'specs'];

Array Operations#

1-- Access array element (0-indexed) 2SELECT metadata->'tags'->0 FROM products; 3-- Result: "electronics" 4 5-- Check if array contains value 6SELECT * FROM products 7WHERE metadata->'tags' ? 'electronics'; 8 9-- Array contains all values 10SELECT * FROM products 11WHERE metadata->'tags' ?& array['electronics', 'computer']; 12 13-- Expand array to rows 14SELECT id, jsonb_array_elements_text(metadata->'tags') as tag 15FROM products; 16 17-- Array length 18SELECT jsonb_array_length(metadata->'tags') FROM products; 19 20-- Filter by array contents 21SELECT * FROM products 22WHERE metadata @> '{"tags": ["electronics"]}';

Containment and Existence#

1-- Contains (left contains right) 2SELECT * FROM products 3WHERE attributes @> '{"brand": "Apple"}'; 4 5-- Contained by (left is contained by right) 6SELECT * FROM products 7WHERE '{"brand": "Apple", "specs": {"ram": 16}}'::jsonb @> attributes; 8 9-- Complex containment 10SELECT * FROM products 11WHERE attributes @> '{"specs": {"ram": 16}}'; 12 13-- Top-level key existence 14SELECT * FROM products 15WHERE attributes ? 'brand'; 16 17-- Nested key check (use path) 18SELECT * FROM products 19WHERE attributes #> '{specs}' ? 'ram';

Modifying JSON#

1-- Set/update value 2UPDATE products 3SET attributes = jsonb_set(attributes, '{brand}', '"Dell"') 4WHERE id = 1; 5 6-- Set nested value 7UPDATE products 8SET attributes = jsonb_set(attributes, '{specs, ram}', '32') 9WHERE id = 1; 10 11-- Create nested path if missing 12UPDATE products 13SET attributes = jsonb_set( 14 attributes, 15 '{specs, gpu}', 16 '"NVIDIA RTX 4090"', 17 true -- create_if_missing 18) 19WHERE id = 1; 20 21-- Remove key 22UPDATE products 23SET attributes = attributes - 'brand' 24WHERE id = 1; 25 26-- Remove nested key 27UPDATE products 28SET attributes = attributes #- '{specs, ram}' 29WHERE id = 1; 30 31-- Remove array element by index 32UPDATE products 33SET metadata = metadata #- '{tags, 0}' 34WHERE id = 1; 35 36-- Concatenate/merge JSON 37UPDATE products 38SET attributes = attributes || '{"color": "silver", "weight": 1.4}'::jsonb 39WHERE id = 1; 40 41-- Insert into array 42UPDATE products 43SET metadata = jsonb_set( 44 metadata, 45 '{tags}', 46 metadata->'tags' || '"portable"'::jsonb 47) 48WHERE id = 1;

Aggregation and Transformation#

1-- Build JSON object from columns 2SELECT jsonb_build_object( 3 'id', id, 4 'name', name, 5 'brand', attributes->>'brand' 6) as product_json 7FROM products; 8 9-- Build JSON array 10SELECT jsonb_agg(name) as product_names 11FROM products; 12 13-- Aggregate to object 14SELECT jsonb_object_agg(name, attributes) 15FROM products; 16 17-- Transform rows to JSON 18SELECT row_to_json(products) FROM products; 19 20-- Pretty print 21SELECT jsonb_pretty(attributes) FROM products; 22 23-- Get all keys 24SELECT DISTINCT jsonb_object_keys(attributes) 25FROM products; 26 27-- Expand object to rows 28SELECT id, key, value 29FROM products, jsonb_each(attributes); 30 31-- Expand with text values 32SELECT id, key, value 33FROM products, jsonb_each_text(attributes);

Indexing JSON#

1-- GIN index for containment queries (@>, ?, ?|, ?&) 2CREATE INDEX idx_products_attributes ON products USING GIN (attributes); 3 4-- GIN index with jsonb_path_ops (smaller, @> only) 5CREATE INDEX idx_products_attributes_path ON products 6USING GIN (attributes jsonb_path_ops); 7 8-- Expression index for specific path 9CREATE INDEX idx_products_brand ON products 10USING BTREE ((attributes->>'brand')); 11 12-- Partial index 13CREATE INDEX idx_active_products ON products 14USING GIN (attributes) 15WHERE (metadata->>'active')::boolean = true; 16 17-- Query using GIN index 18EXPLAIN ANALYZE 19SELECT * FROM products 20WHERE attributes @> '{"brand": "Apple"}'; 21 22-- Query using expression index 23EXPLAIN ANALYZE 24SELECT * FROM products 25WHERE attributes->>'brand' = 'Apple';

JSON Path Queries (PostgreSQL 12+)#

1-- JSONPath syntax 2SELECT * FROM products 3WHERE attributes @? '$.specs.ram ? (@ >= 16)'; 4 5-- Extract with JSONPath 6SELECT jsonb_path_query(attributes, '$.specs.*') 7FROM products; 8 9-- Check existence 10SELECT jsonb_path_exists(attributes, '$.specs.ram') 11FROM products; 12 13-- Query with filter 14SELECT jsonb_path_query_array( 15 metadata, 16 '$.tags[*] ? (@ like_regex "^elec")' 17) 18FROM products; 19 20-- Complex path queries 21SELECT * FROM products 22WHERE attributes @? '$.specs ? (@.ram >= 16 && @.storage >= 256)';

Practical Examples#

1-- E-commerce product filtering 2SELECT * FROM products 3WHERE attributes @> '{"brand": "Apple"}' 4 AND (attributes->'specs'->>'ram')::int >= 16 5 AND metadata->'tags' ? 'laptop'; 6 7-- User preferences with defaults 8SELECT 9 id, 10 COALESCE(settings->>'theme', 'light') as theme, 11 COALESCE((settings->>'notifications')::boolean, true) as notifications 12FROM users; 13 14-- Aggregate product stats by brand 15SELECT 16 attributes->>'brand' as brand, 17 COUNT(*) as count, 18 AVG((attributes->'specs'->>'ram')::numeric) as avg_ram 19FROM products 20WHERE attributes ? 'brand' 21GROUP BY attributes->>'brand'; 22 23-- Search in nested arrays 24SELECT * FROM products 25WHERE EXISTS ( 26 SELECT 1 27 FROM jsonb_array_elements_text(metadata->'tags') as tag 28 WHERE tag ILIKE '%computer%' 29); 30 31-- Build API response 32SELECT jsonb_build_object( 33 'products', jsonb_agg( 34 jsonb_build_object( 35 'id', id, 36 'name', name, 37 'brand', attributes->>'brand', 38 'tags', metadata->'tags' 39 ) 40 ), 41 'total', COUNT(*) 42) as response 43FROM products;

Best Practices#

Schema Design: ✓ Use JSONB over JSON ✓ Index frequently queried paths ✓ Consider partial indexes ✓ Don't over-normalize into JSON Performance: ✓ Use containment operators with GIN ✓ Expression indexes for equality ✓ Avoid full-table JSON parsing ✓ Monitor query plans Data Integrity: ✓ Use CHECK constraints ✓ Validate JSON structure ✓ Handle NULL vs missing keys ✓ Consider generated columns

Conclusion#

PostgreSQL's JSON support enables flexible schemas while maintaining query performance. Use JSONB with GIN indexes for most cases, expression indexes for specific paths, and JSONPath for complex queries. Balance flexibility with structure for maintainable data models.

Share this article

Help spread the word about Bootspring