eduardweb.
Database & PrismaIntermediar#databases#backend-architecture#sql#gdpr

Soft delete vs Hard delete: de ce `deleted_at` nu e mereu soluția salvatoare

De Gabriela Neagu, 24 apr. 2026 · 2 vizualizări · 2 like-uri

Postat acum 3 zile
sql
-- Exemplu de index parțial în Postgres pentru a evita conflictele la soft delete
CREATE UNIQUE INDEX idx_user_email_active 
ON users (email) 
WHERE deleted_at IS NULL;

-- Exemplu de anonimizare pentru GDPR conformitate
UPDATE users 
SET 
    email = md5(email || now()::text) || '@deleted.com',
    first_name = 'Anonymized',
    last_name = 'User',
    deleted_at = now()
WHERE id = 12345;

M-am lovit de dilema asta de nenumărate ori în ultimii 10 ani. La început, pe orice proiect nou, instinctul e să bagi un deleted_at peste tot și să zici că ești acoperit. Sună bine, nu? Dacă un user șterge ceva din greșeală, îi dai un restore în 5 secunde dintr-un update SQL și ești eroul zilei. Dar am pățit la un proiect cu vreo 12k useri activi ca schema asta să se transforme într-un coșmar de performanță și, mai rău, într-o vulnerabilitate legală.

Problema principală e că soft delete-ul e, de fapt, o minciună pe care o spui bazei de date. Tu îi zici că datele sunt acolo, dar aplicația trebuie să se prefacă mereu că nu le vede. Asta înseamnă că la fiecare query, absolut la fiecare SELECT, trebuie să adaugi WHERE deleted_at IS NULL. Uiți o dată? Felicitări, tocmai ai afișat date fantomă în UI sau, mai rău, în rapoartele financiare.

Coșmarul indexurilor unice

Aici e buba cea mare. Să zicem că ai o tabelă de users cu un index unic pe email. Userul își șterge contul (soft delete), deci deleted_at primește un timestamp. După două zile, vrea să se înregistreze din nou cu același email. Ghici ce? Baza de date o să urle că email-ul există deja.

Am încercat diverse workaround-uri. Unii pun deleted_at în cheia unică, dar în Postgres, de exemplu, NULL nu e considerat egal cu alt NULL, deci poți avea duplicate dacă nu ești atent. O soluție pe care am aplicat-o cu succes a fost folosirea indexurilor parțiale. Adică pui constrângerea de unicitate doar pentru rândurile unde deleted_at IS NULL. A redus numărul de conflicte în DB cu aproape 90% pe un proiect de SaaS unde userii aveau prostul obicei să-și tot șteargă și refacă profilul.

GDPR și dreptul de a fi uitat

Aici se rupe filmul cu soft delete-ul clasic. Dacă vine un audit sau un user nervos care invocă articolul 17 din GDPR, nu poți să-i zici: „Stai liniștit, am pus un flag acolo și nu se mai vede în site”. Datele sunt tot pe disk, tot în backup-uri, tot în log-uri. Dacă ai un leak, datele alea „șterse” sunt la fel de expuse ca cele active.

La un proiect de fintech, am implementat un sistem de anonimizare în loc de ștergere fizică imediată. Când userul dădea delete, făceam un update în care suprascriam câmpurile PII (Personally Identifiable Information) cu valori random sau hash-uri, și abia apoi puneam deleted_at. Așa păstram integritatea referențială pentru analytics — știam că a existat o comandă de 500 de lei, dar nu mai știam că a fost a lui Popescu de la etajul 3. Am economisit cam 30% din timpul de procesare a cererilor de ștergere automatizând fluxul ăsta.

Trade-off-ul sincer

Hard delete-ul e curat, dar periculos. E ca și cum ai merge pe sârmă fără plasă de siguranță. Dacă ai un bug în cod și ștergi din greșeală o tabelă de legătură, ai dat de dracu'. Soft delete-ul e plasa de siguranță, dar vine cu o taxă de mentenanță pe care o plătești la fiecare query scris.

Pe scurt: folosesc soft delete (sau anonimizare) pentru entități core, unde am nevoie de istoric și audit (useri, tranzacții, setări de cont). Pentru chestii tranzitorii, log-uri de activitate sau notificări vechi, merg direct pe hard delete. Nu merită să cari după tine 50GB de date inutile doar de dragul de a avea un flag în DB.

Voi cum gestionați unicitatea când aveți soft delete? Mergeți pe indexuri parțiale sau mutați datele șterse în tabele de arhivă?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.