Soft deletes sound simple: instead of deleting a record, you mark it as deleted.
UPDATE users SET deleted = true WHERE id = 123;
Done, right?
Not quite.
At small scale, this works fine. But as your data grows, soft deletes can quietly wreck query performance, bloat indexes, and complicate logic.
Letโs break down how to do it properlyโwithout turning your database into a slow-moving legacy monster.
๐ง Why Soft Deletes Exist
We use soft deletes when:
- You need auditability
- You want undo/restore capability
- Data must be retained for compliance or business logic
Instead of deleting:
DELETE FROM orders WHERE id = 10;
You do:
UPDATE orders SET deleted_at = NOW() WHERE id = 10;
โ ๏ธ The Hidden Problems
1. Query Performance Degrades
Every query now needs:
WHERE deleted_at IS NULL;
If you forget it? ๐ Congrats, you just exposed deleted data.
If you include it everywhere? Your indexes might not help anymore.
2. Indexes Become Less Effective
A typical index:
CREATE INDEX idx_users_email ON users(email);
Now your query:
SELECT * FROM users
WHERE email = 'a@b.com'
AND deleted_at IS NULL;
๐ That index is no longer optimal.
3. Table Bloat
Soft-deleted rows still sit there:
- Increasing table size
- Slowing scans
- Affecting cache efficiency
โ The Right Way to Implement Soft Deletes
1. Use deleted_at Instead of Boolean
Avoid:
deleted = true
Use:
deleted_at TIMESTAMP NULL
Why?
- Stores when deletion happened
- Helps in cleanup jobs
- Enables auditing
2. Use Partial Indexes (Game Changer)
If you're using PostgreSQL:
CREATE INDEX idx_users_active_email
ON users(email)
WHERE deleted_at IS NULL;
Now this query is fast again:
SELECT * FROM users
WHERE email = 'a@b.com'
AND deleted_at IS NULL;
๐ Only active rows are indexed = smaller, faster index.
3. Default Scope / Global Filters (Use Carefully)
In ORMs like Spring Data / Hibernate:
@Where(clause = "deleted_at IS NULL")
This ensures deleted records are automatically excluded.
โ ๏ธ But:
- Can confuse developers
- Harder to debug when data "disappears"
Use it, but document it clearly.
4. Archive Old Data
Soft delete โ keep forever.
Move old deleted data:
INSERT INTO users_archive
SELECT * FROM users
WHERE deleted_at < NOW() - INTERVAL '90 days';
DELETE FROM users
WHERE deleted_at < NOW() - INTERVAL '90 days';
๐ Keeps main table lean and fast.
5. Be Smart with Unique Constraints
Problem:
UNIQUE(email)
Soft delete a user โ can't reuse email โ
Fix with partial unique index:
CREATE UNIQUE INDEX uniq_active_email
ON users(email)
WHERE deleted_at IS NULL;
Now:
- Active users must be unique โ
- Deleted users donโt block reuse โ
6. Batch Your Deletes
Avoid:
UPDATE users SET deleted_at = NOW();
Instead:
- Process in batches (e.g., 1000 rows at a time)
- Prevent locks and performance spikes
7. Donโt Overuse Soft Deletes
Not everything needs it.
Avoid soft deletes for:
- High-frequency log tables
- Temporary/session data
- Event streams
Sometimes, hard delete + backup is cleaner.
๐งช Real-World Pattern (Recommended Setup)
deleted_at TIMESTAMP NULL- Partial indexes on active data
- Background job for cleanup/archival
- Explicit queries (not overly magical ORM behavior)
โก TL;DR
Soft deletes are not free.
If you:
- Add
deleted_at - Use partial indexes
- Archive old data
- Handle uniqueness correctly
๐ You get the benefits without killing performance.
If you donโtโฆ
๐ Youโre slowly building a performance bug that shows up at scale (usually at the worst possible time ๐).
๐ฌ Final Thought
Soft deletes are like adding a feature flag to your data layerโpowerful, but easy to misuse.
Design it right early, or youโll end up debugging โwhy is this query suddenly slow?โ six months later.
Top comments (0)