By WebNest Studio Engineering Team
PostgreSQL Tutorial
Array Columns in PostgreSQL
Unlike most relational databases, PostgreSQL supports native array columns — a single column can hold an ordered list of values of any base type, such as TEXT[], INTEGER[], or even arrays of a composite type. This can be a convenient way to store small, unordered-enough collections (like tags) without introducing a separate join table.
Arrays are powerful but easy to overuse. They work best for simple, denormalized lists that are always read and written together with their parent row, not for data you need to query, join, or update independently.
Why it matters
PostgreSQL can store a list of values in a single column, which is convenient for small sets such as tags or the days a shop is open. Used in the right place it saves a table and a join. Used in the wrong place it brings back the problems that normalization exists to solve.
Declaring and Inserting Arrays
You declare an array column by appending square brackets to a type: tags TEXT[]. Array literals use curly braces in SQL: '{"postgres","sql","database"}', or you can build them with the ARRAY[...] constructor: ARRAY['postgres', 'sql', 'database']. Elements are accessed with 1-based indexing, e.g. tags[1].
Querying with ANY() and unnest()
To check whether a value exists anywhere in an array, use value = ANY(array_column), which is the array equivalent of IN. To turn an array into a set of rows — for example, to JOIN against it, aggregate over it, or filter with normal WHERE clauses per element — use unnest(array_column), which expands each element into its own row.
When Arrays Are (and Aren't) the Right Choice
Arrays are appropriate for small, denormalized, order-sensitive lists that belong wholly to one row, such as a list of tags on a blog post or a set of allowed statuses. They become the wrong choice once you need to query "which rows reference this specific tag" efficiently at scale without an index, enforce referential integrity on the elements (foreign keys cannot target elements inside an array), or frequently add/remove individual elements — a proper join table (post_id, tag_id) is more relationally correct for those cases.
Examples
Creating an array column, inserting, and querying with ANY()
CREATE TABLE posts (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title TEXT NOT NULL,
tags TEXT[] DEFAULT '{}'
);
INSERT INTO posts (title, tags)
VALUES ('Intro to PostgreSQL', ARRAY['postgres', 'sql', 'database']);
SELECT title FROM posts WHERE 'sql' = ANY(tags);
CREATE TABLE
INSERT 0 1
title
----------------------
Intro to PostgreSQL
Expanding an array column into rows with unnest()
SELECT title, unnest(tags) AS tag FROM posts;
title | tag
----------------------+----------
Intro to PostgreSQL | postgres
Intro to PostgreSQL | sql
Intro to PostgreSQL | database
Common Mistakes
- Using arrays to model a genuine one-to-many relationship that needs its own foreign keys, indexing per element, or independent updates — a join table fits better.
- Forgetting PostgreSQL array indexing starts at 1, not 0, when porting logic from most programming languages.
- Writing WHERE array_column = value instead of value = ANY(array_column), which compares against the whole array rather than checking membership.
- Not adding a GIN index on an array column that is frequently searched with ANY() or the overlap operator (&&) at scale, leading to slow sequential scans.
Exercise
This exercise uses PostgreSQL arrays, so it needs a PostgreSQL server. Create an articles table with a text array column tags. Insert an article tagged sql and postgres, and another tagged python. Write a query that finds the titles of the articles tagged sql, and a second query that returns one row for each article and tag pair.
Expected result: the first query returns the first article; the second returns three rows.
-- TODO: articles (id, title, tags text array)
-- TODO: insert the two articles
-- TODO: titles of the articles that have the tag 'sql'
-- TODO: one row per article and tag
Show hints
- An array literal is written
ARRAY['sql', 'postgres'], and the column type isTEXT[]. - Membership is tested with
'sql' = ANY(tags), andunnest(tags)expands an array into rows.
Show solution
CREATE TABLE articles (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
tags TEXT[] NOT NULL DEFAULT '{}'
);
INSERT INTO articles (id, title, tags) VALUES
(1, 'Indexing basics', ARRAY['sql', 'postgres']),
(2, 'Decorators', ARRAY['python']);
SELECT title
FROM articles
WHERE 'sql' = ANY(tags);
SELECT title, unnest(tags) AS tag
FROM articles;
Quick quiz
1. At which index does a PostgreSQL array start by default?
- A. 0
- B. -1
- C. It depends on the element type
- D. 1
Show answer
D. tags[1] is the first element, unlike arrays in most programming languages.
2. What does unnest(tags) do?
- A. Expands an array into one row per element
- B. Sorts the array
- C. Removes duplicates
- D. Counts the elements
Show answer
A. array_agg does the reverse, collecting rows into an array.
3. When is an array column a poor choice?
- A. For a short list of tags
- B. When the elements need foreign keys, their own attributes, or are frequently joined and updated
- C. For a list of weekdays
- D. When the list is read as a whole
Show answer
B. That is a relationship, and it belongs in a separate table.
Interview questions
When would you use an array column instead of a separate table?
Show answer
When the list is small, belongs entirely to the row, is read and written together with it, and its elements are simple values with no attributes of their own: tags, a set of flags, a few labels. A separate table is right when the elements refer to other rows and need foreign keys, carry extra data, are queried or updated individually, or can grow without limit. Arrays cannot have foreign key constraints on their elements.
How do you search efficiently inside an array column?
Show answer
Create a GIN index on the column and use the array operators it supports: @> for "contains these elements" and && for "has any element in common". For example, WHERE tags @> ARRAY['sql'] can use the index. The form 'sql' = ANY(tags) is convenient to write but does not use a GIN index.
Key Points to Remember
- PostgreSQL supports native array columns for any base type, declared with type[].
- value = ANY(array_column) checks membership; unnest() expands an array into rows for joins and aggregation.
- Arrays suit small, denormalized, whole-row-owned lists — not relationships needing independent integrity or indexing.
- A GIN index can accelerate array containment/overlap queries at scale.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering