Files
2nd/10_Wiki/Topics/Coding/DB_SQLite_Patterns.md
T
2026-05-09 21:08:02 +09:00

8.0 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
db-sqlite-patterns SQLite 패턴 — Embedded / WAL / 동시성 Coding draft B conceptual 2026-05-09 2026-05-09
database
sqlite
embedded
vibe-coding
language applicable_to
TS / SQL
Backend
Mobile
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)

yarn add better-sqlite3
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)

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

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)

import { createClient } from '@libsql/client';

const turso = createClient({ url: 'file:app.db' });
await turso.execute('CREATE TABLE ...');

→ SQLite + replication + embedded replica.

WAL mode (필수)

PRAGMA journal_mode = WAL;
-- Read 가 write 차단 X
-- Concurrent read OK
-- 하지만 single writer
PRAGMA synchronous = NORMAL;
-- WAL + NORMAL = FAST + 보통 안전 (power loss 일부 위험 OK)
-- FULL = 더 안전, 느림
-- OFF = 매우 빠름, 위험

Busy timeout

PRAGMA busy_timeout = 5000;
-- Write lock 5초 대기 후 SQLITE_BUSY
// 또는 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

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)

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

BEGIN IMMEDIATE;
-- 즉시 write lock — 다른 process 가 read OK 그러나 write X
-- 작업
COMMIT;

→ 큰 transaction 시 안전.

Schema

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

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)

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)

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

// Online backup
db.backup('backup.db', { progress: ({ totalPages, remainingPages }) => {
  console.log(`${100 * (totalPages - remainingPages) / totalPages}%`);
} });
# 또는 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

VACUUM;
-- DELETE 후 빈 공간 정리
-- 큰 작업 — 한 번 lock
PRAGMA auto_vacuum = INCREMENTAL;
PRAGMA incremental_vacuum;
-- 점진 vacuum

EXPLAIN

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!)

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)

// iOS — GRDB 권장
import GRDB

let dbQueue = try DatabaseQueue(path: "app.db")
try dbQueue.write { db in
    try User(id: ..., email: ...).insert(db)
}
// 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
# 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.

🔗 관련 문서