INSERT 문으로 테이블에 행 추가하기
새로운 행을 테이블에 넣을 때 쓰는 명령이 바로 INSERT입니다. 문법은 짧고 일관돼서 한 번 익히면 헷갈릴 일이 거의 없습니다:
여기서 눈여겨봐야 할 부분은 세 가지입니다.
INSERT INTO books— 데이터를 넣을 대상 테이블입니다.(title, author, year)— 값을 넣을 컬럼 목록이고요.VALUES (...)— 컬럼 순서에 맞춰 넣을 실제 값입니다.
컬럼 목록에 id가 빠져 있죠? id는 INTEGER PRIMARY KEY라서 SQLite가 알아서 rowid를 채워 줍니다. 이렇게 생략한 컬럼은 기본값이 있다면 그 값으로, 없다면 NULL로 채워집니다.
컬럼 이름은 항상 명시하자
물론 컬럼 목록을 빼고, 선언된 순서대로 모든 컬럼의 값을 한 번에 넣을 수도 있습니다.
-- 動作するが、脆弱:
INSERT INTO books VALUES (NULL, 'Dune', 'Frank Herbert', 1965);
그렇게 하지 마세요. books 테이블에 컬럼이 하나라도 추가되는 순간, 위와 같은 구문은 전부 깨지거나 엉뚱한 컬럼에 값이 들어가기 시작합니다. 컬럼명을 명시적으로 적어주세요:
컬럼명을 명시적으로 적는 건 일종의 문서화이기도 합니다. 테이블 정의를 따로 찾아보지 않아도 INSERT 문만 보고 어떤 값이 어디에 들어가는지 바로 파악할 수 있죠.
SQLite 다중 행 INSERT
값 튜플을 쉼표로 이어서 나열하면, 한 번의 INSERT 문으로 여러 행을 한꺼번에 넣을 수 있습니다:
이렇게 쓰면 INSERT 문을 세 번 따로 쓰는 것보다 훨씬 깔끔하고, SQLite도 이걸 하나의 문장으로 처리합니다. 다만 대량 적재에서 진짜 성능 차이를 만드는 건 트랜잭션으로 묶는 것인데, 이건 바로 다음에서 살펴볼게요.
sqlite bulk insert는 트랜잭션으로 묶자
SQLite는 기본적으로 INSERT 하나하나가 각각 별도의 트랜잭션입니다. 매번 끝날 때마다 fsync를 호출하기 때문에, 단순 반복문으로 INSERT를 돌리면 느려지는 거예요. INSERT 자체가 느린 게 아닙니다.
이렇게 묶어주세요:
fsync를 다섯 번 부르던 걸 한 번으로 줄이는 거죠. 행이 수천 개쯤 되면 두세 자릿수의 성능 차이가 납니다. 중간에 뭐 하나라도 터지면 ROLLBACK이 배치 전체를 깔끔하게 되돌려 줍니다.
이게 바로 sqlite bulk insert 트랜잭션 패턴의 정석입니다. Python에서 부르든 Node나 Rust에서 부르든 상관없이, 반복문은 BEGIN / COMMIT으로 감싸 주세요.
INSERT ... SELECT: 다른 테이블에서 복사해 넣기
리터럴 값을 직접 넣는 대신, 쿼리 결과로 테이블을 채울 수도 있습니다:
SELECT 절에서 가져온 컬럼들은 INSERT의 컬럼 목록과 이름이 아니라 순서로 매칭됩니다. 컬럼명이 같을 필요는 없고, 순서만 맞으면 됩니다. 이 패턴은 행을 아카이브하거나, 리포팅용 테이블을 만들거나, 마이그레이션 중에 데이터 일부만 복사할 때 흔히 쓰는 표준적인 방법입니다.
DEFAULT VALUES와 생략한 컬럼 처리
DEFAULT 절이 정의된 컬럼이라면 컬럼 목록에서 빼도 됩니다. 그러면 SQLite가 알아서 기본값을 채워 넣습니다:
created_at에는 값을 넣지 않았기 때문에 현재 시각이 자동으로 채워집니다. 모든 컬럼을 기본값으로만 채워서 행을 만들고 싶다면 — 임시 자리표시용 행을 넣을 때 유용하죠 — DEFAULT VALUES 구문을 쓰면 됩니다:
두 개의 행이 새로 들어가는데, 둘 다 value = 0이고 id는 자동으로 할당됩니다.
INSERT OR IGNORE: 중복 무시 insert
UNIQUE 제약이나 PRIMARY KEY 제약을 위반하는 행을 넣으려고 하면, 기본 동작은 에러와 함께 해당 문장을 중단(abort)하는 것입니다:
Error: UNIQUE constraint failed: users.email
INSERT OR IGNORE를 쓰면 충돌이 발생한 행은 에러 없이 조용히 건너뜁니다:
세 행이 살아남고, 중복된 행은 에러 없이 그대로 버려집니다. 시드 데이터처럼 단순한 경우에 "없을 때만 insert" 동작을 표현하는 SQLite다운 방식이죠. 미리 SELECT로 확인할 필요도, 예외 처리를 따로 걸 필요도 없습니다.
INSERT OR REPLACE: 중복 행 덮어쓰기
INSERT OR REPLACE는 충돌이 발생한 기존 행을 지우고 그 자리에 새 행을 넣습니다:
한 가지 주의할 점이 있습니다. REPLACE는 UPDATE가 아니라 DELETE + INSERT로 동작합니다. 삭제되는 행을 참조하는 외래 키에 ON DELETE CASCADE가 걸려 있다면, 자식 행들까지 함께 삭제됩니다. 그리고 새 INSERT에서 명시하지 않은 컬럼은 기존 값을 유지하는 게 아니라 기본값으로 리셋됩니다.
"있으면 업데이트, 없으면 삽입" 같은 패턴이 필요하다면 사실 진짜 upsert 문법인 ON CONFLICT ... DO UPDATE를 쓰는 게 맞습니다. 이건 별도 페이지에서 다룹니다.
핵심 정리
INSERT INTO table (cols) VALUES (...)— 기본 형태입니다. 컬럼은 항상 명시하세요.- 다중 행 insert는
VALUES뒤에 튜플을 콤마로 나열합니다. - 본격적인 bulk insert를 할 때는
BEGIN/COMMIT으로 트랜잭션을 감싸세요. INSERT INTO ... SELECT ...로 쿼리 결과를 그대로 복사할 수 있습니다.DEFAULT VALUES는 기본값만으로 행을 만듭니다. 생략된 컬럼도 기본값으로 채워집니다.INSERT OR IGNORE는 충돌하는 행을 건너뛰고,INSERT OR REPLACE는 (삭제 후 삽입 방식으로) 덮어씁니다.
다음: UPDATE
행을 삽입하는 건 절반에 불과합니다. 나머지 절반은 이미 존재하는 행을 바꾸는 일이죠 — 카운터 증가, 오타 수정, 주문 상태를 배송 완료로 표시하는 것 같은 작업 말입니다. 이게 바로 UPDATE인데, 제대로 다루려면 알아둬야 할 습관들이 따로 있습니다(특히 WHERE 절). 다음 글에서 이어집니다.
자주 묻는 질문
SQLite에서 한 행은 어떻게 INSERT 하나요?
기본 형태는 INSERT INTO 테이블 (col1, col2) VALUES (val1, val2); 입니다. 컬럼 목록은 생략할 수 있지만 꼭 적어주는 걸 권장합니다. 나중에 테이블에 컬럼이 추가되더라도 기존 INSERT 문이 그대로 돌아가거든요. 컬럼 목록을 생략하면 테이블에 선언된 순서대로 모든 컬럼 값을 넘겨야 합니다.
여러 행을 한 번에 INSERT 하려면 어떻게 하나요?
VALUES 뒤에 괄호로 묶은 튜플을 콤마로 이어 붙이면 됩니다: INSERT INTO t (a, b) VALUES (1, 2), (3, 4), (5, 6);. 다만 수천 건 이상 대량 입력을 할 때는 BEGIN과 COMMIT으로 묶어 하나의 트랜잭션으로 처리하는 게 핵심입니다. 속도 차이는 다중 행 문법 자체보다 트랜잭션에서 나옵니다.
INSERT OR IGNORE는 어떤 동작을 하나요?
INSERT OR IGNORE는 UNIQUE, PRIMARY KEY, NOT NULL 제약을 위반하는 행이 있을 때 에러를 내지 않고 그 행만 조용히 건너뜁니다. 충돌한 행은 무시되고 나머지는 그대로 입력됩니다. "이미 있으면 그냥 두고, 없을 때만 넣기" 같은 동작을 별도의 존재 여부 체크 없이 구현하고 싶을 때 유용합니다.
INSERT 시 'UNIQUE constraint failed' 에러가 왜 발생하나요?
UNIQUE나 PRIMARY KEY로 지정된 컬럼에 이미 같은 값이 들어 있다는 뜻입니다. 실제로 중복된 값이거나, 시드(seed) 스크립트를 다시 돌려서 생기는 경우가 많죠. 중복을 그냥 건너뛰려면 INSERT OR IGNORE, 기존 행을 덮어쓰려면 INSERT OR REPLACE, 좀 더 세밀한 제어가 필요하면 ON CONFLICT ... DO UPDATE (upsert)를 쓰면 됩니다.