Array Data Type in PostgreSQL

Learn how to use PostgreSQL arrays: defining array columns, inserting data, accessing elements by 1-based index, searching with ANY() and @>, and expanding arrays with unnest().

5 min read · Back to overview

Quick Answer

PostgreSQL arrays store multiple values of the same data type in a single column. Define an array column with datatype[] syntax. Access elements with 1-based subscripts (arr[1]), search with ANY() or @>, and expand to rows with unnest().

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

PostgreSQL allows columns to store multiple values of the same type in a single array. Every data type has a companion array type — integer[], text[], boolean[], and so on. Arrays are useful for storing multi-valued attributes like product tags, image URLs, or notification channels without requiring a separate join table.

Syntax

column_name datatype[]       -- one-dimensional array
column_name datatype[][]     -- two-dimensional array

Array subscripts in PostgreSQL are 1-based — the first element is at index 1.

Practical Example

Store product tags and available sizes in array columns:

CREATE TABLE products (
  id           SERIAL PRIMARY KEY,
  sku          VARCHAR(50)  NOT NULL UNIQUE,
  name         VARCHAR(200) NOT NULL,
  tags         TEXT[]       NOT NULL DEFAULT '{}',
  sizes        VARCHAR(10)[]
);

-- Using the ARRAY constructor
INSERT INTO products (sku, name, tags, sizes)
VALUES
  ('BOLT-TI-M6', 'Titanium M6 Bolt',
   ARRAY['fastener', 'titanium', 'metric'],
   ARRAY['M6x10', 'M6x16', 'M6x20', 'M6x25']),
  ('HOODIE-ECO', 'Eco Merino Hoodie',
   ARRAY['apparel', 'merino', 'eco'],
   ARRAY['XS', 'S', 'M', 'L', 'XL']),
  ('LAMP-SOLAR', 'Solar Desk Lamp',
   ARRAY['lighting', 'solar', 'eco'],
   NULL);

Using curly-brace literal syntax as an alternative:

INSERT INTO products (sku, name, tags)
VALUES ('NOTEBOOK-A5', 'Recycled Notebook', '{"stationery","recycled","eco"}');

Accessing Array Elements

-- First element (1-based index)
SELECT sku, tags[1] AS primary_tag FROM products;

-- Second element
SELECT sku, sizes[2] AS second_size FROM products WHERE sizes IS NOT NULL;

Searching Arrays

-- ANY(): check if a value exists anywhere in the array
SELECT sku, name
FROM products
WHERE 'eco' = ANY(tags);

-- @>: containment — does the array contain ALL listed values?
SELECT sku, name
FROM products
WHERE tags @> ARRAY['eco', 'apparel'];

-- &&: overlap — does the array contain ANY of the listed values?
SELECT sku, name
FROM products
WHERE tags && ARRAY['titanium', 'solar'];

Updating Array Data

-- Update a single element
UPDATE products
SET sizes[1] = 'M6x12'
WHERE sku = 'BOLT-TI-M6'
RETURNING sku, sizes;

-- Append an element with array_append()
UPDATE products
SET tags = array_append(tags, 'sale')
WHERE sku = 'HOODIE-ECO';

-- Remove an element with array_remove()
UPDATE products
SET tags = array_remove(tags, 'sale')
WHERE sku = 'HOODIE-ECO';

Expanding Arrays to Rows

-- unnest() returns one row per array element
SELECT sku, unnest(tags) AS tag
FROM products
ORDER BY sku, tag;

-- WITH ORDINALITY adds the position of each element
SELECT sku, tag, pos
FROM products,
     unnest(tags) WITH ORDINALITY AS t(tag, pos)
WHERE sizes IS NOT NULL
ORDER BY sku, pos;

GIN Indexing

Create a GIN index on the array column to speed up @> and && containment queries:

CREATE INDEX CONCURRENTLY idx_products_tags
ON products USING GIN (tags);

EXPLAIN ANALYZE
SELECT sku FROM products WHERE tags @> ARRAY['eco'];

Testing with Vela

Array columns are easy to get right in development but can produce slow sequential scans in production when the GIN index is missing or stale. Before deploying a feature that queries by array values, test the exact query plan on a database branch loaded with production-scale data to confirm the GIN index is being used and the query latency is acceptable.

Production Tips

  • Array subscripts are 1-based in PostgreSQL — the first element is arr[1], not arr[0].
  • For large tables that need to search inside arrays, create a GIN index with CREATE INDEX CONCURRENTLY to avoid downtime.
  • Avoid deeply nesting arrays or storing very large arrays in a single column — consider a child table with a foreign key for unbounded one-to-many relationships.
  • Use unnest() with WITH ORDINALITY when you need the position of each element alongside the value.
  • Use array_length(col, 1) to get the count of elements in a one-dimensional array, not length() which is for strings.

Continue in PostgreSQL Data Types: User-defined Data Types.

Related in this section: Boolean · CHAR, VARCHAR, and TEXT · NUMERIC

Frequently Asked Questions

What is a PostgreSQL array?
A PostgreSQL array is an ordered collection of values sharing the same data type, stored in a single column. Every data type has a companion array type — integer[], text[], boolean[], etc. Array subscripts in PostgreSQL are 1-based by default, so the first element is at index 1, not 0.
Does inserting large arrays lock the table?
No. INSERT and UPDATE with array values acquire only row-level locks. Very large array values (megabytes) trigger TOAST storage automatically, but this does not cause extra locking.
What happens to GIN indexes when array elements are updated?
When an array element changes, the old GIN index entries for the changed elements are marked dead and new entries are created. The dead entries are reclaimed by the next VACUUM or autovacuum cycle.
How do I check whether an array contains a specific value?
Use ANY() for a direct equality check: WHERE 'value' = ANY(col). Use @> for containment (check if the array contains all elements of another array): WHERE col @> ARRAY['value']. The @> operator benefits from a GIN index; ANY() does not.
What is the safest way to search large array columns in production?
Create a GIN index on the array column: CREATE INDEX CONCURRENTLY idx ON t USING GIN (col). Use CONCURRENTLY to avoid blocking during index creation. Then use @> for containment queries — they will use the GIN index. Test on a Vela branch first to estimate build time and disk usage.