Preis-Historie mit gültig_von / gültig_bis (SCD Typ 2)
gueltig_bis = jetzt) und legt eine neue offene Version an (gueltig_bis = NULL). Das ist eine „Slowly Changing Dimension Typ 2“. Die WHEN-Bedingung sorgt dafür, dass nur echte Preisänderungen eine Version erzeugen.🧪 Ausprobieren – Vorher / Nachher
🌍 Dialekte nebeneinander
Ausgeführt werden nur SQLite (sql.js) und – wo markiert – PostgreSQL (PGlite). MySQL/MariaDB, SQL Server und Oracle sind nach der offiziellen Dokumentation geschrieben, laufen hier aber nicht.
CREATE TRIGGER artikel_preis_historie AFTER UPDATE OF preis ON artikel FOR EACH ROW WHEN (OLD.preis IS DISTINCT FROM NEW.preis) EXECUTE FUNCTION preis_historie_fn(); -- Funktion siehe PGlite-Code
Vor 11 hieß es EXECUTE PROCEDURE (weiterhin akzeptiert, aber veraltet).
DELIMITER // CREATE TRIGGER artikel_preis_historie AFTER UPDATE ON artikel FOR EACH ROW BEGIN IF NOT (OLD.preis <=> NEW.preis) THEN UPDATE preis_historie SET gueltig_bis = NOW() WHERE artikel_nr = NEW.artikel_nr AND gueltig_bis IS NULL; INSERT INTO preis_historie (artikel_nr, preis, gueltig_von) VALUES (NEW.artikel_nr, NEW.preis, NOW()); END IF; END// DELIMITER ;
Kein UPDATE OF spalte und keine WHEN-Klausel – die Bedingung steht im Rumpf. <=> ist der NULL-sichere Vergleich.
CREATE OR ALTER TRIGGER artikel_preis_historie ON artikel AFTER UPDATE AS BEGIN SET NOCOUNT ON; DECLARE @jetzt datetime2 = SYSUTCDATETIME(); -- anweisungsweise: inserted/deleted enthalten ALLE geänderten Zeilen UPDATE h SET gueltig_bis = @jetzt FROM preis_historie h JOIN inserted i ON i.artikel_nr = h.artikel_nr JOIN deleted d ON d.artikel_nr = i.artikel_nr WHERE h.gueltig_bis IS NULL AND i.preis <> d.preis; INSERT INTO preis_historie (artikel_nr, preis, gueltig_von) SELECT i.artikel_nr, i.preis, @jetzt FROM inserted i JOIN deleted d ON d.artikel_nr = i.artikel_nr WHERE i.preis <> d.preis; END;
Trigger feuern einmal pro Anweisung – immer mengenbasiert schreiben, nie „SELECT @x = preis FROM inserted“. CREATE OR ALTER ab 2016 SP1.
CREATE OR REPLACE TRIGGER artikel_preis_historie AFTER UPDATE OF preis ON artikel FOR EACH ROW WHEN (OLD.preis <> NEW.preis) BEGIN UPDATE preis_historie SET gueltig_bis = SYSTIMESTAMP WHERE artikel_nr = :NEW.artikel_nr AND gueltig_bis IS NULL; INSERT INTO preis_historie (artikel_nr, preis, gueltig_von) VALUES (:NEW.artikel_nr, :NEW.preis, SYSTIMESTAMP); END; /
In der WHEN-Klausel ohne Doppelpunkt (NEW.preis), im Rumpf mit (:NEW.preis).
CREATE TRIGGER artikel_preis_historie AFTER UPDATE OF preis ON artikel WHEN old.preis IS NOT new.preis BEGIN UPDATE preis_historie SET gueltig_bis = CURRENT_TIMESTAMP WHERE artikel_nr = new.artikel_nr AND gueltig_bis IS NULL; INSERT INTO preis_historie (artikel_nr, preis, gueltig_von) VALUES (new.artikel_nr, new.preis, CURRENT_TIMESTAMP); END;
IS NOT ist in SQLite der NULL-sichere Ungleich-Vergleich.
⚠️ Fallstricke
- Intervalle halb-offen speichern:
[gueltig_von, gueltig_bis). Dann schließen sich Versionen lückenlos an, und eine Abfrage „Stand am“ trifft genau eine Zeile. - Die Historie nur per Trigger zu füllen heißt: Bulk-Importe, die Trigger umgehen (z. B. Bulk-Load ohne FIRE_TRIGGERS in SQL Server), erzeugen Lücken.
- Zwei Änderungen in derselben Uhrzeit-Auflösung erzeugen zwei Versionen mit gleichem
gueltig_von– der Primärschlüssel (artikel_nr, gueltig_von) schlägt dann Alarm. Feinere Zeitstempel oder eine Versionsnummer verwenden. - Einen Index auf (artikel_nr, gueltig_bis) bzw. einen partiellen Index
WHERE gueltig_bis IS NULLanlegen – sonst sucht jede Änderung die offene Version per Scan.
📚 Belege
- SQLite: CREATE TRIGGER – nur FOR EACH ROW, INSTEAD OF nur auf Views, RAISE()
- PostgreSQL: CREATE TRIGGER – BEFORE/AFTER/INSTEAD OF, FOR EACH ROW/STATEMENT, REFERENCING (Transition Tables), EXECUTE FUNCTION
- PostgreSQL: PL/pgSQL – Trigger-Funktionen – NEW/OLD, TG_OP, RETURN NEW/NULL, Audit-Trigger-Beispiel
- MySQL: CREATE TRIGGER – BEFORE/AFTER, nur FOR EACH ROW, FOLLOWS/PRECEDES; REPLACE aktiviert DELETE- und INSERT-Trigger
- SQL Server: CREATE TRIGGER (Transact-SQL) – AFTER/INSTEAD OF, anweisungsweise, inserted/deleted, max. 32 Ebenen
- SQL Server: CREATE OR ALTER – CREATE OR ALTER seit 2016 SP1
- Oracle: CREATE TRIGGER – BEFORE/AFTER, FOR EACH ROW, INSTEAD OF, :NEW/:OLD