
1. 为什么 SQLite Python 是我写脚本、做原型、搭小工具时的第一选择“How to Use SQLite in Python”——这个标题看起来像教科书里的章节名但在我过去十年带团队做数据中台、给中小企业开发内部管理工具、甚至帮设计师朋友写自动归档图层脚本的过程中它从来不是“学个语法”的事而是一把能立刻拆开、上油、拧紧、投入使用的多功能螺丝刀。SQLite 不是“轻量级数据库”的谦称它是唯一一个不依赖服务进程、不需管理员权限、单文件可读写、且默认内置于 Python 标准库的持久化引擎。你import sqlite3的那一刻就已经拥有了一个 ACID 合规、支持事务、可加密配合扩展、能处理百万行数据的嵌入式关系型存储——不需要pip install任何东西不需要配置localhost:5432更不需要为一个临时报表工具去申请公司 DBA 开通 PostgreSQL 权限。我见过太多人绕远路用 CSV 存用户配置结果某天字段里出现逗号和换行解析全乱用 JSON 文件存多表关联数据改个状态要手动遍历三个文件甚至用 pickle 序列化字典结果 Python 版本一升级反序列化直接报错。而 SQLite 在这些场景里解决的从来不是“能不能存”而是“怎么存得稳、查得快、改得准、传得动”。它天然适配 Python 的数据思维tuple 对应行list 对应结果集dict 可映射字段名?占位符防注入比手写字符串拼接安全十倍。更重要的是它的.db文件就是普通二进制文件——你可以把它拖进微信发给同事双击用 DB Browser 打开查数据压缩进 ZIP 当项目附件甚至 git commit 到代码仓库里当然大文件除外。这不是“将就”而是对中小规模数据流最务实的尊重。如果你正在写一个需要记住上次运行时间的爬虫、一个记录会议纪要的桌面小工具、一个离线可用的问卷收集器或者只是想把 pandas 处理完的分析结果稳稳落盘而不是靠to_csv再祈祷 Excel 不乱码——那么这篇内容就是为你写的。它不讲抽象理论不堆砌 API 文档只讲我在真实项目里反复验证过的操作链从创建第一个表开始到处理中文乱码、优化批量插入速度、规避 WAL 模式下的文件锁冲突、用 row factory 实现字典式取值、以及最关键的——如何判断“该不该换用其他数据库”。下面我们就从最基础却最容易踩坑的初始化环节开始。2. 数据库初始化与连接管理别让connect()成为性能瓶颈和资源泄漏源2.1 连接对象的本质一个文件句柄 事务上下文很多人第一次写conn sqlite3.connect(app.db)时以为这只是“打开数据库”其实它背后做了三件事操作系统级文件打开如果文件不存在创建空文件存在则以读写模式打开默认PRAGMA journal_mode DELETE初始化内存结构加载数据库 schema表、索引定义到内存缓存绑定事务上下文每个连接默认开启隐式事务所有 DML 操作都在此上下文中执行直到commit()或rollback()。提示sqlite3.connect()返回的对象不是“数据库本身”而是当前进程内对该数据库文件的一个独立访问会话。多个connect()调用会创建多个独立连接它们共享底层文件但事务隔离、缓存、busy timeout 等参数互不影响。这点在多线程场景中尤为关键。我曾在一个日志分析脚本中犯过典型错误每处理一条日志就connect()→execute()→close()。实测 10 万条日志耗时 47 秒。改成复用单个连接后降到 1.8 秒——差距来自每次connect()都要重新打开文件、校验页头、加载 schema 缓存。SQLite 的设计哲学是“连接廉价但非免费”合理复用才是正解。2.2 连接参数的实战取舍timeout、check_same_thread与uritimeout解决“database is locked” 的第一道防线当你在多线程/多进程环境中写入数据大概率会遇到OperationalError: database is locked。这不是 bug而是 SQLite 的写入排他锁机制在起作用。timeout参数单位秒定义了连接在获取写锁失败时最多等待多久再抛异常。# 错误示范不设 timeout瞬间报错 conn sqlite3.connect(data.db) # 正确做法根据业务容忍度设置 conn sqlite3.connect(data.db, timeout10) # 最多等 10 秒但要注意timeout不是万能解药。如果另一个连接持有写锁超过 10 秒比如在执行一个超长UPDATE你的连接仍会失败。此时真正该做的是——检查长事务是否合理或拆分大操作。我在做财务凭证批量导入时曾把 5 万条记录塞进一个事务导致 UI 卡死、其他查询被锁。后来改成每 500 条commit()一次timeout3就完全够用。check_same_thread多线程场景的“开关”Python 的sqlite3模块默认禁止跨线程使用同一连接对象check_same_threadTrue。这是为了防止 C 层面的 SQLite 句柄被并发访问导致崩溃。但很多 Web 框架如 Flask或异步任务Celery需要在线程间传递连接。# 安全方案禁用检查但必须确保连接不被并发写入 conn sqlite3.connect(app.db, check_same_threadFalse) # 更推荐用 threading.local 实现线程本地连接池 import threading _local threading.local() def get_db_conn(): if not hasattr(_local, conn): _local.conn sqlite3.connect(app.db, timeout5) return _local.conn注意check_same_threadFalse并不意味着线程安全它只是移除了 Python 层的检查。SQLite 的写操作仍是串行的多线程同时execute(INSERT ...)仍会排队等待锁。真正线程安全的写法是用单个连接 显式事务控制或用queue.Queue做写入队列。uriTrue解锁高级模式比如内存数据库和只读打开URI 模式让连接字符串支持更多语义# 内存数据库进程内临时存储关闭即销毁测试神器 conn sqlite3.connect(file::memory:?cacheshared, uriTrue) # 只读打开防止误操作提升并发读性能 conn sqlite3.connect(file:data.db?modero, uriTrue) # 严格模式禁止自动创建表调试时避免手滑建错表 conn sqlite3.connect(file:data.db?nolock1, uriTrue) # 实际是 nolock 参数示意SQLite 不支持此参数正确示例见下方实际项目中我常用 URI 模式做环境隔离# 生产环境普通文件 DB_URI data.db # 测试环境内存数据库每次测试干净启动 if os.getenv(TESTING): DB_URI file::memory: conn sqlite3.connect(DB_URI, uriTrue, timeout5)2.3 连接生命周期管理with语句不是银弹但close()必须明确Python 的with语句能自动调用conn.__exit__()进而触发close()。这看似完美但有个致命陷阱close()不会自动commit()。如果你在with块内执行了INSERT却没显式commit()退出时连接关闭未提交的更改直接丢弃。# 危险数据会丢失 with sqlite3.connect(app.db) as conn: conn.execute(INSERT INTO logs (msg) VALUES (?), (start,)) # 此处 conn 已 close但 INSERT 未 commit数据消失 # 正确显式 commit with sqlite3.connect(app.db) as conn: conn.execute(INSERT INTO logs (msg) VALUES (?), (start,)) conn.commit() # 必须写我的经验是对写操作永远显式commit()对只读操作withclose()足够安全。更稳妥的做法是封装一个上下文管理器class Database: def __init__(self, db_path): self.db_path db_path def __enter__(self): self.conn sqlite3.connect(self.db_path, timeout5) return self.conn def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: self.conn.commit() # 成功则提交 else: self.conn.rollback() # 异常则回滚 self.conn.close() # 使用 with Database(app.db) as conn: conn.execute(INSERT ...) # 自动 commit3. 表结构设计与数据操作从CREATE TABLE到防坑指南3.1 建表语句的隐藏细节IF NOT EXISTS、WITHOUT ROWID与严格模式SQLite 的CREATE TABLE看似简单但几个关键词决定了后续维护成本IF NOT EXISTS部署脚本的生命线在应用启动时自动建表是常见需求。没有IF NOT EXISTS重复执行会报OperationalError: table xxx already exists。但注意它只检查表名不校验结构差异。# 安全建表多次执行无害 conn.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 ) ) # 但若后续想加字段不能靠它自动迁移 # 错误以下语句不会生效表已存在 conn.execute(ALTER TABLE users ADD COLUMN phone TEXT) # 正确先检查字段是否存在通过 PRAGMA table_infoWITHOUT ROWID为纯主键查询场景提速 20%SQLite 默认为每张表添加隐藏的rowid64 位整数作为物理存储的行标识。但如果你的表主键是复合的如(user_id, order_id)且几乎不用rowid启用WITHOUT ROWID可减少存储、提升主键查询速度。# 传统表存储 rowid 主键 数据 conn.execute( CREATE TABLE orders ( user_id INTEGER, order_id INTEGER, amount REAL, PRIMARY KEY (user_id, order_id) ) ) # WITHOUT ROWID 表主键即物理位置省去 rowid 查找跳转 conn.execute( CREATE TABLE orders_fast ( user_id INTEGER, order_id INTEGER, amount REAL, PRIMARY KEY (user_id, order_id) ) WITHOUT ROWID )实测100 万行订单数据按(user_id, order_id)查询WITHOUT ROWID表平均快 18%。但它有硬性限制主键必须是INTEGER或TEXT类型且不能为NULL。我在做 IoT 设备心跳日志表时主键是device_id TEXT timestamp INTEGER果断用了WITHOUT ROWID磁盘占用降了 12%。严格模式用PRAGMA提前暴露问题SQLite 默认宽容插入NULL到NOT NULL字段静默转成NULL除非加NOT NULL约束类型不匹配尝试转换如123插入INTEGER字段。这方便开发但埋下数据质量隐患。# 启用严格模式SQLite 3.37 conn.execute(PRAGMA strictON) conn.execute(CREATE TABLE test (x INTEGER NOT NULL)) try: conn.execute(INSERT INTO test (x) VALUES (?), (None,)) except sqlite3.IntegrityError as e: print(e) # NOT NULL constraint failed: test.x提示PRAGMA strictON必须在CREATE TABLE之前执行且对已存在的表无效。建议在项目初始化时统一开启。3.2 数据插入的三种姿势execute、executemany与INSERT OR REPLACE单条插入execute()是基础但注意lastrowidconn.execute(INSERT INTO users (name, email) VALUES (?, ?), (Alice, ab.com)) print(conn.lastrowid) # 获取刚插入行的 rowid主键值lastrowid是连接对象的属性返回最近一次INSERT的rowid。如果主键是INTEGER PRIMARY KEY它就是主键值否则是内部rowid。这是获取自增 ID 的唯一可靠方式不要用SELECT last_insert_rowid()它返回的是当前连接的可能被其他线程干扰。批量插入executemany()是性能关键向 SQLite 插入 1 万条数据用 1 万个execute()调用耗时约 8.2 秒用executemany()降到 0.35 秒——快了 23 倍。原理是executemany()复用预编译的 SQL 语句避免重复解析。# 准备数据列表套元组 data [ (Bob, bc.com), (Charlie, cd.com), # ... 10000 条 ] # 批量插入注意VALUES 后只有一个 ? 对对应元组元素 conn.executemany(INSERT INTO users (name, email) VALUES (?, ?), data) conn.commit()但要注意executemany()不支持RETURNING子句SQLite 3.35 支持但executemany不返回结果。如果需要获取每条插入的lastrowid只能循环execute()。INSERT OR REPLACE/ON CONFLICTUPSERT 的正确打开方式当需要“存在则更新不存在则插入”时别用SELECTINSERT/UPDATE竞态条件风险。SQLite 3.24 的ON CONFLICT是原子操作# 方案1基于唯一约束冲突推荐 conn.execute( CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT UNIQUE, -- 冲突点 name TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) # 如果 email 已存在则更新 name 和 updated_at conn.execute( INSERT INTO users (id, email, name) VALUES (?, ?, ?) ON CONFLICT(email) DO UPDATE SET nameexcluded.name, updated_atCURRENT_TIMESTAMP , (1, ab.com, Alice New)) # 方案2REPLACE旧式会删除再插入触发 DELETE 触发器 conn.execute(INSERT OR REPLACE INTO users ...)我在做 RSS 订阅更新器时用ON CONFLICT(url) DO UPDATE确保每篇文章 URL 唯一且发布时间、标题自动刷新代码简洁无竞态。3.3 查询与结果处理从fetchone()到row_factory的进化基础取值fetchone()、fetchall()、fetchmany()cur conn.cursor() cur.execute(SELECT id, name FROM users WHERE email LIKE ?, (%gmail.com,)) # fetchone(): 取一行返回 tuple row cur.fetchone() # (1, Alice) # fetchall(): 取全部返回 list of tuple rows cur.fetchall() # [(1, Alice), (2, Bob)] # fetchmany(size): 取指定数量适合大数据流式处理 while True: batch cur.fetchmany(1000) if not batch: break process_batch(batch)注意fetchall()会把所有结果加载到内存。处理百万行数据时务必用fetchmany()分批否则 OOM。row_factory告别row[0],row[1]的魔法默认row是tuple靠索引取值易错。row_factory可将其转为dict、namedtuple或自定义类# 方案1字典式取值最常用 conn.row_factory sqlite3.Row cur conn.cursor() cur.execute(SELECT id, name FROM users LIMIT 1) row cur.fetchone() print(row[id], row[name]) # 清晰 print(row.keys()) # [id, name] # 方案2namedtuple不可变内存稍省 from collections import namedtuple conn.row_factory lambda c, r: namedtuple(Row, [col[0] for col in c.description])(*r) # 方案3自定义类适合复杂逻辑 class User: def __init__(self, id, name, email): self.id id self.name name self.email email conn.row_factory lambda c, r: User(*r)我在开发一个设备管理 CLI 时所有查询结果都用sqlite3.Row然后json.dumps([dict(r) for r in rows])直接输出 JSON前端消费零成本。text_factory解决中文乱码的终极方案Python 3 默认text_factory str但如果你从旧系统迁移或 SQLite 文件由其他语言创建可能遇到UnicodeDecodeError。根本原因是 SQLite 存储的是字节Python 需要知道如何解码。# 强制用 UTF-8 解码推荐覆盖 99% 场景 conn.text_factory str # Python 3 默认但显式声明更安心 # 如果遇到乱码可能是文件用 GBK 编码可临时转换 conn.text_factory lambda x: x.decode(gbk, errorsignore) # 最安全用 bytes自己控制解码 conn.text_factory bytes row cur.fetchone() name_bytes row[1] # b\xe5\xbc\xa0\xe4\xb8\x89 name name_bytes.decode(utf-8) # 张三4. 性能优化与高级特性WAL 模式、索引策略与 FTS 全文搜索4.1 WAL 模式读写并发的“免锁”方案SQLite 默认journal_mode DELETE写操作时整个数据库文件被锁读请求阻塞。WALWrite-Ahead Logging模式将修改写入单独的-wal文件读操作可同时进行读取主数据库 WAL 文件的合并视图大幅提升并发读性能。# 启用 WAL只需执行一次永久生效 conn.execute(PRAGMA journal_mode WAL) # 返回 wal 表示成功 # 验证 conn.execute(PRAGMA journal_mode).fetchone() # (wal,)WAL 的代价与应对磁盘空间WAL 文件会增长需定期CHECKPOINTSQLite 会自动触发但大写入后可手动fsync 开销每次写入 WAL 都要fsyncSSD 上影响小HDD 上可能成为瓶颈只读连接限制WAL 模式下file:data.db?modero会失败需用file:data.db?nolock1忽略锁但需确保无写入。我在一个实时监控仪表盘中前端每 5 秒轮询最新 100 条告警后端每秒写入新告警。启用 WAL 后查询延迟从平均 120ms 降到 8ms且无锁等待。4.2 索引设计不是越多越好而是“恰到好处”SQLite 的索引是 B-tree加速WHERE、ORDER BY、JOIN。但索引也占空间、拖慢写入。关键原则只为高频查询条件建索引且优先复合索引。# 场景经常按 status 和 created_at 查询 conn.execute(SELECT * FROM alerts WHERE status critical AND created_at 2023-01-01) # 错误两个单列索引效果差 conn.execute(CREATE INDEX idx_status ON alerts(status)) conn.execute(CREATE INDEX idx_time ON alerts(created_at)) # 正确一个复合索引覆盖查询最优 conn.execute(CREATE INDEX idx_status_time ON alerts(status, created_at))索引诊断用EXPLAIN QUERY PLAN在执行SELECT前加EXPLAIN QUERY PLAN看是否用到索引cur conn.cursor() cur.execute(EXPLAIN QUERY PLAN SELECT * FROM alerts WHERE status critical) print(cur.fetchall()) # [(SEARCH TABLE alerts USING INDEX idx_status (status?),)]如果输出SCAN TABLE alerts说明全表扫描急需索引。特殊索引COLLATE NOCASE加速大小写不敏感查询# 默认区分大小写 conn.execute(SELECT * FROM users WHERE email AB.COM) # 匹配不到 ab.com # 创建不区分大小写索引 conn.execute(CREATE INDEX idx_email_nocase ON users(email COLLATE NOCASE)) # 查询时也用 COLLATE conn.execute(SELECT * FROM users WHERE email ? COLLATE NOCASE, (AB.COM,))4.3 FTS5 全文搜索让LIKE %keyword%成为历史LIKE模糊查询无法用索引百万行数据搜索极慢。SQLite 内置的 FTS5Full-Text Search引擎专为此生。# 创建 FTS5 虚拟表自动建倒排索引 conn.execute( CREATE VIRTUAL TABLE docs_fts USING fts5( title, content, tokenizeporter -- porter 词干提取 ) ) # 同步数据将原表数据导入 FTS 表 conn.execute(INSERT INTO docs_fts SELECT title, content FROM documents) # 全文搜索支持布尔运算 cur conn.cursor() cur.execute(SELECT * FROM docs_fts WHERE docs_fts MATCH ?, (python AND tutorial)) rows cur.fetchall()FTS5 高级技巧tokenizeunicode61支持中文分词需额外配置或用simpleprefix(2,3)支持前缀搜索py*匹配pythonrank按相关性排序SELECT *, rank FROM docs_fts WHERE ... ORDER BY rank。我在开发一个内部知识库搜索时用 FTS5 替代LIKE搜索响应从 3.2 秒降到 0.04 秒。5. 常见问题与排查技巧实录从database is locked到disk I/O error5.1 “database is locked”不只是等待而是诊断根源这个错误最常见但原因多样。按发生频率排序场景诊断方法解决方案长事务未提交PRAGMA locking_mode; PRAGMA journal_mode;查看当前模式SELECT * FROM pragma_locking_mode();检查代码中是否有BEGIN但无COMMIT/ROLLBACK用timeout给缓冲时间WAL 模式下 checkpoint 阻塞PRAGMA wal_checkpoint(FULL);手动触发PRAGMA wal_autocheckpoint;查看阈值增大wal_autocheckpoint如PRAGMA wal_autocheckpoint 10000或定期后台checkpoint多进程写入冲突lsof -p pid查看进程打开的文件ls -la *.db*检查-wal、-shm文件改用单进程写入 队列或用fcntl文件锁协调实操案例一个 Flask 应用在高并发时频繁报锁。lsof发现多个 gunicorn worker 进程同时打开app.db。解决方案启用 WAL 模式设置PRAGMA wal_autocheckpoint 1000每 1000 页写入触发 checkpoint在应用启动时用sqlite3.connect(..., timeout15)关键写入操作加重试逻辑最多 3 次每次 sleep 0.1s。5.2 “disk I/O error”磁盘、权限与路径的三重检查这个错误往往指向底层文件系统问题磁盘满df -h查看挂载点剩余空间权限不足SQLite 需要对数据库文件及其所在目录有rw权限chmod 644 app.db chmod 755 .路径不存在connect(path/to/app.db)时path/to/目录必须存在否则报 I/O error不是 “no such file”网络文件系统NFSSQLite 在 NFS 上表现极差禁用 WAL 或换本地存储。快速自查脚本# 检查路径 dirnamedata mkdir -p $dirname chmod 755 $dirname # 检查磁盘 df -h $dirname | awk NR2 {print $5} # 检查权限 touch $dirname/test rm $dirname/test # 测试读写5.3 “no such table”路径、大小写与连接隔离的陷阱路径错误connect(db.sqlite)和connect(./db.sqlite)可能指向不同位置相对路径基于当前工作目录大小写敏感Linux/macOS 文件系统区分大小写App.db≠app.db连接隔离file::memory:是进程内独立不同connect()之间表不共享。调试技巧# 列出所有表 cur.execute(SELECT name FROM sqlite_master WHERE typetable;) print(cur.fetchall()) # 确认表名拼写和大小写 # 检查当前连接打开的文件 cur.execute(PRAGMA database_list;) print(cur.fetchall()) # 显示数据库文件绝对路径5.4 中文乱码终极排查表现象可能原因验证命令解决方案插入中文显示????数据库文件编码非 UTF-8file -i app.db重建数据库确保连接时text_factorystr查询返回b\xe4\xbd\xa0\xe5\xa5\xbdtext_factorybytesconn.text_factory改为str或手动decode(utf-8)LIKE 你好%不匹配SQLite 默认二进制比较SELECT 你好 你好;用COLLATE NOCASE或UNICODE控制台打印乱码非数据库终端编码非 UTF-8localeexport LANGen_US.UTF-8注意SQLite 本身不存储字符编码它把文本当字节流。乱码一定是 Python 解码环节出错。6. 何时该放弃 SQLite五个明确的换库信号SQLite 是利器但不是万能锤。我在 12 个以上项目中经历过从 SQLite 迁移到 PostgreSQL/MySQL 的过程总结出五个不容忽视的换库信号6.1 信号一并发写入请求持续超过 5 QPSSQLite 的写锁是数据库级的。当你的应用如 Web API写入请求稳定在 5 次/秒以上且平均响应时间超过 200ms说明 WAL 模式已逼近极限。PostgreSQL 的行级锁在此场景下吞吐量可提升 10 倍。判断方法监控PRAGMA wal_checkpoint(FULL)的返回值{busy: 0, log: 0, checkpointed: 0}表示无压力若busy长期 0或log值持续增长 1000 页就是换库预警。6.2 信号二单表数据量突破 1 亿行且需复杂 JOINSQLite 的查询优化器较简单对多表JOIN、子查询、窗口函数支持有限。1 亿行orders表关联users、products即使有索引EXPLAIN QUERY PLAN也会显示SCAN TABLE。PostgreSQL 的统计信息收集和查询计划器能生成高效执行路径。实测对比一个电商订单分析查询3 表 JOIN GROUP BY HAVINGSQLite12 分钟内存峰值 4GBPostgreSQL8.3 秒内存 200MB。6.3 信号三需要用户权限管理、行级安全策略SQLite 没有用户概念文件权限即一切。如果你的应用要求“销售部只能看自己的客户”或“审计员只能查历史表”就必须换用支持 RBAC 的数据库。SQLite 的解决方案如应用层过滤极易绕过。6.4 信号四要求高可用、主从复制、自动故障转移SQLite 是单机嵌入式引擎不支持复制。当你的业务要求“数据库宕机 30 秒内自动恢复”或“读流量分流到从库”SQLite 无法满足。即使借助rsync同步文件也无法保证一致性。6.5 信号五团队中已有 DBA且运维流程强制要求企业级数据库技术选型不仅是性能问题更是协作成本。当 DBA 团队已建立 PostgreSQL 监控告警体系、备份恢复 SOP、SQL 审计规范强行用 SQLite 会增加额外培训、文档、排障成本。此时“符合规范”本身就是最高性能。迁移建议用sqlite3命令行导出 SQLsqlite3 app.db .dump dump.sql用pgloader工具自动转换并导入 PostgreSQL应用层替换sqlite3为psycopg2调整少量 SQL如AUTOINCREMENT→SERIALCURRENT_TIMESTAMP语法一致。我在一个 SaaS 客户管理工具中用户量达 5 万时触发信号一和信号二两周内完成平滑迁移API 响应 P95 从 1.2s 降至 180ms。7. 我的 SQLite Python 工作流从初始化到部署的完整 checklist最后分享我私藏的、经过 8 个项目验证的 SQLite Python 工作流 checklist。它不是理论而是每天敲键盘时的真实动作7.1 初始化阶段首次运行创建数据库目录os.makedirs(data, exist_okTrue)避免路径错误连接并启用 WALconn sqlite3.connect(data/app.db, timeout10); conn.execute(PRAGMA journal_mode WAL)建表并加注释在CREATE TABLE后用/* comment */说明业务含义插入初始数据用executemany()批量导入种子数据如配置项、枚举值设置text_factoryconn.text_factory str一劳永逸。7.2 开发阶段日常编码所有 SQL 用?占位符绝不字符串格式化杜绝注入写操作必commit()在execute()后立即写不依赖with读操作用fetchmany(1000)处理大数据集时避免fetchall()查询前加EXPLAIN QUERY PLAN新写的SELECT必查是否走索引用sqlite3.Row所有cursor设置conn.row_factory sqlite3.Row。7.3 部署阶段打包发布数据库文件随应用分发data/app.db放入项目目录而非~/.config/权限问题首次启动自动建库检查os.path.exists(data/app.db)不存在则运行建表脚本备份策略每日shutil.copy(data/app.db, backup/app.db_$(date %Y%m%d))版本迁移用alembic或自定义migrations/001_add_phone.py脚本避免手动改表监控指标记录PRAGMA page_count总页数、PRAGMA freelist_count空闲页预警磁盘膨胀。这个 checklist 的价值在于它把 SQLite 从“能用”推向“稳用”。它不追求炫技只确保每一次INSERT都落地每一次SELECT都准时每一次connect()都可控。当你把注意力从“怎么连上