Generated Columns praktisch nutzen
AI generated
SELECT
JOIN
SQL · Generated Columns · Schema-Design
Generated Columns praktisch nutzen
abgeleitete Werte deklarativ im Schema statt in Trigger oder Anwendungscode

Eine Generated Column berechnet ihren Wert automatisch aus anderen Spalten derselben Zeile, ohne Trigger und ohne dass die Anwendung den Wert selbst pflegen muss. Dieser Beitrag zeigt STORED gegen VIRTUAL, praktische Einsatzfälle von abgeleiteten Preisen bis Volltextsuche-Indizes, Migrationen bei bestehenden Tabellen und die wichtigsten Vendor-Unterschiede zwischen MySQL und PostgreSQL.

17 Min. Lesezeit STORED · VIRTUAL · GENERATED ALWAYS AS PostgreSQL · MySQL

1. Was Generated Columns sind und welches Problem sie lösen

Eine Generated Column ist eine Spalte, deren Wert die Datenbank automatisch aus einem Ausdruck über andere Spalten derselben Zeile berechnet, statt dass die Anwendung diesen Wert selbst schreibt. Anstatt einen abgeleiteten Wert wie einen Bruttopreis oder einen vollständigen Namen bei jedem INSERT und UPDATE manuell im Anwendungscode zu berechnen und zu synchron zu halten, definiert eine Generated Column die Berechnung einmalig im Schema, und die Datenbank garantiert, dass der Wert immer korrekt und aktuell ist.

Das Problem, das eine Generated Column löst, ist Datenduplikation und -drift: ohne sie muss entweder jede Anwendung, die schreibend zugreift, dieselbe Berechnungslogik erneut implementieren, mit dem Risiko unterschiedlicher, inkonsistenter Ergebnisse, oder ein Trigger übernimmt die Berechnung mit dem zusätzlichen Wartungsaufwand einer prozeduralen Funktion. Eine Generated Column ist dagegen rein deklarativ, direkt im CREATE TABLE oder ALTER TABLE sichtbar, und garantiert Konsistenz unabhängig vom Zugriffsweg, genau wie ein Constraint.

Der SQL-Standard kennt zwei Varianten einer Generated Column, STORED und VIRTUAL, die sich fundamental darin unterscheiden, ob der berechnete Wert physisch auf der Festplatte gespeichert wird oder bei jedem Lesezugriff neu berechnet wird. Die folgenden Abschnitte zeigen praktische Einsatzfälle für beide Varianten, von einfachen abgeleiteten Werten bis zu Volltextsuche-Indizes.

2. STORED vs VIRTUAL Generated Columns

Eine STORED Generated Column berechnet ihren Wert bei jedem INSERT und UPDATE und speichert das Ergebnis physisch wie eine normale Spalte. Das bedeutet zusätzlichen Speicherplatz und etwas mehr Schreibaufwand, dafür ist der Wert bei jedem Lesezugriff sofort verfügbar, ohne erneute Berechnung, und kann direkt indiziert werden wie jede andere Spalte auch.

Eine VIRTUAL Generated Column speichert dagegen keinen Wert, sondern berechnet ihn bei jedem SELECT neu, ähnlich einer Sicht auf Spaltenebene. Das spart Speicherplatz und Schreibaufwand, kostet aber Rechenzeit bei jedem Lesezugriff und lässt sich in manchen Datenbanksystemen nicht direkt indizieren. MySQL unterstützt beide Varianten über GENERATED ALWAYS AS (ausdruck) STORED beziehungsweise VIRTUAL, wobei VIRTUAL der Standard ist, wenn kein Schlüsselwort angegeben wird. PostgreSQL unterstützt bis einschließlich Version 17 ausschließlich STORED Generated Columns, VIRTUAL Generated Columns sind erst mit PostgreSQL 18 hinzugekommen.


-- MySQL: both STORED and VIRTUAL are supported, VIRTUAL is the implicit default
CREATE TABLE product (
    product_id    BIGINT PRIMARY KEY AUTO_INCREMENT,
    net_price     DECIMAL(10,2) NOT NULL,
    vat_rate      DECIMAL(4,2) NOT NULL DEFAULT 19.00,
    -- computed on every read, no extra storage
    gross_price_virtual DECIMAL(10,2)
        GENERATED ALWAYS AS (net_price * (1 + vat_rate / 100)) VIRTUAL,
    -- computed on write, stored physically, directly indexable
    gross_price_stored DECIMAL(10,2)
        GENERATED ALWAYS AS (net_price * (1 + vat_rate / 100)) STORED
);

-- PostgreSQL: only STORED is available up to and including version 17
CREATE TABLE product_pg (
    product_id    BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    net_price     NUMERIC(10,2) NOT NULL,
    vat_rate      NUMERIC(4,2) NOT NULL DEFAULT 19.00,
    gross_price   NUMERIC(10,2)
        GENERATED ALWAYS AS (net_price * (1 + vat_rate / 100)) STORED
);

3. Abgeleitete Werte praktisch berechnen

Der häufigste praktische Einsatzfall einer Generated Column ist ein einfacher, deterministischer Wert, der aus wenigen anderen Spalten berechnet wird, etwa ein vollständiger Name aus Vor- und Nachname, ein Bruttopreis aus Nettopreis und Steuersatz, oder eine normalisierte Version eines Textfelds. Diese Muster ersetzen Anwendungscode, der sonst bei jedem Lesezugriff dieselbe Berechnung wiederholen müsste, oder einen Trigger, der den Wert bei jedem Schreibzugriff separat pflegen müsste.

Ein wichtiger praktischer Vorteil: eine Generated Column lässt sich in WHERE-, ORDER BY- und GROUP BY-Klauseln genauso verwenden wie jede reguläre Spalte, was bei einer reinen Anwendungslogik ohne Datenbankunterstützung nicht möglich wäre. Eine Suche oder Sortierung nach dem vollständigen Namen funktioniert damit direkt auf Datenbankebene, ohne dass die Anwendung die Berechnung dupliziert in eine SQL-Abfrage übersetzen muss.


-- PostgreSQL: full name as a stored generated column, directly queryable
CREATE TABLE customer (
    customer_id  BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    first_name   VARCHAR(100) NOT NULL,
    last_name    VARCHAR(100) NOT NULL,
    full_name    VARCHAR(201)
        GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED
);

INSERT INTO customer (first_name, last_name) VALUES ('Anna', 'Schmidt');

-- The generated column can be used like any regular column
SELECT customer_id, full_name FROM customer
WHERE full_name ILIKE 'anna%'
ORDER BY full_name;

4. Volltextsuche-Indizes mit Generated Columns

Eine der elegantesten Anwendungen einer Generated Column ist die Pflege eines Volltextsuche-Indexes in PostgreSQL. Der Datentyp tsvector repräsentiert einen Text, der bereits in durchsuchbare, gestemmte Lexeme zerlegt wurde, und wird typischerweise über die Funktion to_tsvector aus einem oder mehreren Textfeldern erzeugt. Ohne Generated Column müsste diese Umwandlung entweder bei jeder Suchanfrage erneut berechnet werden, was bei großen Tabellen langsam ist, oder über einen Trigger bei jedem Schreibzugriff gepflegt werden.

Eine STORED Generated Column mit einem GIN-Index auf dem tsvector-Wert kombiniert beide Vorteile: die Berechnung erfolgt einmalig beim Schreiben, der Index macht die Suche danach extrem schnell, und die gesamte Logik ist deklarativ im Schema sichtbar, statt in einem separaten Trigger versteckt zu sein. Dieses Muster ist deutlich wartungsärmer als die traditionelle Trigger-basierte Pflege eines tsvector-Feldes, die vor der Einführung von Generated Columns in PostgreSQL der Standardansatz war.


-- PostgreSQL: full-text search index maintained via a generated column
CREATE TABLE article (
    article_id  BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    title       TEXT NOT NULL,
    body        TEXT NOT NULL,
    search_vector TSVECTOR
        GENERATED ALWAYS AS (
            setweight(to_tsvector('german', coalesce(title, '')), 'A') ||
            setweight(to_tsvector('german', coalesce(body, '')), 'B')
        ) STORED
);

CREATE INDEX idx_article_search ON article USING gin (search_vector);

-- Fast full-text search using the maintenance-free generated column
SELECT article_id, title
FROM article
WHERE search_vector @@ websearch_to_tsquery('german', 'datenbank trigger')
ORDER BY ts_rank(search_vector, websearch_to_tsquery('german', 'datenbank trigger')) DESC;

5. Generated Columns kombiniert mit Indizes

Eine STORED Generated Column lässt sich mit einem regulären B-Tree-Index versehen, was in vielen Datenbanksystemen einer eleganteren Alternative zu einem funktionalen Index (auch Expression Index genannt) entspricht. Ein funktionaler Index auf LOWER(email) erzwingt bei jeder Abfrage exakt denselben Ausdruck in der WHERE-Klausel, damit der Index greift, während eine Generated Column mit demselben Ausdruck als reguläre, benannte Spalte in jeder Abfrage direkt referenziert werden kann, was die Lesbarkeit deutlich verbessert.

Ein zusätzlicher Vorteil: eine indizierte Generated Column lässt sich auch mit einem UNIQUE Constraint kombinieren, etwa um case-insensitive Eindeutigkeit einer E-Mail-Adresse durchzusetzen, ohne dass die Anwendung selbst für konsistente Kleinschreibung vor jedem INSERT sorgen muss. Dieses Muster verbindet die Vorteile von Constraints und Generated Columns in einer einzigen, deklarativen Lösung.


-- PostgreSQL: generated column as a readable, indexable, unique alternative
-- to a functional index on LOWER(email)
CREATE TABLE account (
    account_id    BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email         VARCHAR(255) NOT NULL,
    email_normalized VARCHAR(255)
        GENERATED ALWAYS AS (lower(trim(email))) STORED,
    CONSTRAINT uq_account_email_normalized UNIQUE (email_normalized)
);

-- Readable, named column instead of repeating LOWER(TRIM(email)) everywhere
SELECT * FROM account WHERE email_normalized = lower(trim('Anna@Example.com '));

6. Grenzen: was in einer Generated Column nicht geht

Der Ausdruck einer Generated Column darf sich, ähnlich wie ein CHECK Constraint, ausschließlich auf Spalten derselben Zeile beziehen, niemals auf andere Zeilen oder andere Tabellen. Eine Aggregation über mehrere Zeilen, etwa eine laufende Summe, lässt sich deshalb nicht als Generated Column ausdrücken, dafür sind Window Functions in einer regulären Abfrage oder eine materialisierte Sicht die passenden Werkzeuge.

Ebenso müssen die verwendeten Funktionen deterministisch sein: CURRENT_TIMESTAMP, RANDOM() oder ein Aufruf einer nicht als IMMUTABLE markierten Funktion sind in den meisten Systemen für Generated Columns verboten, aus denselben Gründen wie bei CHECK Constraints mit Funktionsaufrufen. Eine Generated Column darf außerdem nicht selbst wieder auf eine andere Generated Column derselben Tabelle verweisen, die zirkuläre Abhängigkeit würde die Berechnungsreihenfolge undefiniert machen.

7. Generated Columns zu bestehenden Tabellen hinzufügen

Das Hinzufügen einer STORED Generated Column zu einer bereits befüllten Tabelle erfordert, dass die Datenbank den berechneten Wert für jede bestehende Zeile einmalig ermittelt und speichert. Bei einer sehr großen Tabelle mit vielen Millionen Zeilen kann dieser einmalige Rewrite spürbar lange dauern und in PostgreSQL sowie in älteren MySQL-Versionen eine exklusive Tabellensperre erfordern, was während der Migration zu einer kurzzeitigen Downtime führt.

Für produktive Systeme mit hohen Verfügbarkeitsanforderungen ist es üblich, eine solche Migration außerhalb der Hauptlastzeiten durchzuführen, oder bei sehr großen Tabellen ein Online-Schema-Change-Tool zu verwenden, das die Tabelle in Batches umschreibt, statt eine einzige lange laufende, sperrende Operation auszuführen. Eine VIRTUAL Generated Column vermeidet dieses Problem vollständig, weil kein Rewrite bestehender Daten nötig ist, dafür fehlt ihr die Möglichkeit einer direkten Indizierung in den meisten Systemen.


-- MySQL: adding a STORED generated column to an existing, populated table
-- triggers a full table rewrite, plan for a maintenance window on large tables
ALTER TABLE product
    ADD COLUMN gross_price DECIMAL(10,2)
    GENERATED ALWAYS AS (net_price * (1 + vat_rate / 100)) STORED;

-- Verify the migration computed correct values for existing rows
SELECT product_id, net_price, vat_rate, gross_price
FROM product
LIMIT 10;

8. Vendor-Unterschiede: MySQL, PostgreSQL, Oracle

MySQL unterstützt seit Version 5.7 sowohl STORED als auch VIRTUAL Generated Columns über die einheitliche Syntax GENERATED ALWAYS AS. PostgreSQL beschränkte sich bis Version 17 auf STORED, VIRTUAL Generated Columns kamen erst mit PostgreSQL 18 hinzu. Oracle nennt das Konzept traditionell Virtual Column und unterstützt es bereits seit Version 11g, standardmäßig im Verhalten einer VIRTUAL-Spalte, mit der Option, die Spalte zusätzlich zu indizieren, was faktisch dem STORED-Verhalten für Indexzwecke entspricht.

Diese Unterschiede sind bei Portabilitätsentscheidungen relevant: ein Schema, das für MySQL mit VIRTUAL Generated Columns entworfen wurde, muss bei einer Migration nach PostgreSQL 17 oder älter zwingend auf STORED umgestellt werden, mit dem entsprechenden zusätzlichen Speicherbedarf. Wer plattformunabhängige Migrationen plant, sollte sich deshalb von Anfang an auf STORED als kleinsten gemeinsamen Nenner festlegen, statt sich auf die VIRTUAL-Variante zu verlassen.

Datenbank STORED VIRTUAL
MySQL 5.7+ Unterstützt Unterstützt, Standard ohne Schlüsselwort
PostgreSQL bis 17 Unterstützt Nicht verfügbar
PostgreSQL ab 18 Unterstützt Unterstützt
Oracle 11g+ Über Index simulierbar Standardverhalten (Virtual Column)

9. Generated Column vs. Trigger vs. View

Die Wahl zwischen einer Generated Column, einem Trigger und einer View hängt vom Umfang der Berechnung und der Frage ab, ob mehrere Zeilen oder Tabellen einbezogen werden müssen. Eine Generated Column ist die richtige Wahl für deterministische, zeilenlokale Berechnungen. Ein Trigger übernimmt komplexere Logik, die andere Tabellen einbezieht. Eine View eignet sich für Berechnungen, die mehrere Zeilen aggregieren oder Joins über mehrere Tabellen erfordern, ohne dass ein physischer Wert gespeichert werden muss.

Mironsoft

Datenmodellierung, Schema-Design und Datenbankberatung

Abgeleitete Werte ohne Trigger und ohne Duplikation?

Wir entwerfen Generated Columns für abgeleitete Werte, Volltextsuche und funktionale Indizes, und planen Migrationen bei großen Bestandstabellen ohne unnötige Downtime.

Schema-Design

Generated Columns für abgeleitete Werte und Suchindizes

Volltextsuche

Wartungsarme tsvector-Indizes für performante Suchfunktionen

Migrationsplanung

Downtime-arme Einführung neuer Generated Columns auf großen Tabellen

10. Zusammenfassung

Eine Generated Column berechnet abgeleitete Werte deklarativ im Schema, ohne dass Anwendungscode oder ein Trigger die Berechnung dupliziert. STORED speichert den Wert physisch und ist indizierbar, VIRTUAL berechnet ihn bei jedem Lesezugriff neu und spart Speicherplatz. Praktische Einsatzfälle reichen von einfachen abgeleiteten Werten wie einem Bruttopreis bis zu wartungsarmen Volltextsuche-Indizes mit tsvector in PostgreSQL.

Die Grenzen liegen bei zeilenübergreifenden Aggregationen und nicht-deterministischen Ausdrücken, hier übernehmen Views, Window Functions oder Trigger. MySQL unterstützt beide Varianten seit Langem, PostgreSQL erst ab Version 18 auch VIRTUAL. Wer eine Generated Column zu einer großen Bestandstabelle hinzufügt, sollte den nötigen Rewrite und die damit verbundene Sperre in die Migrationsplanung einbeziehen.

Generated Columns praktisch nutzen, das Wichtigste auf einen Blick

STORED vs VIRTUAL

STORED speichert physisch und ist indizierbar, VIRTUAL berechnet bei jedem Lesezugriff neu.

Volltextsuche

tsvector als Generated Column ersetzt trigger-basierte Pflege, kombiniert mit GIN-Index.

Grenzen

Nur zeilenlokale, deterministische Ausdrücke, keine Aggregation über mehrere Zeilen.

Migration

Nachträgliches Hinzufügen einer STORED-Spalte erfordert einen Rewrite bestehender Zeilen.

11. FAQ: Generated Columns praktisch nutzen

1Was ist eine Generated Column?
Eine Spalte, deren Wert automatisch aus anderen Spalten derselben Zeile berechnet wird, statt manuell geschrieben zu werden.
2STORED vs VIRTUAL?
STORED speichert physisch und ist indizierbar, VIRTUAL berechnet bei jedem Lesezugriff neu, ohne Speicherplatz zu belegen.
3Unterstützt PostgreSQL VIRTUAL?
Erst ab Version 18, davor nur STORED.
4Wie nutze ich Generated Columns für Volltextsuche?
Mit einer STORED TSVECTOR-Spalte über to_tsvector, kombiniert mit einem GIN-Index für schnelle Suche.
5Kann eine Generated Column andere Tabellen lesen?
Nein, nur Spalten derselben Zeile. Für tabellenübergreifende Werte sind Views oder Trigger richtig.
6Kann ich CURRENT_TIMESTAMP verwenden?
Nein, der Ausdruck muss deterministisch sein, nicht-deterministische Funktionen sind verboten.
7Wie füge ich eine Generated Column zu einer großen Tabelle hinzu?
Ein einmaliger Rewrite aller Zeilen ist nötig, bei großen Tabellen empfiehlt sich ein Wartungsfenster.
8Kann ich UNIQUE auf eine Generated Column setzen?
Ja, eine STORED Generated Column verhält sich wie eine reguläre Spalte und kann Constraints tragen.
9Wann View statt Generated Column?
Wenn die Berechnung mehrere Zeilen aggregiert oder Joins über mehrere Tabellen benötigt.
10Unterstützt Oracle Generated Columns?
Ja, als Virtual Column seit Version 11g, mit optionaler Indizierung.