前言

提到資料庫,大家腦中浮現的通常是 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 做即時備份,可靠性也不輸給正式的資料庫伺服器。

別再低估它了。

延伸閱讀