---
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]]
