[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user