Menu

SQLite DELETE 문: WHERE로 안전하게 행 삭제하기

SQLite DELETE 문 사용법 정리 — WHERE 조건으로 안전하게 행 삭제하기, 전체 행 삭제, ON DELETE CASCADE, 그리고 RETURNING으로 삭제된 행 확인까지.

이 페이지에는 실행 가능한 에디터가 있습니다 — 편집하고 실행하면 결과를 바로 볼 수 있습니다.

DELETE는 행만 지운다, 그 이상은 없다

DELETE는 테이블에서 행을 빼내는 명령입니다. 테이블 자체를 삭제하거나 스키마를 바꾸지 않고, 다른 테이블도 건드리지 않습니다(CASCADE를 따로 걸어두지 않았다면요). 문법은 아주 간단합니다:

DELETE FROM users WHERE id = 2;는 조건에 맞는 행을 찾아서 지워 버립니다. 나머지 두 행은 그대로 남아 있고요. 테이블 자체는 멀쩡히 살아 있어서 계속 INSERT를 할 수 있습니다.

이렇게 생각하면 편합니다. DELETESELECT인데, 매칭된 행을 돌려주는 대신 버려 버리는 동작이라고요.

SQLite WHERE 조건 삭제가 모든 걸 결정한다

제대로 된 DELETE 문의 성패는 결국 WHERE 절에 달려 있습니다. WHERE만 정확히 쓰면 의도한 행만 깔끔하게 지워지지만, 한 번 잘못 쓰면 필요 이상으로, 심하면 테이블 전체가 날아갑니다.

게시되지 않았고 조회수가 0인 초안 두 건이 모두 삭제되었습니다. 게시된 행들은 조건에 해당되지 않아 그대로 남아 있죠. WHERE 절에는 IN, LIKE, BETWEEN, 서브쿼리, AND/OR 조합 등 어떤 표현식이든 자유롭게 쓸 수 있습니다.

들이면 좋은 습관 하나. DELETE를 실행하기 전에, 같은 WHERE 조건을 먼저 SELECT로 돌려보세요.

-- 삭제될 행 미리보기:
SELECT * FROM posts WHERE published = 0 AND views = 0;

-- 결과가 만족스럽나요? 그럼 이제 삭제합니다:
DELETE FROM posts WHERE published = 0 AND views = 0;

이 두 단계의 절차가 그 어떤 백업 툴보다 더 많은 데이터베이스를 살려냈습니다.

WHERE 없는 DELETE는 테이블을 통째로 비운다

WHERE를 빼고 DELETE를 실행하면 모든 행이 삭제됩니다:

테이블은 비워졌지만 구조는 그대로 남아 있습니다. SQLite에는 TRUNCATE 문이 따로 없는데, DELETE FROM table; 이 그 역할을 대신합니다. SQLite는 내부적으로 "truncate optimization"을 적용해 행을 하나씩 지우는 대신 페이지 전체를 한 번에 날려버립니다. 속도는 빠르지만 어디까지나 트랜잭션 작업이라 롤백도 가능합니다.

기본 키에 AUTOINCREMENT를 걸어둔 경우, 카운터는 자동으로 초기화되지 않습니다. id를 다시 1부터 시작하게 하려면 시퀀스 행까지 함께 비워줘야 합니다.

DELETE FROM log;
DELETE FROM sqlite_sequence WHERE name = 'log';

AUTOINCREMENT 없이 그냥 INTEGER PRIMARY KEY만 쓰는 경우라면 SQLite가 알아서 id를 재활용하기 때문에 따로 신경 쓸 필요가 없습니다.

여러 행을 한 번에 삭제하기

삭제할 행이 정해져 있다면 IN을 쓰는 방식이 가장 깔끔합니다:

삭제할 행을 조인이나 다른 테이블을 기준으로 골라야 할 때는 서브쿼리로 DELETE를 돌릴 수도 있습니다:

SQLite는 MySQL처럼 DELETE ... JOIN 구문을 지원하지 않지만, WHERE 절에 서브쿼리를 쓰면 똑같은 효과를 낼 수 있습니다.

RETURNING 절: 삭제한 행 그대로 돌려받기

DELETE 문 끝에 RETURNING을 붙이면 방금 삭제한 행을 마치 SELECT처럼 결과셋으로 받아볼 수 있습니다:

삭제된 모든 행의 id와 email을 그대로 돌려받을 수 있습니다. 이 기능은 다음과 같은 상황에서 정말 유용합니다:

  • 어떤 데이터가 지워졌는지 정확히 로그로 남길 때
  • 되돌리기(undo) 기능을 만들 때 (반환된 행을 어딘가에 보관해 두면 됩니다)
  • 삭제가 의도한 행에만 영향을 줬는지 한 번의 왕복으로 확인하고 싶을 때

RETURNING 절은 INSERT, UPDATE, DELETE 모두에서 동작합니다. 자세한 내용은 별도 페이지에서 다룹니다.

연관된 행을 함께 지우는 ON DELETE CASCADE

부모 테이블과 자식 테이블이 외래 키로 연결돼 있을 때, 부모 행을 삭제하면 자식 행은 부모 없는 고아 상태로 남게 됩니다. SQLite에 연쇄 삭제(cascade)를 하라고 알려주지 않는 한 말이죠:

저자를 삭제하면 해당 저자의 책도 함께 삭제됩니다. ON DELETE CASCADE가 없다면 같은 DELETE 문은 외래 키가 꺼져 있을 때는 그냥 성공하면서 고아 레코드를 남기고, 켜져 있을 때는 제약 조건 오류로 실패합니다.

여기서 가장 큰 함정 하나. SQLite는 기본적으로 외래 키가 꺼져 있습니다. 연결할 때마다 매번 PRAGMA foreign_keys = ON;을 실행해 줘야 합니다. 이 PRAGMA를 설정하지 않으면 ON DELETE CASCADE는 조용히 무시되고, 책 데이터는 그대로 남습니다. 대부분의 애플리케이션 드라이버는 이 옵션을 자동으로 켜 주거나 별도 설정을 제공하니, 사용 중인 드라이버 문서를 꼭 확인하세요.

알아 두면 좋은 다른 CASCADE 옵션들도 있습니다. ON DELETE SET NULL은 외래 키 컬럼을 NULL로 비워 두고, ON DELETE RESTRICT는 자식 행이 있으면 삭제 자체를 거부합니다. ON DELETE NO ACTION은 기본값이며, 대부분의 경우 RESTRICT와 동일하게 동작합니다.

SQLite DELETE에서 LIMIT 사용하기 (컴파일 옵션)

일부 SQLite 빌드는 DELETE ... LIMIT 구문을 지원합니다. 거대한 테이블을 한 번에 다 비우지 않고 배치 단위로 조금씩 정리할 때 유용합니다:

DELETE FROM logs
WHERE created_at < '2024-01-01'
ORDER BY created_at
LIMIT 1000;

이 기능을 쓰려면 SQLite가 SQLITE_ENABLE_UPDATE_DELETE_LIMIT 옵션으로 컴파일되어 있어야 합니다. 다행히 공식 바이너리와 대부분의 언어 바인딩(파이썬의 sqlite3, Node의 better-sqlite3 등)은 이 옵션이 켜진 상태로 배포됩니다. 만약 사용 중인 환경에서 이 옵션이 빠져 있다면 문법 오류가 발생하니, 이럴 때는 서브쿼리 방식으로 우회하면 됩니다:

DELETE FROM logs
WHERE id IN (
    SELECT id FROM logs
    WHERE created_at < '2024-01-01'
    ORDER BY created_at
    LIMIT 1000
);

배치 단위로 나눠 삭제하면 트랜잭션을 작게 유지할 수 있는데, 이는 다른 커넥션이 동시에 데이터베이스를 읽고 있을 때 특히 중요합니다.

대량 SQLite DELETE는 트랜잭션으로 감싸기

DELETE 문은 기본적으로 트랜잭션 단위로 동작합니다. 즉, 조건에 맞는 행이 전부 삭제되거나 하나도 삭제되지 않거나 둘 중 하나죠. 하지만 한 번에 많은 행을 지울 때는 명시적으로 트랜잭션을 열어두는 편이 좋습니다. 결과가 이상하다 싶으면 ROLLBACK으로 되돌릴 수 있으니까요:

ROLLBACK을 실행하면 삭제 작업이 통째로 되돌아갑니다. 실제 작업 중이라면 삭제된 행 수가 예상과 맞을 때 COMMIT으로 마무리하면 됩니다. 또한 행을 한 건씩 여러 번 삭제할 때 트랜잭션으로 묶으면 속도가 비약적으로 빨라집니다. 반복문 전체를 BEGINCOMMIT으로 감싸면 매 DELETE마다 fsync가 호출되는 일을 막을 수 있거든요.

삭제처럼 보이지만 삭제가 아닌 것들

자주 혼동되는 부분 몇 가지를 짚고 넘어가겠습니다.

  • DELETE FROM table;은 테이블을 비울 뿐, 테이블 자체를 없애지는 않습니다. 테이블을 통째로 제거하려면 DROP TABLE table;을 써야 합니다.
  • DELETE는 데이터베이스 파일 크기를 줄여주지 않습니다. 비워진 페이지는 재사용 가능 상태로 표시될 뿐이죠. 디스크 공간을 실제로 회수하려면 VACUUM;을 실행해야 합니다 (성능 챕터에서 자세히 다룹니다).
  • 부모 행을 삭제해도 다른 테이블의 자식 행은 함께 삭제되지 않습니다. ON DELETE CASCADE가 설정되어 있고 동시에 외래 키 제약이 활성화된 경우에만 연쇄 삭제가 일어납니다.
  • 조건에 맞는 행이 하나도 없는 DELETE는 오류가 아닙니다. 정상적으로 실행된 문장이며 changes() = 0이 반환될 뿐입니다. 실제로 몇 건이 지워졌는지 알아야 한다면 행 수를 확인하세요.

다음 주제: UPSERT

사실 우리가 원하는 건 삭제가 아닐 때가 많습니다. 새로운 행이면 INSERT, 이미 있으면 UPDATE — 이런 동작이 필요하죠. SQLite에서는 이를 UPSERT라고 부르며, ON CONFLICT 절을 활용하면 세 단계로 나눌 일을 한 문장으로 끝낼 수 있습니다. 다음 장에서 살펴보겠습니다.

자주 묻는 질문

SQLite에서 행 하나를 어떻게 삭제하나요?

기본 형태는 DELETE FROM table_name WHERE condition; 입니다. WHERE 절이 어떤 행을 지울지 결정하죠. 예를 들어 DELETE FROM users WHERE id = 7;을 실행하면 id가 7인 사용자 한 명만 삭제됩니다. 만약 WHERE를 빠뜨리면 테이블의 모든 행이 날아가니 주의하세요.

SQLite 테이블의 모든 행을 삭제하려면 어떻게 하나요?

WHERE 절 없이 DELETE FROM table_name;만 실행하면 됩니다. SQLite에는 TRUNCATE 문이 따로 없는데, 조건 없는 DELETE가 그 역할을 대신하고 내부적으로도 최적화되어 처리됩니다(소위 'truncate optimization'). AUTOINCREMENT 카운터까지 초기화하고 싶다면 그 다음에 sqlite_sequence 테이블에서도 해당 항목을 지워주면 됩니다.

SQLite에서도 연쇄 삭제(CASCADE)가 가능한가요?

네, 외래 키에 ON DELETE CASCADE를 선언하고 PRAGMA foreign_keys = ON;으로 외래 키 기능을 켜두면 됩니다. SQLite는 외래 키가 기본적으로 꺼져 있다는 점이 함정입니다 — 이 PRAGMA를 빼먹으면 CASCADE 선언이 있어도 조용히 무시되니 꼭 확인하세요.

어떤 행이 삭제됐는지 어떻게 확인하나요?

RETURNING 절을 붙이면 됩니다. 예를 들어 DELETE FROM users WHERE active = 0 RETURNING id, email;처럼 쓰면 삭제된 행을 SELECT처럼 돌려받을 수 있어요. 로그를 남기거나, 되돌리기(undo) 기능을 만들거나, 의도한 대로 정확히 지워졌는지 확인할 때 아주 유용합니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기