코딩 에이전트가 방금 작업을 마쳤다. 마이그레이션을 작성해 실행하고, dev.db에 픽스처를 시딩했다. 요약에는 “orders.status 컬럼을 기본값 'pending'으로 추가하고 3,200개 행을 백필했다”라고 적혀 있다. 믿기 전에 직접 눈으로 확인하고 싶어진다. ORM이 직접 작성하지 않은 마이그레이션을 생성했을 때도, 모바일 앱이 테스트 기기에서 로컬 데이터베이스에 데이터를 쓰고 화면이 왜 비어 있는지 알아보려고 파일을 꺼냈을 때도 마찬가지다. velog나 Tistory에 트러블슈팅 기록을 남기는 개발자들 사이에서도 낯설지 않은 장면이다.
가장 손쉬운 방법은 행 데이터를 AI 채팅에 붙여넣고 무엇이 잘못됐는지 물어보는 것이다. 하지만 실제 데이터베이스 대부분에서는 그렇게 해서는 안 된다. 스테이징에서 복사한 dev.db에는 고객 이메일이 들어 있다. 모바일 앱 데이터베이스에는 세션 토큰이 들어 있다. CLI의 상태 파일에는 API 키가 들어 있다. 파일은 이미 있는 그 기기에서, 사람이 직접 읽어야 한다.
뷰어는 브라우저 탭 안에서 파일을 연다. 테이블, 뷰, 컬럼, 인덱스, CREATE 문을 보여주고, 행을 페이지 단위로 넘기고, 입력한 SQL을 실행하고, CSV로 내보낸다. 어떤 것도 업로드되지 않으며, 이 도구 페이지는 분석이나 광고 스크립트를 로드하지 않는다. 이 가이드의 나머지 부분에서는 SQLite 파일 안에 무엇이 들어 있는지, 뷰어가 이를 어떻게 읽는지, 그리고 파일이 예상과 다르게 보이는 경우를 설명한다.
브라우저 기반 SQLite 뷰어가 적합한 상황
| 상황 | 확인하는 것 | 브라우저 뷰어가 적합한 이유 |
|---|---|---|
| 에이전트나 스크립트가 마이그레이션을 실행했을 때 | 새 컬럼, 기본값, 백필된 값, 새 인덱스 | 파일을 열고 구조 패널을 확인한 뒤 SELECT 한 번 실행하고 탭을 닫으면 끝난다 |
| ORM 마이그레이션 검토 | SQLite가 실제로 저장한 CREATE TABLE 문(모델과 다를 수 있음) | 뷰어가 저장된 CREATE 문을 그대로 보여준다 |
| 모바일 앱 로컬 데이터베이스 | 앱이 테스트 기기에서 기록한 행 | 데스크톱 클라이언트를 설치할 필요가 없고, 파일은 내 기기에 그대로 남는다 |
| Electron 앱이나 CLI 상태 | 설정, 캐시, 큐 테이블 | 많은 데스크톱 앱과 CLI가 상태를 하나의 SQLite 파일에 보관한다 |
| 테스트 픽스처 확인 | 저장소에 커밋된 픽스처 데이터베이스 | 픽스처가 테스트가 가정하는 내용을 담고 있는지 확인한다 |
| 동료에게 데이터를 전달할 때 | 데이터베이스 전체가 아닌 쿼리 결과 하나 | 쿼리를 실행하고 CSV로 내보내 그 CSV를 전달한다 |
| 실제로 실행하기 전에 문장을 시험해볼 때 | UPDATE나 DELETE의 효과 | SQL은 메모리 사본에서 실행되며, 디스크의 파일은 변경되지 않는다 |
파일을 제자리에서 편집하거나, 매우 큰 파일을 다루거나, 암호화된 데이터베이스를 열 때는 로컬 도구를 사용한다. 아래 절에서는 그 이유와 각 경우에 맞는 도구를 설명한다.
SQLite 파일 안에는 무엇이 들어 있나
SQLite 데이터베이스는 하나의 평범한 파일이다. 형식은 SQLite Database File Format 페이지에 전부 문서화돼 있으며, 그중 몇 가지 사실만으로도 뷰어가 하는 일 대부분을 설명할 수 있다.
100바이트 헤더
모든 데이터베이스 파일의 첫 100바이트는 파일 헤더다. 그중 첫 16바이트는 고정돼 있다.
53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00
S Q L i t e f o r m a t 3 \0
이는 UTF-8 문자열 SQLite format 3 뒤에 nul 바이트가 붙은 형태다. 이 16바이트로 시작하지 않는 파일은 일반 SQLite 3 데이터베이스가 아니다. 파일 확장자는 아무런 근거가 되지 않는다. .db, .sqlite, .sqlite3, .db3는 모두 흔히 쓰이며, .db 파일 중에도 실제로는 다른 형식인 경우가 많다.
파일이 이상하게 동작할 때 유용한 다른 헤더 필드는 다음과 같다.
| 오프셋 | 크기 | 필드 |
|---|---|---|
| 16 | 2바이트 | 페이지 크기(빅엔디안) |
| 18 | 1바이트 | 파일 형식 쓰기 버전 |
| 19 | 1바이트 | 파일 형식 읽기 버전 |
| 40 | 4바이트 | 스키마 쿠키(스키마가 변경될 때마다 증가) |
| 56 | 4바이트 | 텍스트 인코딩 |
| 60 | 4바이트 | 사용자 버전(앱에서 마이그레이션 카운터로 자주 사용) |
| 68 | 4바이트 | 애플리케이션 ID |
| 96 | 4바이트 | 파일을 마지막으로 기록한 SQLite 라이브러리의 버전 번호 |
페이지 크기는 2의 거듭제곱이다. 3.7.0.1까지의 버전은 512바이트에서 32,768바이트까지 허용했다. SQLite 3.7.1(2010)에서 65,536바이트 페이지가 추가됐다. 65,536은 2바이트에 담을 수 없으므로 값 1로 저장된다. 18번, 19번 바이트는 롤백 저널 데이터베이스에서는 둘 다 1이고 WAL 데이터베이스에서는 둘 다 2이므로, 파일을 열지 않고도 저널 모드를 알 수 있다.
스키마 테이블
파일의 1번 페이지는 sqlite_schema라는 테이블의 루트다. 오래된 코드와 대부분의 튜토리얼은 이를 sqlite_master라고 부르는데, 이는 지금도 유효한 별칭이다. 이 테이블에는 type, name, tbl_name, rootpage, sql이라는 다섯 개 컬럼이 있다. 모든 테이블, 인덱스, 뷰, 트리거는 각각 한 행을 가지며, sql 컬럼에는 원본 CREATE 문 텍스트가 담긴다.
sqlite_로 시작하는 이름은 SQLite가 자체적으로 만드는 내부 객체다. AUTOINCREMENT 카운터를 위한 sqlite_sequence나 쿼리 플래너 통계를 위한 sqlite_stat1이 그 예다. SQLite는 애플리케이션이 이 접두어로 객체를 만드는 것을 허용하지 않는다.
뷰어가 파일을 읽는 방식
파일을 드롭한 순간부터 테이블 목록이 뜨기까지 일어나는 일을 순서대로 정리하면 다음과 같다.
1. 크기 확인. 100MB를 넘는 파일은 바이트를 읽기 전에 거부된다. 이유는 메모리다. 파일은 디스크에서 읽은 버퍼로 한 번, SQLite 엔진 내부에서 또 한 번 보관된다. 큰 데이터베이스가 두 벌 존재하면 브라우저 탭이 불안정해진다.
2. 헤더 확인. 뷰어는 File API로 첫 100바이트만 읽고, 그중 첫 16바이트를 SQLite format 3\0과 비교한다. 파일이 100바이트보다 짧거나 바이트가 다르면 “Not a SQLite 3 database” 오류가 뜬다. 파일 확장자는 절대 신뢰하지 않는다.
3. 엔진 로드. 헤더 확인을 통과한 뒤에야 페이지가 SQLite 엔진을 가져온다. 엔진은 sql.js 1.14.2로, SQLite 3.49.1을 WebAssembly로 컴파일한 것이다. 두 파일을 합쳐 압축 후 약 340KB이며, 브라우저는 이를 한 번만 가져온다. 잘못된 파일이나 PNG, CSV는 이 다운로드를 절대 유발하지 않는다.
4. 메모리 상에서 열기. 파일 전체를 Uint8Array로 읽어 new SQL.Database(bytes)에 전달한다. sql.js는 이 데이터베이스를 가상의 메모리 파일 시스템에 보관한다. 이 시점부터 모든 쿼리는 이 사본을 대상으로 실행된다.
5. 객체 목록. 뷰어는 sqlite_master를 조회해 테이블과 뷰를 가져온다. 내부 sqlite_% 객체는 목록에서 숨겨지지만 SQL 에디터에서는 여전히 조회할 수 있다. 각 테이블에는 COUNT(*)가 표시된다. 뷰는 개수 없이 목록에 나타난다.
6. 구조. 테이블이나 뷰를 선택하면 뷰어는 테이블 값 프라그마 함수를 실행한다.
SELECT name, type, "notnull", dflt_value, pk FROM pragma_table_info(?);
SELECT name, "unique", origin FROM pragma_index_list(?);
SELECT name FROM pragma_index_info(?) ORDER BY seqno;
이 프라그마의 테이블 값 형태는 SQLite 3.16.0(2017)부터 존재한다. 구조 패널은 각 컬럼의 이름, 선언된 타입, NOT NULL 여부, 기본값, 기본 키 위치를 보여준다. 인덱스 패널은 이름, 컬럼, 고유성, origin을 보여준다. origin은 CREATE INDEX로 만든 인덱스는 c, UNIQUE 제약으로 만든 인덱스는 u, PRIMARY KEY 제약으로 만든 인덱스는 pk다. 인덱스 항목이 rowid이거나 표현식일 때 pragma_index_info는 컬럼 이름으로 NULL을 반환하며, 뷰어는 이런 항목을 (expr)로 표시한다. 인덱스 아래에는 스키마 테이블에 저장된 그대로의 CREATE 문이 나온다.
7. 행. 데이터 그리드는 LIMIT 100 OFFSET n으로 테이블을 100행씩 페이지 단위로 넘긴다. 범위 표시줄에는 “Rows 101–200 of 3,200”처럼 나타나 항상 현재 위치를 알 수 있다.
뷰어가 SQL에 넣는 모든 테이블명과 컬럼명은 큰따옴표로 감싸지며, 이름 안에 큰따옴표가 있으면 두 번 반복해 이스케이프한다. order, user data, "quoted"처럼 이름이 붙은 테이블이나 비라틴 문자로 된 이름도 정상적으로 열린다.
”업로드 없음”이 의미하는 것
파일은 File API를 통해 디스크에서 페이지의 메모리로 전달된다. 도구가 보내는 유일한 네트워크 요청은 엔진 파일용이며, 이들은 같은 사이트의 정적 파일이다. 이 도구 페이지는 사이트의 애널리틱스와 광고 스크립트를 건너뛰도록 설정되어 있는데, 입력이 비공개 데이터베이스이기 때문이다. 탭을 닫거나 새로고침하면 메모리 사본은 사라진다.
SQLite 값은 컬럼이 말하는 것과 다르다
SQLite 파일을 읽을 때 생기는 혼란은 대부분 타입 시스템에서 비롯된다. Datatypes In SQLite 페이지가 이를 설명한다. 요약하면 다음과 같다.
- 값은 다섯 가지 저장 클래스 중 하나를 가진다:
NULL,INTEGER,REAL,TEXT,BLOB. - SQLite는 동적 타이핑을 사용한다: 타입은 컬럼이 아니라 값에 속한다.
- 선언된 컬럼 타입은 어피니티(
TEXT,NUMERIC,INTEGER,REAL,BLOB)만 지정하며, SQLite는 가능한 경우 삽입 시 이를 이용해 값을 변환한다.
어피니티는 선언된 타입 이름에 부분 문자열 규칙을 적용해 결정된다. 타입 이름에 INT가 포함되면 INTEGER 어피니티가 된다. CHAR, CLOB, TEXT가 포함되면 TEXT 어피니티가 되므로, VARCHAR(255)는 TEXT이고 255는 무시된다. BLOB이거나 타입이 아예 없으면 BLOB 어피니티가 된다. REAL, FLOA, DOUB이 포함되면 REAL이 된다. 그 외는 모두 NUMERIC이 된다.
실제로는 INTEGER로 선언된 컬럼에도 무언가가 기록했다면 문자열 'n/a'가 들어갈 수 있다는 뜻이다. created_at DATETIME으로 선언한 ORM이 어떤 행에는 ISO-8601 텍스트를, 다른 행에는 유닉스 타임스탬프를 저장할 수도 있다. SQLite에는 날짜 타입도 불리언 타입도 없다. 날짜는 TEXT, REAL(율리우스일) 또는 INTEGER(유닉스 시간)로 저장되고, 불리언은 정수 0과 1로 저장된다.
뷰어는 선언된 컬럼 타입이 아니라 실제로 돌아온 값을 기준으로 각 셀을 렌더링한다:
| 값 | 그리드에서 | CSV 내보내기에서 |
|---|---|---|
NULL | 흐리게 표시된 NULL 마커 | 빈 필드 |
빈 문자열 '' | 빈 셀 | 빈 필드 |
| 숫자(INTEGER 또는 REAL) | 오른쪽 정렬 | 저장된 그대로 |
| 텍스트 | 텍스트로 표시. 긴 텍스트는 셀 안에서 잘림 | 전체 값, 필요 시 따옴표로 묶음 |
| BLOB | BLOB · 4 B · 89504E47(바이트 길이와 처음 16바이트를 16진수로 표시) | 전체 내용을 대문자 16진수로 |
컬럼이 이상해 보이면 SQLite에 직접 무엇이 들어 있는지 물어본다:
SELECT typeof(created_at) AS storage_class, COUNT(*)
FROM orders
GROUP BY 1;
이 쿼리가 text와 integer를 모두 반환한다면, 서로 다른 두 코드 경로가 이 컬럼을 다른 형식으로 기록하고 있다는 뜻이다. SQLite 3.37.0(2021)은 STRICT 테이블을 추가했다. 이 테이블은 컬럼 타입으로 INT, INTEGER, REAL, TEXT, BLOB, ANY만 허용하고 타입이 맞지 않는 값을 거부한다. 저장된 CREATE 문이 STRICT로 끝난다면 해당 테이블에서는 타입 혼재 문제가 발생할 수 없다.
메모리 사본에 SQL 실행하기
데이터 그리드 아래의 에디터는 SQLite 3.49.1이 이해하는 모든 SQL을 받아들인다. Run SQL을 누르거나 Ctrl/Cmd + Enter를 누른다.
- 여러 문을 한 번에. 에디터에 있는 모든 문이 순서대로 실행된다. 결과 그리드는
SELECT나UPDATE ... RETURNING처럼 컬럼을 반환하는 마지막 문을 보여준다. - 오류는 SQLite에서 온다. 오타가 있으면
near "SELEC": syntax error같은 SQLite 자체 메시지가 상태 줄에 나타난다. 이전 결과는 지워지므로 새 결과와 혼동할 일이 없다. - 데이터를 변경하는 문은 “Done. N row(s) changed in the in-memory copy.”를 표시한다. 이 개수는 실행 전후의
total_changes()로 계산된다. - 스키마 변경도 반영된다. 실행 결과 데이터나 스키마 버전이 바뀌면 테이블 목록이 다시 로드된다. 방금 실행한
CREATE TABLE이 목록에 나타나고,INSERT나DELETE이후에는 행 개수가 갱신된다. - 큰 결과. 그리드는 페이지가 반응성을 유지하도록 처음 1,000행만 렌더링하며, 그렇다는 사실도 표시한다. Export CSV는 항상 결과의 모든 행을 기록한다.
낯선 데이터베이스에서 유용한 쿼리 몇 가지:
-- Every object and its CREATE statement, including indexes and triggers
SELECT type, name, tbl_name, sql FROM sqlite_master ORDER BY type, name;
-- Migration counter many apps keep in the header
PRAGMA user_version;
-- Foreign keys declared on a table
SELECT * FROM pragma_foreign_key_list('orders');
-- Check the file for corruption
PRAGMA integrity_check;
쓰기는 메모리에만 남는다
브라우저에는 선택한 파일에 대한 쓰기 권한이 없다. 엔진은 메모리 안의 사본에서 작동하므로 INSERT, UPDATE, DELETE, CREATE, DROP이 모두 실행되고 뷰어에 그 결과가 반영되지만, 디스크의 파일은 그대로 남는다. 페이지를 새로고침하거나 다른 파일을 열면 변경 사항은 사라진다. 이 도구에는 수정된 사본을 다운로드하는 옵션이 없다.
덕분에 에디터는 실제로 실행하기 전에 문을 시험해볼 수 있는 안전한 공간이 된다. 예를 들어 정리 작업이 몇 개의 행에 영향을 미치는지 먼저 확인한 다음, 남는 데이터를 살펴볼 수 있다:
DELETE FROM sessions WHERE expires_at < unixepoch();
SELECT COUNT(*) AS remaining FROM sessions;
캐스케이드 삭제를 테스트할 때 중요한 세부 사항이 하나 있다. 외래 키 강제는 애플리케이션이 켜지지 않는 한 새 SQLite 연결에서 기본적으로 꺼져 있으며, 뷰어에서도 마찬가지다. 테스트에서 ON DELETE CASCADE가 작동하게 하려면 먼저 PRAGMA foreign_keys = ON;을 실행한다.
실제 파일을 변경하려면 자신의 컴퓨터에서 sqlite3 커맨드라인 셸이나 DB Browser for SQLite를 사용한다.
CSV 내보내기
Export CSV 버튼은 두 개다. 데이터 그리드 위에 있는 버튼은 선택한 테이블이나 뷰 전체를 내보내며, 현재 페이지만이 아니라 모든 행을 포함한다. SQL 에디터 아래에 있는 버튼은 마지막 쿼리의 전체 결과를 내보낸다.
출력은 RFC 4180을 따른다: 쉼표 구분자, CRLF 줄바꿈, 컬럼 이름이 담긴 헤더 행, 그리고 콤마·큰따옴표·CR·LF가 포함된 필드는 "로 묶는다. 필드 안의 큰따옴표는 두 번 반복해 표시한다. 파일 이름은 테이블 이름에서 가져오며, 문자·숫자·.·_·-를 제외한 문자는 _로 바뀐다. 쿼리 결과의 경우 파일 이름은 query.csv가 된다.
CSV라는 포맷이 가진 두 가지 결과:
NULL과 빈 문자열은 둘 다 빈 필드가 된다. 차이가 중요하다면 내보내기 전에 쿼리에서COALESCE(col, '<null>')나col IS NULL AS col_is_null을 선택한다.- BLOB은 대문자 16진수가 된다. 4바이트 PNG 시그니처는
89504E47로 내보내진다. 이렇게 하면 CSV가 유효한 텍스트로 유지되며, 대부분의 언어에서 Python의bytes.fromhex()처럼 한 번의 호출로 다시 디코딩할 수 있다.
동료에게 데이터를 건넬 때는 필요한 쿼리 결과만 내보낸다. 데이터베이스 파일은 그대로 자신에게 남는다.
함정과 예외 상황
최근 행이 보이지 않는 경우: WAL 파일
가장 흔히 겪는 놀라움이다. Write-Ahead Logging 페이지에서 설명하는 WAL 모드에서는, SQLite가 커밋된 변경 사항을 메인 데이터베이스 파일에 즉시 기록하지 않는다. 대신 데이터베이스 이름에 -wal 접미사가 붙은 별도 파일에 이를 추가하며, -shm 인덱스 파일도 함께 둔다. 체크포인트는 WAL의 트랜잭션을 메인 파일로 다시 옮긴다. 기본적으로 SQLite는 WAL이 1,000페이지에 도달하면 자동으로 체크포인트를 수행하며, 마지막 연결이 닫히면 WAL은 보통 삭제된다.
따라서 실행 중인 앱에서 app.db를 복사하거나, 앱이 열려 있는 동안 휴대폰에서 파일을 가져오면 가장 최근의 트랜잭션이 여전히 app.db-wal에 남아 있을 수 있다. 뷰어는 메인 파일만 읽으므로 이런 행은 보이지 않는다. SQLite 공식 문서도 데이터베이스 파일을 WAL에서 분리하면 커밋된 트랜잭션이 유실되거나 데이터베이스가 손상될 수 있다고 경고한다.
헤더의 18, 19번째 바이트를 보면 파일이 WAL을 사용하는지 알 수 있다(둘 다 2이면 사용 중이다). WAL을 메인 파일에 병합하려면, 파일을 기록하는 애플리케이션을 종료한 다음 다음을 실행한다:
sqlite3 app.db "PRAGMA wal_checkpoint(TRUNCATE);"
TRUNCATE는 모든 프레임을 체크포인트한 다음 WAL 파일을 0바이트로 자른다. 이후 app.db를 다시 연다. 롤백 모드 데이터베이스에 남은 -journal 파일에도 같은 원리가 적용된다: 뷰어는 메인 파일만 읽으므로, 먼저 소유 애플리케이션이나 sqlite3 셸이 데이터베이스를 열도록 한다.
분명히 데이터베이스인 파일에서 “Not a SQLite 3 database” 오류가 나는 경우
암호화된 데이터베이스는 이 오류를 낸다. SQLCipher는 처음 16바이트에 무작위 salt를 저장하고 나머지를 암호화하므로, 파일 전체가 무작위 데이터처럼 보이고 SQLite format 3 헤더가 사라진다. SQLite Encryption Extension(SEE)도 파일을 암호화한다. 뷰어는 암호화된 데이터베이스를 열지 않는다. sqlcipher 셸로 복호화하거나, SQLCipher 파일을 지원하는 DB Browser for SQLite에서 파일을 열어야 한다.
.db 확장자를 달았을 뿐 실제로는 다른 형식인 파일, 그리고 잘려나간 사본에서도 같은 오류가 나타난다. head -c 16 app.db | xxd로 앞부분 바이트를 확인한다.
파일이 100MB보다 큰 경우
이 제한은 고정값이다. 파일을 여는 동안 데이터베이스를 메모리에 두 번 올려두기 때문이다. 더 큰 파일은 로컬에서 sqlite3 app.db를 실행하거나, 먼저 더 작은 추출본을 만든다.
sqlite3 big.db "ATTACH 'small.db' AS s; CREATE TABLE s.orders AS SELECT * FROM orders WHERE created_at >= '2026-09-01';"
그다음 small.db를 뷰어에서 연다.
테이블에 행 대신 오류가 표시되는 경우
일부 테이블은 SQLite 코어에 포함되지 않은 코드를 필요로 한다. SpatiaLite geometry 테이블, sqlite-vec 벡터 테이블, FTS5 전문 검색 인덱스, R-Tree 공간 인덱스 같은 가상 테이블은 그 테이블을 만든 애플리케이션에 컴파일되어 들어간 모듈에 의존한다. 뷰어가 사용하는 sql.js 1.14.2 빌드에는 fts5나 rtree 모듈이 포함되어 있지 않아서, 이런 테이블을 쿼리하면 SQLite 자체의 오류(예: no such module: fts5)가 그대로 나온다.
이런 테이블의 행 수 조회가 실패하면 목록에는 —가 표시되고 데이터 창에는 오류가 뜬다. 나머지 데이터베이스는 계속 탐색할 수 있다. FTS5는 데이터를 일반 “shadow” 테이블(예: notes_fts_content)에도 저장하는데, 이런 테이블은 평범한 테이블이라 읽을 수 있다.
아무것도 일치하지 않는 쿼리
일치하는 행이 0개인 SELECT도 열 헤더는 그대로 표시하고, 상태줄에는 “0 row(s)“라고 나온다. 즉 쿼리는 실행되었고 열 이름도 맞다는 뜻이므로, 살펴봐야 할 곳은 WHERE 절이다. SELECT COUNT(*) ... WHERE ...는 항상 한 행을 반환하므로 답을 명확하게 보여준다.
긴 텍스트와 넓은 테이블
셀 너비는 제한되어 있고, 긴 텍스트는 말줄임표로 잘린다. 60자가 넘는 텍스트는 마우스를 올리면 툴팁에서 최대 2,000자까지 볼 수 있다. 컬럼에 저장된 JSON 문서 전체를 읽으려면 해당 값만 선택해 결과를 CSV로 내보내거나, 쿼리에서 json_extract()로 필요한 부분만 뽑아낸다.
운영 중인 데이터베이스를 안전하게 복사하기
애플리케이션이 파일에 쓰는 동안 cp로 복사하면 파일이 찢어진(torn) 사본이 나올 수 있다. SQLite 3.15.0부터 쓸 수 있는 VACUUM INTO는 트랜잭션적으로 일관된 스냅샷을 새 파일에 기록하고 원본은 건드리지 않는다. 이 스냅샷은 WAL에 커밋된 내용까지 포함하는 단일 파일이다.
sqlite3 app.db "VACUUM INTO 'snapshot.db'"
코드 예제
Python: 헤더를 확인한 다음 읽기 전용으로 행 수 세기
표준 라이브러리 sqlite3 모듈은 URI를 통해 파일을 읽기 전용으로 열 수 있다. 이 스크립트는 뷰어와 같은 방식으로 헤더를 확인하고, 페이지 크기와 저널 모드를 출력한 뒤 행 수를 나열한다.
import sqlite3
import sys
path = sys.argv[1]
with open(path, "rb") as f:
header = f.read(100)
if len(header) < 100 or header[:16] != b"SQLite format 3\x00":
sys.exit(f"{path}: no SQLite 3 header (encrypted, truncated, or not a database)")
page_size = int.from_bytes(header[16:18], "big")
if page_size == 1:
page_size = 65536 # 1 is the magic value for 64 KiB pages
journal = "WAL" if header[18] == 2 else "rollback"
print(f"page size: {page_size} journal mode: {journal}")
con = sqlite3.connect(f"file:{path}?mode=ro", uri=True) # read-only
tables = con.execute(
"SELECT name FROM sqlite_master WHERE type = 'table' "
"AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' ORDER BY name"
).fetchall()
for (name,) in tables:
quoted = '"' + name.replace('"', '""') + '"'
count = con.execute(f"SELECT COUNT(*) FROM {quoted}").fetchone()[0]
print(f"{name:<32}{count:>10}")
con.close()
JavaScript: Node.js에서도 같은 엔진
sql.js는 Node.js에서도 동작한다. 브라우저 도구와 같은 방식이다. 파일을 메모리로 불러오고, 쓰기 작업은 그 사본만 바꾼다.
import { readFileSync } from "node:fs";
import initSqlJs from "sql.js";
const bytes = readFileSync(process.argv[2]);
const SQL = await initSqlJs();
const db = new SQL.Database(bytes); // in-memory copy of the file
const [schema] = db.exec(
"SELECT type, name FROM sqlite_master WHERE type IN ('table', 'view') ORDER BY name"
);
for (const [type, name] of schema?.values ?? []) console.log(type.padEnd(6), name);
// Writes change only the copy; the file on disk is untouched.
db.run("DELETE FROM users WHERE email LIKE ?", ["%@example.com"]);
console.log("rows deleted in memory:", db.getRowsModified());
// db.export() returns the modified database as a Uint8Array if you want to keep it.
db.close();
Bash: 뷰어에서 볼 파일을 준비하고, CLI에서 내보내기
# Fold pending WAL transactions into the main file (close the writing app first)
sqlite3 app.db "PRAGMA wal_checkpoint(TRUNCATE);"
# Or take a consistent single-file snapshot and leave the original alone
sqlite3 app.db "VACUUM INTO 'snapshot.db'"
# Confirm the header before opening it anywhere
head -c 16 snapshot.db | xxd
# CSV export from the shell; hex() keeps BLOBs as text like the viewer does
sqlite3 -header -csv snapshot.db \
"SELECT id, email, hex(avatar) AS avatar FROM users LIMIT 100" > users.csv
hex() 호출이 중요하다. sqlite3 셸은 BLOB 컬럼을 CSV에 원본 바이트 그대로 쓰는데, 이는 대부분의 CSV 리더에서 파일을 깨뜨린다.
다른 SQLite 도구와 비교
아래 도구들은 각각 다른 용도에 맞는 좋은 선택지다.
| 도구 | 실행 환경 | 읽는 대상 | 파일에 쓰기 | 암호화 파일 |
|---|---|---|---|---|
| ZeroTool SQLite Viewer | 브라우저 탭, 설치 불필요 | 최대 100MB의 파일 하나 | 아니오. 변경 사항은 메모리 사본에만 남는다 | 아니오 |
sqlite3 명령줄 셸 | 로컬 터미널 | 로컬 디스크의 파일, WAL 포함 | 예 | 아니오 (sqlcipher 셸 사용) |
| DB Browser for SQLite | Windows, macOS, Linux용 데스크톱 앱, 오픈소스 | 로컬 디스크의 파일 | 예 | 예, SQLCipher |
sqlite3 셸은 기준이 되는 도구다. 자체적인 크기 제한 없이 파일을 그 자리에서 열고, WAL을 읽으며, 스크립트로 다루기 좋다. 그냥 들여다보기만 할 때는 sqlite3 -readonly app.db를 쓴다. 한계는 dot 명령을 외워야 하고 넓은 테이블을 터미널에서 읽어야 한다는 점이다.
DB Browser for SQLite는 완전한 데스크톱 편집기다. 테이블을 만들고 바꿀 수 있고, 그리드에서 셀을 편집할 수 있으며, SQLCipher 암호화를 추가하거나 제거할 수 있다. 파일을 변경해야 할 때 쓴다.
브라우저 뷰어는 빠르게 훑어볼 때를 위한 도구다. 구조 패널, 페이지 단위 행 목록, SQL 에디터, CSV 내보내기를 설치도 계정도 업로드도 없이 제공한다. 구조상 파일에 대해 읽기 전용이므로, 들여다보는 데이터베이스를 손상시킬 수 없다.
관련 도구와 참고 자료
데이터베이스 파일과 함께 쓰기 좋은 ZeroTool 도구들.
- CSV to SQL은 CSV를
CREATE TABLE과INSERT문으로 바꿔주므로, 새 SQLite 파일에 테스트 데이터를 넣을 때 쓴다. - SQL Formatter는 긴
CREATE문이나 쿼리를 에디터에 붙여넣기 전에 읽기 좋게 정리해준다. - CSV ↔ JSON은 내보낸 쿼리 결과를 JSON으로 변환해 픽스처나 API 목(mock)으로 쓸 수 있게 해준다.
- JSON Formatter는 TEXT 컬럼에 저장된 JSON 문서를 다룰 때 유용하다.
이 가이드에서 사용한 1차 자료.
- Database File Format: 헤더 구조, 페이지 크기, 스키마 테이블
- Write-Ahead Logging: WAL,
-wal과-shm파일, 체크포인트 - Datatypes In SQLite: 저장 클래스와 타입 어피니티
- STRICT Tables: 3.37.0부터의 타입 지정 컬럼
- PRAGMA Statements:
table_info,index_list,wal_checkpoint - VACUUM:
VACUUM INTO스냅샷 - sql.js on GitHub: WebAssembly로 컴파일된 SQLite