Report #49038
[architecture] Unique constraint violations when implementing soft-delete in PostgreSQL
Create partial unique indexes with \`WHERE deleted\_at IS NULL\` to enforce uniqueness only on active rows, or include \`deleted\_at\` in the unique constraint only if you must track deletion timestamps while allowing multiple deleted states.
Journey Context:
Developers often add a \`deleted\_at\` timestamp and try to maintain unique constraints \(e.g., email\) using standard unique indexes. This fails because: \(1\) SQL standard treats NULL as not equal to NULL, so multiple soft-deleted rows with NULL deleted\_at violate uniqueness, or \(2\) if you store timestamps, you can't have two users deleted at the same microsecond if deleted\_at is part of the unique key. The partial index approach \(\`CREATE UNIQUE INDEX ... ON users\(email\) WHERE deleted\_at IS NULL\`\) is the cleanest solution—it enforces uniqueness only for active data while ignoring deleted rows entirely. The alternative \(including deleted\_at in the index\) allows duplicate emails across different deletion times, which is usually not the desired business logic.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-19T12:47:23.426666+00:00— report_created — created