Low Impact

Index JSONB Columns for Efficient Querying

Use GIN indexes with jsonb_path_ops for fast JSON queries

Why This Matters

JSONB columns are powerful for storing flexible, schema-less data, but queries on JSONB can be slow without proper indexing. GIN indexes make JSONB queries fast, especially when using the jsonb_path_ops operator class.

Index JSONB columns when:

Tip: Use jsonb_path_ops for containment queries (@>). It's smaller and faster than the default GIN index, but only supports containment operators.

The Problem

Querying JSONB without indexes causes sequential scans:

Incorrect - Sequential scan on JSONB
-- No index, full table scan
SELECT * FROM products 
WHERE metadata @> '{"category": "electronics"}';

-- Or using path operator
SELECT * FROM products 
WHERE metadata->>'status' = 'active';

-- EXPLAIN shows:
-- Seq Scan on products (cost=0.00..25000.00 rows=1000)
--   Filter: (metadata @> '{"category": "electronics"}'::jsonb)
--   Rows Removed by Filter: 999000

The Solution

Create a GIN index on the JSONB column:

Correct - GIN index for fast JSONB queries
-- Standard GIN index (supports all operators)
CREATE INDEX products_metadata_idx 
ON products USING gin (metadata);

-- Or use jsonb_path_ops for containment queries (smaller, faster)
CREATE INDEX products_metadata_idx 
ON products USING gin (metadata jsonb_path_ops);

-- Now queries use index
SELECT * FROM products 
WHERE metadata @> '{"category": "electronics"}';

-- EXPLAIN shows:
-- Bitmap Index Scan on products_metadata_idx
--   Index Cond: (metadata @> '{"category": "electronics"}'::jsonb)

Index Types

Query Patterns

Different operators work with different index types:

-- Containment (@>) - works with both index types
SELECT * FROM products 
WHERE metadata @> '{"status": "active"}';

-- Key existence (?) - only works with default GIN
SELECT * FROM products 
WHERE metadata ? 'category';

-- Path queries - use expression index
CREATE INDEX products_status_idx 
ON products ((metadata->>'status'));

SELECT * FROM products 
WHERE metadata->>'status' = 'active';

Result: JSONB queries that took 500ms+ with sequential scans now complete in 5-20ms with GIN indexes, even on tables with millions of rows.

Optimize Your JSONB Queries

Connect your Supabase project to identify missing indexes on JSONB columns and optimize query performance.

Start Supabase Audit

Common Mistakes

Learn More