前言
提到資料庫,大家腦中浮現的通常是 PostgreSQL、MySQL 或 MongoDB 這些需要獨立部署的伺服器級方案。SQLite 呢?「那不是手機上用的嗎?」「那個小東西能幹嘛?」——這是我常聽到的反應。
但事實是,SQLite 可能是全世界部署量最大的資料庫引擎。你的手機、瀏覽器、作業系統裡面都有它。而且近年來,隨著 Litestream、LiteFS 等工具的出現,SQLite 在伺服器端的應用場景也越來越廣。
這篇文章,我想替 SQLite 說句公道話,聊聊它的架構優勢、那些你可能不知道的進階功能,以及什麼場景下它其實是最佳選擇。
SQLite 的架構優勢
零伺服器、零設定
SQLite 不需要安裝、不需要設定、不需要啟動任何服務。它就是一個 C 語言函式庫,直接嵌入在你的應用程式裡。整個資料庫就是一個檔案。
import sqlite3
# 就這樣,資料庫就建好了
conn = sqlite3.connect("my_app.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Alice", "alice@example.com")
)
conn.commit()
# 查詢
for row in cursor.execute("SELECT * FROM users"):
print(row)
conn.close()
不需要連線字串、不需要認證、不需要 Docker。這對於快速原型開發、CLI 工具、桌面應用來說,是無法被取代的便利性。
效能出乎意料地好
很多人以為 SQLite 很慢。其實在單機場景下,SQLite 的讀取效能通常比 PostgreSQL 和 MySQL 還快——因為它沒有網路來回的開銷。
# 用 SQLite 的命令列工具做簡單的效能測試
sqlite3 bench.db <<'SQL'
CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT, num REAL);
-- 插入 100 萬筆資料
WITH RECURSIVE cnt(x) AS (
SELECT 1
UNION ALL
SELECT x + 1 FROM cnt WHERE x < 1000000
)
INSERT INTO test
SELECT x, 'value_' || x, random() * 1000.0
FROM cnt;
CREATE INDEX idx_num ON test(num);
-- 計時查詢
.timer on
SELECT COUNT(*) FROM test WHERE num > 500;
SELECT AVG(num) FROM test;
SELECT value FROM test WHERE id = 500000;
SQL
單檔案 = 簡單的備份和部署
# 備份?複製檔案就好
cp my_app.db my_app.db.backup
# 或用 SQLite 內建的備份功能(在線備份,不鎖定)
sqlite3 my_app.db ".backup backup.db"
# 搬遷環境?把 .db 檔案複製過去就好
scp my_app.db user@server:/app/data/
你可能不知道的進階功能
WAL 模式(Write-Ahead Logging)
預設的 SQLite 在寫入時會鎖定整個資料庫,但開啟 WAL 模式後,讀取和寫入可以同時進行:
import sqlite3
conn = sqlite3.connect("my_app.db")
# 開啟 WAL 模式(一次性設定,會持久化)
conn.execute("PRAGMA journal_mode=WAL")
# 其他推薦的 PRAGMA 設定
conn.execute("PRAGMA synchronous=NORMAL") # 效能和安全的平衡
conn.execute("PRAGMA busy_timeout=5000") # 等待鎖定 5 秒
conn.execute("PRAGMA cache_size=-64000") # 64MB 快取
conn.execute("PRAGMA foreign_keys=ON") # 啟用外鍵約束
conn.execute("PRAGMA temp_store=MEMORY") # 暫存表放記憶體
WAL 模式的好處:
- 讀取不會被寫入阻塞
- 寫入效能更好(特別是大量小寫入)
- 資料庫不容易損壞
JSON 支援
SQLite 3.38 以後內建了強大的 JSON 功能:
-- 建立包含 JSON 欄位的表
CREATE TABLE events (
id INTEGER PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入 JSON 資料
INSERT INTO events (event_type, payload) VALUES
('page_view', '{"url": "/home", "user_id": 1, "duration": 5.2}'),
('click', '{"element": "buy_button", "user_id": 2, "page": "/product/123"}'),
('page_view', '{"url": "/about", "user_id": 1, "duration": 3.1}'),
('purchase', '{"product_id": 123, "user_id": 2, "amount": 1500}');
-- 用 JSON 函式查詢
SELECT
event_type,
json_extract(payload, '$.user_id') AS user_id,
json_extract(payload, '$.url') AS url,
json_extract(payload, '$.duration') AS duration
FROM events
WHERE event_type = 'page_view'
AND json_extract(payload, '$.duration') > 4.0;
-- JSON 欄位建立索引(產生的欄位索引)
CREATE INDEX idx_events_user_id
ON events(json_extract(payload, '$.user_id'));
-- 聚合 JSON 資料
SELECT
json_extract(payload, '$.user_id') AS user_id,
COUNT(*) AS event_count,
json_group_array(event_type) AS event_types
FROM events
GROUP BY json_extract(payload, '$.user_id');
全文搜尋(FTS5)
-- 建立全文搜尋虛擬表
CREATE VIRTUAL TABLE articles_fts USING fts5(
title,
content,
content='articles', -- 指向原始表
content_rowid='id'
);
-- 手動同步資料
INSERT INTO articles_fts(rowid, title, content)
SELECT id, title, content FROM articles;
-- 全文搜尋
SELECT * FROM articles_fts WHERE articles_fts MATCH 'docker container';
-- BM25 排名
SELECT
rowid,
title,
rank
FROM articles_fts
WHERE articles_fts MATCH 'python web framework'
ORDER BY rank;
-- 片段高亮
SELECT
highlight(articles_fts, 0, '<b>', '</b>') AS title,
snippet(articles_fts, 1, '<b>', '</b>', '...', 50) AS content_preview
FROM articles_fts
WHERE articles_fts MATCH 'database performance';
窗口函式
SQLite 3.25 以後支援窗口函式,和 PostgreSQL 的語法完全相容:
-- 排名
SELECT
name,
score,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM players;
-- 移動平均
SELECT
date,
temperature,
AVG(temperature) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7day
FROM daily_weather;
-- 與前一筆的差異
SELECT
date,
revenue,
revenue - LAG(revenue) OVER (ORDER BY date) AS revenue_change,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY date)) * 100.0
/ LAG(revenue) OVER (ORDER BY date), 1
) AS change_pct
FROM monthly_revenue;
Litestream — SQLite 的即時複寫備份
Litestream 是一個讓 SQLite 可以即時備份到 S3(或相容儲存)的工具,讓它在伺服器端的可用性大幅提升。
安裝和設定
# macOS
brew install benbjohnson/litestream/litestream
# Linux
wget https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz
tar -xzf litestream-v0.3.13-linux-amd64.tar.gz
sudo mv litestream /usr/local/bin/
# litestream.yml
dbs:
- path: /data/my_app.db
replicas:
- type: s3
bucket: my-backup-bucket
path: backups/my_app
region: ap-northeast-1
# 也支援本地檔案系統
- type: file
path: /backup/my_app
# 啟動即時複寫
litestream replicate -config litestream.yml
# 從備份還原
litestream restore -config litestream.yml /data/my_app.db
# 搭配你的應用程式一起啟動
litestream replicate -config litestream.yml -exec "python app.py"
Docker 部署範例
# docker-compose.yml
version: '3.8'
services:
app:
build: .
volumes:
- app_data:/data
environment:
DATABASE_PATH: /data/app.db
LITESTREAM_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
LITESTREAM_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
volumes:
app_data:
# Dockerfile
FROM python:3.12-slim
# 安裝 Litestream
ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz /tmp/
RUN tar -xzf /tmp/litestream-v0.3.13-linux-amd64.tar.gz -C /usr/local/bin/
COPY litestream.yml /etc/litestream.yml
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
# 用 Litestream 包裹應用程式啟動
CMD ["litestream", "replicate", "-config", "/etc/litestream.yml", "-exec", "python app.py"]
SQLite 適合什麼場景?
非常適合
- CLI 工具和桌面應用:不需要使用者安裝資料庫
- 嵌入式系統和 IoT:資源有限的環境
- 快速原型和 MVP:開發速度最快
- 測試:不需要啟動外部服務
- 單機 Web 應用:流量不大的部落格、內部工具
- 資料分析腳本:比 CSV 強大,比正式資料庫輕便
- 行動應用:iOS 和 Android 都內建支援
不適合
- 高併發寫入:同一時間只能有一個寫入者
- 多機部署:沒有原生的複寫功能(但 LiteFS 正在解決這個問題)
- 超大資料量:理論上支援 281 TB,但實務上建議在數 GB 以內
- 需要細粒度權限控制:SQLite 沒有使用者/角色系統
效能參考數字
import sqlite3
import time
conn = sqlite3.connect(":memory:") # 記憶體資料庫,極致效能
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("CREATE TABLE bench (id INTEGER PRIMARY KEY, value TEXT, num REAL)")
# 寫入效能測試
start = time.time()
conn.execute("BEGIN")
for i in range(100000):
conn.execute("INSERT INTO bench VALUES (?, ?, ?)",
(i, f"value_{i}", i * 0.1))
conn.execute("COMMIT")
elapsed = time.time() - start
print(f"100K inserts: {elapsed:.2f}s ({100000/elapsed:.0f} rows/s)")
# 通常可達 50 萬+/秒(記憶體模式)
# 讀取效能測試
conn.execute("CREATE INDEX idx_num ON bench(num)")
start = time.time()
for _ in range(10000):
conn.execute("SELECT * FROM bench WHERE num BETWEEN 500 AND 600").fetchall()
elapsed = time.time() - start
print(f"10K range queries: {elapsed:.2f}s ({10000/elapsed:.0f} queries/s)")
實戰:用 SQLite 建一個 CLI 待辦事項工具
#!/usr/bin/env python3
"""一個用 SQLite 驅動的 CLI 待辦事項工具"""
import sqlite3
import sys
from datetime import datetime
from pathlib import Path
DB_PATH = Path.home() / ".todo.db"
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
conn.execute("""
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
priority TEXT DEFAULT 'medium' CHECK(priority IN ('low','medium','high')),
done INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP
)
""")
return conn
def add_todo(title, priority="medium"):
conn = get_conn()
conn.execute(
"INSERT INTO todos (title, priority) VALUES (?, ?)",
(title, priority)
)
conn.commit()
print(f"Added: {title} [{priority}]")
def list_todos(show_done=False):
conn = get_conn()
query = "SELECT * FROM todos"
if not show_done:
query += " WHERE done = 0"
query += " ORDER BY CASE priority WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END, created_at"
for row in conn.execute(query):
status = "[x]" if row["done"] else "[ ]"
print(f" {row['id']}. {status} [{row['priority']}] {row['title']}")
def complete_todo(todo_id):
conn = get_conn()
conn.execute(
"UPDATE todos SET done = 1, completed_at = ? WHERE id = ?",
(datetime.now().isoformat(), todo_id)
)
conn.commit()
print(f"Completed todo #{todo_id}")
def stats():
conn = get_conn()
row = conn.execute("""
SELECT
COUNT(*) AS total,
SUM(CASE WHEN done THEN 1 ELSE 0 END) AS completed,
SUM(CASE WHEN NOT done THEN 1 ELSE 0 END) AS pending,
SUM(CASE WHEN priority = 'high' AND NOT done THEN 1 ELSE 0 END) AS high_priority
FROM todos
""").fetchone()
print(f"Total: {row['total']} | Done: {row['completed']} | "
f"Pending: {row['pending']} | High Priority: {row['high_priority']}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: todo <add|list|done|stats> [args...]")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "add":
title = " ".join(sys.argv[2:])
add_todo(title)
elif cmd == "list":
list_todos(show_done="--all" in sys.argv)
elif cmd == "done":
complete_todo(int(sys.argv[2]))
elif cmd == "stats":
stats()
小結
SQLite 不是一個「玩具資料庫」,它是一個經過 20 多年打磨、有航空等級品質保證的嵌入式資料庫引擎。在適合的場景下,它比任何 client-server 架構的資料庫都簡單、都快。
我的經驗法則是:如果你的應用是單機部署、寫入不太頻繁(每秒數百次以下)、資料量在幾 GB 以內,那 SQLite 幾乎一定是最好的選擇。搭配 Litestream 做即時備份,可靠性也不輸給正式的資料庫伺服器。
別再低估它了。
延伸閱讀
- SQLite 官方文件
- Litestream 官網
- LiteFS — 分散式 SQLite
- SQLite is not a toy database
- 「Appropriate Uses For SQLite」(SQLite 官方的使用場景建議)