前言
这类标题在问答站上很常见,前缀是「图文实例」。需要说明的是:本文不插图片------数据库操作的「图」其实是控制台输出和执行结果,用代码块和表格呈现比截图更清晰,也方便你直接复制运行。所以下面每个例子都按「代码 → 预期输出」的形式给出。
先把定位说准:sqlite3 是 Python 标准库模块,不需要安装、不需要启动服务、不需要账号密码。数据库就是磁盘上的一个文件。任何「先装 SQLite、再启动 SQLite 服务」的步骤都是多余的,SQLite 根本没有服务端进程。
再看一个必须提前说明的陷阱:sqlite3.version 和 sqlite3.version_info 这两个常量在 Python 3.12 被弃用、3.14 已被移除。 想查版本,用 sqlite3.sqlite_version(字符串)和 sqlite3.sqlite_version_info(元组),它们反映的是底层 SQLite 库的版本。老代码里写着 sqlite3.version 的,赶紧改。
本文用一个「学生成绩」的小库把 sqlite3 的主干用法走一遍,全程参数化,不拼接 SQL。
一、准备数据库文件
python
# 适用于 Python 3.8+
import sqlite3
from pathlib import Path
print("模块支持的 DB-API 级别:", sqlite3.apilevel)
print("底层 SQLite 库版本:", sqlite3.sqlite_version)
print("线程安全等级:", sqlite3.threadsafety)
text
预期输出
模块支持的 DB-API 级别:2.0
底层 SQLite 库版本:3.45.1
线程安全等级:3
threadsafety 是一个整数常量,含义遵循 DB-API:1 表示「线程可以共享模块,但不能共享连接」 ,3 表示「线程可以共享模块、连接和游标」 。它由底层 SQLite 库的编译期线程模式 决定------单线程模式对应 0,多线程模式对应 1,串行模式对应 3。所以不要凭印象写死,读这个常量就好。
判断文件是否存在,用 pathlib 而不是字符串拼接路径:
python
# 适用于 Python 3.8+
db_path = Path(__file__).resolve().parent / "school.db"
con = sqlite3.connect(db_path)
print("数据库文件:", db_path)
con.execute("PRAGMA journal_mode=WAL") # 可选:写多读多时更友好
con.close()
text
预期输出
数据库文件: /path/to/your/project/school.db
PRAGMA journal_mode=WAL 会把日志模式切成预写日志(WAL)。它的作用是让读操作不再被写操作阻塞 (普通模式下写会锁库)。WAL 是持久设置,写一次就记住了;用完后数据库目录里会多出 -wal 和 -shm 两个辅助文件,备份时要把它们一起考虑,或者用 backup() 方法。
二、建表
python
# 适用于 Python 3.8+
import sqlite3
con = sqlite3.connect("school.db")
con.row_factory = sqlite3.Row
con.executescript("""
CREATE TABLE IF NOT EXISTS student (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sno TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
class_name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS score (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id INTEGER NOT NULL REFERENCES student(id) ON DELETE CASCADE,
subject TEXT NOT NULL,
points REAL NOT NULL CHECK (points >= 0 AND points <= 100),
UNIQUE (student_id, subject)
);
""")
con.commit()
con.close()
三点说明:
executescript()会先隐式提交一次 (在默认的遗留事务控制下,若存在未决事务)。所以不要让executescript里夹着需要和其他语句共享事务的内容------写建库脚本时用它很合适,业务代码里要谨慎。REFERENCES ... ON DELETE CASCADE只是声明 。SQLite 的外键约束默认是关闭的 ,而且是每个连接单独开关 。要让它真正生效,必须在每个连接 上执行PRAGMA foreign_keys=ON。INTEGER PRIMARY KEY在 SQLite 里就是 rowid 的别名 。写成INTEGER PRIMARY KEY AUTOINCREMENT会额外维护一张内部表来保证 id 单调递增、不复用空洞;不需要这个保证时,只写INTEGER PRIMARY KEY就够了。
三、写入数据
python
# 适用于 Python 3.8+
import sqlite3
con = sqlite3.connect("school.db")
con.row_factory = sqlite3.Row
con.execute("PRAGMA foreign_keys=ON")
con.execute("PRAGMA journal_mode=WAL")
with con:
con.executemany(
"INSERT OR IGNORE INTO student (sno, name, class_name) VALUES (?, ?, ?)",
[
("2026001", "张三", "高一(1)班"),
("2026002", "李四", "高一(1)班"),
("2026003", "王五", "高一(2)班"),
],
)
# 取学生 id,用参数化的查询,不用字符串拼学号
id_map = {
row["sno"]: row["id"]
for row in con.execute("SELECT id, sno FROM student")
}
con.executemany(
"INSERT OR REPLACE INTO score (student_id, subject, points) VALUES (?, ?, ?)",
[
(id_map["2026001"], "数学", 92.5),
(id_map["2026001"], "语文", 88.0),
(id_map["2026002"], "数学", 76.0),
(id_map["2026003"], "数学", 95.0),
],
)
print("学生总数 =", con.execute("SELECT COUNT(*) FROM student").fetchone()[0])
con.close()
text
预期输出
学生总数 = 3
关键点:
INSERT OR IGNORE:遇到唯一约束冲突就跳过这一条,不报错。适合做可重复执行的初始化脚本。INSERT OR REPLACE:冲突时先删掉旧行再插入新行 。注意「替换」在这里的语义是删除+插入,如果表上有外键引用它,级联删除可能被触发 。要「有则更新、无则插入」的语义,更精确的写法是用ON CONFLICT ... DO UPDATE(需要 SQLite 3.24 及以上,2018 年发布)。用之前先确认底层库版本,代码里可以用sqlite3.sqlite_version_info判断。with con:只管事务:正常退出提交,抛异常回滚。它不会关闭连接。
四、查询:条件、排序、分页、聚合
python
# 适用于 Python 3.8+
import sqlite3
con = sqlite3.connect("school.db")
con.row_factory = sqlite3.Row
# 条件 + 排序:值走占位符
rows = con.execute(
"SELECT sno, name, class_name FROM student WHERE class_name = ? ORDER BY sno",
("高一(1)班",),
).fetchall()
for r in rows:
print(f"{r['sno']} {r['name']} {r['class_name']}")
# 分页:LIMIT 和 OFFSET 也能参数化
page = con.execute(
"SELECT sno, name FROM student ORDER BY sno LIMIT ? OFFSET ?",
(2, 0),
).fetchall()
print("第一页:", [r["name"] for r in page])
# 聚合 + 分组
stats = con.execute("""
SELECT s.class_name,
COUNT(*) AS people,
ROUND(AVG(c.points), 2) AS avg_points
FROM student s
JOIN score c ON c.student_id = s.id
GROUP BY s.class_name
ORDER BY avg_points DESC
""").fetchall()
for s in stats:
print(f"{s['class_name']}: {s['people']} 人,均分 {s['avg_points']}")
con.close()
text
预期输出
2026001 张三 高一(1)班
2026002 李四 高一(1)班
第一页: ['张三', '李四']
高一(2)班: 1 人,均分 95.0
高一(1)班: 1 人,均分 84.25
fetchone() 取一行、fetchmany(size) 取若干行、fetchall() 取全部,也可以直接 for row in con.execute(...) 迭代。同一个游标的结果集只能顺次消费一次 ,fetchall() 之后再 fetchone() 只会拿到 None。
五、更新、删除与元信息查看
python
# 适用于 Python 3.8+
import sqlite3
con = sqlite3.connect("school.db")
con.row_factory = sqlite3.Row
con.execute("PRAGMA foreign_keys=ON")
with con:
cur = con.execute(
"UPDATE score SET points = ? WHERE student_id = (SELECT id FROM student WHERE sno = ?) AND subject = ?",
(90.0, "2026002", "数学"),
)
print("更新行数 =", cur.rowcount)
cur = con.execute("DELETE FROM student WHERE sno = ?", ("2026003",))
print("删除行数 =", cur.rowcount)
# 查看表结构:PRAGMA table_info 返回每个列的描述
for col in con.execute("PRAGMA table_info(student)"):
print(f" {col['name']:<12}{col['type']:<10}"
f"非空={col['notnull']} 主键={col['pk']} 默认值={col['dflt_value']}")
# 列出库里所有对象
print("表和索引:", [r[0] for r in con.execute(
"SELECT name FROM sqlite_master WHERE type IN ('table', 'index') ORDER BY name")])
con.close()
text
预期输出
更新行数 = 1
删除行数 = 1
id INTEGER 非空=0 主键=1 默认值=None
sno TEXT 非空=1 主键=0 默认值=None
name TEXT 非空=1 主键=0 默认值=None
class_name TEXT 非空=1 主键=0 默认值=None
表和索引: ['score', 'sqlite_autoindex_student_1', 'student', ...]
注意 PRAGMA foreign_keys=ON 只在当前连接 有效。上面删掉 2026003 时,级联删除会把它在 score 表里的成绩一并删掉------但只有在开了外键开关的连接上才会发生 。换一个没开开关的连接去执行同样的 DELETE,就只删学生、留孤儿成绩。这是 SQLite 上最容易让人困惑的行为差异之一。
六、导出、备份与命令行
python
# 适用于 Python 3.8+
import sqlite3
src = sqlite3.connect("school.db")
# 1) 导出成 SQL 文本(等价于 sqlite3 命令行的 .dump)
with open("school_dump.sql", "w", encoding="utf-8") as fp:
for line in src.iterdump():
fp.write(f"{line}\n")
# 2) 热备份到另一个文件:可在数据库被并发访问时进行
dst = sqlite3.connect("school_backup.db")
src.backup(dst)
dst.close()
src.close()
另外,从 Python 3.12 起可以把 sqlite3 当成命令行工具用:
bash
python -m sqlite3 school.db "SELECT name, points FROM score ORDER BY points DESC"
导出文件一定要显式写 encoding="utf-8"。不写的话会走平台默认编码,Windows 上通常是 GBK,含中文的导出文件换到 Linux 或交给别人就可能是乱码。
常见坑点
1. 用已移除的 sqlite3.version
❌ print(sqlite3.version)------3.12 起弃用,3.14 已移除,直接报 AttributeError。
✅ print(sqlite3.sqlite_version),需要数值比较时用 sqlite3.sqlite_version_info。
2. 以为声明了外键就会自动生效
❌ 建表写了 REFERENCES student(id) ON DELETE CASCADE,就认为删学生时成绩会自动清掉------实际不会,外键默认关闭。
✅ 每个连接 都执行 con.execute("PRAGMA foreign_keys=ON")。
3. 以为 with con: 会关闭连接
❌ with sqlite3.connect("a.db") as con: ... 之后不再 close(),文件句柄一直挂着,Windows 上可能连删除文件都失败。
✅ with 只管事务;连接用 try/finally 或显式 close() 释放。
4. 用字符串拼接构造 SQL
❌ con.execute(f"SELECT * FROM student WHERE sno = '{sno}'")------学号来自外部输入时就是注入入口。
✅ con.execute("SELECT * FROM student WHERE sno = ?", (sno,))。
5. 单元素参数写成标量
❌ con.execute("... WHERE sno = ?", ("2026001"))------这是字符串,不是元组,会报参数绑定错误。
✅ con.execute("... WHERE sno = ?", ("2026001",))。
6. 用 rowcount 看 SELECT 拿到多少行
❌ cur = con.execute("SELECT ..."); print(cur.rowcount)------对 SELECT 通常是 -1。
✅ 用 len(cur.fetchall()),或判断 cur.fetchone() is None。
7. 把 INSERT OR REPLACE 当成「更新」
❌ 用 INSERT OR REPLACE 更新一行,结果该行被删除重建,主键变了,引用它的外键级联触发,关联数据被清空。
✅ 明确语义:要更新就用 UPDATE;要「有则更新无则插入」用 ON CONFLICT ... DO UPDATE(需 SQLite 3.24+)。
8. 备份时只拷贝 .db 文件
❌ 开了 WAL 之后,直接把 .db 文件复制走,-wal 里还没落盘的事务丢失,备份出来的库可能不完整或损坏。
✅ 用 src.backup(dst) 做在线备份,或者先关闭所有连接再整体拷贝三个文件。
总结
| 环节 | 正确做法 |
|---|
|----|------------------------------------|
| 建库 | connect(pathlib.Path);WAL 模式可选开启 |
|----|-----------------------------------|
| 建表 | executescript() 适合建库脚本;注意它会隐式提交 |
|----|-------------------------------------------|
| 外键 | 默认关闭,每个连接 都要 PRAGMA foreign_keys=ON |
|----|---------------------------------------------|
| 写入 | executemany 批量;INSERT OR IGNORE 用于可重复脚本 |
|----|-------------------------------------------------|
| 查询 | 一律 ? 占位符;fetchone/fetchmany/fetchall 顺次消费一次 |
|----|---------------------------------|
| 事务 | 默认手动提交;with con: 只提交/回滚,不关闭连接 |
|-----|-------------------------------------|
| 元信息 | PRAGMA table_info、sqlite_master |
|----|---------------------------------------------------------------|
| 备份 | backup() 在线备份;iterdump() 导出 SQL(写文件记得 encoding="utf-8") |
sqlite3 的上手成本极低,但外键开关、事务边界、WAL 备份这三处如果不清楚,出问题时会很难定位------因为它们都「不报错,只是结果和你想的不一样」。