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
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:
-- 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:
myapp=> SELECT id, title, tags FROM articles;
id | title | tags
----+-------------------+-------------------------------------
1 | Postgres-Einstieg | {postgres,tutorial,sql}
2 | Indexe | {postgres,performance,indexes}Auf Elemente zugreifen
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 | performanceAchtung: 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:
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 2Suchen und Filtern
Drei wichtige Operatoren:
| Operator | Bedeutung | Beispiel |
|---|---|---|
= ANY(arr) | Wert kommt im Array vor | 'sql' = ANY(tags) |
@> (contains) | Array enthält alle Werte des rechten Arrays | tags @> ARRAY['postgres'] |
&& (overlap) | Arrays haben mindestens ein gemeinsames Element | tags && ARRAY['sql', 'mysql'] |
-- 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
-- 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):
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 | indexesarray_agg() ist die Umkehrung — aggregiert mehrere Zeilen in ein 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:
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.
-- 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
- Arrays – PostgreSQL Documentation
- Array Functions and Operators
- GIN Indexes
- Aggregate Functions: array_agg