LabHub

블로그

SQLite Internals Deep Dive — B-tree, WAL, VFS, Virtual Table, Query Planner 완전 정복 (2025)

TL;DR


1. SQLite의 기원

1.1 2000년 — 선박 위의 프로그래머

D. Richard Hipp은 1998년 General Dynamics에서 US Navy 미사일 구축함용 소프트웨어를 개발 중이었다. Oracle과 PostgreSQL이 필요했지만 선박 내 데이터베이스 서버 관리는 비현실적. 서버 크래시 → 하루 종일 수리 → 훈련 중단.

Hipp의 아이디어: "서버 없는 DB". 라이브러리로 앱에 링크되고 파일 하나에 저장. 설치/관리 불필요.

기존 솔루션:

2000년 5월, Hipp가 SQLite 1.0을 공개. 단순한 C 코드, 퍼블릭 도메인 (저작권 포기). "Because smart engineers will just need a simple database that works".

1.2 초기 성장

초기 수년 동안 사용자가 느리게 늘었다. 전환점:

폭발적 성장. 2010년경 "모든 모바일 앱이 SQLite에 의존"하는 상황이 됐다.

1.3 2025년 현황

1.4 왜 SQLite가 승리했는가

단순한 이유:

  1. 파일 하나: 복사/백업/이동이 쉽다.
  2. 서버 불필요: 설치 제로.
  3. 작다: 바이너리 ~500KB.
  4. 안정적: 비정상 종료에 강하다.
  5. 빠르다: 파일시스템 I/O에 거의 근접.
  6. SQL 완전: 대부분 SQL-92 지원.

Hipp의 고집: "SQLite is small, fast, reliable. Choose any three." 세 가지 모두 포기하지 않는다.


2. 아키텍처 개요

2.1 5+ 레이어

SQLite는 잘 정의된 레이어로 구성:

┌────────────────────────────────┐
Interface (C API)├────────────────────────────────┤
SQL Command Processor│   ┌──────────────────────┐    │
│   │ Tokenizer            │    │
│   ├──────────────────────┤    │
│   │ Parser               │    │
│   ├──────────────────────┤    │
│   │ Code Generator       │    │
│   └──────────────────────┘    │
├────────────────────────────────┤
Virtual Machine (VDBE)├────────────────────────────────┤
B-tree                       │
├────────────────────────────────┤
Pager├────────────────────────────────┤
OS Interface (VFS)└────────────────────────────────┘

각 레이어는 위에만 의존. 하위 레이어 교체 가능 (특히 VFS).

2.2 실행 흐름

SQL 문이 실행되면:

  1. Tokenizer: SELECT name FROM users WHERE id = 1 → 토큰 [SELECT, name, FROM, users, WHERE, id, =, 1].
  2. Parser: 토큰 → AST.
  3. Code Generator: AST → VDBE bytecode.
  4. VDBE: bytecode 실행 → B-tree 레이어 호출.
  5. B-tree: 페이지 조회 → Pager 호출.
  6. Pager: 캐시 확인 → 없으면 VFS로 디스크 I/O.
  7. VFS: OS 파일시스템 API (read/write/fsync).

이 분리 덕분에 각 레이어를 독립적으로 테스트, 교체 가능.

2.3 Amalgamation

SQLite 소스 코드는 100+ .c 파일로 개발되지만, 배포 시 하나의 거대 파일 sqlite3.c로 합쳐진다. amalgamation.

# 빌드
gcc sqlite3.c -o libsqlite3.a -c

단일 파일의 장점:

단점: 디버깅 시 줄 번호 거대. 하지만 SQLite 자체 개발자에게만 문제.


3. B-tree 저장

3.1 파일 레이아웃

SQLite DB 파일은 고정 크기 페이지로 나뉜다. 기본 4KB (configurable 512B - 65KB).

File header (100 bytes) | Page 1 | Page 2 | Page 3 | ... | Page N

3.2 파일 헤더

첫 100 바이트:

Offset  Size  Description
0       16    "SQLite format 3\000"
16      2     Page size (2의 거듭제곱, 512-65536)
18      1     File format write version (1=legacy, 2=WAL)
19      1     File format read version
20      1     Reserved space per page
21      1     Max embedded payload fraction
24      4     File change counter
28      4     Total number of pages
32      4     First freelist page
36      4     Number of freelist pages
40      16    Schema cookie, ...
...

파일 하나가 전체 DB. 헤더로 자기 설명.

3.3 테이블 = B+tree

각 테이블은 B+tree 하나.

B+tree의 특성:

3.4 Row ID

SQLite의 핵심 특징: **각 행이 64-bit integer rowid**를 가진다.

CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);

INTEGER PRIMARY KEY는 특별히 rowid의 alias. 다른 column을 PK로 쓰면 rowid는 별도.

테이블 B+tree의 키는 rowid. Leaf node는 rowid → row_data.

3.5 WITHOUT ROWID

Rowid가 없는 테이블:

CREATE TABLE users (email TEXT PRIMARY KEY, name TEXT) WITHOUT ROWID;

3.6 Index = 또 다른 B-tree

인덱스도 B+tree. 키 = indexed column + rowid.

CREATE INDEX idx_name ON users(name);

이 인덱스의 B-tree:

Key: (name, rowid) → rowid

쿼리 WHERE name = 'Alice':

  1. Index B-tree 조회: ('Alice', *) → rowid 리스트.
  2. Main table B-tree에서 각 rowid 조회 → 실제 행.

2단계 조회 (index scan + table lookup).

3.7 Covering Index

인덱스만으로 쿼리 답이 충족되면 table lookup skip:

CREATE INDEX idx_name_age ON users(name, age);

SELECT name, age FROM users WHERE name = 'Alice';
-- age까지 인덱스에 있음 → main table 조회 불필요

Covering index. 빠름.

3.8 Varint

SQLite는 정수를 저장할 때 varint(variable-length integer)를 쓴다. 작은 수는 1 바이트, 큰 수는 더.

0-127:         1 byte
128-16K:       2 bytes
...
Max 8 bytes for 64-bit int

Rowid, 페이지 포인터, record header 등에 활용. 공간 절약.

3.9 Record Format

한 행의 인코딩:

Header size (varint)
Serial types (varint 리스트)
Body (serial types 순서대로)

Serial type:

Type affinity: SQLite의 독특한 특징 — 컬럼 타입이 힌트일 뿐 강제 아님. 실제 저장은 serial type 기반.


4. Pager와 Page Cache

4.1 Pager의 역할

B-tree 레이어는 "페이지 번호"로 요청. Pager는:

  1. 페이지 캐시 확인 (메모리).
  2. 없으면 디스크에서 읽기 (VFS 통해).
  3. 캐시에 저장.
  4. 페이지 반환.

Cache Manager 역할 + transaction 관리.

4.2 Page Cache

기본 2000 페이지 캐시 (약 8MB). 설정 가능:

PRAGMA cache_size = -20000;  -- 20 MB

음수는 KB, 양수는 페이지 수.

LRU처럼 동작하지만 실제로는 단순 해시 테이블 + 사용 카운터.

4.3 Dirty Pages

수정된 페이지는 dirty 표시. 디스크에 아직 기록 안 됨.

커밋 시:

  1. 모든 dirty 페이지를 디스크에 쓰기.
  2. fsync 호출 (디스크에 강제 flush).
  3. 커밋 완료.

fsync는 비싸다 (수 ms). 대량 insert는 트랜잭션으로 묶어서 한 번의 fsync로.

BEGIN;
INSERT INTO ...
INSERT INTO ...
...
COMMIT;  -- 이때 fsync 한 번

트랜잭션 없이 각 INSERT가 autocommit → 각자 fsync → 수백 배 느림.


5. Rollback Journal — 전통적 트랜잭션

5.1 ACID 보장

SQLite는 처음부터 ACID를 보장. 비정상 종료에도 일관성 유지.

5.2 Rollback Journal

2010 이전 기본 모드.

흐름:

  1. 페이지 변경 전: 원본 페이지를 journal 파일(.db-journal)에 저장.
  2. 메모리에서 수정.
  3. 커밋 시: 수정된 페이지를 메인 파일에 쓰기 → fsync → journal 파일 삭제.
  4. 롤백 시: journal의 원본으로 메인 파일 복원.

5.3 복구

비정상 종료 후 재시작 시 SQLite가:

  1. .db-journal 파일 존재 확인.
  2. 있으면 "트랜잭션 중 죽음" 판단.
  3. Journal의 원본을 메인 파일로 복원.
  4. Journal 삭제.

"crash-safe" 보장.

5.4 단점

Writer가 readers를 블록한다:

2010년 이전엔 모바일 앱이 UI + 백그라운드 sync → 서로 락 경쟁.


6. WAL Mode — 2010년의 혁명

6.1 왜 WAL

Write-Ahead Logging은 잘 알려진 기법 (PostgreSQL, Oracle도 사용). SQLite 3.7 (2010)에 도입.

핵심: 변경을 메인 파일에 바로 쓰지 않고 별도 WAL 파일에 추가. 나중에 배치로 메인 파일에 반영.

6.2 동작

DB 파일: users.db
         users.db-wal    ← 변경 기록
         users.db-shm    ← shared memory (인덱스)

쓰기:

  1. 변경된 페이지를 users.db-walappend.
  2. fsync (WAL 파일만).
  3. 메인 파일은 건드리지 않음.
  4. Commit 완료.

읽기:

  1. 원하는 페이지가 wal에 있는지 확인.
  2. 있으면 WAL에서 읽기 (가장 최신 버전).
  3. 없으면 메인 파일에서.

Checkpoint:

6.3 동시성 이득

Writer가 WAL에만 쓰고 메인 파일을 건드리지 않으므로:

실질적 MVCC (Multi-Version Concurrency Control). 각 reader는 자기 읽기 시점의 snapshot을 본다.

6.4 활성화

PRAGMA journal_mode = WAL;

파일에 기록되므로 한 번만 하면 됨.

6.5 Checkpoint 전략

Passive (기본): 다음 writer가 체크포인트 수행. Writer가 살짝 느려짐.

Full: 모든 WAL 내용을 flush.

Restart: WAL을 재사용 가능하게 정리.

Truncate: WAL을 0 바이트로 축소.

6.6 WAL의 한계

대부분 경우 WAL이 rollback journal보다 훨씬 낫다.


7. VFS — Virtual Filesystem

7.1 추상화

SQLite는 OS에 직접 의존하지 않는다. VFS 레이어가 파일 I/O를 추상화.

typedef struct sqlite3_vfs sqlite3_vfs;
struct sqlite3_vfs {
    int (*xOpen)(sqlite3_vfs*, const char *zName, sqlite3_file*, int flags, int *pOutFlags);
    int (*xDelete)(sqlite3_vfs*, const char *zName, int syncDir);
    int (*xAccess)(sqlite3_vfs*, const char *zName, int flags, int *pResOut);
    int (*xFullPathname)(sqlite3_vfs*, const char *zName, int nOut, char *zOut);
    // ... 수십 개 함수
};

기본 VFS:

7.2 커스텀 VFS 사용 사례

In-memory DB:

:memory:

VFS가 RAM만 사용. 디스크 I/O 없음.

암호화 VFS (SEE, SQLCipher): 파일 I/O 시 모든 바이트 암호화/해독. 투명하게 동작.

HTTP VFS: HTTP range request로 원격 DB 읽기. 클라이언트가 일부만 다운로드.

Cloud VFS: S3, GCS 저장소. 페이지 단위 HTTP GET.

Android contentProvider: 안드로이드가 자체 VFS로 DB 접근 제어.

7.3 WASM VFS

브라우저에서 SQLite 실행 (2020+):

브라우저 DB가 가능해지면서 클라이언트 사이드 앱이 SQLite 기반 local-first로 진화.


8. VDBE — Virtual Machine

SQLite는 SQL을 직접 실행하지 않는다. 먼저 bytecode로 컴파일 → VDBE(Virtual Database Engine)가 실행.

8.1 Bytecode 예제

SELECT name FROM users WHERE id = 1;
addr  opcode          p1  p2  p3  p4          p5  comment
----  -------------   --  --  --  --          --  -------
0     Init            0   7   0               0   Start at 7
1     OpenRead        0   2   0   2           0   root=2 iDb=0; users
2     RowId           0   1   0               0   r[1]=rowid
3     IdxGE           1   6   2   r[1]        1   key=r[1]
4     Column          0   1   3               0   r[3]=users.name
5     ResultRow       3   1   0               0   output=r[3..3]
6     Halt            0   0   0               0
7     Transaction     0   0   1   0           1
8     Goto            0   1   0               0

VDBE는 이 프로그램을 해석하며 실행.

8.2 왜 Bytecode

8.3 EXPLAIN

EXPLAIN SELECT name FROM users WHERE id = 1;

바이트코드 리스트 반환. "왜 이 쿼리가 이렇게 실행되는지" 파악 가능.

EXPLAIN QUERY PLAN SELECT name FROM users WHERE id = 1;

더 높은 수준의 플랜 (테이블 스캔 vs 인덱스 사용 등).

8.4 Register-Based

VDBE는 레지스터 기반 VM. Python의 스택 기반과 다름. 각 명령이 특정 레지스터를 읽고 쓴다.

p1, p2, p3 같은 operand가 레지스터/페이지/문자열 등을 가리킴.

대략 150+ opcode. SQL의 모든 구문을 커버.


9. Query Planner

9.1 역할

SELECT * FROM users WHERE name = 'Alice' AND age > 30을 어떻게 실행?

Query planner가 가장 싼 것을 선택.

9.2 비용 모델

SQLite는 간단한 cost 모델:

ANALYZE:

ANALYZE users;

통계 수집. Query planner가 더 나은 결정.

9.3 Join 순서

N개 테이블 join → N!개 순서 가능. 모두 시도하지 않고 heuristic.

9.4 Index Selection

같은 쿼리에 여러 인덱스 가능:

CREATE INDEX idx1 ON users(name);
CREATE INDEX idx2 ON users(age);
CREATE INDEX idx3 ON users(name, age);

WHERE name = 'Alice' AND age > 30: idx3가 최적 (covering).

Query planner가 결정.

9.5 Partial Index

CREATE INDEX idx_active_users ON users(name) WHERE active = 1;

active = 1 행만 인덱싱 → 인덱스 작음, 빠름. 특정 WHERE 패턴에 최적.

9.6 Expression Index

CREATE INDEX idx_lower_name ON users(LOWER(name));

SELECT * FROM users WHERE LOWER(name) = 'alice';
-- ↑ 인덱스 사용 가능

표현식 자체를 인덱싱.


10. Virtual Table

"실제 파일에 저장되지 않는 가상 테이블". SQLite의 가장 강력한 확장 메커니즘.

10.1 사용 사례

Virtual table이 가능하게 하는 것:

10.2 Module

Virtual table은 module로 구현:

typedef struct sqlite3_module sqlite3_module;
struct sqlite3_module {
    int iVersion;
    int (*xCreate)(sqlite3*, void *pAux, int argc, const char *const*argv,
                   sqlite3_vtab **ppVTab, char**);
    int (*xConnect)(sqlite3*, void *pAux, int argc, const char *const*argv,
                    sqlite3_vtab **ppVTab, char**);
    int (*xBestIndex)(sqlite3_vtab *pVTab, sqlite3_index_info*);
    int (*xDisconnect)(sqlite3_vtab *pVTab);
    int (*xDestroy)(sqlite3_vtab *pVTab);
    int (*xOpen)(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor);
    int (*xClose)(sqlite3_vtab_cursor*);
    int (*xFilter)(sqlite3_vtab_cursor*, int idxNum, const char *idxStr,
                   int argc, sqlite3_value **argv);
    int (*xNext)(sqlite3_vtab_cursor*);
    int (*xEof)(sqlite3_vtab_cursor*);
    int (*xColumn)(sqlite3_vtab_cursor*, sqlite3_context*, int);
    int (*xRowid)(sqlite3_vtab_cursor*, sqlite_int64 *pRowid);
    ...
};

구현자는 "행을 어떻게 얻나"를 콜백으로 제공.

Virtual table로 구현된 전문 검색 엔진.

CREATE VIRTUAL TABLE articles USING fts5(title, body);

INSERT INTO articles VALUES ('SQLite Tutorial', 'Learn SQLite...');
INSERT INTO articles VALUES ('PostgreSQL Guide', 'Master Postgres...');

SELECT * FROM articles WHERE articles MATCH 'sqlite OR postgres';

내부:

임베디드 검색 엔진이 DB 안에. 별도 Elasticsearch 불필요.

10.4 R-tree — 공간 인덱스

지리적 검색:

CREATE VIRTUAL TABLE restaurants_idx USING rtree(
    id,
    minLat, maxLat,
    minLng, maxLng
);

INSERT INTO restaurants_idx VALUES (1, 37.5, 37.5, 127.0, 127.0);

SELECT * FROM restaurants_idx
WHERE minLat >= 37.0 AND maxLat <= 38.0
  AND minLng >= 126.0 AND maxLng <= 128.0;

PostGIS의 축소판. 모바일 앱에서 지도 검색.

10.5 JSON1 / JSONB

SQLite 3.45+ (2024) JSONB 네이티브 지원.

CREATE TABLE events (id INTEGER, data JSONB);

INSERT INTO events VALUES (1, jsonb('{"name": "Alice", "age": 30}'));

SELECT json_extract(data, '$.name') FROM events;
SELECT * FROM events WHERE json_extract(data, '$.age') > 25;

PostgreSQL JSONB 스타일. 빠른 경로 조회.


11. Transactions와 Concurrency

11.1 BEGIN / COMMIT / ROLLBACK

BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;

고전적 ACID 트랜잭션.

11.2 Transaction Types

DEFERRED (기본): BEGIN은 실제로 락을 걸지 않음. 첫 SELECT에서 SHARED, 첫 수정에서 RESERVED.

IMMEDIATE: BEGIN IMMEDIATE 즉시 RESERVED lock. 다른 writer 없음을 보장.

EXCLUSIVE: BEGIN EXCLUSIVE 즉시 EXCLUSIVE. 다른 프로세스 일체 접근 불가.

11.3 Lock 진화 (Rollback Journal 모드)

UNLOCKEDSHAREDRESERVEDPENDINGEXCLUSIVE

11.4 WAL 모드에서

위의 락 모델이 단순해진다:

Much better concurrency.

11.5 Savepoint

Nested transactions:

BEGIN;
SAVEPOINT sp1;
UPDATE ...;
ROLLBACK TO sp1;
UPDATE ...;
COMMIT;

ROLLBACK TO는 savepoint 이후만 되돌림.


12. 신뢰성 — 광적인 테스트

12.1 숫자

이것이 SQLite가 Boeing 787, 의료 기기, 원자력 시설에서 쓰이는 이유.

12.2 테스트 종류

TCL Tests: 기본 단위/통합 테스트.

TH3 (Test Harness 3): 전문 상용 테스트 (Hwaci가 항공 인증을 위해 개발). 100% MC/DC 커버리지.

MC/DC (Modified Condition/Decision Coverage): DO-178B 항공 인증 표준. 모든 조건의 각 결과를 개별 검증. 비행기 소프트웨어 수준.

SQLite Logic Test: SQL 표준 준수 체크.

Fuzz Testing: 연중무휴 fuzz로 버그 찾기. Google의 OSS-Fuzz.

Soak Tests: 수일 동안 무작위 쿼리 실행.

Crash Tests: I/O 에러 주입 — "fsync 중에 전원 끊기면?" "디스크 풀?" "OS 버그?" — 수백만 시뮬레이션.

12.3 TH3와 인증

TH3는 상용 라이선스로만 제공. Hwaci의 수익원. 하지만 SQLite 자체는 public domain.

Boeing이 DO-178B 인증을 위해 TH3로 검증된 SQLite를 사용. 비행 중요 소프트웨어에서 DB 사용 증명.

12.4 Bug Report 정책

SQLite 팀의 원칙:

업계에서 가장 신중한 개발 문화.

12.5 Fossil

SQLite 프로젝트는 Fossil이라는 자체 버전 관리 시스템 사용 (Git이 아닌). Hipp이 직접 개발.

특징:

SQLite가 Fossil을 뒷받침하고 Fossil이 SQLite를 관리. 원형 관계.


13. 성능 튜닝

13.1 PRAGMA

SQLite의 설정 명령어.

필수적인 것:

PRAGMA journal_mode = WAL;    -- WAL 모드 (강력 권장)
PRAGMA synchronous = NORMAL;  -- NORMAL (WAL에서 안전)
PRAGMA cache_size = -20000;   -- 20 MB 캐시
PRAGMA temp_store = MEMORY;   -- 임시 테이블을 RAM에
PRAGMA mmap_size = 268435456; -- 256 MB memory-mapped I/O

13.2 synchronous 레벨

Rollback journal에선 FULL 권장. WAL에선 NORMAL 충분.

13.3 Batch Insert

# BAD
for row in data:
    cursor.execute("INSERT INTO t VALUES (?, ?)", row)
# 각자 자동 커밋 → 각자 fsync

# GOOD
conn.execute("BEGIN")
for row in data:
    cursor.execute("INSERT INTO t VALUES (?, ?)", row)
conn.execute("COMMIT")
# 한 번의 fsync

100배 이상 차이.

13.4 Prepared Statement

stmt = conn.prepare("INSERT INTO t VALUES (?, ?)")
for row in data:
    stmt.execute(row)

SQL 파싱을 한 번만. Bind parameter로 재사용.

13.5 Index 활용

인덱스 스캔을 확인:

EXPLAIN QUERY PLAN SELECT ...;

SCAN TABLE이면 전체 스캔. SEARCH TABLE USING INDEX가 정상.

13.6 VACUUM

VACUUM;

DB 파일을 재구성 → 삭제된 공간 회수, 페이지 재정렬. 오프라인 작업.

Auto vacuum:

PRAGMA auto_vacuum = INCREMENTAL;
PRAGMA incremental_vacuum(100);  -- 100 페이지 회수

13.7 Memory-mapped I/O

PRAGMA mmap_size = 268435456;  -- 256 MB

DB 파일을 mmap → OS가 페이지 캐시 관리. 큰 DB에서 빠름. 단점: 일부 플랫폼 이슈.


14. 최신 기능 (2023-2025)

14.1 STRICT Tables

CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    age INTEGER
) STRICT;

INSERT INTO users VALUES (1, 123, 'Alice');
-- Error: NOT NULL constraint failed (name is INTEGER)

타입 체크 강제. 기본은 loose (SQLite의 type affinity). STRICT로 엄격 타입.

14.2 RETURNING

INSERT INTO users (name) VALUES ('Alice') RETURNING id, name;

PostgreSQL 스타일. Insert/Update/Delete 결과 반환.

14.3 JSONB (3.45)

위에서 본 것. 바이너리 JSON 포맷으로 파싱 오버헤드 감소.

14.4 Math Functions

SELECT SIN(3.14), LOG(100), POW(2, 10);

수학 함수 내장. 이전엔 extension 필요.

14.5 Window Functions

SELECT 
    name, 
    salary,
    RANK() OVER (ORDER BY salary DESC) as rank,
    SUM(salary) OVER (PARTITION BY department) as dept_total
FROM employees;

현대 SQL 표준. SQLite 3.25+ 지원.

14.6 CTE와 Recursive CTE

WITH RECURSIVE cnt(x) AS (
    SELECT 1
    UNION ALL
    SELECT x + 1 FROM cnt WHERE x < 10
)
SELECT * FROM cnt;

재귀 쿼리. 트리 구조 순회에 유용.


15. SQLite 대체/경쟁자

15.1 DuckDB

"SQLite for analytics". OLAP용 임베디드. 컬럼나 저장.

차이:

상호 보완. DuckDB가 CSV/Parquet/Arrow 읽기 우수.

15.2 LibSQL

SQLite의 오픈 fork. Turso가 개발. 특징:

"SQLite + 분산". Cloudflare D1도 유사.

15.3 Cloudflare D1

Cloudflare Workers의 SQLite 기반 DB. Durable Objects 위에서 실행.

15.4 Turso

LibSQL 기반 관리형 서비스. SQLite를 엣지 컴퓨팅 플랫폼으로.

15.5 전통 경쟁자

아무도 SQLite의 조합(SQL + 임베디드 + 신뢰성 + 작음)을 이기지 못했다.


16. 실전 사용 패턴

16.1 iOS/Android 앱

Core Data (iOS), Room (Android)이 SQLite를 wrap.

// Swift
let manager = NSManagedObjectContext(...)
let users = manager.fetch(NSFetchRequest<User>())

내부적으로 SQLite 쿼리.

16.2 Chrome

모두 SQLite 파일. sqlite3 CLI로 열어서 탐색 가능.

16.3 Dropbox

초기 Dropbox는 SQLite에 파일 메타데이터 저장. 지금도 일부 사용.

16.4 데스크톱 앱

Skype, 1Password, Evernote, Notion (local cache) 등 셀 수 없이 많다.

16.5 서버 사이드?

SQLite는 일반적으로 고동시성 웹 서비스엔 부적합. 하지만:

Litestream, LiteFS 같은 도구로 SQLite를 replication/backup → "서버 사이드 SQLite" 재유행.


17. 학습 리소스

공식:

:

영상:

코드 탐험:

Tools:


18. 요약 — 한 장 정리

┌─────────────────────────────────────────────────────┐
SQLite Cheat Sheet├─────────────────────────────────────────────────────┤
│ 설계:│   단일 C 파일 (~150K 라인, amalgamation)Public Domain│   서버 없음, 파일 하나                                  │
│                                                       │
아키텍처 (위에서 아래로):C APITokenizerParserCode GeneratorVDBE (bytecode VM)B-tree                                              │
Pager + Page CacheVFS (OS interface)│                                                       │
│ 저장:│   4KB 페이지 기본                                      │
B+tree (테이블, 인덱스 각각)Row ID (64-bit)Varint + Serial Types│                                                       │
Transaction 모드:Rollback Journal (전통)- 원본 페이지를 journal에 저장                    │
- Writer가 readers 차단                           │
WAL (2010+, 권장)- 변경을 별도 WAL 파일에                          │
- Reader/writer 동시성                             │
│                                                       │
VFS:OS 파일시스템 추상                                   │
│   커스텀 구현 가능                                     │
- In-memory, 암호화                                │
- HTTP, S3- WASM/IndexedDB│                                                       │
Virtual Table:FTS5 (full-text search)R-tree (공간 인덱스)CSV, JSON1/JSONB│   커스텀 가능                                          │
│                                                       │
VDBE:Bytecode VM (register-based)150+ opcodes                                        │
EXPLAIN으로 확인                                    │
│                                                       │
Query Planner:Cost-based                                          │
ANALYZE로 통계 수집                                  │
Index selection                                    │
Partial, expression index                          │
│                                                       │
│ 신뢰성:│   92M 줄 테스트 (150K 코드의 600)100% MC/DC 커버리지 (DO-178B)Boeing, 의료기기에서 인증                            │
│                                                       │
│ 튜닝:PRAGMA journal_mode = WALPRAGMA synchronous = NORMALPRAGMA cache_size = -20000│   배치 insert (트랜잭션)Prepared statement                                  │
│                                                       │
최신 (2023-2025):STRICT tables                                       │
RETURNINGJSONB (3.45)Math functions                                      │
Window functions                                    │
│                                                       │
│ 사용처:│   모든 iOS/Android 앱                                 │
Chrome, Firefox, SafariMac, Windows, LinuxBoeing 787, 의료기기                                │
Dropbox, Skype, Slack (local)└─────────────────────────────────────────────────────┘

19. 퀴즈

Q1. SQLite가 "서버리스"라는 말의 의미는?

A. 별도 프로세스가 필요 없다는 뜻 — 흔히 말하는 serverless 클라우드와는 다른 개념이다. 일반적인 DB(PostgreSQL, MySQL)는 postgres, mysqld 같은 데몬이 돌고 클라이언트가 소켓(TCP 또는 Unix)으로 연결한다. 이 모델은 관리 오버헤드가 크다 — 설치, 시작, 포트, 인증, 백업 등. SQLite는 라이브러리 형태라 애플리케이션 프로세스 자체가 DB를 직접 읽고 쓴다. "연결"이 메모리 내 함수 호출일 뿐이다. 덕분에 "설치 제로, 관리 제로, 설정 제로". 파일을 복사하면 DB가 복사된다. 이 아키텍처 선택이 SQLite를 iPhone부터 비행기까지 배포 가능하게 만든 핵심.

Q2. Rollback Journal과 WAL 모드의 가장 큰 차이는?

A. 동시성 모델. Rollback Journal은 "쓰기 전에 원본을 journal에 저장 → 메인 파일 수정 → 성공 시 journal 삭제". 이 방식에서 writer가 메인 파일을 수정하는 동안 reader는 일관되지 않은 상태를 보게 되므로 차단해야 한다 — 한 번에 하나의 writer XOR 여러 readers. WAL은 반대로 "변경을 별도 WAL 파일에 append → 메인 파일은 건드리지 않음". Reader는 메인 파일(또는 WAL의 snapshot)을 그대로 사용할 수 있어서 writer와 readers가 동시 작동. 실질적 MVCC. 모바일 앱(UI + 백그라운드 sync)에 필수. 2010년 SQLite 3.7이 WAL 도입 후 SQLite의 동시성 수준이 완전히 바뀌었다 — "SQLite는 느리다"던 통념이 깨진 시점.

Q3. Virtual Table이 SQLite의 확장성을 어떻게 높이는가?

A. "SQL 가능한 데이터 소스"를 임의로 플러그인 가능하게 한다. 기본 테이블은 B-tree에 저장되지만, virtual table은 데이터가 어디에 어떻게 저장되는지 완전히 커스터마이즈 가능 — 파일, 메모리, 원격 API, 또는 완전히 다른 구조. FTS5(full-text search), R-tree(공간 인덱스), CSV reader, JSON1, carray 등이 모두 virtual table. 개발자가 C에서 xOpen, xNext, xColumn, xFilter 같은 콜백을 구현하면 SQLite가 일반 테이블처럼 SELECT/INSERT 가능. 결과: SQLite가 단순 DB가 아니라 임베디드 데이터 질의 엔진. "이 외부 API 결과를 SQL JOIN하고 싶다" 같은 시나리오를 가능하게 한다. 작은 확장 API 하나가 생태계를 폭발시킨 사례.

Q4. SQLite의 "92M 줄 테스트"가 주는 실질적 이익은?

A. 극한 환경에서의 신뢰성과 인증 가능성. 92M 줄 테스트 중 상당 부분이 TH3(Test Harness 3)로 100% MC/DC 커버리지를 달성한다 — 이는 DO-178B(항공기 소프트웨어 인증) 표준의 가장 엄격한 수준이다. 모든 브랜치, 모든 조건, 모든 에러 경로가 검증됨. 실질적 결과: (1) Boeing 787의 비행 중요 시스템에서 SQLite 사용, (2) 의료 기기(MRI, 초음파), (3) 원자력 시설, (4) 자동차 ECU에서 신뢰. 보통 오픈소스는 "알아서 쓰시오"인데 SQLite는 "항공 인증 수준"을 보장한다. 이 테스트 문화가 SQLite의 진짜 차별점이고, 왜 더 최신/빠른 대안들이 SQLite를 대체하지 못하는지의 이유. "작동하는 것이 빠른 것보다 중요하다"는 철학의 극단적 구현.

Q5. VDBE (Virtual Database Engine)가 왜 필요한가?

A. SQL 파싱과 실행의 분리로 prepared statement 재사용과 최적화 분리를 가능하게 한다. SQL 문자열을 직접 실행하면 매번 파싱/최적화해야 한다 → 비용 큼. VDBE는 SQL을 한 번 bytecode로 컴파일하고, 이 bytecode를 VM이 실행. 장점: (1) Prepared statement: 파라미터만 바꿔 같은 bytecode 재실행, (2) 최적화 분리: Query planner가 bytecode 생성에 집중, VDBE는 단순 실행에 집중, (3) 디버깅: EXPLAIN으로 bytecode 직접 조회 가능 — "왜 내 쿼리가 느린지" 정확히 파악. 150+ opcode로 SQL의 모든 구문 표현. JVM, Python VM과 같은 접근이지만 DB에 특화. 덕분에 SQLite가 단순하면서도 고성능.

Q6. Litestream/LiteFS가 "서버사이드 SQLite"를 가능하게 하는 원리는?

A. SQLite의 WAL 파일을 복제해서 replication과 backup을 구현. 전통적으로 SQLite는 "서버사이드 웹 서비스에 부적합"이라 여겨졌다 — 고동시성과 분산이 약하기 때문. 하지만 WAL 모드에서 WAL 파일 자체가 "변경 이력"이라 이걸 object storage(S3, R2)에 지속적으로 복제하면 실질적 backup + point-in-time recovery가 된다. Litestream(Ben Johnson)이 이 아이디어를 구현, 나중에 LiteFS로 발전시켜 노드 간 SQLite replication까지 가능하게 했다. Fly.io는 LiteFS로 "엣지에 SQLite 분산"을 상용화. 결과: "Read-heavy + Write-single-writer" 워크로드에서 Postgres보다 훨씬 단순한 인프라로 비슷한 결과. "이미 20년된 기술의 재발견"이 최근 몇 년의 트렌드 — 때로는 오래된 도구를 새로운 방식으로 쓰는 것이 혁신이다.

Q7. SQLite의 "Type Affinity" 시스템이 왜 독특한가?

A. 컬럼 타입이 엄격한 제약이 아니라 힌트. 다른 DB(PostgreSQL, MySQL)는 age INTEGER 컬럼에 문자열을 insert하면 에러. SQLite는 기본적으로 허용 — 실제 값이 저장되고, 필요 시 변환만 한다. 이유: Hipp가 "현실 데이터는 지저분하고 쿼리도 지저분하다"는 철학. age에 "25"를 insert해도 작동하면 좋지 않나? 정수 연산 필요 시 SQLite가 자동 변환. 장점: 느슨함으로 인한 편의성, 레거시 데이터 수용. 단점: 타입 안전성 부족 — 버그가 런타임까지 숨을 수 있다. 이에 대응해 SQLite 3.37(2021)은 STRICT 테이블 추가: CREATE TABLE t (...) STRICT로 엄격 타입 강제. 기본은 여전히 느슨하지만 엄격함이 필요하면 선택 가능. "철학적 결정을 20년 후에 옵트인으로 바꾸는" 흥미로운 사례.


이 글이 도움이 됐다면 다음 포스트도 확인해 보세요:

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다