趁着最近空下来,我把 SQLite3 数据库从头到尾又认真复习了一遍。这个年纪再回头去看这些基础内容,和当年在学校做课程设计时的感觉完全不一样——以前只是照着教程敲命令,这次一边敲一边把文件存储结构、事务提交、锁机制、Python 怎么配合都顺了一遍,顺手整理成了这篇文章。如果你也正在准备数据库课设、马上要面试,或者手头要写一个带本地存储的小工具,这篇应该能帮你在最短时间内把 SQLite3 复习到可以直接上手的程度。
这篇文章不适合零基础从头学数据库的人看,更适合已经接触过 SQL、但想系统梳理一遍 SQLite3 常用操作细节的人。我会先讲清楚 SQLite3 的定位和边界,再重点过命令行基础和 Python 操作,最后把内存数据库、事务、锁这些容易忽略的内容串起来,顺带附上我这次复习踩过的坑和排查记录。
1. 复习前先把 SQLite3 的运行逻辑和使用边界理清楚
1.1 嵌入式数据库到底意味着什么
很多教材上来就让你建表、插入、查询,结果学完 SQLite3 的人还是会困惑:为什么别人连 MySQL 要先启动服务、设置账号密码,我这里只需要一个 .db 文件就能跑?
这里面的核心差异在于,SQLite3 是一个嵌入式关系型数据库。它不是一个独立运行的服务器进程,而是一个 C 语言库,被直接链接到你的应用程序里。你的应用进程调用它的接口,它在同一个进程里直接读写磁盘上的数据库文件。整个数据库就是单个普通文件,这个文件可以拷贝、压缩、用 U 盘带走,没有端口号、没有配置文件、没有单独的数据库实例概念。
我给你们打个比方,MySQL 这类服务端数据库像一家独立的银行网点,你要存款取款,先排队、报账号、通过验证,银行后台系统处理完再给你回执。SQLite3 更像你随身带的记账本,所有交易都是你自己翻开本子直接写,不需要经过任何第三方机构。所以 SQLite3 的核心优势就是零配置、免维护、启动快、文件即数据。
这个"文件即数据库"的特征还带来一个实用的想法:备份数据库等于复制文件。我以前做项目时,要导出 SQLite 数据给同事,直接压缩 .db 文件发过去,对方放进项目目录就能打开,连导入导出流程都省了。
1.2 什么是适合用的场景,什么情况千万别硬上
正是因为"嵌入式"这个属性,SQLite3 最适合处理本地数据、单机应用、移动端存储、嵌入式设备这类场景。比如桌面小工具的配置数据、手机 App 的离线缓存、物联网设备上的记录存储、数据分析过程中的中间结果,还有学校课程设计里那种并发量很低的信息管理系统,这些场景用 SQLite3 都非常舒服。
但如果你指望它扛住几十个用户同时频繁写入,甚至作为电商网站的核心存储,那就不合适了。SQLite3 的写入锁是数据库级的,也就是说任何一个时刻只能有一个连接成功写入,并发写多的时候会出现明显的锁等待。此外它也没有用户权限管理、主从复制、集群这些服务端数据库的能力,不适合多客户端通过局域网/外网大规模共享访问。
我自己记忆使用边界的方法是看两个指标:一是数据量级,单库别去玩 TB 级别;二是写入并发频率,如果需求里出现"多用户同时提交订单",
基本可以考虑换 PostgreSQL 或 MySQL。数据量几百万行以内、日活在几千以下、以单机应用为主,SQLite3 完全能胜任。
1.3 和 MySQL、PostgreSQL 快速对比
整理对比表是我这次复习的一个收尾动作。这里不罗列官网参数,只从实际选型角度做个对照:
| 对比项 | SQLite3 | MySQL | PostgreSQL |
|---|---|---|---|
| 架构 | 嵌入式库,零配置文件 | 独立服务,需启动和账号 | 独立服务,功能强大 |
| 数据库形态 | 单个 .db 文件 | 数据目录+系统表 | 数据目录+系统表 |
| 部署复杂度 | 极低,放个文件就行 | 较高,需安装配置 | 较高 |
| 并发写入 | 库级写锁,弱 | 行级锁,中等 | MVCC,较强 |
| 适用规模 | 小型项目、边缘设备 | 中大型 Web | 中大型复杂业务 |
| 备份方式 | 复制文件即可 | mysqldump 等工具 | pg_dump 等工具 |
这里提醒一下,不要因为这是个"轻量级数据库"就看不起它。SQLite 是全世界部署量最大的数据库,手机、浏览器、嵌入式设备里到处都是。你得选对工具做对的事,面试时能把这个取舍讲清楚,比背多少 SQL 语法都加分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 命令行基础一轮过:从打开数据库到增删改查
2.1 环境和打开数据库的几种方式
复习命令行操作之前,先确保环境里有 sqlite3 执行文件。Linux 和 macOS 基本自带,Windows 下需要到 SQLite 官网下载预编译的二进制文件,把 sqlite3.exe 放进任意目录或加入 PATH。
打开数据库最常见的命令是:
bash复制sqlite3 school.db
注意如果这个文件不存在,SQLite3 会先创建一个空文件,再进入命令行交互模式。如果你只想先看看某个文件里有什么,先不带文件名启动,再用 .open 指定路径也可以:
bash复制sqlite3
.open school.db
进到交互模式后,第一件事永远是看表结构。下面这条命令会列出所有表:
bash复制.tables
想查看某张表的建表语句,用 .schema:
bash复制.schema students
我复习的时候发现很多人只会用 SELECT 查数据,忘记了还有一堆点命令。这些命令以点开头,是 SQLite 命令行客户端的特殊指令,不是 SQL 语句。常见的有:
- .databases:列出当前连接打开的数据库文件
- .tables:列出所有表
- .schema 表名:查看建表语句
- .headers on:开启结果集表头,默认关闭
- .mode column:以列对齐方式显示结果
- .nullvalue NULL:把 NULL 值显示为 NULL
- .quit 或 .exit:退出
强烈建议一开库就把前两行敲掉,否则查询结果挤成一团,完全没法看:
bash复制.headers on
.mode column
2.2 建表时的字段类型和主键细节
无论操作什么数据库,建表都是第一步。下面是学生成绩管理系统里常见的一张某课程表:
sql复制CREATE TABLE IF NOT EXISTS courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
credit REAL DEFAULT 2.0,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
);
复习建表重点要理解两个概念。
第一,SQLite3 的类型体系是"动态类型"。它表面上支持 INTEGER、TEXT、REAL、BLOB、NULL 五种存储类,实际上它不强制你做类型检查。你可以在 INTEGER 字段里塞一个 'abc' 字符串,它不会报错,只是把这个字段的存储类型变成 TEXT。这对快速开发是友好的,但对数据质量要求高的项目是个坑。所以建表时仍要写清楚类型,同时要依赖应用层的校验,别指望数据库帮你卡死数据。
第二,主键写法有讲究。SQLite3 有个隐藏的 rowid 列,rowid 是自增的行标识。当你的表里有 INTEGER PRIMARY KEY 时,这一列会成为 rowid 的别名,插入时如果不填这一列,它会自动生成一个递增整数。
有一个知识点容易搞混:如果主键类型写的是别的类型,比如 TEXT PRIMARY KEY,那一列就不是 rowid 的别名了。另外 AUTOINCREMENT 不是必须的,只有在你不希望 rowid 被复用的情况下才需要它。删除最大 id 后再插入,普通的 INTEGER PRIMARY KEY 可能会复用之前的 id,加 AUTOINCREMENT 后就不会,但会有额外的开销。SQLite 内部会自动维护一个 sqlite_sequence 表来记录递增,代价其实不大,但按我的习惯,纯本地应用没必要开,省一点是一点。
2.3 增删改查完整操作,配合一个成绩系统案例
有了表之后,真正的重头戏是增删改查,也就是大家熟知的 CRUD。
插入数据:
sql复制INSERT INTO courses (name, credit) VALUES ('数据库原理', 4.0);
INSERT INTO courses (name, credit) VALUES ('数据结构', 3.5);
INSERT INTO courses (name, credit) VALUES ('操作系统', 3.0);
注意,SQLite3 命令行交互模式下,每一条 INSERT 提交后会自动执行,不需要手动 COMMIT。但用 Python 操作时不是这样,后面我会专门讲。
查询数据:
sql复制SELECT * FROM courses;
SELECT name, credit FROM courses WHERE credit >= 3.5 ORDER BY credit DESC;
直接在命令行里看效果,就会发现 .headers on 和 .mode column 这两个点命令非常关键。
更新某一行,这也是搜索热词里出现频率最高的问题:
sql复制UPDATE courses SET credit = 4.5 WHERE name = '数据结构';
理解 UPDATE 的重点是 WHERE。很多新手犯的错误是忘写 WHERE,直接把全表的 credit 都改成同一个值,这个事故上课时老师反复强调过,实际开发中一旦发生,如果没有备份,恢复起来非常麻烦。我的习惯是先 SELECT 查出目标行的 id,再用 id 去定位更新,既安全又直观。
删除数据:
sql复制DELETE FROM courses WHERE id = 3;
DELETE 同样要重视 WHERE。另外 DELETE 只是把数据标记删除,物理空间不会自动释放,高版本可以用 VACUUM 命令回收空间。平时本地文件无所谓,如果是给客户交付的数据库,建议定期清理历史数据后执行一次 VACUUM。
为了让复习更真实,我顺手建了一张学生表用于课程设计场景:
sql复制CREATE TABLE students (
id INTEGER PRIMARY KEY,
student_no TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
major TEXT,
enroll_year INTEGER
);
然后模拟一个场景:某同学转专业,需要更新他的专业和年级:
sql复制UPDATE students
SET major = '计算机科学与技术', enroll_year = 2022
WHERE student_no = '202300101';
完成一个综合查询:看看数据库中所有选了数据库原理课并按成绩从高到低排列,单表也可以表达,这里不展开 JOIN,以免偏离复习主线。
2.4 命令行复习的收获:SQL 语法真的是熟能生巧
这一轮命令行过完,我最大的感受是没必要背多少复杂的 SQL 特性,日常 90% 的操作就是增删改查加 WHERE、ORDER BY、LIMIT、JOIN。SQLite3 语法基本兼容标准的 SQL,你只要会用命令行把增删改查练熟,看 MySQL、PostgreSQL 的资料也能快速迁移。
复习到后面,我推荐大家做一件事:不要只写 SELECT *,尽量在查询时把需要的字段列清楚。这样既减少数据量,也方便以后换用别的数据库。
3. Python 操作 SQLite3:插入、更新某行、覆盖重复记录
3.1 连接、游标和提交的版本经验
如果只是命令行操作,SQLite3 还算不上方便。真正让 SQLite3 火起来的场景,是配合 Python 做本地工具和数据分析。Python 官方自带 sqlite3 模块,不需要装第三方包,这是很多人不知道的隐藏福利。
最基本的套路是先建立连接,再获取游标:
python复制import sqlite3
conn = sqlite3.connect("school.db")
cursor = conn.cursor()
如果数据库文件不存在,connect 方法会自动创建一个空文件。所以即使你还没有建好表,只要执行了 connect,磁盘上就会出现一个文件。
接着执行 SQL:
python复制cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY,
student_no TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
score REAL
)
""")
很多 Python 新手在这里就会踩坑:execute 执行了 INSERT 或 UPDATE,然而在另一个连接里查询数据却看不到。原因就是 Python sqlite3 模块不会像命令行那样自动提交每一条 DML 语句,你必须显式调用:
python复制conn.commit()
我的建议是不要纠结于 Python 版本的自动提交行为,把 commit 写出来永远是对的。在每个事务的最后调用 conn.commit(),想回滚就调用 conn.rollback(),这样最清晰,也最不容易出错。
查询数据时,常见写法的坑是直接 fetchall:
python复制cursor.execute("SELECT * FROM students")
rows = cursor.fetchall()
for row in rows:
print(row)
这样得到的是一个元组列表,数据多时会一次性把全部结果加载到内存。想按列名访问,需要额外把连接的行工厂设置为 Row:
python复制conn.row_factory = sqlite3.Row
cursor.execute("SELECT * FROM students WHERE student_no = ?", ("202300101",))
row = cursor.fetchone()
print(row["name"])
如果用完连接记得关闭,养成好习惯:
python复制cursor.close()
conn.close()
这里有一个经验教训:Python 的 with 语句只对连接对象没有自动关闭的作用,不保证进程结束后会执行什么。sqlite3 官方推荐的简单写法是配合上下文管理器来管理事务。比如:
python复制with conn:
conn.execute("UPDATE students SET score = ? WHERE student_no = ?", (95, "202300101"))
with 块结束时会自动提交事务,中途抛出异常则会回滚。注意,这只是替你把 commit/rollback 处理掉了,连接仍然需要主动 close。
3.2 参数占位为什么比字符串拼接安全
很多刚接触 python sqlite3 的人会写出这样的代码:
python复制name = "张三"
cursor.execute(f"SELECT * FROM students WHERE name = '{name}'")
看起来没问题,实际上隐患很大。如果 name 变量里出现单引号或特殊字符,要么 SQL 执行报错,要么被别有用心的人利用,构造出破坏性语句。这就是 SQL 注入。SQLite3 模块的解决方案是问号占位符:
python复制cursor.execute("SELECT * FROM students WHERE name = ?", (name,))
注意参数必须是元组,哪怕只有一个参数也要写成 (“张三”,),不能漏掉逗号。这个语法不是 Python 风格的 %s,而是 SQLite 的 ? 占位符,每次看到一个参数就想成“?”,别慌。我用这个小口诀记:写 SQL 时不拼字符串,占位符一律用问号,参数传进第二个参数。
插入数据也同理:
python复制cursor.execute(
"INSERT INTO students (student_no, name, score) VALUES (?, ?, ?)",
("20230102", "李四", 88.5)
)
conn.commit()
3.3 “两个字段相同则覆盖”的正确实现
这是我在搜索热词里看到出现频率非常高的需求,值得单独拿出一节细讲。需求通常是这样的:我有一张任务进度表,字段包括 user_id、task_id 和 progress。同步数据时发现同一个 user_id 和 task_id 已经存在,如果用户再次提交进度,应该更新旧记录而不是再插入一条新记录。
最笨的写法是先用 SELECT 查出来,判断有没有这条记录,再决定 INSERT 还是 UPDATE。这样写没有问题,但在多次操作和网络同步场景下会多一次查询,而且存在竞态风险。
更优的方式是在表结构上先声明唯一性:
sql复制CREATE TABLE IF NOT EXISTS progress (
user_id INTEGER NOT NULL,
task_id INTEGER NOT NULL,
progress INTEGER DEFAULT 0,
updated_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (user_id, task_id)
);
这里把 user_id 和 task_id 设为联合主键,或者你也可以用 UNIQUE (user_id, task_id),效果类似。如果你已经有现成表,要补一个唯一约束会比较麻烦,SQLite 不支持直接 ALTER TABLE 加唯一约束,只能重建表。所以能早设计就早设计。
表建好之后,Python 里面写:
python复制data = (1001, 2001, 80)
# 方式一:INSERT OR REPLACE
cursor.execute(
"INSERT OR REPLACE INTO progress (user_id, task_id, progress) VALUES (?, ?, ?)",
data
)
# 方式二:ON CONFLICT DO UPDATE
cursor.execute(
"""
INSERT INTO progress (user_id, task_id, progress)
VALUES (?, ?, ?)
ON CONFLICT(user_id, task_id) DO UPDATE SET
progress = excluded.progress,
updated_at = datetime('now')
""",
data
)
两种方式有区别。INSERT OR REPLACE 本质是先删除旧行再插入新行,所以它会改变 rowid,也可能触发外键级联删除。ON CONFLICT DO UPDATE 是标准的 UPSERT,冲突时只更新指定字段,不会删除原本的行,rowid 保持不变。如果表里没外键、不关心 rowid,用哪个都行;如果项目里有关联关系,我更推荐 ON CONFLICT DO UPDATE,更安全可控。SQLite 3.24 以上才支持 ON CONFLICT DO UPDATE 语法,查询版本时留意一下。
3.4 executemany 批量插入,一个小动作提升一个量级
复习时千万不要忽略 executemany。往 SQLite 插入一百条数据,一条条 execute,每一条都可能触发完整的事务提交和磁盘同步,速度惨不忍睹。批量插入要用 executemany,一次传入一个可迭代的元组列表:
python复制data_list = [
("1001", "张三", 90),
("1002", "李四", 85),
("1003", "王五", 88),
]
cursor.executemany(
"INSERT INTO students (student_no, name, score) VALUES (?, ?, ?)",
data_list
)
conn.commit()
如果是上万条数据,建议人工控制在事务里批处理,而不是把所有数据一把梭。分批提交既能避免内存暴涨,也能保证部分写入失败时不用回滚全部。我实测过,单条插入一万条和 executemany 一万条,性能差距能到几十倍,这种优化是零成本拿到手的。
4. 内存数据库、事务与锁:复习最容易漏掉的两个关键点
4.1 内存数据库:适合测试但不适合生产直接裸用
我在复习 SQLite 时看到 "sqlite3 内存数据库" 这个关键词,想展开聊聊。SQLite3 支持把数据库放在内存里,连接时传一个特殊的路径 :
python复制conn = sqlite3.connect(":memory:")
和磁盘文件相比,内存数据库完全不落盘,读写速度非常快,非常适合单元测试、数据计算中间过程、临时数据的场景。比如你写了一个入库预处理程序,需要对一批 CSV 数据做去重、清洗、统计,这些中间结果可以用内存数据库暂存,处理完再写回正式表。
但有几个坑必须提前知道:
第一,数据库生命周期和连接绑定。一旦连接关闭,内存数据库的所有数据就消失了。如果你用同一个连接创建了表、插入了数据,然后又新建一个连接去访问同一个 :memory:,你会发现新连接里什么都没有,因为每个连接对应一个独立的内存库。
第二,跨连接的共享需要特殊处理。SQLite 支持用 URI 方式开启共享缓存:
python复制conn = sqlite3.connect("file::memory:?cache=shared", uri=True)
这样多个连接可以共享同一个内存数据库,但需要非常小心锁和引用计数,处理不好比好处还多。我的建议是:单元测试直接每测试用例建一个新连接;生产环境尽量别用内存库替代文件库,毕竟程序崩溃后数据就没了。
复习知识点别停在 ":memory:" 这个写法上,最好结合 UR I模式理解:SQLite3 连接时的字符串不一定是文件路径,只要设置了 uri=True,你就能用它传参数。这种用法如果你提前了解,遇到特殊场景会少走弯路。
4.2 什么是事务,为什么批量插入要显式开启事务
事务是数据库复习的核心概念。SQLite3 的事务满足 ACID 特性,也就是原子性、一致性、隔离性和持久性。平时单独执行一条 INSERT、UPDATE、DELETE,SQLite 会自动将它们看作单个事务,提交到数据库。如果有多条操作需要同时成功或同时失败,就必须用显式事务包起来。
典型的场景是转账:一条 UPDATE 扣转出方余额,一条 UPDATE 加转入方余额,中间任何一条失败,整体应该回滚。
python复制try:
with conn:
conn.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
conn.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
except sqlite3.Error as e:
print("事务失败,已回滚", e)
with conn 块内部如果抛出异常,连接不会执行 COMMIT 而是执行 ROLLBACK,这正是我们需要的。如果想手动控制,可以显式写 BEGIN / COMMIT / ROLLBACK:
python复制conn.execute("BEGIN")
try:
conn.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
conn.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
conn.commit()
except Exception:
conn.rollback()
raise
需要注意的是,SQLite3 里 INSERT 会自动开启一个隐式事务,如果语句很多,一条语句一个事务,磁盘同步非常耗时。所以我在批量导入时通常显式开启一个大事务:
python复制conn.execute("BEGIN")
# 循环执行几百上千条 INSERT
for item in big_list:
conn.execute("INSERT INTO ... VALUES (?, ?)", item)
conn.commit()
一个事务包住所有语句,整体提交一次。这个优化对写入性能的提升极其明显,我在本地造十万条测试数据时,没有包事务跑了接近一秒钟,包成单事务后瞬间完成。
4.3 锁机制:理解 database is locked 是怎么来的
复习 SQLite3 时很多人把事务和锁分开学,其实它们是强关联。SQLite3 的锁策略是数据库文件级加锁,写操作会抢占写锁,同一时间只允许一个写者。默认的 journal 模式下,一旦有连接开始写,数据库文件会被独占,其他连接试图写入时就会收到 database is locked 的错误。
这个错误在 Python 里是这样出现的:
python复制conn = sqlite3.connect("school.db", timeout=5)
timeout 参数表示等待锁的最长时间,单位是秒。如果在 timeout 时间内文件锁没有释放,就抛出 sqlite3.OperationalError: database is locked。遇到这个问题,排查方向有几个:
- 是否有另一个进程或连接开启了未提交的事务
- 是否两个连接长时间攥着连接对象不释放
- 是否多个线程共用了同一个写入连接
针对小规模的本地程序,最有效的方案是尽量避免长时间写事务,写之前快速 BEGIN,写完立刻 COMMIT。如果读多写少的场景,还可以开启 WAL 模式:
sql复制PRAGMA journal_mode = WAL;
WAL 是 Write-Ahead Logging 的缩写,它把写入操作先追加到单独的 -wal 文件里,不再直接改原库文件,读事务和写事务可以共存,并发性能明显提升。开启后,你会在数据库文件旁边看到两个临时文件:.db-wal 和 .db-shm。这两个文件不用手动删除,是正常现象,不要随便清除,否则可能造成数据异常。需要定期执行 checkpoint 让 WAL 文件合并回主库。
多进程并发读没问题,多进程并发写就要注意。SQLite3 并不是不能多进程写,只要你的写入频率不高,并且把 busy_timeout 设得足够合理,它可以正常工作。但高并发写入仍然是它的弱项。如果是 Web 服务、多人协作工具这类场景,趁早换 PostgreSQL 或 MySQL,别和自己较劲。
5. 排错实录:打开失败、扩展未开启和锁库的排查思路
5.1 数据库文件怎么打开才不绕弯路
很多人在搜索引擎里问 sqlite3 怎么打开,本质是没有把它当成一个可执行程序,而是双击 .db 文件希望它能自动打开。文件管理器当然无法直接解析 SQLite 二进制格式,双击会提示选择打开方式。
推荐的做法有两条。
第一条是命令行方式,前面已经写过:
bash复制sqlite3 school.db
第二条是用可视化 GUI 工具。DB Browser for SQLite、SQLiteStudio、dbx 数据库工具这几款我都用过。DB Browser for SQLite 最接近 Navicat 的使用习惯,可以直接浏览表、执行 SQL、导入导出数据。如果只是临时看一下数据,用这类工具比命令行直观得多,还能避免编码显示乱码的问题。
如果你之前用过 IDEA、Navicat 或者 DBeaver,也可以直接用 DBeaver 连接 SQLite。DBeaver 是通用的数据库管理工具,支持 MySQL、PostgreSQL、SQLite 等,在 DBeaver 里的新建连接向导中选择 SQLite,指定文件路径即可,不需要启动任何服务。
5.2 服务端环境提示“未检测到 sqlite3 数据库扩展”怎么处理
复习时我搜索了相关热词,发现不少人在部署 PHP/管理中心程序时碰到这样的提示:“未检测到您服务器环境的 sqlite3 数据库扩展,请检查 php.ini 中是否已经开启该扩展”。
如果这是你的场景,要分清两种扩展:sqlite3 扩展和 PDO_SQLITE 扩展。很多 PHP 环境默认没有开启。在 php.ini 里搜索:
ini复制extension=sqlite3
extension=pdo_sqlite
去掉前面的分号,重启 Web 服务器,再刷新页面,通常就能检测到。如果你用的是宝塔面板、PHPStudy 这类环境,可以在 PHP 扩展管理页面勾选 sqlite3、pdo_sqlite,保存后重启 PHP 服务。
需要注意的是,如果你还没找到 php.ini 文件,先在 PHP 里跑:
php复制<?php
phpinfo();
?>
页面里搜索 "Loaded Configuration File",这一行显示的就是当前生效的配置文件路径。改了 php.ini 一定要重启 PHP-FPM 或 Apache,否则不会生效。这个问题和你本地的 Python sqlite3 模块没有关系,Python 的 sqlite3 模块是官方内置的,不需要单独安装扩展,除非你的 Python 是源码编译时故意去掉了 sqlite3 模块,否则不太可能出现这种提示。
5.3 锁库、乱码和“超出最大数据库坐标值”报错排查
复习过程中我收集了几个容易踩的实操问题,做成表格方便速查。
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| sqlite3.OperationalError: database is locked | 另一个连接持有写锁,busy_timeout 太短 | 检查是否有未提交事务;设置 timeout 参数;开启 WAL |
| sqlite3.OperationalError: no such table | 连接的不是同一个文件,表建在别的库 | 查看连接路径;执行 .database 确认当前库 |
| 中文插入后显示乱码 | 客户端编码不对,通常 Windows 控制台 | 命令行用 chcp 65001 切到 UTF-8;GUI 工具检查编码设置 |
| database disk image is malformed | 数据库文件损坏或拷贝不完整 | 停止写操作;使用 .recover 尝试恢复;检查磁盘 |
| 插入数据时提示 UNIQUE constraint failed | 插入的字段与已有记录唯一约束冲突 | 改用 ON CONFLICT DO UPDATE;确认业务逻辑是否要求覆盖 |
| 超出最大数据库坐标值 | 通常不是 SQLite 本身问题,可能是读取 DXF/CAD 等外部数据时单位不一致 | 检查导入文件的单位和程序读入单位是否一致,按实际转换系数处理 |
关于“超出最大数据库坐标值”这个报错,我特意查了一下,多出现在 CAD 相关程序导入 DXF 文件时,报错里提到的坐标值和数据库字段没有关系。如果你遇到,重点查 DXF 文件单位设置和代码里读取的单位比例,而不是去查 SQLite 配置。
5.4 复习后我留下的速查命令
每次复习完我都喜欢把散落的知识浓缩成一页速查表,方便日后忘的时候快速翻。以下这张表是我这次复习后最终保留下来的内容:
| 需求 | 命令/写法 |
|---|---|
| 进入交互命令行 | sqlite3 school.db |
| 查看所有表 | .tables |
| 查看表结构 | .schema students |
| 打开列对齐显示 | .headers on; .mode column |
| 连接内存数据库 | sqlite3.connect(":memory:") |
| Python 查询单行 | cursor.execute(...).fetchone() |
| Python 查询多行 | cursor.execute(...).fetchall() 或遍历 cursor |
| 显式提交事务 | conn.commit() |
| 更新指定行 | UPDATE students SET score=? WHERE student_no=? |
| UPSERT 相同字段覆盖 | INSERT ... ON CONFLICT(字段) DO UPDATE SET ... |
| 控制并发读写的方向 | PRAGMA journal_mode=WAL; |
| 超时时间控制 | sqlite3.connect("school.db", timeout=10) |
| 真空回收空间 | VACUUM |
同时记住两条核心心法:第一,操作前先想清楚 WHERE,增删改查里危险操作只差一个 WHERE;第二,动手写 Python 代码时,始终把参数占位、事务提交、连接关闭这三件事放在脑子里。复习数据库不是说你会背多少条 SQL,而是遇到一个真实需求,能快速判断用什么方案、哪几种写法有坑。这次把 SQLite3 从定位到命令到 Python 实操完整过了一遍,我感觉最有用的不是背下了多少语法,而是真正理解了它为什么设计成文件型、什么时候必须手动 commit、报错时从哪里开始排查。后面再写本地工具或者做课程设计,至少不会再在存储选型上面反复犹豫了。
