The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Construct a typed arrayARRAY['read', 'write']::text[]View examples
Read the first elementpermissions[1]View examples
Count all elementscardinality(permissions)View examples
Match any element'admin' = ANY (permissions)View examples
Test array containmentpermissions @> ARRAY['read', 'write']::text[]View examples
Test array overlappermissions && ARRAY['admin', 'owner']::text[]View examples
Expand with positionsCROSS JOIN LATERAL unnest(t.tags) WITH ORDINALITY AS tag(value, position)View examples
Create a half-open rangetstzrange(starts_at, ends_at, '[)')View examples
Test range containmentvalid_during @> now()View examples
Test range overlapreserved_during && tstzrange($1, $2, '[)')View examples
Prevent overlapping bookingsEXCLUDE USING gist (room_id WITH =, reserved_during WITH && )View examples
Construct a date multirangedatemultirange(daterange('2026-08-01', '2026-08-08', '[)'), daterange('2026-08-15', '2026-08-22', '[)'))View examples
Index array operatorsCREATE INDEX documents_tags_gin_idx ON app.documents USING gin (tags);View examples
Index range operatorsCREATE INDEX bookings_during_gist_idx ON app.bookings USING gist (reserved_during);View examples

Arrays represent an ordered collection inside one row; ranges represent a set between bounds; multiranges represent an ordered set of non-overlapping, non-adjacent ranges. They are valuable when those shapes are truly atomic to the domain. Prefer child tables for independently constrained entities, choose operators that match index support, and test null, empty, infinite, and boundary behavior explicitly.

Step by step

Detailed examples

01

Use arrays for values owned and replaced as one attribute

An array column can hold any built-in or user-defined element type and may be multidimensional, but a declared dimension is documentation rather than an enforced size. PostgreSQL arrays usually start at subscript 1, although stored lower bounds can differ. Keep entities requiring foreign keys, individual lifecycle, or per-element metadata in a related table instead.

Store a validated set-like text array
CREATE TABLE app.api_tokens (
  token_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  permissions text[] NOT NULL DEFAULT ARRAY[]::text[],
  CONSTRAINT permissions_no_nulls CHECK (array_position(permissions, NULL) IS NULL)
);

INSERT INTO app.api_tokens (permissions)
VALUES (ARRAY['read', 'write']::text[]);
Back to quick reference ↑
03

Expand arrays in FROM and preserve order explicitly

unnest emits array elements as rows. Put set-returning functions in FROM with LATERAL so their cardinality is visible, and add WITH ORDINALITY when the source ordering matters. Multiple arrays passed to unnest are padded with nulls to the longest input, which can silently misalign malformed parallel arrays.

Return one ordered row per tag
SELECT d.document_id, tag.value, tag.position
FROM app.documents AS d
CROSS JOIN LATERAL unnest(d.tags) WITH ORDINALITY AS tag(value, position)
ORDER BY d.document_id, tag.position;
Back to quick reference ↑
04

Encode interval boundaries as part of the domain

Range bounds may be inclusive or exclusive, infinite, or empty. Half-open [start,end) ranges compose naturally for adjacent time windows. Discrete range types such as daterange are canonicalized, so text output can differ from input while representing the same set. Use isempty, lower, upper, and bound-inclusivity functions instead of parsing range text.

Inspect a timestamp validity window
WITH rules(valid_during) AS (
  VALUES (tstzrange(
    '2026-08-12 09:00:00+00',
    '2026-08-12 17:00:00+00',
    '[)'
  ))
)
SELECT lower(valid_during) AS starts_at,
       upper(valid_during) AS ends_at,
       lower_inc(valid_during) AS includes_start,
       upper_inc(valid_during) AS includes_end,
       isempty(valid_during) AS is_empty
FROM rules;
Back to quick reference ↑
05

Prevent conflicting intervals with exclusion constraints

An exclusion constraint rejects a pair of rows when every listed operator compares true. For a booking rule, equality on a scalar room identifier and overlap on a time range express the invariant atomically under concurrency. The btree_gist extension supplies GiST operator classes for common scalar types; evaluate extension policy before installing it.

Enforce non-overlapping room reservations
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE app.bookings (
  booking_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  room_id bigint NOT NULL,
  reserved_during tstzrange NOT NULL,
  CONSTRAINT booking_not_empty CHECK (NOT isempty(reserved_during)),
  CONSTRAINT room_booking_no_overlap
    EXCLUDE USING gist (room_id WITH =, reserved_during WITH &&)
);
Back to quick reference ↑
06

Represent discontinuous availability with multiranges

A multirange stores zero or more ordered ranges of one subtype and normalizes overlaps or adjacency. Union, intersection, difference, containment, and expansion with unnest operate on the set rather than application-formatted text. Multiranges are useful for schedules with gaps; separate rows remain better when each interval has independent attributes.

Combine and expand available date windows
WITH calendars(available) AS (
  VALUES (datemultirange(
    daterange('2026-08-01', '2026-08-08', '[)'),
    daterange('2026-08-15', '2026-08-22', '[)')
  ))
)
SELECT slot
FROM calendars
CROSS JOIN LATERAL unnest(available) AS slot
ORDER BY lower(slot);
Back to quick reference ↑
07

Match GIN and GiST indexes to the operators in real queries

GIN is a common choice for array @>, <@, and && operators. GiST and SP-GiST support range operations, while GiST also underpins exclusion constraints. An index does not make every expression equivalent: parameter types, casts, operators, data distribution, and selectivity determine use. Verify representative predicates with EXPLAIN rather than indexing by type alone.

Create operator-oriented indexes
CREATE INDEX documents_tags_gin_idx
  ON app.documents USING gin (tags);

CREATE INDEX bookings_during_gist_idx
  ON app.bookings USING gist (reserved_during);

EXPLAIN (COSTS, VERBOSE)
SELECT booking_id
FROM app.bookings
WHERE reserved_during && tstzrange($1, $2, '[)');
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. PostgreSQL Global Development GroupPostgreSQL: Arrayspostgresql.org
  2. PostgreSQL Global Development GroupPostgreSQL: Array Functions and Operatorspostgresql.org
  3. PostgreSQL Global Development GroupPostgreSQL: Range Typespostgresql.org
  4. PostgreSQL Global Development GroupPostgreSQL: Range and Multirange Functions and Operatorspostgresql.org
  5. PostgreSQL Global Development GroupPostgreSQL: Constraintspostgresql.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback