VillageSQL is a drop-in replacement for MySQL with extensions.
All examples on this page work on VillageSQL. Install Now →
기본 패턴
null 허용 타임스탬프를 추가하세요.NULL은 살아 있음을 뜻하고, 타임스탬프는 삭제되었음을 뜻하며 언제인지 기록합니다:
deleted 불리언보다 타임스탬프를 선호하세요: 비용은 같고, 지원 티켓이 물어볼 “언제”에 공짜로 답해 줍니다.
문제 1: 고유 제약 조건
email에 대한 고유 인덱스는 재등록을 영원히 차단합니다: 소프트 삭제된 행이 여전히 그 값을 차지하고 있기 때문입니다.
NULL을 몇 개든 허용하므로 삭제된 행은 절대 충돌하지 않고, 같은 이메일을 가진 살아 있는 행 두 개는 여전히 실패합니다. 동작 원리는 생성 열을 참조하세요.
문제 2: 모든 쿼리가 기억해야 함
WHERE deleted_at IS NULL을 한 번만 잊어도 삭제된 데이터가 보고서나 API 응답으로 새어 나갑니다. 두 가지 완화책:
- 살아 있는 부분집합에 대한 뷰. 읽기 경로를 뷰로 향하게 하면 필터를 잊을 수 없습니다:
- ORM 스코프. 대부분의 ORM은 필터를 전역으로 적용할 수 있습니다(Rails
default_scope, Django 매니저, Prisma 미들웨어). 모든 쿼리를 감사하는 대신 한 곳에서 켜세요.
ON DELETE CASCADE가 절대 발동하지 않습니다. 자식이 부모와 함께 사라져야 한다면, 애플리케이션(또는 트리거)이 deleted_at을 직접 캐스케이드해야 합니다.
소프트 삭제를 하지 말아야 할 때
- 규제에 따른 삭제. GDPR 삭제 요청은 데이터가 숨겨지는 것이 아니라 사라지는 것을 뜻합니다. 소프트 삭제된 행도 여전히 개인 데이터로 간주됩니다.
- 변동이 많은 테이블. 수백만 건씩 삭제되는 세션, 큐 작업, 로그는 테이블과 그 위의 모든 인덱스를 부풀릴 뿐입니다.
- 아무도 복원하지 않을 데이터. 되돌리기 시나리오가 없다면 필터 비용을 치러도 얻는 것이 없습니다.
deleted_at이 보존 기간보다 오래된 행을 하드 삭제합니다:
함께 보기
- 생성 열 — 고유 제약 조건 해결책 뒤의 메커니즘
- 뷰 — 필터를 잊을 수 없는 살아 있는 행 뷰
- 외래 키 — 소프트 삭제에서 캐스케이드가 발동하지 않는 이유
- MySQL DELETE — 이 패턴이 대체하는 하드 삭제