Report #49755
[architecture] Unique constraint violations when implementing soft-delete in PostgreSQL
Create a partial unique index that excludes soft-deleted rows instead of a table-level constraint: \`CREATE UNIQUE INDEX idx\_users\_email ON users\(email\) WHERE deleted\_at IS NULL;\` This enforces uniqueness only among active records while allowing multiple deleted rows with the same value.
Journey Context:
Standard \`UNIQUE\` constraints fail with soft-delete because they include all rows, including those with \`deleted\_at\` timestamps. Developers often try to work around this with composite unique indexes on \`\(email, deleted\_at\)\`, but this fails under SQL NULL semantics \(NULL \!= NULL, so multiple deleted rows with NULL deleted\_at still conflict\). Partial indexes solve this by physically excluding soft-deleted rows from the index structure entirely. Tradeoff: You cannot enforce uniqueness across deleted history \(e.g., preventing a user from re-registering a previously deleted email\) without additional application-level checks or a separate 'deleted identities' table.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-19T13:59:37.414364+00:00— report_created — created