CPCODELAB
PostgreSQL

JSONB & Arrays

13 min read

JSONB & Arrays

PostgreSQL's jsonb type lets you store semi-structured data alongside relational columns. Unlike plain json, the jsonb type parses the JSON on write and stores it in a decomposed binary format, making reads significantly faster and enabling indexing.

Storing and Querying JSONB

sql
CREATE TABLE products (
  id       bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name     text NOT NULL,
  metadata jsonb NOT NULL DEFAULT '{}'
);

-- Insert a product with arbitrary metadata
INSERT INTO products (name, metadata) VALUES
  ('Laptop Pro', '{"brand": "Acme", "specs": {"ram_gb": 16, "ssd_gb": 512}, "tags": ["tech", "new"]}');

-- -> returns a JSON value (keeps type as jsonb)
SELECT metadata -> 'brand' FROM products;
-- Result: "Acme"   (with quotes, it is still jsonb)

-- ->> returns a text value (casts to text)
SELECT metadata ->> 'brand' FROM products;
-- Result: Acme    (plain text, no quotes)

-- Nested access
SELECT metadata -> 'specs' ->> 'ram_gb' FROM products;

-- #> and #>> for path arrays
SELECT metadata #>> '{specs,ram_gb}' FROM products;

Filtering on JSONB

sql
-- Find products where brand is Acme
SELECT name FROM products
  WHERE metadata ->> 'brand' = 'Acme';

-- Containment operator @> (does the left side contain the right?)
SELECT name FROM products
  WHERE metadata @> '{"brand": "Acme"}';

-- Check if a key exists
SELECT name FROM products
  WHERE metadata ? 'brand';

-- Check if any of multiple keys exist
SELECT name FROM products
  WHERE metadata ?| ARRAY['brand', 'sku'];

Indexing JSONB

sql
-- GIN index makes @> and ? operators fast
CREATE INDEX idx_products_metadata ON products USING GIN (metadata);

-- Index a specific key with a functional index
CREATE INDEX idx_products_brand ON products ((metadata ->> 'brand'));

PostgreSQL Arrays

Postgres arrays let you store multiple values of the same type in a single column. They are useful for tags, permission lists, or small sets of values that do not warrant a separate join table.

sql
CREATE TABLE articles (
  id   bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  tags text[] NOT NULL DEFAULT '{}'
);

INSERT INTO articles (tags) VALUES (ARRAY['postgres', 'databases', 'tutorial']);

-- Access element (1-indexed!)
SELECT tags[1] FROM articles;  -- 'postgres'

-- Check containment
SELECT * FROM articles WHERE tags @> ARRAY['postgres'];

-- Overlap (any element in common)
SELECT * FROM articles WHERE tags && ARRAY['postgres', 'python'];

-- Append an element
UPDATE articles SET tags = tags || ARRAY['newTag'] WHERE id = 1;

-- Array length
SELECT array_length(tags, 1) FROM articles;

Arrays are convenient but can become a design smell. If you find yourself querying into an array frequently, consider a normalised join table instead — it is more flexible and easier to index.

Ready to test yourself?
Practice PostgreSQL— quiz & coding exercises