Python数据库sqlite3图文实例详解

前言

这类标题在问答站上很常见,前缀是「图文实例」。需要说明的是:本文不插图片------数据库操作的「图」其实是控制台输出和执行结果,用代码块和表格呈现比截图更清晰,也方便你直接复制运行。所以下面每个例子都按「代码 → 预期输出」的形式给出。

先把定位说准: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()

三点说明:

  1. executescript() 会先隐式提交一次 (在默认的遗留事务控制下,若存在未决事务)。所以不要让 executescript 里夹着需要和其他语句共享事务的内容------写建库脚本时用它很合适,业务代码里要谨慎。
  2. REFERENCES ... ON DELETE CASCADE 只是声明 。SQLite 的外键约束默认是关闭的 ,而且是每个连接单独开关 。要让它真正生效,必须在每个连接 上执行 PRAGMA foreign_keys=ON。
  3. 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 备份这三处如果不清楚,出问题时会很难定位------因为它们都「不报错,只是结果和你想的不一样」。

相关推荐
计算机毕业编程指导师1 小时前
【计算机毕业设计选题】基于Hadoop+Spark的乳腺癌数据分析与可视化系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
hadoop·python·spark·毕业设计·课程设计·乳腺癌·计算机毕设
老王爱玩车1 小时前
文件操作从打开到缓冲区
c语言·开发语言·学习
SunnyDays10112 小时前
Java 合并 PDF 文件:完整合并、指定页面合并与流合并
java·开发语言·合并pdf
金銀銅鐵2 小时前
[Java] 用GUI展示class文件顶层的 access_flags
后端·python·ai编程
kimnoic2 小时前
Python操作xlwings的实例详解
开发语言·python
2601_957883842 小时前
2026年10月 外星人笔记本维修须知
python·电脑
泡茶喝茶写代码2 小时前
A股量化数据工程:从 REST 接口到策略信号(第 17 篇):创新高与新低扫描
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
栗子~~3 小时前
java - logstash-logback-encoder 集成 demo
java·开发语言·logback
VIP_CQCRE3 小时前
用 Ace Data Cloud 接入 OpenAI 兼容语音转文字:一份能直接运行的 API 指南
python·openai·api·语音识别·acedatacloud