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.
Inhaltsverzeichnis
- 1. Was Generated Columns sind und welches Problem sie lösen
- 2. STORED vs VIRTUAL Generated Columns
- 3. Abgeleitete Werte praktisch berechnen
- 4. Volltextsuche-Indizes mit Generated Columns
- 5. Generated Columns kombiniert mit Indizes
- 6. Grenzen: was in einer Generated Column nicht geht
- 7. Generated Columns zu bestehenden Tabellen hinzufügen
- 8. Vendor-Unterschiede: MySQL, PostgreSQL, Oracle
- 9. Generated Column vs. Trigger vs. View
- 10. Zusammenfassung
- 11. FAQ
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.