eduardweb.
Database & PrismaIntermediar#postgresql#backend#database#gdpr

Soft delete vs hard delete: cum gândești schema ca să nu te bată GDPR la ușă

De Marian Apostol, 13 iun. 2026 · 15 vizualizări · 3 like-uri

Postat 13 iun. 2026
sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL
);

-- Cream un index unic partial doar pentru userii activi
CREATE UNIQUE INDEX uid_users_active_email 
ON users (email) 
WHERE deleted_at IS NULL;

-- Query-ul de selectare a userilor activi
SELECT * FROM users 
WHERE email = 'contact@eduardweb.ro' 
AND deleted_at IS NULL;

Am văzut prea multe proiecte pornite la drum cu un simplu flag de is_deleted adăugat în grabă pe toate tabelele. Pare soluția ideală: nu pierzi date, dai un update rapid și gata, „ai șters” entitatea. Dar când vine primul audit GDPR sau când baza de date trece de câteva milioane de rânduri, începe coșmarul.

Hai să fim sinceri: soft delete-ul e o binecuvântare pentru suportul tehnic și un iad pentru designul bazei de date.

Unde se împute treaba cu soft delete-ul clasic

Cel mai des m-am lovit de problema constrângerilor de unicitate. Imaginează-ți că ai o tabelă de users cu un index unic pe email. Dacă userul își șterge contul (soft delete) și apoi vrea să se înregistreze din nou cu același email, primești o eroare de la baza de date. Email-ul există deja în bază, chiar dacă are is_deleted = true.

Cum o rezolvi? Unii fac un workaround urât: concatenează un timestamp la email când dau soft delete (gen email_deleted_1620000000). E o cârpeală obositoare care aduce bug-uri în cascadă în logica de business.

A doua mare problemă e GDPR-ul. „Dreptul de a fi uitat” nu înseamnă să ascunzi rândul din interfață în timp ce el rămâne intact în PostgreSQL. Inspectorii vor ca datele personale să dispară fizic de pe disc. Dacă ai doar soft delete simplu și păstrezi datele de contact pe veci, ești în ilegalitate din start.

Cum facem să fie bine?

La un proiect cu peste 150.000 de clienți activi, am implementat o schemă hibridă care ne-a salvat de la amenzi și ne-a păstrat și integritatea referențială.

Pentru date pur personale (profil de user), folosim anonimizare completă. Anonimizarea înseamnă că suprascriem câmpurile sensibile (nume, email, telefon) cu valori hash-uite sau constante de tipul user_deleted_123, dar păstrăm ID-ul. Astfel, nu stricăm statisticile de vânzări și facturile istorice care depind de acel user_id.

Pentru entități unde avem nevoie de istoric pentru o perioadă limitată, dar vrem să evităm problemele cu indecșii unici, folosim o coloană de tip deleted_at (timestamp nullable) în loc de boolean, împreună cu un index parțial. În PostgreSQL, chestia asta funcționează de minune.

Trade-off-ul e sincer: Hard-delete-ul e curat și conform GDPR, dar dacă ai chei străine peste tot, riști să ștergi în cascadă date pe care voiai să le păstrezi (cum ar fi istoricul financiar). Soft-delete-ul îți păstrează istoricul, dar îți umflă baza de date și complică toate query-urile, pentru că trebuie să ții minte să pui mereu WHERE deleted_at IS NULL.

Prin abordarea din exemplul de cod, indexul unic se aplică doar pe rândurile active. Când userul își șterge contul, setăm deleted_at = NOW(), eliberând astfel email-ul pentru o nouă înregistrare. Pentru GDPR, rulăm un cron job la 14 zile care anonimizează complet sau face hard delete pe rândurile care au deleted_at mai vechi de perioada de grație.

Voi cum gestionați treaba asta? Mergeți pe soft delete peste tot sau preferați tabele separate de arhivă?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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