363 lines
8.0 KiB
Markdown
363 lines
8.0 KiB
Markdown
---
|
|
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]]
|