Python实战:SQLite轻量级数据库操作全解析
今天这篇是《99天精通Python》系列的第30天主题是数据库操作主角是SQLite。可能很多初学者看到“数据库”三个字就发怵觉得要装MySQL、配服务、写一堆连接参数才能开始玩。但SQLite完全不是这个路子它不需要独立的服务进程数据库就是一个.db文件Python标准库里自带sqlite3模块导入即用零配置文件、零权限设置。我把这天的内容定位成“随身携带的轻量级数据库”一点不夸张——你把这个.db文件拷到U盘里换一台机器照样能读写迁移这才是它最有魅力的地方。这30天里我们写过爬虫清理数据写过自动化脚本存日志写过量化策略回测时的临时结果。说实话这些场景都有一个共同痛点数据规模说大不大、说小不小用CSV存吧查个记录要把整个文件读进内存还要自己写filter用MySQL吧本地还没装又嫌服务太重。SQLite正好卡在这个中间地带。这篇我会从为什么选SQLite讲起把连接、建表、增删改查、事务、索引、备份、并发排查这些实操点一次说透你在自己项目里完全可以照着写。1. 别急着上MySQL先看看SQLite想解决什么问题1.1 SQLite到底是什么为什么叫“随身携带”SQLite是一个嵌入式关系型数据库所谓“嵌入式”核心意思就是它作为一个C语言库直接链接进你的程序不像MySQL、PostgreSQL那样先起一个常驻服务进程再通过网络端口通信。你用Python代码连SQLite本质上不是“连接一个服务器”而是“打开一个文件”数据库引擎在进程内帮你处理SQL语句然后把结果写回文件。这个设计带来的体验差异非常大。用MySQL你得先安装数据库服务、启动服务、建账号、建库这些步骤对只想记录任务状态、存点配置、留个操作日志的程序员来说属于纯负担。而SQLite只需要一个文件所有表、索引、视图塞进同一个.db里移动、备份、删除都极其方便所以叫“随身携带的数据库”。数据库文件放在U盘里都能用手机App里的本地存储、桌面软件的配置库、浏览器扩展的缓存背后往往都是SQLite你每天用的微信、浏览器里就有成千上万个SQLite文件在默默工作。有人会担心文件型数据库会不会很低级实际上SQLite是应用最广泛的数据库引擎装机量远超MySQL它只是在并发写性能上做了取舍。理解“文件型”不等于“玩具”SQLite完整实现了SQL标准的大部分能力支持事务、触发器、视图、递归CTE、窗口函数你日常能想到的SQL操作它都能干。很多排名靠前的项目初期数据量没那么大时直接用SQLite扛着跑完全没问题。1.2 和CSV、MySQL比它赢在哪、输在哪我把三类存储方式放在一张表里对比这样你一眼能看出SQLite的位置。这张表也是我每次给别人介绍SQLite时必放的免得劝人“别急着上MySQL”被当成反智言论。对比维度CSV文件SQLiteMySQL部署成本无任何编辑器可开无Python内置模块直接连需要安装服务、配置账号端口数据访问方式全量读入内存再筛选SQL查询走索引SQL查询走索引可网络访问单表数据量支撑几十万行就开始卡顿几百GB内表现良好更适合海量数据集群并发写支持不支持支持单写多读并发写弱支持高并发写事务能力无完整ACID完整ACID多进程共享文件锁容易互相覆盖基于文件锁WAL适合本机天然支持多人远程访问从这个表能看到SQLite的定位在“本地单机应用”这个区间它是综合体验最佳的选择。CSV如果能满足需求那当然可以继续用但一旦你要做where条件过滤、多表关联、局部字段更新CSV就变成体力活了——你写Python手工实现一个left join试试很少有比SQL更省心的方案。而MySQL的学习成本和运维成本都不低如果你只在一台机器上跑个小工具服务进程挂了都得去排查半天SQLite完全没有这个烦恼。1.3 什么场景别用它讲适用场景之前先讲清楚边界免得有人拿SQLite去做明显不适合的事然后回来骂。有四种情况我通常不建议大家硬上SQLite。第一是极高并发写入。SQLite同一时刻只允许一个进程写数据虽然WAL模式可以把读和写并行但多个进程同时高频写同一个库仍然会出现“database is locked”。如果你的业务像订单系统一样每秒几十上百次写选PostgreSQL或MySQL会轻松很多。第二是多人远程访问。SQLite没有网络协议只适合跑在本机或同机共享磁盘。除非你用网盘同步整个db文件否则不要指望别人通过远程方式读写你本地的SQLite。第三是需要细粒度权限管理。MySQL有用户、库表权限、行级权限控制SQLite没有用户体系只有一个文件权限控制。所以多角色、多租户的Web系统后端数据库千万别用SQLite。第四是那种动辄几十TB、需要分布式扩展的数据仓库。单文件设计决定了SQLite的读写会受文件系统限制虽然SQLite理论上支持非常大的数据库但跟分布式存储比还是不适合做超大规模数据平台。搞清楚这些边界之后剩下的场景基本都是SQLite的舒适区桌面软件、移动App、爬虫存储、数据分析中间层、自动化工具、本地开发环境。2. 零安装不等于零准备环境与工具2.1 Python自带的sqlite3模块够我用了开始写代码前你得确认Python环境就绪。如果你还没装Python去官网下载安装包安装时记得勾选“Add Python to PATH”这里就不啰嗦了。关键点是Python标准库自带了sqlite3模块不需要安装任何第三方库也不需要单独安装SQLite软件。你可以直接打开命令行验证import sqlite3 print(sqlite3.sqlite_version)能打印出类似3.45.1这样的版本号说明环境没问题。这里很多人会惊讶为什么Python库的名字也是sqlite3因为Python的sqlite3模块底层链接的就是SQLite的C语言库它提供了Python数据库API规范DB-API 2.0所要求的接口所以你学这一套接口以后换别的数据库也事半功倍。在Windows、macOS、Linux上上述代码行为完全一致。也就是说不管你的部署环境是哪一种只要Python能跑SQLite就能跑。这就是我为什么总跟团队说如果脚本里的临时数据存储不确定要不要上中间件先默认用SQLite顶着后面真要扩展再迁移因为迁移成本比其他数据库低得多。2.2 可视化工具DB Browser for SQLitedb4s写代码多了难免想“看看”数据库里到底存了什么。命令行的sqlite3工具虽然也能查但对新手不友好我更推荐装一个叫DB Browser for SQLite的开源工具简称DB4S。它提供图形界面双击db文件就能看到所有表、索引、触发器的结构还能在SQL执行窗口里运行任意SQL语句。我习惯在写完增删改查代码后用DB4S打开同一个db文件做检查。这个过程特别能帮助新手建立“代码操作”和“数据存储”之间的对应关系。比如你执行了一行INSERT去DB4S里刷新数据表看到记录出现你心里就有底了执行了DELETE看到记录消失你对提交时机的理解又会深一层。DB4S还能导出CSV、压缩数据库很多运维操作都能覆盖。如果你不喜欢额外装软件那用DBeaver、JetBrains系IDE的数据库插件也都可以但DB4S是最轻量的。一个提醒用DB4S打开db文件时它会占用这个文件的读连接如果你的Python代码正处于一个长事务里没提交可能在界面上看到的是旧数据这不是bug而是隔离级别在起作用。另一种情况就更常见了开着DB4S占着数据库Python那边再去写偶尔会出现锁等待。所以排错时先把可视化工具关掉再跑代码往往能排除掉“怎么刚才还能写现在被锁”的干扰。2.3 数据文件该放哪怎么规划才不乱既然SQLite的“库”就是一个文件那这个文件放在什么路径就成了一个工程问题。我见过不少新手直接把db文件生成在项目根目录跟源码、日志混在一起跑了一阵子目录乱七八糟备份也不知道该copy哪个文件。我的建议是建立一个data目录专门放数据文件和临时导出文件。路径别写死绝对路径尽量用绝对路径或基于项目目录动态计算。你在自己电脑上写路径C:\Users\xxx\data换个电脑跑就崩非常尴尬。更推荐的做法是from pathlib import Path DB_PATH Path(__file__).resolve().parent / data / tasks.db DB_PATH.parent.mkdir(parentsTrue, exist_okTrue)把DB_PATH扔到一个config模块里全项目统一引用。mkdir(parentsTrue, exist_okTrue)的原因很简单如果data目录不存在SQLite连接时会直接报错而不是自动帮你创建目录。提前把目录建好数据库文件的存放路径就永远可控了。另外如果你的项目用了Git要想清楚db文件要不要提交到仓库。数据库文件里可能是测试数据、缓存数据、甚至用户隐私这类文件一般不提交用.gitignore忽略掉但如果你的项目需要用固定初始数据比如配置表、字典表那可以考虑保留一个seed.db作为种子。我通常会把真正的业务库忽略掉单独做一个schema.sql版本管理这样既不会让git仓库膨胀又能保留数据库结构的变更历史。3. 连接、建表、增删改查把这些一次搞清楚3.1 Connection、Cursor和一条SQL的关系Python的sqlite3模块里最核心的两个对象是Connection和Cursor。Connection代表你跟数据库文件之间的通道它管理事务、提交、回滚Cursor则像是通道上的一个游标你拿着它去执行SQL、取回结果。最基础的连接方式import sqlite3 conn sqlite3.connect(app.db)这个操作会创建connection同时打开app.db文件。文件不存在时SQLite会自动创建这一点和MySQL要先手动建库不一样。连接之后可以拿到cursorcur conn.cursor() cur.execute(SELECT 1) print(cur.fetchone())如果只是简单查询也可以直接用conn.execute它内部会临时创建一个游标执行并返回。我项目里大量用了conn.execute因为少一行cur的写法代码更干净。但当你同时执行多条SQL并且需要逐个处理结果时显式使用cursor逻辑更清楚。重点来了Connection还管理着事务。执行INSERT、UPDATE、DELETE这些会改变数据的语句后你必须调用conn.commit()才会真正落盘如果出错可以conn.rollback()回滚。很多初学SQLite的代码跑完什么都不显示大概率就是忘了commit。后面第五部分我会专门讲这个问题。3.2 建表以及SQLite的数据类型哲学建表是数据库操作的第一步SQLite对数据类型的处理比MySQL宽松得多这是它的灵活之处也是很多人掉坑的地方。SQLite有五种存储类NULL、INTEGER、REAL、TEXT、BLOB。注意这里叫“存储类”而不是严格的数据类型因为SQLite是动态类型——一个字段里可以存任意类型的数据即使你声明了INTEGER往里存个字符串也可能不报错。这个特性在一些特殊场景很有用但在规范项目里反而容易埋隐患所以我建议建表时照常写清楚类型让自己和同事都心里有数。一个典型的建表语句CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT pending, priority INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime(now, localtime)) );这里有几个要点。INTEGER PRIMARY KEY AUTOINCREMENT会自动分配自增ID但如果没有特别需要只用INTEGER PRIMARY KEY也行SQLite会自动为它生成rowid。NOT NULL约束保证必要字段不会缺失。DEFAULT给字段预设默认值datetime(now, localtime)会生成当前本地时间比手动在Python里再传时间更省事。CREATE TABLE和CREATE INDEX这些结构定义语句可以用executescript一次执行多段SQL适合做初始化脚本。SQLite的索引、约束、外键不如MySQL那么重度但基本功能都有。我想特别提醒SQLite的外键约束默认是关闭的你需要执行PRAGMA foreign_keys ON;才能启用这个特性很多教程压根不提等你发现数据完整性被破坏时已经晚了。3.3 增删改查的标准写法与提交时机数据库操作里最核心的就是四个字增删改查。我直接给一套可以照抄的模板。INSERT示例conn sqlite3.connect(app.db) cur conn.cursor() cur.execute( INSERT INTO tasks (title, status, priority) VALUES (?, ?, ?), (整理读书笔记, pending, 1) ) conn.commit()批量插入用executemany更高效rows [ (买菜, done, 0), (提交周报, pending, 2), ] cur.executemany( INSERT INTO tasks (title, status, priority) VALUES (?, ?, ?), rows ) conn.commit()SELECT的常规写法cur conn.execute( SELECT id, title, status FROM tasks WHERE status ? ORDER BY priority DESC, (pending,) ) rows cur.fetchall() for row in rows: print(row[0], row[1], row[2])执行SELECT之后不需要commit但update、delete一定需要。DELETE示例conn.execute(DELETE FROM tasks WHERE id ?, (1,)) conn.commit()关于提交时机我的建议是一个批量业务操作做完统一commit一次不要每条语句都commit。每次commit都涉及磁盘同步频繁的小事务会拖慢批量插入。但如果你需要保证某条关键数据及时落盘那该commit就commit别等。事务的粒度要结合业务判断没有一个万能间隔。3.4 参数化查询把SQL注入拒之门外这一小节的重要性怎么强调都不为过。很多新手图方便喜欢用f-string拼SQLtitle task; DROP TABLE tasks; cur.execute(fSELECT * FROM tasks WHERE title {title})这种写法一旦title里出现引号或SQL关键字轻则语法报错重则被注入攻击。你的数据库可能不是公开服务但爬虫拿到的外部数据、用户输入的内容都可能携带危险字符串用拼接方式写SQL等于拿自己的数据开玩笑。正确做法是用占位符?把参数和SQL模板分开cur.execute( SELECT * FROM tasks WHERE title ?, (title,) )sqlite3模块会负责把参数安全地转换成SQL值自动处理引号、反斜杠、特殊类型从机制上杜绝SQL注入。serialized参数还可以用命名占位符适合参数较多的情况cur.execute( INSERT INTO tasks (title, status) VALUES (:title, :status), {title: 写博客, status: pending} )注意参数只负责替换“值”不能用来替换表名、字段名这些标识符。如果你确实需要动态指定表名那必须用一个白名单先校验表名是否在一组合法值里再拼接SQL。否则就老老实实写死表名。这里再说一个容易踩的细节当查询结果只有一行时fetchone()返回一个元组或None多行用fetchall()返回列表数据量大时直接for循环遍历cursor内部边读边取不会一次性把所有行都塞进内存。取回结果后row[0]是按位置取列拿到的是元组。如果想让代码更可读可以设置conn.row_factory sqlite3.Row之后row[title]就可以按列名访问后面实操部分我会给出完整示例。4. 实操一个轻量任务管理库从零到能用4.1 需求梳理和表结构设计说了这么多理论还是得落在一个能跑的例子上。我用“任务待办管理”来做演示因为业务足够简单但又能覆盖增删改查、状态筛选、分页、索引这些核心知识点。假设需求是这样的命令行小工具能往里加任务能查看所有待办能标记任务完成能按优先级排序任务多之后支持分页。这个场景放在SQLite里非常合理单机使用读写不频繁一个文件就够。表结构我这么设计id自增主键唯一标识任务。title任务标题不可为空。status状态默认pending完成改为done。priority优先级0普通1重要2紧急。created_at创建时间用SQLite内置函数生成。finished_at完成时间标记完成时写入方便观察效率。业务查询主要两个方向按状态筛选、按优先级排序。所以给status字段建索引这正好呼应后面的查询优化。4.2 初始化数据库脚本我把初始化逻辑做成一个独立脚本单独运行专门负责建表和创建索引。这样做的好处是结构变更时可以重跑不会跟业务代码纠缠在一起。import sqlite3 from pathlib import Path DB_PATH Path(__file__).resolve().parent / data / tasks.db SCHEMA CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT pending, priority INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)), finished_at TEXT ); CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); def init_db(): DB_PATH.parent.mkdir(parentsTrue, exist_okTrue) conn sqlite3.connect(DB_PATH) try: conn.executescript(SCHEMA) print(f[OK] 数据库初始化完成: {DB_PATH}) finally: conn.close() if __name__ __main__: init_db()这里有个细节值得解释executescript在执行多段SQL时会先隐式提交当前事务再运行整个脚本。对初始化建表这种场景非常合适。但如果你用普通execute做多个写操作还是得手动commit。executescript也不是银弹后续业务代码里的多次execute我建议用下一小节的方法封装事务。4.3 查询、分页、索引组合拳初始化完之后就是日常操作。我喜欢用一个函数封装“自动提交事务”这样每个业务函数不需要重复写commit/rollbackfrom contextlib import contextmanager contextmanager def db_connect(db_pathDB_PATH): conn sqlite3.connect(db_path) conn.row_factory sqlite3.Row try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close()这个上下文管理器最大的意义是不管代码中途是否抛异常它都会把事务正确收尾。yield后面接commit异常时走rollback比在每个函数里try/except要干净得多。后续函数直接:def add_task(title, priority0): with db_connect() as conn: conn.execute( INSERT INTO tasks (title, priority) VALUES (?, ?), (title, priority) )想想看这就把“数据库操作”的真正常态变成了连接-执行SQL-依赖上下文自动提交完全不用关心那些样板代码。查询和分页更需要细心。LIMIT和OFFSET可以用参数但我强烈建议先对page和page_size做整数转换否则用户传入一个字符串拼接成“limit abc”会一脸懵。完整示例def list_tasks(statuspending, page1, page_size20): page max(int(page), 1) page_size max(int(page_size), 1) offset (page - 1) * page_size with db_connect() as conn: rows conn.execute( SELECT id, title, status, priority, created_at FROM tasks WHERE status ? ORDER BY priority DESC, id ASC LIMIT ? OFFSET ?, (status, page_size, offset), ).fetchall() return [dict(row) for row in rows]这里出现了WHERE、ORDER BY、LIMIT、OFFSET。索引就体现在查询里status字段有索引SQLite会通过索引快速定位符合条件的行而不需要全表扫描。怎么验证执行计划看一眼EXPLAIN QUERY PLAN SELECT id, title FROM tasks WHERE status pending ORDER BY priority DESC LIMIT 20;如果输出里出现了SEARCH tasks USING INDEX idx_tasks_status说明索引生效了。如果不加索引可能是SCAN tasks数据一多性能差距就会很明显。但也不要给每个字段都加索引因为索引会占用空间、拖慢写入只在真实过滤和排序的列上加就够了。4.4 PRAGMA让SQLite按你的习惯工作SQLite的很多运行行为都靠PRAGMA语句控制相当于动态配置。我用得最多的有四个每个都踩过坑才真正理解。第一个PRAGMA journal_modeWAL;。默认的日志模式在写操作时会把整个数据库锁住读操作也会被阻塞。改成WALWrite-Ahead Logging之后写操作追加到单独的日志文件读操作还能继续读取旧快照显著提升并发读场景的体验。执行方式conn.execute(PRAGMA journal_modeWAL;)第二个PRAGMA busy_timeout5000;。当数据库被其他进程锁定时默认行为是立刻抛database is locked异常。设置busy_timeout可以让SQLite在报错前等待指定的毫秒数。这对偶发冲突很有效但要记住它只是缓解等待不能解决真正的长时间锁。第三个PRAGMA foreign_keysON;。前面说的SQLite默认不开外键约束。只要你的表之间有关联连接建立后一定执行这句并在文档里写清楚否则孤儿数据出现时你根本找不到原因。第四个PRAGMA integrity_check;。数据库文件被kill -9杀进程、磁盘写入异常或者文件从U盘拔下来时机不对都有可能出现文件损坏。跑一次完整性检查能确认数据文件是否健康正常返回ok。我建议隔一段时间或者在备份前跑一次。PRAGMA的设置不是永久保存的尤其busy_timeout、foreign_keys这类每次新建连接都需要重新设置。这就是为什么我把它们放在db_connect上下文管理器里的原因conn.execute(PRAGMA busy_timeout5000;) conn.execute(PRAGMA foreign_keysON;)每来一个连接先补齐这些运行参数再让业务代码执行。这样即使忘了配置也不会因为连接复用导致状态串了。4.5 备份、迁移和其他语言借道访问SQLite的备份非常直观因为它就是一个文件。但直接copy文件在库正被写入时可能拷到不一致的数据。Python官方接口提供了backup方法专门处理这种场景def backup_db(src_path, dst_path): src_conn sqlite3.connect(src_path) dst_conn sqlite3.connect(dst_path) try: src_conn.backup(dst_conn) print(f[OK] 备份完成: {dst_path}) finally: dst_conn.close() src_conn.close()还有一种更传统的备份方式是用SQL转储sqlite3 tasks.db .dump tasks.sql导出的是SQL文本文件需要恢复时再执行回库里。这种方式更适合跨版本迁移比如你担心新版本SQLite不能读旧文件格式时用dump重建最稳。聊到迁移很多人的实际需求是把MySQL里的数据转过来。操作思路通常是先用工具把MySQL表结构导出成SQL再手工调整自增语法和注释不兼容的部分数据层面如果量不大导出CSV再插入SQLite也是笨办法但有效。Windows下“mysql转sqlite”有很多现成图形工具选开源的即可但转完一定要抽查数据行数和类型因为整数、日期、二进制字段在不同库里的表示有细微差别。最后说一个SQLite特别讨喜的点一个.db文件不止Python能读。C#里的System.Data.SQLite、VB.NET、Java的JDBC、Go、Rust都能直接打开同一个文件。你在Python里建好的数据库在C#程序里连接读写完全没有障碍因为文件格式是公开稳定的。所以如果你在Linux上用C#和VSCode写SQLite读写也不存在跨平台障碍把连接字符串指向同一个db文件就行。对于团队协作、异构系统临时交换数据SQLite文件比CSV靠谱得多既保留数据类型又支持SQL查询。5. 我踩过的那些坑——常见问题排查实录5.1 “database is locked”绝不是玄学这是我见过最多人问的问题也是我自己最早踩的坑。表面报错是sqlite3.OperationalError: database is locked背后原因几乎逃不开三种存在另一个进程长时间占用写锁有一个事务没提交没关闭还攥着锁多进程同时写同一个库。解决思路分三层。第一层先找“谁占着锁”把DB4S、命令行终端、别的Python进程都检查一遍有时候只是你忘了关一个交互式窗口。第二层给连接加上合适的busy_timeout给它几秒钟等待锁释放。第三层改代码结构让写事务尽量短小。把一次几千条的循环插入拆成若干小批次每批单独commit而不是一个事务从头跑到尾这样其他流程插入进来的写请求就不至于等太久。如果读多写少但并发读频繁打开WAL模式是最有效的。WAL模式下读操作不阻塞写写操作也不锁整个文件最直接的体验就是“database is locked”出现频率断崖式下降。如果你的程序是真正的多进程高频写SQLite确实不是最佳选择别硬抗换数据库才是正解。5.2 增删改都执行了为什么数据没了这类问题最常见的回答是“你没commit”。SQLite为了保证事务一致性执行的增删改都先在事务里缓冲只有commit后才真正同步到磁盘。很多新手用conn.execute跑完就不再管程序一退出数据就丢了。这里有个认知陷阱Python的with sqlite3.connect(app.db) as conn:并不能自动帮你commit写操作。它只负责关闭连接不负责提交事务。所以你看到很多教程里写with conn:然后里面做SELECT没问题但做INSERT却悄悄丢了数据。正确姿势要么显式调用conn.commit()要么像我前面写的db_connect上下文管理器那样在yield之后统一commit。另外程序运行过程中被强杀也会丢数据。如果业务里有些数据不能丢那就真得在关键写入点commit并且读完就关闭连接。我遇到过同事因为忘记关闭连接几天下来连接数堆积导致锁冲突变多虽然SQLite不占网络端口但句柄资源同样需要释放。5.3 中文读写没问题但别踩这几个坑SQLite的文本默认按UTF-8存储Python 3的字符串也是Unicode所以中文读写基本无障碍。真正容易出错的地方有几个。第一个是Windows控制台的编码。你print中文时如果出现乱码多半不是SQLite的问题而是终端编码问题。可以先把系统终端切到UTF-8或者打印时统一用repr查看调试。第二个是路径中的中文。connect字符串里的路径如果包含中文Windows下偶尔会因为默认编码出问题建议路径上多用Path对象而不是手写字符串。第三个是数据来源不是UTF-8比如从GBK编码的旧CSV读入后直接插入会造成乱码。插入之前先做一次统一编码转换。最后建议的设置是PRAGMA encoding UTF-8;在建库初期执行从源头保证数据库内部编码统一。已建立的库也可以查看但中途修改编码通常没有意义所以还是建库时就定好。5.4 并发场景怎么调优很多人的并发需求其实没想象中那么高。本地工具、爬虫脚本充其量几个线程同时读不会有太大压力。但真到了多线程场景还是有一些明确的调优方向。先说线程。SQLite默认一个连接只允许创建它的线程使用如果你把同一个conn丢给其他线程会直接报sqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread。除非你有特殊理由否则我建议每个线程创建自己的连接用完即关不要共享connection。这样代码最稳也不会出现诡异的幽灵锁。再说写入。SQLite虽然是单写者但单写者并不意味着每秒只能写一次。设置WAL模式、busy_timeout配合短小事务单台机器上每秒几百次写入仍然可以做。真正压垮它的是多进程并发写同一个文件尤其是事务长时间不提交。如果你的架构里多个进程都在写要么通过一个写入队列串行化要么换MySQL。把期望值放对位置SQLite的表现会超出你的想象。5.5 快速排查速查表下面这张表是我平时参考最多的整理成速查格式遇到问题对着检索就行。症状可能原因解决思路执行INSERT后数据消失没有commit执行write后conn.commit()或封装自动提交database is locked其他进程占写锁 / 长事务busy_timeout5000缩短事务开启WAL中文输出乱码终端编码 / 数据源非UTF-8先检查print编码插入前统一转换为UTF-8多线程使用conn报错connection跨线程使用每个线程独立连接用完关闭查询慢缺少索引或全表扫描EXPLAIN QUERY PLAN分析给筛选列加索引db文件损坏强制杀进程或磁盘异常PRAGMA integrity_check检查用backup恢复备份外键约束不生效默认关闭每个连接执行PRAGMA foreign_keysON这张表不是让你死记硬背而是强调一个排查原则数据库问题先从“连接、事务、PRAGMA配置”这老三样看起再往SQL逻辑和业务代码深处查。顺序反了很容易查了半天发现是忘了commit。6. 一些关于使用SQLite的个人习惯文章写到这我想分享几个这些年养成的习惯。第一个是数据库文件永远放在独立的data目录并且用Path对象动态计算路径绝不把绝对路径写死在代码里。这样项目从一台机器整体拷到另一台机器所有数据存储逻辑不用改一行这种“可挪动性”正是SQLite最独特也是最宝贵的优势。第二个习惯是给每个项目都留一个init_db.py脚本。即使最初只有一张表也把建表语句和PRAGMA配置放在独立脚本里管理后续加表、加索引都往这个脚本里补充跑一遍不会重复建。这个习惯让我避免了很多次“明明改了表结构Python代码里还在用老字段名”的尴尬。第三个习惯是保持最小代价的备份。本地开发库的数据量通常不大备份本质就是copy文件但我不会裸copy而是用Python的backup方法做在线备份这样即使服务在跑备份结果也一致。备份文件命名带上时间戳保留最近若干份即可。Python标准库、零第三方依赖让这一切都保持轻巧。第四个习惯是不过度设计。SQLite常被人当成升级MySQL路上的“临时垫脚石”写进代码时喜欢套一堆ORM动不动建几十张表。但SQLite最适合的状态就是小而精一个工具、一组数据、几张表和几个索引用标准库的sqlite3直接操作已经足够清晰。真到了需要ORM的程度那时候再迁移也不迟。数据文件小、结构清晰、接口统一这些特性让SQLite始终是我做本机数据持久化的第一选择。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →