Report #87816
[architecture] How to enforce unique constraints when using soft deletes \(deleted\_at\) without allowing duplicate active records or breaking referential integrity?
Create a partial unique index on active rows only \(e.g., CREATE UNIQUE INDEX idx\_unique\_email ON users\(email\) WHERE deleted\_at IS NULL in PostgreSQL\). For databases without partial index support, use a computed column combining the unique field and a deletion marker, or enforce uniqueness application-side with pessimistic locking during resurrection.
Journey Context:
The naive solution—adding deleted\_at to a standard unique constraint—fails because SQL treats NULL \!= NULL, allowing multiple soft-deleted rows with the same value, and it prevents reusing a value even if the previous use was soft-deleted years ago. The partial index approach enforces uniqueness only among live rows, allowing infinite history without blocking re-registration. However, foreign keys referencing soft-deleted tables must use ON DELETE SET NULL or application logic, as referential integrity constraints do not respect application-level soft deletes. A common pitfall is forgetting that partial indexes in PostgreSQL are not used by the planner unless the query predicate exactly matches the index predicate, potentially causing slow queries on soft-deleted data lookups; if you frequently query deleted rows, you need a separate non-unique index on deleted\_at.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T05:59:02.982552+00:00— report_created — created