Data modification & MERGE
Concept  ·  SQL & Querying

Soft delete moves deletion into data (deleted_at) — every query inherits a mandatory filter, and the filter is a NULL comparison: deleted_at IS NULL, never = NULL.

Soft delete trades storage-level deletion for a convention — and conventions don't enforce themselves. Two failure modes: forgetting the filter resurrects deleted rows (in counts, joins, exports), and mis-writing it as = NULL deletes everything from view. The mitigations are structural, not vigilance: an active view as the default query surface; deletion-aware unique keys (partial indexes); joins audited (a LEFT JOIN to a soft-deleted dimension resurrects retired lookups).

See it break
designs
namedeleted_at
activeNULL
gilt2027-01-01
The trap
SELECT
  name
FROM
  designs
WHERE
  deleted_at = NULL
ORDER BY
  name;
The fix
SELECT
  name
FROM
  designs
WHERE
  deleted_at IS NULL
ORDER BY
  name;

The active-designs query written as deleted_at = NULL returned zero rows — a comparison with NULL is UNKNOWN, never TRUE, so everything vanished; deleted_at IS NULL keeps the one live design.

The rule

The active-view convention; deletion-aware unique keys; IS NULL, mechanically.

Shows up elsewhere
NULL = NULL is not TRUEThe LEFT JOIN that turned INNER
Try one →Prove it in practice →