Skip to content

05 SQLite 实战指南 ​

本章通过真实案例展示 SQLite 在各种场景下的最佳实践,包括 Web 应用、移动开发、数据分析、并发处理、数据库迁移等。

5.1 Web 应用中的 SQLite ​

5.1.1 Flask 应用示例 ​

python
from flask import Flask, g, jsonify, request
import sqlite3

app = Flask(__name__)
DATABASE = 'myapp.sqlite'

def get_db():
    """获取数据库连接(每个请求一个连接)"""
    if 'db' not in g:
        g.db = sqlite3.connect(DATABASE)
        g.db.row_factory = sqlite3.Row
        # 优化配置
        g.db.execute('PRAGMA journal_mode=WAL')
        g.db.execute('PRAGMA foreign_keys=ON')
        g.db.execute('PRAGMA busy_timeout=5000')
    return g.db

@app.teardown_appcontext
def close_db(exception):
    """请求结束关闭连接"""
    db = g.pop('db', None)
    if db is not None:
        db.close()

def init_db():
    """初始化数据库"""
    db = get_db()
    db.executescript('''
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            username TEXT UNIQUE NOT NULL,
            email TEXT UNIQUE NOT NULL,
            password_hash TEXT NOT NULL,
            created_at TEXT DEFAULT (datetime('now'))
        );

        CREATE TABLE IF NOT EXISTS posts (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL,
            title TEXT NOT NULL,
            content TEXT,
            created_at TEXT DEFAULT (datetime('now')),
            FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
        );

        CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id);
        CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at);
    ''')

@app.route('/api/users', methods=['POST'])
def create_user():
    db = get_db()
    data = request.json
    try:
        with db:
            cursor = db.execute(
                'INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)',
                (data['username'], data['email'], data['password_hash'])
            )
        return jsonify({'id': cursor.lastrowid}), 201
    except sqlite3.IntegrityError:
        return jsonify({'error': 'Username or email already exists'}), 409

@app.route('/api/users/<int:user_id>/posts')
def get_user_posts(user_id):
    db = get_db()
    posts = db.execute('''
        SELECT p.*, u.username
        FROM posts p
        JOIN users u ON p.user_id = u.id
        WHERE p.user_id = ?
        ORDER BY p.created_at DESC
        LIMIT 20
    ''', (user_id,)).fetchall()
    return jsonify([dict(post) for post in posts])

if __name__ == '__main__':
    with app.app_context():
        init_db()
    app.run(debug=True)

5.1.2 Django 配置 ​

python
# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
        'OPTIONS': {
            'timeout': 5,
            'init_command': '''
                PRAGMA journal_mode=WAL;
                PRAGMA synchronous=NORMAL;
                PRAGMA foreign_keys=ON;
                PRAGMA cache_size=-20000;
            ''',
        },
    }
}

5.1.3 FastAPI 示例 ​

python
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
import sqlite3
from contextlib import contextmanager

app = FastAPI()

DATABASE = 'myapp.sqlite'

@contextmanager
def get_db():
    """数据库连接上下文管理器"""
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    conn.execute('PRAGMA journal_mode=WAL')
    conn.execute('PRAGMA foreign_keys=ON')
    conn.execute('PRAGMA busy_timeout=5000')
    try:
        yield conn
    finally:
        conn.close()

class UserCreate(BaseModel):
    username: str
    email: str

@app.post('/users')
def create_user(user: UserCreate):
    with get_db() as conn:
        try:
            with conn:
                cursor = conn.execute(
                    'INSERT INTO users (username, email) VALUES (?, ?)',
                    (user.username, user.email)
                )
            return {'id': cursor.lastrowid}
        except sqlite3.IntegrityError:
            raise HTTPException(status_code=409, detail='User already exists')

@app.get('/users/{user_id}')
def get_user(user_id: int):
    with get_db() as conn:
        user = conn.execute(
            'SELECT * FROM users WHERE id = ?', (user_id,)
        ).fetchone()
        if not user:
            raise HTTPException(status_code=404, detail='User not found')
        return dict(user)

5.2 移动应用开发 ​

5.2.1 Android(Kotlin) ​

kotlin
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class AppDatabase(context: Context) : SQLiteOpenHelper(
    context, DATABASE_NAME, null, DATABASE_VERSION
) {
    companion object {
        const val DATABASE_NAME = "myapp.db"
        const val DATABASE_VERSION = 1
    }

    override fun onCreate(db: SQLiteDatabase) {
        // 启用 WAL 模式
        db.enableWriteAheadLogging()

        // 创建表
        db.execSQL("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT UNIQUE,
                created_at TEXT DEFAULT (datetime('now'))
            )
        """)

        db.execSQL("""
            CREATE TABLE notes (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                title TEXT NOT NULL,
                content TEXT,
                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
            )
        """)

        // 创建索引
        db.execSQL("CREATE INDEX idx_notes_user_id ON notes(user_id)")
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        if (oldVersion < 2) {
            db.execSQL("ALTER TABLE users ADD COLUMN phone TEXT")
        }
    }

    override fun onConfigure(db: SQLiteDatabase) {
        super.onConfigure(db)
        db.setForeignKeyConstraintsEnabled(true)
    }
}

// 使用示例
class UserRepository(private val db: AppDatabase) {
    fun insertUser(name: String, email: String): Long {
        val values = ContentValues().apply {
            put("name", name)
            put("email", email)
        }
        return db.writableDatabase.insert("users", null, values)
    }

    fun getUser(userId: Long): User? {
        val cursor = db.readableDatabase.rawQuery(
            "SELECT * FROM users WHERE id = ?",
            arrayOf(userId.toString())
        )
        return cursor.use {
            if (it.moveToFirst()) {
                User(
                    id = it.getLong(0),
                    name = it.getString(1),
                    email = it.getString(2)
                )
            } else null
        }
    }
}

5.2.2 iOS(Swift) ​

swift
import SQLite3

class DatabaseManager {
    private var db: OpaquePointer?
    private let dbPath: String

    init() {
        let fileManager = FileManager.default
        let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
        dbPath = documentsDirectory.appendingPathComponent("myapp.sqlite").path

        openDatabase()
        createTables()
    }

    private func openDatabase() {
        if sqlite3_open(dbPath, &db) != SQLITE_OK {
            print("Error opening database")
            return
        }

        // 启用 WAL 模式
        executeQuery("PRAGMA journal_mode=WAL")
        executeQuery("PRAGMA foreign_keys=ON")
        executeQuery("PRAGMA busy_timeout=5000")
    }

    private func createTables() {
        let createUsersTable = """
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT UNIQUE,
                created_at TEXT DEFAULT (datetime('now'))
            )
        """
        executeQuery(createUsersTable)

        let createNotesTable = """
            CREATE TABLE IF NOT EXISTS notes (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id INTEGER NOT NULL,
                title TEXT NOT NULL,
                content TEXT,
                FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
            )
        """
        executeQuery(createNotesTable)

        executeQuery("CREATE INDEX IF NOT EXISTS idx_notes_user_id ON notes(user_id)")
    }

    private func executeQuery(_ query: String) {
        var errMsg: UnsafeMutablePointer<CChar>?
        if sqlite3_exec(db, query, nil, nil, &errMsg) != SQLITE_OK {
            let errorMessage = String(cString: errMsg!)
            print("Error executing query: \(errorMessage)")
            sqlite3_free(errMsg)
        }
    }

    func insertUser(name: String, email: String) -> Int64? {
        let insertQuery = "INSERT INTO users (name, email) VALUES (?, ?)"
        var statement: OpaquePointer?

        if sqlite3_prepare_v2(db, insertQuery, -1, &statement, nil) == SQLITE_OK {
            sqlite3_bind_text(statement, 1, (name as NSString).utf8String, -1, nil)
            sqlite3_bind_text(statement, 2, (email as NSString).utf8String, -1, nil)

            if sqlite3_step(statement) == SQLITE_DONE {
                let id = sqlite3_last_insert_rowid(db)
                sqlite3_finalize(statement)
                return id
            }
        }

        sqlite3_finalize(statement)
        return nil
    }

    func getUsers() -> [(id: Int64, name: String, email: String)] {
        var users: [(Int64, String, String)] = []
        let query = "SELECT id, name, email FROM users ORDER BY created_at DESC"
        var statement: OpaquePointer?

        if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
            while sqlite3_step(statement) == SQLITE_ROW {
                let id = sqlite3_column_int64(statement, 0)
                let name = String(cString: sqlite3_column_text(statement, 1))
                let email = String(cString: sqlite3_column_text(statement, 2))
                users.append((id, name, email))
            }
        }

        sqlite3_finalize(statement)
        return users
    }
}

5.3 数据分析与处理 ​

5.3.1 CSV 数据处理 ​

python
import sqlite3
import csv
import pandas as pd

# 方法 1:使用 pandas(简单)
df = pd.read_csv('large_data.csv')
conn = sqlite3.connect('analysis.db')
df.to_sql('data', conn, index=False, if_exists='replace')

# 使用 SQL 分析
result = pd.read_sql('''
    SELECT category,
           COUNT(*) as count,
           AVG(value) as avg_value,
           SUM(value) as total
    FROM data
    GROUP BY category
    ORDER BY total DESC
''', conn)

# 方法 2:纯 SQLite(内存效率更高)
conn = sqlite3.connect('analysis.db')
conn.execute('PRAGMA journal_mode=WAL')

# 创建表
conn.execute('''
    CREATE TABLE IF NOT EXISTS data (
        id INTEGER PRIMARY KEY,
        category TEXT,
        value REAL,
        date TEXT
    )
''')

# 批量导入
with open('large_data.csv', 'r') as f:
    reader = csv.DictReader(f)
    batch = []
    for row in reader:
        batch.append((row['category'], float(row['value']), row['date']))
        if len(batch) >= 10000:
            conn.executemany(
                'INSERT INTO data (category, value, date) VALUES (?, ?, ?)',
                batch
            )
            batch = []
    if batch:
        conn.executemany(
            'INSERT INTO data (category, value, date) VALUES (?, ?, ?)',
            batch
        )

conn.commit()

# 创建索引加速分析
conn.execute('CREATE INDEX idx_data_category ON data(category)')
conn.execute('CREATE INDEX idx_data_date ON data(date)')

# 分析查询
results = conn.execute('''
    WITH monthly_stats AS (
        SELECT
            strftime('%Y-%m', date) as month,
            category,
            COUNT(*) as count,
            AVG(value) as avg_value,
            SUM(value) as total
        FROM data
        GROUP BY month, category
    )
    SELECT * FROM monthly_stats
    WHERE total > (SELECT AVG(total) * 2 FROM monthly_stats)
    ORDER BY total DESC
''').fetchall()

conn.close()

5.3.2 JSON 数据处理 ​

python
import sqlite3
import json

conn = sqlite3.connect('json_data.db')

# 创建包含 JSON 列的表
conn.execute('''
    CREATE TABLE IF NOT EXISTS events (
        id INTEGER PRIMARY KEY,
        event_type TEXT,
        event_data TEXT,  -- JSON 格式
        created_at TEXT DEFAULT (datetime('now'))
    )
''')

# 插入 JSON 数据
events = [
    ('click', {'page': '/home', 'button': 'signup', 'user_id': 123}),
    ('view', {'page': '/products', 'duration': 45, 'user_id': 456}),
    ('purchase', {'product_id': 789, 'amount': 99.99, 'user_id': 123})
]

conn.executemany(
    'INSERT INTO events (event_type, event_data) VALUES (?, ?)',
    [(e[0], json.dumps(e[1])) for e in events]
)
conn.commit()

# 查询 JSON 数据
results = conn.execute('''
    SELECT
        event_type,
        json_extract(event_data, '$.user_id') as user_id,
        json_extract(event_data, '$.page') as page
    FROM events
    WHERE json_extract(event_data, '$.user_id') = 123
''').fetchall()

# 聚合 JSON 数据
results = conn.execute('''
    SELECT
        event_type,
        COUNT(*) as count,
        json_group_array(json_extract(event_data, '$.user_id')) as user_ids
    FROM events
    GROUP BY event_type
''').fetchall()

conn.close()

5.4 并发处理模式 ​

5.4.1 多进程访问 ​

python
import sqlite3
import multiprocessing
import time

def worker(worker_id, db_path):
    """工作进程"""
    conn = sqlite3.connect(db_path, timeout=10)
    conn.execute('PRAGMA journal_mode=WAL')
    conn.execute('PRAGMA busy_timeout=10000')

    for i in range(100):
        try:
            with conn:
                conn.execute(
                    'INSERT INTO logs (worker_id, message) VALUES (?, ?)',
                    (worker_id, f'Message {i} from worker {worker_id}')
                )
        except sqlite3.OperationalError as e:
            print(f'Worker {worker_id} error: {e}')
            time.sleep(0.1)

    conn.close()

if __name__ == '__main__':
    db_path = 'multi_process.db'

    # 初始化数据库
    conn = sqlite3.connect(db_path)
    conn.execute('PRAGMA journal_mode=WAL')
    conn.execute('''
        CREATE TABLE IF NOT EXISTS logs (
            id INTEGER PRIMARY KEY,
            worker_id INTEGER,
            message TEXT,
            created_at TEXT DEFAULT (datetime('now'))
        )
    ''')
    conn.close()

    # 启动多个进程
    processes = []
    for i in range(5):
        p = multiprocessing.Process(target=worker, args=(i, db_path))
        p.start()
        processes.append(p)

    for p in processes:
        p.join()

    # 验证结果
    conn = sqlite3.connect(db_path)
    count = conn.execute('SELECT COUNT(*) FROM logs').fetchone()[0]
    print(f'Total logs: {count}')  # 应该是 500
    conn.close()

5.4.2 读写分离 ​

python
import sqlite3
import threading

class ReadWriteSplit:
    """读写分离模式"""

    def __init__(self, db_path):
        self.db_path = db_path
        self.write_lock = threading.Lock()

        # 初始化
        self._execute_write('PRAGMA journal_mode=WAL')

    def _get_read_conn(self):
        """获取只读连接"""
        conn = sqlite3.connect(
            f'file:{self.db_path}?mode=ro',
            uri=True,
            check_same_thread=False
        )
        conn.row_factory = sqlite3.Row
        return conn

    def _get_write_conn(self):
        """获取读写连接"""
        conn = sqlite3.connect(self.db_path, check_same_thread=False)
        conn.row_factory = sqlite3.Row
        conn.execute('PRAGMA journal_mode=WAL')
        conn.execute('PRAGMA busy_timeout=5000')
        return conn

    def _execute_write(self, sql, params=()):
        """执行写操作"""
        with self.write_lock:
            conn = self._get_write_conn()
            try:
                with conn:
                    conn.execute(sql, params)
            finally:
                conn.close()

    def read(self, sql, params=()):
        """读操作"""
        conn = self._get_read_conn()
        try:
            return conn.execute(sql, params).fetchall()
        finally:
            conn.close()

    def write(self, sql, params=()):
        """写操作"""
        self._execute_write(sql, params)

# 使用
db = ReadWriteSplit('myapp.db')

# 读操作(可并发)
users = db.read('SELECT * FROM users WHERE age > ?', (18,))

# 写操作(串行化)
db.write('INSERT INTO users (name, age) VALUES (?, ?)', ('Alice', 25))

5.5 数据库迁移 ​

5.5.1 版本管理 ​

python
import sqlite3

def get_version(conn):
    """获取数据库版本"""
    try:
        result = conn.execute('PRAGMA user_version').fetchone()
        return result[0]
    except:
        return 0

def set_version(conn, version):
    """设置数据库版本"""
    conn.execute(f'PRAGMA user_version = {version}')

def migrate(db_path):
    """执行迁移"""
    conn = sqlite3.connect(db_path)
    current_version = get_version(conn)

    migrations = [
        # 版本 1:初始表
        (1, [
            '''CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT UNIQUE
            )''',
            '''CREATE TABLE IF NOT EXISTS posts (
                id INTEGER PRIMARY KEY,
                user_id INTEGER,
                title TEXT,
                FOREIGN KEY (user_id) REFERENCES users(id)
            )'''
        ]),

        # 版本 2:添加 phone 字段
        (2, [
            'ALTER TABLE users ADD COLUMN phone TEXT',
            'CREATE INDEX idx_users_phone ON users(phone)'
        ]),

        # 版本 3:添加 created_at
        (3, [
            "ALTER TABLE users ADD COLUMN created_at TEXT DEFAULT (datetime('now'))",
            "ALTER TABLE posts ADD COLUMN created_at TEXT DEFAULT (datetime('now'))"
        ])
    ]

    for version, queries in migrations:
        if current_version < version:
            print(f'Migrating to version {version}...')
            try:
                for query in queries:
                    conn.execute(query)
                set_version(conn, version)
                conn.commit()
                print(f'Successfully migrated to version {version}')
            except Exception as e:
                conn.rollback()
                print(f'Migration failed: {e}')
                raise

    conn.close()

# 使用
migrate('myapp.db')

5.5.2 Alembic 集成 ​

python
# alembic.ini
[alembic]
script_location = alembic
sqlalchemy.url = sqlite:///myapp.db

# alembic/env.py
from alembic import context
from sqlalchemy import engine_from_config, pool

config = context.config
target_metadata = None

def run_migrations_online():
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix='sqlalchemy.',
        poolclass=pool.NullPool,
        connect_args={
            'timeout': 10,
            'check_same_thread': False
        }
    )

    with connectable.connect() as connection:
        # 启用 WAL 模式
        connection.execute("PRAGMA journal_mode=WAL")
        connection.execute("PRAGMA foreign_keys=ON")

        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            render_as_batch=True  # SQLite 需要 batch 模式
        )

        with context.begin_transaction():
            context.run_migrations()

# 迁移脚本示例
# alembic/versions/001_initial.py
def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('name', sa.String(100), nullable=False),
        sa.Column('email', sa.String(255), unique=True)
    )

def downgrade():
    op.drop_table('users')

5.6 备份与恢复策略 ​

5.6.1 自动备份脚本 ​

python
import sqlite3
import shutil
from datetime import datetime
import os

class SQLiteBackup:
    def __init__(self, db_path, backup_dir):
        self.db_path = db_path
        self.backup_dir = backup_dir
        os.makedirs(backup_dir, exist_ok=True)

    def backup(self):
        """创建备份"""
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        backup_path = os.path.join(
            self.backup_dir,
            f'backup_{timestamp}.sqlite'
        )

        # 使用 SQLite 备份 API(安全,不锁定数据库)
        source = sqlite3.connect(self.db_path)
        target = sqlite3.connect(backup_path)

        try:
            source.backup(target)
            print(f'Backup created: {backup_path}')

            # 验证备份
            self._verify_backup(backup_path)

        finally:
            source.close()
            target.close()

        return backup_path

    def _verify_backup(self, backup_path):
        """验证备份完整性"""
        conn = sqlite3.connect(backup_path)
        try:
            result = conn.execute('PRAGMA integrity_check').fetchone()
            if result[0] == 'ok':
                print('Backup integrity: OK')
            else:
                print(f'Backup integrity check failed: {result}')
        finally:
            conn.close()

    def cleanup(self, keep_days=7):
        """清理旧备份"""
        cutoff = datetime.now().timestamp() - (keep_days * 86400)

        for filename in os.listdir(self.backup_dir):
            if filename.startswith('backup_') and filename.endswith('.sqlite'):
                filepath = os.path.join(self.backup_dir, filename)
                if os.path.getmtime(filepath) < cutoff:
                    os.remove(filepath)
                    print(f'Removed old backup: {filename}')

# 使用
backup_manager = SQLiteBackup('myapp.db', '/backups/sqlite')
backup_manager.backup()
backup_manager.cleanup(keep_days=30)

5.6.2 增量备份 ​

python
import sqlite3
import subprocess
from datetime import datetime

class IncrementalBackup:
    def __init__(self, db_path, backup_dir):
        self.db_path = db_path
        self.backup_dir = backup_dir

    def full_backup(self):
        """完整备份"""
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        backup_path = f'{self.backup_dir}/full_{timestamp}.sqlite'

        source = sqlite3.connect(self.db_path)
        target = sqlite3.connect(backup_path)
        source.backup(target)
        source.close()
        target.close()

        # 记录备份位置
        conn = sqlite3.connect(self.db_path)
        page_count = conn.execute('PRAGMA page_count').fetchone()[0]
        conn.close()

        with open(f'{backup_path}.meta', 'w') as f:
            f.write(f'page_count={page_count}\n')
            f.write(f'timestamp={timestamp}\n')

        return backup_path

    def wal_checkpoint(self):
        """检查点 WAL 文件"""
        conn = sqlite3.connect(self.db_path)
        conn.execute('PRAGMA wal_checkpoint(TRUNCATE)')
        conn.close()

# 定时任务示例(cron)
# 每小时执行一次 WAL 检查点
# 0 * * * * python /scripts/wal_checkpoint.py
# 每天凌晨 2 点完整备份
# 0 2 * * * python /scripts/full_backup.py

5.7 常见问题解决 ​

5.7.1 "database is locked" 错误 ​

python
import sqlite3
import time

def safe_execute(conn, sql, params=(), max_retries=5):
    """安全执行,处理数据库锁定"""
    for attempt in range(max_retries):
        try:
            with conn:
                return conn.execute(sql, params)
        except sqlite3.OperationalError as e:
            if 'database is locked' in str(e):
                if attempt < max_retries - 1:
                    wait_time = 0.1 * (2 ** attempt)  # 指数退避
                    print(f'Database locked, retrying in {wait_time}s...')
                    time.sleep(wait_time)
                else:
                    raise
            else:
                raise

# 预防措施
conn = sqlite3.connect('myapp.db')
conn.execute('PRAGMA journal_mode=WAL')
conn.execute('PRAGMA busy_timeout=10000')  # 10 秒超时
conn.execute('PRAGMA synchronous=NORMAL')

5.7.2 数据库损坏恢复 ​

python
import sqlite3
import subprocess

def check_integrity(db_path):
    """检查数据库完整性"""
    conn = sqlite3.connect(db_path)
    result = conn.execute('PRAGMA integrity_check').fetchall()
    conn.close()

    if result[0][0] == 'ok':
        print('Database integrity: OK')
        return True
    else:
        print('Database corruption detected:')
        for row in result:
            print(f'  {row[0]}')
        return False

def recover_database(db_path, recovered_path):
    """恢复损坏的数据库"""
    # 方法 1:使用 .recover 命令
    try:
        result = subprocess.run(
            ['sqlite3', db_path, '.recover'],
            capture_output=True,
            text=True
        )

        if result.returncode == 0:
            # 将恢复的 SQL 导入新数据库
            with open(recovered_path, 'w') as f:
                f.write(result.stdout)

            # 创建新数据库
            new_db = sqlite3.connect(f'{recovered_path}.db')
            new_db.executescript(result.stdout)
            new_db.close()

            print(f'Recovery successful: {recovered_path}.db')
            return True
    except Exception as e:
        print(f'Recovery failed: {e}')

    # 方法 2:导出可读取的数据
    try:
        conn = sqlite3.connect(db_path)
        tables = conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table'"
        ).fetchall()

        recovered_conn = sqlite3.connect(recovered_path)

        for (table_name,) in tables:
            try:
                # 尝试读取表数据
                data = conn.execute(f'SELECT * FROM {table_name}').fetchall()

                # 获取表结构
                schema = conn.execute(
                    f"SELECT sql FROM sqlite_master WHERE type='table' AND name='{table_name}'"
                ).fetchone()[0]

                # 在恢复的数据库中创建表
                recovered_conn.execute(schema)

                # 插入数据
                if data:
                    placeholders = ','.join(['?' for _ in data[0]])
                    recovered_conn.executemany(
                        f'INSERT INTO {table_name} VALUES ({placeholders})',
                        data
                    )

                print(f'Recovered table: {table_name} ({len(data)} rows)')

            except Exception as e:
                print(f'Failed to recover table {table_name}: {e}')

        conn.close()
        recovered_conn.close()

    except Exception as e:
        print(f'Recovery failed: {e}')
        return False

    return True

# 使用
if not check_integrity('myapp.db'):
    recover_database('myapp.db', 'recovered.sql')

5.7.3 性能问题排查 ​

python
import sqlite3

def diagnose_performance(db_path):
    """诊断性能问题"""
    conn = sqlite3.connect(db_path)

    print('=== SQLite Performance Diagnosis ===\n')

    # 1. 基本配置
    print('1. Configuration:')
    for pragma in ['journal_mode', 'synchronous', 'cache_size',
                   'page_size', 'wal_autocheckpoint']:
        result = conn.execute(f'PRAGMA {pragma}').fetchone()
        print(f'   {pragma}: {result[0]}')

    # 2. 数据库大小
    print('\n2. Database Size:')
    page_count = conn.execute('PRAGMA page_count').fetchone()[0]
    page_size = conn.execute('PRAGMA page_size').fetchone()[0]
    db_size_mb = (page_count * page_size) / (1024 * 1024)
    print(f'   Total size: {db_size_mb:.2f} MB')
    print(f'   Page count: {page_count}')
    print(f'   Page size: {page_size} bytes')

    # 3. 表统计
    print('\n3. Table Statistics:')
    tables = conn.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
    ).fetchall()

    for (table_name,) in tables:
        count = conn.execute(f'SELECT COUNT(*) FROM {table_name}').fetchone()[0]
        print(f'   {table_name}: {count} rows')

    # 4. 索引统计
    print('\n4. Index Statistics:')
    indexes = conn.execute(
        "SELECT name, tbl_name FROM sqlite_master WHERE type='index'"
    ).fetchall()

    for idx_name, table_name in indexes:
        print(f'   {idx_name} on {table_name}')

    # 5. WAL 状态
    print('\n5. WAL Status:')
    wal_size = conn.execute('PRAGMA wal_checkpoint').fetchone()
    print(f'   WAL checkpoint: {wal_size}')

    # 6. 碎片检查
    print('\n6. Fragmentation:')
    freelist = conn.execute('PRAGMA freelist_count').fetchone()[0]
    print(f'   Free pages: {freelist}')
    if freelist > 0:
        print(f'   Suggestion: Run VACUUM to reclaim {freelist * page_size / 1024 / 1024:.2f} MB')

    conn.close()

# 使用
diagnose_performance('myapp.db')

5.8 最佳实践总结 ​

5.8.1 架构设计 ​

text
✅ 推荐:
  · 单写者模式(避免写入冲突)
  · 读写分离(多个只读连接)
  · 连接池(复用连接)
  · 定期备份(自动 + 手动)
  · 版本管理(user_version)

❌ 避免:
  · 多进程同时写入
  · 长时间持有连接
  · 大事务(拆分小事务)
  · 忽略错误处理
  · 不备份数据库

5.8.2 代码规范 ​

python
# ✅ 好的实践
class DatabaseManager:
    def __init__(self, db_path):
        self.db_path = db_path

    def get_connection(self):
        """获取优化配置的連接"""
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        conn.execute('PRAGMA journal_mode=WAL')
        conn.execute('PRAGMA foreign_keys=ON')
        conn.execute('PRAGMA busy_timeout=5000')
        return conn

    def execute(self, sql, params=()):
        """执行写操作"""
        conn = self.get_connection()
        try:
            with conn:
                cursor = conn.execute(sql, params)
                return cursor.lastrowid
        finally:
            conn.close()

    def query(self, sql, params=()):
        """执行查询"""
        conn = self.get_connection()
        try:
            return conn.execute(sql, params).fetchall()
        finally:
            conn.close()

# ❌ 避免的做法
# 1. 不关闭连接
conn = sqlite3.connect('mydb.db')
conn.execute('INSERT INTO ...')
# 忘记关闭!

# 2. 不用事务
for item in items:
    conn.execute('INSERT INTO ...', item)  # 每行自动提交

# 3. SQL 注入
conn.execute(f"SELECT * FROM users WHERE name = '{user_input}'")

# 4. 忽略错误
try:
    conn.execute('INSERT INTO ...')
except:
    pass  # 忽略所有错误

5.8.3 部署检查清单 ​

text
部署前检查:
  □ 启用 WAL 模式
  □ 启用外键约束
  □ 设置 busy_timeout
  □ 创建必要的索引
  □ 测试数据库迁移
  □ 配置自动备份
  □ 设置监控告警
  □ 文档化恢复流程

性能检查:
  □ 使用 EXPLAIN QUERY PLAN 检查查询
  □ 批量操作使用事务
  □ 预编译常用查询
  □ 定期 ANALYZE
  □ 定期 VACUUM(如果需要)

安全检查:
  □ 使用参数化查询
  □ 限制文件权限
  □ 加密敏感数据(SQLCipher)
  □ 定期备份到异地

本章小结 ​

  • Web 应用:每个请求一个连接,启用 WAL,使用上下文管理器
  • 移动应用:启用 WAL,使用原生 API,处理版本升级
  • 数据分析:批量导入,创建索引,使用窗口函数
  • 并发处理:单写者模式,读写分离,忙等超时
  • 数据库迁移:版本管理,Alembic 集成
  • 备份恢复:自动备份,增量备份,完整性检查
  • 问题排查:数据库锁定、损坏恢复、性能诊断
  • 最佳实践:连接管理、事务使用、参数化查询

练习:把「05 SQLite 实战指南」变成可验证能力 ​

  1. 从本文任选一个示例,在可丢弃环境重跑;记录运行时/服务版本、输入、关键输出和清理动作。
  2. 保持其他条件不变,只制造一个边界:空值、重复请求、并发、超时、断连、权限不足或资源耗尽。
  3. 用四句话复盘:现象是什么、原理在哪一层、证据是什么、生产中如何止损。

验收清单

  • [ ] 示例能从空环境重复执行,必要依赖和版本已写明。
  • [ ] 同时保留成功路径与至少一个失败路径,不用“应该没问题”冒充证据。
  • [ ] 结论写明适用边界,并能链接回对应学习路线的能力域。
  • [ ] 练习结束后已清理临时数据、凭据、端口或进程。

对照 数据库练习答案与复盘尺。先交自己的证据再翻答案;答案是镜子,不是传送门。

用心记录每一步成长