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