--- id: db-sqlite-patterns title: SQLite 패턴 — Embedded / WAL / 동시성 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, sqlite, embedded, vibe-coding] tech_stack: { language: "TS / SQL", applicable_to: ["Backend", "Mobile"] } applied_in: [] aliases: [SQLite, better-sqlite3, libSQL, WAL mode, busy_timeout, embedded DB] --- # SQLite Patterns > 가장 사용된 DB. **Single file, embedded, 0 setup**. Mobile / desktop / edge / 작은 web. WAL mode + busy_timeout = 동시성 OK. ## 📖 핵심 개념 - WAL: write-ahead log. read 안 차단. - Journal mode: DELETE / WAL. - BEGIN IMMEDIATE: write lock 즉시. - Vacuum: 빈 공간 회수. ## 💻 코드 패턴 ### Node — better-sqlite3 (sync, fast) ```bash yarn add better-sqlite3 ``` ```ts import Database from 'better-sqlite3'; const db = new Database('app.db'); db.pragma('journal_mode = WAL'); db.pragma('synchronous = NORMAL'); db.pragma('busy_timeout = 5000'); // Prepared statement (재사용) const insertUser = db.prepare('INSERT INTO users (id, email) VALUES (?, ?)'); insertUser.run(uuid(), 'a@b.com'); // Get const getUser = db.prepare('SELECT * FROM users WHERE id = ?'); const user = getUser.get(id); // Many const all = db.prepare('SELECT * FROM users').all(); ``` → Sync API — async overhead 없음. 가장 빠름. ### Node — node:sqlite (Node 22.5+ built-in) ```ts import { DatabaseSync } from 'node:sqlite'; const db = new DatabaseSync('app.db'); db.exec('CREATE TABLE IF NOT EXISTS users (...)'); const r = db.prepare('SELECT * FROM users WHERE id = ?').get(id); ``` ### Bun ```ts import { Database } from 'bun:sqlite'; const db = new Database('app.db'); db.exec('PRAGMA journal_mode = WAL'); const users = db.prepare('SELECT * FROM users').all(); ``` ### libSQL (Turso fork) ```ts import { createClient } from '@libsql/client'; const turso = createClient({ url: 'file:app.db' }); await turso.execute('CREATE TABLE ...'); ``` → SQLite + replication + embedded replica. ### WAL mode (필수) ```sql PRAGMA journal_mode = WAL; -- Read 가 write 차단 X -- Concurrent read OK -- 하지만 single writer ``` ```sql PRAGMA synchronous = NORMAL; -- WAL + NORMAL = FAST + 보통 안전 (power loss 일부 위험 OK) -- FULL = 더 안전, 느림 -- OFF = 매우 빠름, 위험 ``` ### Busy timeout ```sql PRAGMA busy_timeout = 5000; -- Write lock 5초 대기 후 SQLITE_BUSY ``` ```ts // 또는 retry loop for (let i = 0; i < 10; i++) { try { db.exec('UPDATE users SET ...'); break; } catch (e) { if (e.code === 'SQLITE_BUSY') await sleep(50); else throw e; } } ``` ### Transaction ```ts const tx = db.transaction((users: User[]) => { for (const u of users) insertUser.run(u.id, u.email); }); tx(users); // 자동 BEGIN / COMMIT, 실패 시 ROLLBACK ``` → 1000개 insert = 매 INSERT 보다 100x 빠름. ### Concurrent write (queue) ```ts import PQueue from 'p-queue'; const writeQueue = new PQueue({ concurrency: 1 }); async function writeUser(u: User) { return writeQueue.add(() => insertUser.run(u.id, u.email)); } ``` → Single-writer queue. SQLITE_BUSY 회피. ### BEGIN IMMEDIATE / EXCLUSIVE ```sql BEGIN IMMEDIATE; -- 즉시 write lock — 다른 process 가 read OK 그러나 write X -- 작업 COMMIT; ``` → 큰 transaction 시 안전. ### Schema ```sql CREATE TABLE users ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'pro')), created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TEXT ) STRICT; -- type 강제 (3.37+) CREATE INDEX users_email ON users(email); CREATE INDEX users_active ON users(created_at) WHERE deleted_at IS NULL; ``` → STRICT mode 가 type 안전. ### JSON ```sql CREATE TABLE events ( id TEXT PRIMARY KEY, data TEXT, -- JSON created_at TEXT ); INSERT INTO events VALUES ('1', json('{"key": "value"}'), datetime('now')); -- Query SELECT * FROM events WHERE json_extract(data, '$.key') = 'value'; -- Index on JSON CREATE INDEX events_key ON events(json_extract(data, '$.key')); ``` ### Full-text search (FTS5) ```sql CREATE VIRTUAL TABLE docs_fts USING fts5(title, body); INSERT INTO docs_fts (title, body) VALUES ('Hello', 'World'); SELECT * FROM docs_fts WHERE docs_fts MATCH 'world'; SELECT * FROM docs_fts WHERE docs_fts MATCH '"exact phrase"'; SELECT *, bm25(docs_fts) AS score FROM docs_fts WHERE docs_fts MATCH 'foo' ORDER BY score; ``` ### Vector search (sqlite-vss / vec) ```sql CREATE VIRTUAL TABLE vss_demo USING vss0( embedding(1536) ); INSERT INTO vss_demo (embedding) VALUES (?); SELECT rowid, distance FROM vss_demo WHERE vss_search(embedding, vss_search_params(?, 10)); ``` ### Backup ```ts // Online backup db.backup('backup.db', { progress: ({ totalPages, remainingPages }) => { console.log(`${100 * (totalPages - remainingPages) / totalPages}%`); } }); ``` ```bash # 또는 sqlite3 CLI sqlite3 app.db ".backup backup.db" # 또는 file copy + WAL (위험 — WAL checkpoint 후) sqlite3 app.db "PRAGMA wal_checkpoint(FULL)" cp app.db backup.db ``` ### VACUUM ```sql VACUUM; -- DELETE 후 빈 공간 정리 -- 큰 작업 — 한 번 lock ``` ```sql PRAGMA auto_vacuum = INCREMENTAL; PRAGMA incremental_vacuum; -- 점진 vacuum ``` ### EXPLAIN ```sql EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'a@b.com'; -- SCAN TABLE users (bad — table scan) -- SEARCH TABLE users USING INDEX users_email (good) ``` ### Foreign keys (default off!) ```sql PRAGMA foreign_keys = ON; -- 매 connection 필수 CREATE TABLE orders ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); ``` ### Mobile (iOS / Android) ```swift // iOS — GRDB 권장 import GRDB let dbQueue = try DatabaseQueue(path: "app.db") try dbQueue.write { db in try User(id: ..., email: ...).insert(db) } ``` ```kotlin // Android — Room (위 [[Android_Room_Patterns]]) @Database(entities = [User::class], version = 1) abstract class AppDb : RoomDatabase() ``` ### Use case ``` - Mobile app - Desktop app - Embedded device / IoT - 작은 web (single user) - Test fixture - Local cache - Edge worker (D1, Cloudflare) - Single-process server (read-heavy) ``` ### Server use case (놀라운) ``` "SQLite 가 production server 가능?" 가능. 단: - Single writer (queue) - Same machine (no network) - 작은 / 중간 (TB 미만) - LiteFS / Litestream 으로 replication ``` ```bash # Litestream — S3 backup + replica litestream replicate -config litestream.yml ``` ### Common pitfalls ``` 1. WAL 안 활성: lock 자주. 2. busy_timeout 0: SQLITE_BUSY 자주. 3. Foreign keys off (default): 의도 안 됨. 4. Concurrent writer 다중: 큰 lock. 5. Transaction 없는 batch INSERT: 매 commit fsync. 6. Big TEXT / BLOB: vacuum 비싸. 7. Migration breaks: TEXT type drift. ``` ### Cross-process locking ``` 같은 file 다중 process — OS file lock. WAL = read 다중 OK + 1 writer. NFS / network filesystem: SQLite 권장 X. ``` ### libSQL vs SQLite ``` libSQL = SQLite fork (Turso). + Replication (sync to remote) + HTTP API + TLS + 일부 extension built-in (vss) 대부분 호환. ``` ## 🤔 의사결정 기준 | 환경 | 추천 | |---|---| | Mobile | Native (Room / GRDB) | | Desktop | better-sqlite3 / node:sqlite | | Edge | D1 / libSQL / Turso | | Local dev | better-sqlite3 | | 작은 server | SQLite + Litestream | | 큰 / 다중 writer | Postgres | | Analytic | DuckDB | ## ❌ 안티패턴 - **WAL 안 활성**: lock 지옥. - **busy_timeout 0**: BUSY 자주. - **Foreign keys off**: 의도 깨짐. - **No transaction batch insert**: 100x 느림. - **NFS / 네트워크 file**: corruption 위험. - **VACUUM prod 큰 table**: lock — INCREMENTAL. - **Backup 으로 file cp**: WAL 미반영 — sqlite3 backup. ## 🤖 LLM 활용 힌트 - WAL + busy_timeout + foreign_keys 항상. - better-sqlite3 가 빠름 (sync). - Transaction 으로 batch. - Litestream 가 server SQLite + backup. ## 🔗 관련 문서 - [[DB_DuckDB_Embedded]] - [[DB_Serverless_Edge]] - [[Android_Room_Patterns]]