Working with JSON and JSONB in PostgreSQL

Maria Garcia Feb 2026
2 tabs
-- Create table with JSONB column
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50),
  profile JSONB,
  preferences JSONB,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insert JSON data
INSERT INTO users (username, profile, preferences) VALUES
(
  'alice',
  '{"age": 25, "city": "New York", "skills": ["SQL", "Python", "JavaScript"]}',
  '{"theme": "dark", "notifications": true, "language": "en"}'
);

-- Insert with jsonb_build_object
INSERT INTO users (username, profile) VALUES
(
  'bob',
  jsonb_build_object(
    'age', 30,
    'city', 'San Francisco',
    'skills', jsonb_build_array('Ruby', 'Go'),
    'contact', jsonb_build_object('email', 'bob@example.com', 'phone', '555-0100')
  )
);

-- Extract JSON field (returns JSON)
SELECT
  username,
  profile -> 'city' AS city_json,
  profile ->> 'city' AS city_text
FROM users;

-- -> returns JSON, ->> returns text

-- Extract nested field
SELECT
  username,
  profile -> 'contact' ->> 'email' AS email,
  profile #> '{contact,email}' AS email_path
FROM users;

-- Filter by JSON field
SELECT username
FROM users
WHERE profile ->> 'city' = 'New York';

-- Check key existence
SELECT username
FROM users
WHERE profile ? 'age';  -- Has 'age' key

-- Check multiple keys
SELECT username
FROM users
WHERE profile ?& ARRAY['age', 'city'];  -- Has all keys

SELECT username
FROM users
WHERE profile ?| ARRAY['age', 'location'];  -- Has any key

-- Containment (@> contains, <@ contained by)
SELECT username
FROM users
WHERE profile @> '{"city": "New York"}';

-- Find users with specific skill
SELECT username
FROM users
WHERE profile @> '{"skills": ["SQL"]}';

-- JSON array operations
SELECT
  username,
  jsonb_array_length(profile -> 'skills') AS skill_count,
  jsonb_array_elements_text(profile -> 'skills') AS skill
FROM users;

-- Update JSON field
UPDATE users
SET profile = profile || '{"verified": true}'
WHERE username = 'alice';

-- Update nested field
UPDATE users
SET profile = jsonb_set(
  profile,
  '{contact,phone}',
  '"555-0200"'
)
WHERE username = 'bob';

-- Remove field
UPDATE users
SET profile = profile - 'temporary_field'
WHERE id = 1;

-- Remove nested field
UPDATE users
SET profile = profile #- '{contact,phone}'
WHERE id = 2;

-- GIN index for JSONB
CREATE INDEX idx_users_profile_gin ON users USING GIN (profile);

-- Now containment queries use index
EXPLAIN ANALYZE
SELECT * FROM users
WHERE profile @> '{"city": "New York"}';

-- Index specific JSON path
CREATE INDEX idx_users_profile_city
  ON users ((profile ->> 'city'));

-- Expression index on nested field
CREATE INDEX idx_users_email
  ON users ((profile -> 'contact' ->> 'email'));

-- JSON path queries (PostgreSQL 12+)
SELECT username, profile
FROM users
WHERE profile @? '$.skills[*] ? (@ == "SQL")';

-- jsonb_path_query
SELECT
  username,
  jsonb_path_query(profile, '$.skills[*]') AS skill
FROM users;
2 files · sql Explain with highlit

JSON and JSONB store semi-structured data. JSONB is binary format—faster, indexable. I use JSONB for flexible schemas, API responses, configuration. JSON operators extract values, filter documents. GIN indexes enable fast JSONB queries. Containment operators check for key existence. Path expressions traverse nested structures. JSON aggregation builds complex documents. Understanding JSONB vs JSON trade-offs guides choice. JSONB supports indexing, JSON preserves formatting. Proper use of JSONB reduces schema changes. Essential for modern web applications, analytics, event logging. PostgreSQL JSONB rivals NoSQL databases for document storage.