CSV 임포트는 SQL이 아니라 CLI에서 한다
SQLite의 SQL 문법에는 IMPORT 같은 구문이 없습니다. CSV 불러오기는 sqlite3 커맨드라인 셸이 제공하는 기능이고, 정확히는 .import라는 닷 커맨드(dot-command)죠. MySQL의 LOAD DATA INFILE이나 PostgreSQL의 COPY에 익숙하다면 이 점을 꼭 짚고 넘어가야 합니다. 그쪽은 서버가 직접 처리하지만, .import는 클라이언트 도구가 파일을 읽어서 내부적으로 INSERT 문을 날려주는 방식이거든요.
그래서 이 글의 내용은 모두 sqlite3 셸 안에서 실행한다는 전제로 진행합니다:
sqlite3 mydata.db
애플리케이션 코드(Python, Node, Go 등)에서 직접 임포트해야 한다면, 해당 언어로 CSV를 읽어 들인 뒤 파라미터 바인딩된 INSERT 문으로 처리하는 방식이 일반적입니다. 이 방법은 애플리케이션 연동 챕터에서 따로 다룰 예정이고, 여기서는 CLI에 집중하겠습니다.
기본 .import 명령어 사용법
가장 빠른 방법은 이렇습니다. SQLite에게 입력 형식이 CSV임을 알려준 다음, .import 명령어에 파일 경로와 테이블 이름만 넘겨주면 끝입니다.
.mode csv
.import people.csv people
people 테이블이 이미 있느냐 없느냐에 따라 동작이 두 가지로 갈립니다.
- 테이블이 없을 때 — SQLite가 알아서 테이블을 만들고, CSV의 첫 줄을 컬럼명으로 사용합니다. 모든 컬럼은
TEXT친화도(affinity)로 잡힙니다. - 테이블이 이미 있을 때 — 파일의 모든 행이 그대로 데이터로 들어갑니다. 헤더가 있다면 헤더까지 한 줄의 데이터로 삽입돼 버리죠.
처음 .import 명령어를 써보는 분들이 가장 많이 걸려 넘어지는 부분이 바로 두 번째 경우입니다. CSV에 헤더가 있고 테이블이 이미 존재한다면, 헤더를 명시적으로 건너뛰어야 합니다.
기존 테이블에 CSV 추가할 때 헤더 건너뛰기
.import에게 첫 N줄을 무시하라고 알려주려면 --skip 1 옵션을 쓰면 됩니다:
CREATE TABLE people (
name TEXT,
age INTEGER,
city TEXT
);
.import --csv --skip 1 people.csv people
--csv는 이번 명령에만 적용되는 .mode csv의 단축 표기예요. 따로 모드를 설정하지 않아도 됩니다. --skip 1은 헤더 줄을 건너뛰고요. 나머지 행은 컬럼 순서대로 people 테이블에 들어갑니다.
임포트가 잘 됐는지 바로 확인해 볼까요:
SELECT count(*) FROM people;
SELECT * FROM people LIMIT 5;
파일의 컬럼 순서가 테이블의 컬럼 순서와 정확히 일치해야 합니다. 헤더 이름을 보고 알아서 매핑해 주는 기능 같은 건 없습니다. .import는 그냥 N번째 필드를 N번째 컬럼에 그대로 꽂아 넣을 뿐입니다.
SQLite가 테이블을 자동으로 만들게 하기
데이터를 가볍게 살펴보는 단계라면, CREATE TABLE을 아예 생략하고 .import 명령어가 헤더를 기반으로 테이블을 만들도록 두는 게 가장 편합니다:
.mode csv
.import sales.csv sales
.schema sales
.schema sales를 실행하면 대략 다음과 같은 결과가 나옵니다:
CREATE TABLE sales(
"order_id" TEXT,
"amount" TEXT,
"ordered_at" TEXT
);
모든 컬럼이 TEXT로 잡혀 있다는 점에 주목하세요. 이건 의도된 동작입니다 — .import 명령어는 타입을 추론하지 않거든요. amount를 실제 숫자로, ordered_at을 제대로 된 타임스탬프로 다루고 싶다면, 먼저 원하는 타입으로 테이블을 직접 만든 다음 --skip 1 옵션으로 임포트하면 됩니다. 그러면 SQLite의 타입 친화성(type affinity)이 알아서 숫자 문자열을 정수나 실수로 변환해 INSERT 해줍니다.
구분자 바꾸기: TSV, 파이프, 세미콜론
.mode csv는 콤마를 기준으로 동작합니다. 탭으로 구분된 파일을 다루려면 모드를 바꿔주면 됩니다:
.mode tabs
.import data.tsv events
다른 구분자를 사용하려면 모드를 먼저 지정한 뒤에 .separator를 적용하면 됩니다:
.mode csv
.separator "|"
.import pipe_data.txt events
한 가지 알아둘 점이 있습니다. .mode csv는 RFC 4180 따옴표 규칙을 따르기 때문에, 필드 안에 쉼표나 줄바꿈이 들어 있어도 "로 제대로 감싸져 있으면 문제없이 처리됩니다. 반면 .mode tabs는 단순히 특정 문자로 잘라내는 모드라서 따옴표 처리가 없습니다. 따옴표 안에 구분자가 포함된 필드가 있다면 .mode csv를 유지한 채 구분자만 바꿔주는 게 좋습니다.
실전 예제로 따라해 보기
orders.csv 파일이 다음과 같다고 가정해 봅시다:
order_id,customer,amount,ordered_at
1001,Ada,49.99,2026-01-12
1002,Boris,12.50,2026-01-13
1003,"Chen, Wei",199.00,2026-01-14
3행을 보면 따옴표로 감싼 필드 안에 쉼표가 들어 있다는 점을 눈여겨보세요. 전체 실행 흐름은 다음과 같습니다.
실제 셸에서는 위의 INSERT 블록을 .import --csv --skip 1 orders.csv orders 한 줄로 대체할 수 있습니다. CSV 모드는 따옴표를 그대로 인식하기 때문에 "Chen, Wei" 필드도 손상 없이 들어갑니다. 컬럼 타입을 미리 지정해 둔 덕분에 amount는 실수, order_id는 정수로 정확하게 저장됩니다.
트랜잭션으로 감싸서 CSV 임포트 속도 올리기
.import 명령어는 행 하나마다 INSERT를 하나씩 실행합니다. 수천 건 정도라면 별 문제 없지만, 백만 건쯤 되면 매 행마다 커밋이 일어나는 탓에 속도가 견디기 힘들 만큼 느려집니다. 이럴 때는 임포트 작업 전체를 트랜잭션으로 묶어 주는 게 정석입니다:
BEGIN;
.import --csv --skip 1 big_file.csv events
COMMIT;
이 한 줄만 바꿔도 몇 분 걸리던 임포트가 몇 초로 줄어듭니다. 도중에 뭔가 실패해도 ROLLBACK 한 방으로 부분 로드를 되돌릴 수 있어서 재시도할 때도 편합니다.
여기서 더 빠르게 하고 싶다면 임포트 전에 인덱스를 모두 제거했다가 끝나고 다시 만들어 주세요. 행마다 인덱스를 갱신하는 비용이 의외로 크게 누적됩니다.
자주 만나는 에러와 해결법
Error: expected N columns but found M — 어떤 행의 필드 수가 테이블과 맞지 않는다는 뜻입니다. 흔한 원인은 다음과 같습니다.
- 따옴표로 감싸지 않은 필드 안에 쉼표가 끼어 있는 경우. CSV 따옴표 처리를 제대로 적용해서 다시 내보내거나,
.mode tabs대신.mode csv(RFC 4180)로 바꿔 보세요. - 파일 끝에 빈 줄이 남아 있는 경우. 파일을 직접 수정하거나
--skip을 활용해 보세요. - 테이블 컬럼 수가 CSV보다 많은 경우. 부족한 컬럼을 파일에 추가하거나, 모양이 맞는 스테이징 테이블에 먼저 넣은 뒤 실제 테이블로 옮기면 됩니다.
헤더 행이 데이터로 들어간 경우 — 기존 테이블에 임포트하면서 --skip 1을 깜빡한 상황입니다. 해당 행을 지우고(DELETE FROM t WHERE rowid = 1) 옵션을 붙여 다시 실행하세요.
숫자가 문자열로 저장되는 경우 — .import가 테이블을 자동 생성하도록 두면 모든 컬럼이 TEXT로 만들어집니다. 테이블을 삭제한 뒤 INTEGER/REAL 같은 타입을 명시해서 직접 만들고 다시 임포트하세요.
Error: no such file — 경로는 데이터베이스 파일 기준이 아니라 sqlite3를 실행한 디렉터리 기준입니다. 절대 경로를 쓰거나, 셸을 열기 전에 cd로 해당 디렉터리로 이동하세요.
CLI는 에러 발생 시 라인 번호를 같이 출력해 주기 때문에, 큰 파일에서 문제 행을 찾을 때 이만한 단서가 없습니다.
핵심 정리
.import는 SQL이 아니라 CLI 닷 명령어입니다. 반드시sqlite3셸 안에서 실행하세요.- 따옴표 처리는
--csv로, 헤더 행은--skip 1로 건너뜁니다. - 테이블이 없으면
.import가 헤더를 보고 자동 생성하지만, 모든 컬럼이TEXT가 됩니다. 타입을 제대로 잡으려면 테이블은 직접 만드세요. - 대용량 임포트는
BEGIN/COMMIT으로 감싸서 행마다 트랜잭션이 생기지 않게 하세요. - 파일의 컬럼 순서는 테이블의 컬럼 순서와 정확히 일치해야 합니다.
다음 편: 데이터 내보내기
임포트는 절반에 불과합니다. 같은 셸에서 쿼리 결과나 테이블 전체를 CSV, JSON, SQL 형식으로 다시 뽑아낼 수도 있는데, 백업이나 데이터 파이프라인, 다른 도구로 데이터를 넘길 때 두루 쓰입니다. 이 내용은 다음 데이터 내보내기 편에서 이어집니다.
자주 묻는 질문
SQLite에 CSV 파일을 어떻게 임포트하나요?
sqlite3 CLI로 DB를 열고 .mode csv로 모드를 바꾼 다음 .import data.csv table_name을 실행하면 됩니다. 테이블이 없으면 SQLite가 CSV 첫 줄을 컬럼명으로 써서 새로 만들어 주고, 이미 있으면 모든 줄을 데이터로 그대로 INSERT 합니다. 그래서 기존 테이블에 넣을 때는 보통 --skip 1로 헤더 줄을 건너뛰는 게 정석이에요.
헤더가 있는 CSV를 기존 테이블에 임포트하려면 어떻게 하나요?
.import --csv --skip 1 data.csv table_name 형태로 실행하면 됩니다. --skip 1 옵션이 첫 줄(헤더)을 무시하라는 뜻이에요. 이 옵션을 빼먹으면 컬럼명이 그대로 한 행의 데이터로 들어가 버려서, 나중에 숫자 컬럼에 문자열이 섞이는 등 골치 아픈 상황이 생깁니다.
임포트할 때 'expected N columns but found M' 에러가 나는 이유는?
파일의 어떤 행이 테이블 컬럼 수와 안 맞을 때 나오는 에러입니다. 보통 값 안에 들어 있는 콤마, 따옴표가 제대로 escape 되지 않았거나, 파일 끝에 빈 줄이 남아 있을 때 발생해요. .mode tabs 대신 .mode csv(또는 --csv)를 쓰면 RFC 4180 규칙대로 따옴표 처리를 해 주니 우선 모드부터 확인하세요. CLI가 문제 행 번호를 같이 출력해 주니 그 줄을 텍스트 에디터로 열어 보는 게 가장 빠릅니다.