Soft Delete: löschen, ohne zu löschen
DELETE ein Zeitstempel geloescht_am. Eine Sicht zeigt nur aktive Zeilen, und ein partieller UNIQUE-Index prüft die Eindeutigkeit nur unter den aktiven. So darf dieselbe Adresse einmal aktiv und beliebig oft gelöscht existieren.🧪 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 UNIQUE INDEX ux_konten_email_aktiv ON konten (email) WHERE geloescht_am IS NULL; CREATE VIEW konten_aktiv AS SELECT * FROM konten WHERE geloescht_am IS NULL;
Mit Row Level Security ließe sich der Filter sogar erzwingen.
-- keine partiellen Indizes: Hilfsspalte, die für Gelöschte NULL ist ALTER TABLE konten ADD email_aktiv VARCHAR(255) AS (IF(geloescht_am IS NULL, email, NULL)) STORED; CREATE UNIQUE INDEX ux_konten_email_aktiv ON konten (email_aktiv);
Mehrere NULL-Werte sind in einem UNIQUE-Index erlaubt – die gelöschten Zeilen stören also nicht.
CREATE UNIQUE INDEX ux_konten_email_aktiv ON konten (email) WHERE geloescht_am IS NULL;
CREATE UNIQUE INDEX ux_konten_email_aktiv ON konten ( CASE WHEN geloescht_am IS NULL THEN email END);
CREATE UNIQUE INDEX ux_konten_email_aktiv ON konten (email) WHERE geloescht_am IS NULL;
⚠️ Fallstricke
- Jede Abfrage muss den Filter kennen – eine vergessene Stelle zeigt gelöschte Daten. Deshalb die Sicht (oder RLS) als einzige Schnittstelle.
- Fremdschlüssel wissen nichts von Soft Delete: Bestellungen können weiter auf „gelöschte“ Kunden zeigen – oft gewollt, aber bewusst entscheiden.
- DSGVO: Soft Delete ist kein Löschen im Sinne des Rechts auf Löschung. Personenbezogene Felder nach Frist anonymisieren oder wirklich löschen.
📚 Belege
- SQLite: Partielle Indizes – partielle Indizes seit 3.8.0
- PostgreSQL: Partielle Indizes – CREATE UNIQUE INDEX … WHERE …
- SQL Server: Gefilterte Indizes (2008) – gefilterte Indizes seit 2008
- MySQL: CREATE INDEX (funktionale Schlüsselteile) – funktionale Indizes seit 8.0.13, keine WHERE-Klausel (keine partiellen Indizes)