-- 1. Index unic doar pentru utilizatorii activi (evită conflictele la re-înregistrare)
CREATE UNIQUE INDEX idx_users_active_email
ON users (email)
WHERE deleted_at IS NULL;
-- 2. Soft-delete compliant GDPR: Anonimizăm datele PII, dar păstrăm ID-ul pentru FK-uri
UPDATE users
SET
email = CONCAT('deleted_', id, '@anonymized.local'),
full_name = 'Anonymized User',
phone_number = NULL,
deleted_at = NOW()
WHERE id = 4021 AND deleted_at IS NULL;Am văzut de prea multe ori echipe care trântești un deleted_at în toate tabelele și cred că au rezolvat arhitectura. Până când vine primul mail de la juridic pe GDPR sau până când baze de date cu 15M de rânduri încep să gâfâie din cauza indeșilor umflați degeaba. Astăzi discutăm despre cum să modelezi ștergerile de la început ca să nu faci refactoring cu transpirații reci la 2 noaptea.
Buba cu deleted_at clasic și unicitatea
Problema clasică pe care o văd în producție: ai tabelul users cu email VARCHAR UNIQUE. Userul își șterge contul, tu pui deleted_at = NOW(). Peste două săptămâni, omul se răzgândește și vrea să-și facă cont nou cu același email. Surprise: baza de date îți dă peste cap cu UniqueViolation pentru că rândul vechi e încă acolo.
Dacă ești pe Postgres, poți scăpa curat cu un index parțial. Reduci dimensiunea indexului cu 30-40% și rezolvi unicitatea pentru utilizatorii activi. Dar asta e doar problema tehnică mică. Adevăratul show începe când citești mai atent legislația.
Coșmarul GDPR: Soft delete nu înseamnă ștergere legală
GDPR spune clar: „Dreptul de a fi uitat”. Dacă un user îți cere ștergerea datelor și tu doar pui un flag is_deleted = true, păstrându-i Numele, Prenumele și IP-ul în DB, ești pasibil de amendă. Juridic, datele alea sunt încă în posesia ta și pot fi expuse la un SQL Injection.
Am avut cazul la un SaaS B2B unde un client mare ne-a cerut audit pe procesul de purge. Aveam vreo 120k de conturi marcate ca șterse, dar cu toate PII-urile (Personally Identifiable Information) intacte. Am pierdut două săptămâni de sprint doar ca să scriem scripturi de anonimizare retroactivă și să verificăm dacă nu am spart FK-urile spre facturi.
Soluția sănătoasă: Anonimizare sau Tombstoning
Ca să nu ajungi acolo, recomand una dintre aceste două abordări în funcție de domeniu:
-
Soft Delete cu Anonimizare (Scrubbing): Când userul apasă „Șterge contul”, păstrezi ID-ul primar pentru ca relațiile din DB (comenzi, facturi, loguri) să nu devină orfane. În schimb, suprascrii datele personale:
emaildevinedeleted_12345@anon.local,namedevineDeleted User, iar adresele de livrare le ștergi definitiv (HARD DELETE). -
Tabel separat de Arhivă / Audit: Faci
HARD DELETEdin tabelul operațional (users), dar declanșezi mutarea minimului de date necesar pentru contabilitate (de exemplu: ID-ul și tranzacțiile) într-un tabel separat cu acces ultra-restricționat.
Trade-off-uri pe care trebuie să le accepți
Soft delete-ul cu anonimizare e excelent pentru păstrarea integrității referențiale și pentru analize istorice, dar adaugă complexitate la orice query. Trebuie să te asiguri că ORM-ul sau query-urile tale au mereu clauza WHERE deleted_at IS NULL. Altfel, te trezești cu bug-uri în UI unde apar fantome.
Hard delete-ul e cel mai curat, menține indeșii mici și timpii de scanare rapizi. În schimb, dacă ai setat aiurea ON DELETE CASCADE, poți șterge din greșeală jumătate de bază de date dintr-un singur query nevinovat.
Voi ce strategie folosiți în proiectele curente? Mergeți pe anonimizare directă la nivel de rând sau aveți job-uri separate de purge la 30 de zile?