SQLMentor // learn postgresql

Arrays

PostgreSQL supports arrays as a native data type — any column can store multiple values of the same type. Arrays are useful for tags, labels, multi-value attributes, and denormalized lookup data. They come with rich operators, functions, and indexing support.

Declaring Array Columns

-- Syntax: typename[] for one-dimensional arrays
CREATE TABLE posts (
    id          SERIAL PRIMARY KEY,
    title       TEXT,
    tags        TEXT[],         -- array of text
    scores      INTEGER[],      -- array of integers
    prices      NUMERIC(10,2)[],-- array of numerics
    matrix      INTEGER[][]     -- two-dimensional array
);

Inserting Arrays

-- Using ARRAY[] constructor
INSERT INTO posts (title, tags, scores) VALUES
    ('Intro to SQL',  ARRAY['sql', 'beginner', 'database'], ARRAY[95, 87, 92]),
    ('PostgreSQL Tips', ARRAY['postgresql', 'advanced'],    ARRAY[98, 94]);

-- Using literal syntax with curly braces
INSERT INTO posts (title, tags) VALUES
    ('JSON in Postgres', '{postgresql, json, jsonb}');

-- Mixed approach (both are valid)
INSERT INTO posts (title, tags, scores) VALUES
    ('Window Functions', ARRAY['sql','analytics'], '{88,90,95}');

Accessing Elements

Arrays in PostgreSQL are 1-based (the first element is at index 1, not 0):

-- Get the first element
SELECT tags[1] FROM posts WHERE id = 1;
-- Result: sql

-- Get the second element
SELECT tags[2] FROM posts;

-- Negative indexes are NOT supported (unlike Python)
-- Use ARRAY_LENGTH to find the last element
SELECT tags[ARRAY_LENGTH(tags, 1)] FROM posts WHERE id = 1;
-- Result: database (last tag)

Slicing Arrays

-- Get elements 1 through 2 (inclusive)
SELECT tags[1:2] FROM posts WHERE id = 1;
-- Result: {sql,beginner}

-- Get from index 2 to the end
SELECT tags[2:] FROM posts WHERE id = 1;
-- Result: {beginner,database}

Array Functions

-- Length of an array (dimension 1)
SELECT ARRAY_LENGTH(tags, 1) AS tag_count FROM posts;

-- Number of dimensions
SELECT ARRAY_NDIMS(matrix) FROM posts;

-- Array lower and upper bounds
SELECT ARRAY_LOWER(tags, 1), ARRAY_UPPER(tags, 1) FROM posts;
-- Returns: 1, 3 (for a 3-element array)

-- Append an element
SELECT ARRAY_APPEND(tags, 'new-tag') FROM posts WHERE id = 1;
-- {sql,beginner,database,new-tag}

-- Prepend an element
SELECT ARRAY_PREPEND('first', tags) FROM posts WHERE id = 1;
-- {first,sql,beginner,database}

-- Concatenate arrays
SELECT ARRAY_CAT(ARRAY[1,2], ARRAY[3,4]);
-- {1,2,3,4}

-- Also: || operator for concatenation
SELECT ARRAY[1,2] || ARRAY[3,4];
-- {1,2,3,4}

-- Remove all occurrences of a value
SELECT ARRAY_REMOVE(ARRAY[1,2,3,2,1], 2);
-- {1,3,1}

-- Replace all occurrences
SELECT ARRAY_REPLACE(ARRAY['a','b','a'], 'a', 'z');
-- {z,b,z}

-- Position of an element (returns NULL if not found)
SELECT ARRAY_POSITION(ARRAY['sql','beginner','database'], 'beginner');
-- 2

-- Positions of all occurrences
SELECT ARRAY_POSITIONS(ARRAY[1,2,1,3,1], 1);
-- {1,3,5}

UNNEST — Expand Array to Rows

UNNEST converts an array into a set of rows — extremely useful for joining against arrays:

-- Expand tags to one row per tag
SELECT id, UNNEST(tags) AS tag FROM posts;
id tag
1 sql
1 beginner
1 database
2 postgresql
2 advanced
-- Expand with index position (PostgreSQL 9.4+)
SELECT id, idx, tag
FROM posts, UNNEST(tags) WITH ORDINALITY AS t(tag, idx);
id idx tag
1 1 sql
1 2 beginner
1 3 database
-- Unnest multiple arrays in parallel (short-zip — stops at shortest)
SELECT UNNEST(ARRAY['a','b','c']), UNNEST(ARRAY[1,2,3]);
-- a,1  b,2  c,3

-- Convert rows back to array
SELECT ARRAY_AGG(tag ORDER BY tag) AS sorted_tags FROM UNNEST(ARRAY['sql','beginner','database']) AS t(tag);
-- {beginner,database,sql}

Array Operators

Operator Meaning Example
@> Contains tags @> ARRAY['sql']
<@ Is contained by ARRAY['sql'] <@ tags
&& Overlaps (any element in common) tags && ARRAY['sql','python']
= Arrays are equal tags = ARRAY['sql','db']
<> Arrays are not equal tags <> ARRAY['a','b']
-- Posts tagged with 'sql' (containment check)
SELECT title FROM posts WHERE tags @> ARRAY['sql'];

-- Posts tagged with BOTH 'sql' AND 'advanced'
SELECT title FROM posts WHERE tags @> ARRAY['sql', 'advanced'];

-- Posts tagged with 'sql' OR 'postgresql' (overlap)
SELECT title FROM posts WHERE tags && ARRAY['sql', 'postgresql'];

-- Posts where tags is exactly ['sql', 'beginner']
SELECT title FROM posts WHERE tags = ARRAY['sql', 'beginner'];

ANY and ALL with Arrays

-- Check if a value exists in an array (like IN)
SELECT title FROM posts WHERE 'sql' = ANY(tags);
-- Same as: WHERE tags @> ARRAY['sql']

-- Check if a value is NOT in an array
SELECT title FROM posts WHERE 'python' <> ALL(tags);

-- ANY with comparison operators
SELECT * FROM posts WHERE 95 = ANY(scores);
SELECT * FROM posts WHERE 90 <= ALL(scores);  -- all scores >= 90

GIN Index for Array Containment

-- GIN index enables fast @>, &&, and <@ queries
CREATE INDEX posts_tags_gin ON posts USING GIN (tags);

-- These queries now use the index:
SELECT * FROM posts WHERE tags @> ARRAY['sql'];         -- fast!
SELECT * FROM posts WHERE tags && ARRAY['sql','python']; -- fast!

-- This does NOT use the GIN index (equality check on whole array):
SELECT * FROM posts WHERE tags = ARRAY['sql','beginner']; -- Seq Scan
-- Use a B-tree for exact equality queries on arrays

Multi-Dimensional Arrays

-- 2D array
SELECT ARRAY[[1,2,3],[4,5,6]] AS matrix;

-- Access element at row 1, column 2
SELECT (ARRAY[[1,2,3],[4,5,6]])[1][2];
-- Result: 2

-- Dimensions
SELECT ARRAY_NDIMS(ARRAY[[1,2],[3,4]]);  -- 2
SELECT ARRAY_LENGTH(ARRAY[[1,2],[3,4]], 1);  -- 2 (rows)
SELECT ARRAY_LENGTH(ARRAY[[1,2],[3,4]], 2);  -- 2 (columns)

Practical Patterns

Tag System

-- Store tags as an array, GIN index for fast search
CREATE TABLE articles (
    id   SERIAL PRIMARY KEY,
    title TEXT,
    tags  TEXT[]
);
CREATE INDEX articles_tags_gin ON articles USING GIN (tags);

-- Find articles with a specific tag
SELECT title FROM articles WHERE tags @> ARRAY['postgresql'];

-- Find top 10 most used tags across all articles
SELECT tag, COUNT(*) AS usage_count
FROM articles, UNNEST(tags) AS t(tag)
GROUP BY tag
ORDER BY usage_count DESC
LIMIT 10;

Storing Ordered Lists

-- Recent search history per user (ordered, max 10 items)
CREATE TABLE user_searches (
    user_id      INTEGER PRIMARY KEY,
    recent_terms TEXT[]
);

-- Add a search term to the front, keep last 10
UPDATE user_searches
SET recent_terms = (
    ARRAY[:'new_term'] ||
    (SELECT ARRAY(SELECT UNNEST(recent_terms) LIMIT 9))
)
WHERE user_id = :user_id;
Arrays vs JSONB vs normalized tables — when to choose?

Use arrays when:

  • Elements are all the same type
  • You need containment queries (@>, &&)
  • The list is bounded and not too long (< ~100 elements)
  • You want simplicity over flexibility

Use JSONB when:

  • Elements have different types or structures
  • You need key-value lookups within elements
  • The shape changes over time

Use a junction table (normalized) when:

  • Elements are complex objects
  • You need to query/update individual elements frequently
  • The list can be very long
  • You need referential integrity
Array updates replace the entire array. To update a single element, you must fetch the array, modify it in application code, and write it back — or use ARRAY_REPLACE. For frequent element-level updates, a junction table is usually better than an array.