eduardweb.
Database & PrismaIntermediar#architecture#postgresql#database#sql#gdpr

Soft delete vs hard delete: cum gândești schema ca să nu plângi la auditul GDPR

De Gabriela Neagu, 6 aug. 2026 · 7 vizualizări · 3 like-uri

Postat 6 aug. 2026
sql
-- Schema PostgreSQL recomandată pentru Soft Delete + GDPR Compliance

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    full_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL
);

-- 1. Index unic parțial: permite reutilizarea email-ului de către useri noi
CREATE UNIQUE INDEX idx_users_active_email 
ON users(email) 
WHERE deleted_at IS NULL;

-- 2. Index pentru query-urile normale de citire
CREATE INDEX idx_users_active 
ON users(id) 
WHERE deleted_at IS NULL;

-- 3. Procedură pentru cereri GDPR (Dreptul de a fi uitat)
-- Păstrăm ID-ul pentru relațiile cu facturile, dar eliminăm PII
UPDATE users
SET 
    email = 'anonymized_' || id || '@deleted-user.local',
    full_name = 'Anonymized User',
    deleted_at = NOW()
WHERE id = 42 AND deleted_at IS NULL;

Am văzut prea multe baze de date legate la cap cu is_deleted = true doar pentru că "așa e bine să avem istoric". Problema apare când primul user îți cere ștergerea datelor pe GDPR sau când încerci să pui un index unic pe o tabelă cu 10 milioane de rânduri. Hai să-ți arăt cum abordez eu problema asta ca să nu stau la 2 noaptea să repar migrări eșuate.

Trapa numărul 1: Restricțiile de unicitate

Prima oară când m-am lovit tare de soft delete a fost pe un proiect cu vreo 800k de utilizatori activi. Aveam tabel de users cu email definit ca UNIQUE. Totul frumos până când un client a cerut închiderea contului, i-am setat deleted_at = NOW(), iar după două săptămâni a vrut să-și facă cont din nou cu același email. Postgres a sărit în aer: duplicate key value violates unique constraint.

Dacă pui un UNIQUE clasic pe o coloană și folosești soft delete, ai încurcat-o. Ca să rezolvi asta elegant în PostgreSQL, soluția este un index unic parțial. Practic îi spui bazei de date să verifice unicitatea doar pentru rândurile care sunt încă active (WHERE deleted_at IS NULL).

Dacă folosești MySQL sau MariaDB unde indexul parțial nu e la fel de flexibil, ești obligat fie să incluzi deleted_at în cheia unică (dar atenție la valorile NULL care se comportă ciudat la unicitate în SQL standard), fie să schimbi email-ul în ceva de genul email_deleted_timestamp@domain.com în momentul ștergerii.

GDPR nu înseamnă "pune un flag și ascunde-l din frontend"

A doua mare greșeală este confuzia între audit intern și conformitate GDPR. Legislația îți cere clar: dacă userul cere "dreptul de a fi uitat", datele lui cu caracter personal (PII) trebuie să dispară definitiv. Nu e suficient să pui is_deleted = true și să nu-l mai afișezi în dashboard. DPO-ul companiei sau un auditor extern nu va fi deloc impresionat.

Pe de altă parte, ai legi fiscale care te obligă să păstrezi istoricul tranzacțiilor și facturile timp de 5 sau 10 ani. Nu poți da DELETE FROM orders WHERE user_id = X pentru că îți strici contabilitatea și referințele de foreign key.

Soluția mea pentru asta e anonimizarea (Anonymization on Soft Delete):

  1. Când userul cere ștergere simplă (din app UI), fac soft delete (deleted_at = NOW()).
  2. Când vine o cerere explicită GDPR / Right to be Forgotten, fac un update prin care suprascriu numele, email-ul, telefonul și adresa cu hash-uri sau valori generice (ex: deleted_user_9812@deleted.local), păstrând totuși ID-ul și istoricul tranzacțiilor intacte.

Performanța: Cât te costă coloana aia extra?

Dacă ai o tabelă de audit sau de loguri cu 15-20 de milioane de înregistrări, faptul că adaugi WHERE deleted_at IS NULL la absolut fiecare query de citire va cântări greu. Fără indecși adecvați, baza de date va face seq scan la greu prin rânduri care nici măcar nu te interesează.

Trade-off-ul e simplu:

  • Soft delete e excelent pentru entități critice (utilizatori, organizații, setări) unde o ștergere accidentală din greșeala unui admin poate fi anulată în 5 secunde.
  • Hard delete e mult mai curat pentru tabele de legătură (many-to-many), sesiuni, token-uri de acces sau entități temporare. Nu mai pune soft delete pe user_roles sau refresh_tokens, nu are niciun sens și îți încarcă memoria cache degeaba.

La un proiect recent am mutat tabelele de legătură de la soft delete înapoi la hard delete și am scăzut dimensiunea bazei de date cu 30% și timpii de query pe JOIN-uri cu ~25%.

Tu ce abordare folosești în producție: mergi pe soft delete peste tot "să fie acolo", sau aplici reguli diferite în funcție de tabelă?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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