PostgreSQL kann jeden Datentyp als Array speichern: text[], int[], uuid[], sogar jsonb[]. Praktisch für kleine Listen wie Tags, Berechtigungen oder Mehrfach-Auswahl-Felder — ohne dass man eine separate Tabelle anlegen muss. Bei vielen Elementen oder relationalen Bezügen ist eine eigene Tabelle aber meistens die bessere Wahl.

Array-Typ deklarieren

SQL Tabelle mit Array-Spalten
CREATE TABLE articles (
    id     bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    title  text NOT NULL,
    tags   text[],                              -- Array von Strings
    views  integer[] DEFAULT ARRAY[]::integer[] -- Array von Zahlen, leer per Default
);

text[] ist die kurze Schreibweise für „Array von text”. Geht für jeden Postgres-Typ, auch eigene und Composites.

ARRAY[]::integer[] ist ein leeres int[]. Notwendig wegen Type-Inferenz — ARRAY[] allein lässt Postgres nicht erkennen, was für ein Array das werden soll.

Daten einfügen

Zwei Schreibweisen:

SQL Array-Literale
-- ARRAY[…]-Konstruktor:
INSERT INTO articles (title, tags) VALUES
    ('Postgres-Einstieg', ARRAY['postgres', 'tutorial', 'sql']);

-- Array-Literal als String:
INSERT INTO articles (title, tags) VALUES
    ('Indexe', '{"postgres", "performance", "indexes"}');

ARRAY[…] ist die bevorzugte Schreibweise — lesbarer und besser mit Cast-Operationen kombinierbar. Die '{…}'-Form ist die historische Postgres-Notation.

Ergebnis im psql:

SQL
myapp=> SELECT id, title, tags FROM articles;
 id |       title       |                tags
----+-------------------+-------------------------------------
  1 | Postgres-Einstieg | {postgres,tutorial,sql}
  2 | Indexe            | {postgres,performance,indexes}

Auf Elemente zugreifen

SQL Array-Indexierung
myapp=> SELECT title, tags[1] AS first_tag, tags[2] AS second_tag
        FROM articles;
      title       | first_tag | second_tag
-------------------+-----------+------------
 Postgres-Einstieg | postgres  | tutorial
 Indexe            | postgres  | performance

Achtung: Postgres-Arrays sind 1-basiert, nicht 0-basiert wie in den meisten Programmiersprachen. tags[1] ist das erste Element.

Slice-Notation für mehrere Elemente:

SQL
SELECT tags[1:2] FROM articles;     -- Erste zwei Elemente
SELECT tags[2:]  FROM articles;     -- Ab Element 2 bis Ende
SELECT tags[:2]  FROM articles;     -- Bis Element 2

Suchen und Filtern

Drei wichtige Operatoren:

OperatorBedeutungBeispiel
= ANY(arr)Wert kommt im Array vor'sql' = ANY(tags)
@> (contains)Array enthält alle Werte des rechten Arraystags @> ARRAY['postgres']
&& (overlap)Arrays haben mindestens ein gemeinsames Elementtags && ARRAY['sql', 'mysql']
SQL In WHERE-Klauseln
-- Artikel mit Tag 'postgres'
SELECT title FROM articles
WHERE 'postgres' = ANY(tags);

-- Artikel mit Tags 'postgres' UND 'tutorial' (beide)
SELECT title FROM articles
WHERE tags @> ARRAY['postgres', 'tutorial'];

-- Artikel mit MINDESTENS einem dieser Tags
SELECT title FROM articles
WHERE tags && ARRAY['postgres', 'mysql', 'redis'];

Ändern: anhängen, entfernen, ersetzen

SQL Array-Modifikation
-- Element anhängen
UPDATE articles SET tags = array_append(tags, 'beginner')
WHERE id = 1;

-- Oder Operator || (Concatenation)
UPDATE articles SET tags = tags || 'advanced'
WHERE id = 1;

-- Mehrere anhängen
UPDATE articles SET tags = tags || ARRAY['howto', 'docs']
WHERE id = 1;

-- Element entfernen
UPDATE articles SET tags = array_remove(tags, 'sql')
WHERE id = 1;

-- Element ersetzen
UPDATE articles SET tags = array_replace(tags, 'old-tag', 'new-tag')
WHERE id = 1;

|| für Strings und Arrays — beide werden konkateniert. array_append, array_prepend, array_remove, array_replace sind die expliziten Funktionen.

unnest und array_agg — zwischen Array und Zeilen

unnest() macht aus einem Array eine Tabelle (eine Zeile pro Element):

SQL Array → Zeilen
myapp=> SELECT title, unnest(tags) AS tag FROM articles;
      title       |    tag
-------------------+-------------
 Postgres-Einstieg | postgres
 Postgres-Einstieg | tutorial
 Postgres-Einstieg | sql
 Indexe            | postgres
 Indexe            | performance
 Indexe            | indexes

array_agg() ist die Umkehrung — aggregiert mehrere Zeilen in ein Array:

SQL Zeilen → Array
myapp=> SELECT customer_id, array_agg(order_id ORDER BY created_at)
        FROM orders
        GROUP BY customer_id;
 customer_id |     array_agg
-------------+--------------------
           1 | {42, 47, 51, 53}
           2 | {43, 48, 52}

Sehr praktisch in Reports — eine Zeile pro Gruppe, die Liste der Mitglieder als Array dabei.

Indexe auf Arrays

Für effiziente Suche in Arrays braucht’s einen GIN-Index:

SQL GIN-Index für Arrays
CREATE INDEX articles_tags_idx ON articles USING gin (tags);

Damit werden @>-, &&- und = ANY(arr)-Queries auch auf Millionen von Zeilen schnell. Ohne den Index müsste Postgres jedes Array durchsuchen (Sequential Scan).

Wann KEINE Arrays nehmen

Arrays sind verlockend, aber haben Grenzen. Eine separate Tabelle ist die bessere Wahl, wenn:

  • Die Listen-Elemente eigene Eigenschaften haben (Zeitstempel, Status, …) — kein Array von Composites bauen.
  • Die Listen sehr lang werden (>100 Elemente). Update einer einzigen Komponente schreibt das gesamte Array neu — bei langen Arrays teuer.
  • Foreign-Key-Beziehungen zu den Elementen nötig sind. Postgres hat keine FK-Constraints aus einem Array auf eine andere Tabelle.
  • Die Reihenfolge nicht wichtig ist und die Liste mehrere zehntausend Werte enthält. Inefficient.
SQL Wenn ein Array nicht mehr passt — Junction-Table
-- Statt: articles.tags text[]
-- Lieber: separate tags-Tabelle + article_tags-Junction
CREATE TABLE tags (
    id   bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name text NOT NULL UNIQUE
);

CREATE TABLE article_tags (
    article_id bigint NOT NULL REFERENCES articles(id),
    tag_id     bigint NOT NULL REFERENCES tags(id),
    added_at   timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (article_id, tag_id)
);

Junction-Tables sind unflexibler in der Abfrage (jeder „welche Tags hat Artikel X?”-Query braucht einen Join), dafür referenzielle Integrität, eigene Spalten pro Beziehung (added_at), und keine Größen-Beschränkung.

Faustregel: Arrays für kleine, abgeschlossene Listen ohne Beziehungen (Tags als Strings, Berechtigungen als Codes, Auswahl-Felder). Tabellen für alles, was komplexer wird.

Besonderheiten

Postgres-Arrays sind 1-basiert.

tags[1] ist das erste Element — anders als JavaScript, Python, C, Java (alle 0-basiert). Wer aus diesen Sprachen kommt, vergisst das ständig. SQL-Standard ist tatsächlich 1-basiert; Postgres folgt dem.

NULL-Element in Array vs. NULL-Array.

Ein leeres Array '{}' ist nicht NULL — tags IS NULL ist FALSE, array_length(tags, 1) ist NULL (weil leeres Array keine erste Dimension hat). Wer Listen-Existenz prüfen will: cardinality(tags) = 0 oder tags = '{}'.

Updates in Arrays schreiben das ganze Array.

Auch array_append(tags, 'x') ist intern eine Vollkopie der Spalte. Bei langen Arrays mit häufigen Updates wird das teuer. Das ist einer der Gründe, ab gewisser Größe auf eine Junction-Table umzusteigen.

ANY und ALL — die SQL-Standard-Operatoren.

'foo' = ANY(tags) ist Standard-SQL für „existiert in der Liste”. 'foo' = ALL(tags) heißt „alle Elemente sind ‘foo’” (selten gebraucht). Funktioniert auch mit Subqueries: id = ANY(SELECT user_id FROM blocked).

Multidimensional, aber rechteckig.

Postgres unterstützt mehrdimensionale Arrays (int[][]), aber alle Innen-Arrays müssen dieselbe Länge haben — anders als „Array of Arrays” in JavaScript. Wer ungleichlange Listen braucht: jsonb mit Arrays drin oder eine richtige Tabelle.

Cardinality vs. array_length.

array_length(arr, 1) gibt die Länge der ersten Dimension; bei leerem Array NULL. cardinality(arr) gibt die Gesamtzahl Elemente in allen Dimensionen; bei leerem Array 0. Für 1D-Arrays meist cardinality() benutzen — null-sicher.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Datentypen

Zur Übersicht