关系、键与约束
关系模式描述表、列与约束。主键标识行,外键引用另一表键。作者表及 book.author_id 避免到处重复作者名。NOT NULL 拒绝必需值缺失,UNIQUE 拒绝重复,CHECK 约束局部谓词。表不是有序序列,需 ORDER BY。SQLite 每个连接需开启外键检查才能依赖它。
无 ORDER BY 的 SELECT 保证插入顺序吗?
完整解答
不保证,需明确排序。
查询、连接与聚合
SELECT 选列,WHERE 筛行,JOIN 按关系组合。图书作者按 ID 连接,而非碰巧相同的名字。COUNT 计行,GROUP BY 聚合前分组。NULL 表示缺失或未知,应用 IS NULL。多匹配可使连接行数增加,需检查键基数。参数占位符绑定数据值,不能任意替代表或列名。
为何用作者 ID 而非名字连接?
完整解答
名字未必唯一或稳定。
规范化与建模
重复事实导致更新异常,多个图书行的作者名可能不一致。作者事实保存一次并引用。图书与作者多对多需带复合键连接表,而非逗号字段。版本与实体副本身份不同,应分别建模。规范化帮助一致性,实测需要时可反规范化,但更新策略须保持正确。先从事实与依赖出发。
每本多作者如何表示?
完整解答
包含书 ID 与作者 ID 对的连接表。
索引与查询计划
索引是随表维护的查询结构,能减少精确或范围查询工作,但占空间并增加更新成本。数据库根据结构与统计选择计划,不能假定每个谓词都用索引。EXPLAIN QUERY PLAN 显示策略。书名索引支持精确查询,前导通配子串可能仍扫描。增加索引前需测量负载并计写成本。
为何不自动索引每列?
完整解答
索引占空间与维护成本,未必有用。
事务与安全查询
事务让修改一起提交或回滚。原子性避免借阅与库存不一致,隔离管理并发交互,持久性关注已提交数据在数据库保证下抗故障。条件 UPDATE 仅在库存正数时扣减,插入借阅属同一事务。用户值用 ? 绑定,保持为数据而非 SQL 语法。输入验证与参数化互补,都不能代替授权。
扣减后借阅插入失败应怎样?
完整解答
作为同一事务回滚扣减。
常见误解
- 连接上下文处理事务,不自动关闭连接。
- 唯一性与索引相关,但要求不同。
实验准备
下载脚本,在终端中使用 Python 3.11 或更新版本运行:python m12_database.py. Windows 也可使用 py -3;部分系统使用 python3。实验仅用标准库。先预测结果,再运行并完成变体。不要使用 -O,以保留断言。下方输出由构建器实际运行捕获,两种语言使用相同代码与输出。
实验 1 — 持久保存与查询
建作者图书模式、连接、检查索引,重新打开文件确认跨连接保存。
"""Persist records, join relations and inspect an indexed query."""
import sqlite3, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "catalogue.sqlite"
db = sqlite3.connect(path)
try:
db.execute("PRAGMA foreign_keys = ON")
db.executescript("""
CREATE TABLE authors(id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE books(id INTEGER PRIMARY KEY, title TEXT NOT NULL, author_id INTEGER NOT NULL REFERENCES authors(id));
CREATE INDEX book_title ON books(title);
""")
with db:
db.execute("INSERT INTO authors VALUES (?, ?)", (1, "Frank Herbert"))
db.execute("INSERT INTO books VALUES (?, ?, ?)", (1, "Dune", 1))
query = "SELECT books.title, authors.name FROM books JOIN authors ON authors.id=books.author_id WHERE books.title=?"
print("joined:", db.execute(query, ("Dune",)).fetchall())
plan = db.execute("EXPLAIN QUERY PLAN SELECT id FROM books WHERE title=?", ("Dune",)).fetchall()
indexed = any("INDEX" in row[3] for row in plan)
print("index used:", indexed); assert indexed
attack = "' OR 1=1 --"
assert db.execute("SELECT id FROM books WHERE title=?", (attack,)).fetchall() == []
finally:
db.close()
reopened = sqlite3.connect(path)
try:
print("reopened rows:", reopened.execute("SELECT COUNT(*) FROM books").fetchone()[0])
assert reopened.execute("SELECT COUNT(*) FROM books").fetchone()[0] == 1
finally:
reopened.close()
joined: [('Dune', 'Frank Herbert')]
index used: True
reopened rows: 1
- 加入同作者另一图书。
- 尝试无效作者 ID。
- 按作者统计书数。
完整解答
复用作者 ID 一,无效外键抛 IntegrityError。插入后按作者分组 COUNT 为二,包含零书作者需外连接。
实验 2 — 原子借阅
重复请求在尝试扣减后失败,扣减须回滚。本实验拒绝重复,模块 14 将实现幂等重放。
"""A stock decrement and loan creation commit together or roll back together."""
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("PRAGMA foreign_keys=ON")
db.executescript("""
CREATE TABLE books(id INTEGER PRIMARY KEY, copies INTEGER NOT NULL CHECK(copies>=0));
CREATE TABLE loans(request_id TEXT PRIMARY KEY, book_id INTEGER REFERENCES books(id));
INSERT INTO books VALUES(1, 2);
""")
def borrow(request_id, book_id):
with db:
changed = db.execute("UPDATE books SET copies=copies-1 WHERE id=? AND copies>0", (book_id,)).rowcount
if changed != 1: raise ValueError("unavailable")
db.execute("INSERT INTO loans VALUES (?, ?)", (request_id, book_id))
try:
borrow("request-a", 1)
try:
borrow("request-a", 1)
except sqlite3.IntegrityError:
print("duplicate request rolled back")
assert db.execute("SELECT copies FROM books").fetchone()[0] == 1
borrow("request-b", 1)
try:
borrow("request-c", 1)
except ValueError:
print("out of stock rejected")
print("copies:", db.execute("SELECT copies FROM books").fetchone()[0], "loans:", db.execute("SELECT COUNT(*) FROM loans").fetchone()[0])
assert db.execute("SELECT COUNT(*) FROM loans").fetchone()[0] == 2
finally:
db.close()
duplicate request rolled back
out of stock rejected
copies: 0 loans: 2
- 移除事务分组,指出破坏不变式。
- 测试未知图书。
- 解释条件更新为何优于未保护先查再扣。
完整解答
插入失败前已提交扣减会无借阅地丢库存。未知书影响零行,拒绝。条件修改合并库存谓词与更新,避免单独陈旧读取。
练习与完整解答
先尝试,再展开解答。★ 应用概念;★★ 结合概念;★★★ 进行设计或证明。
区分主外键。
完整解答
主键标识行,外键引用其他候选键,并在启用时约束关系。
如何按书名再 ID 排序?
完整解答
使用 ORDER BY title,id。
为何 title=NULL 不是空值检查?
完整解答
NULL 比较产生未知,应使用 IS NULL。
设计多作者关系。
完整解答
使用两 ID 的连接表、复合主键与双方外键。
带引号书名如何传给 SQL?
完整解答
使用占位符与参数元组,不插入 SQL 文字。
陈述借阅库存不变式。
完整解答
无新增退还时,可用副本加有效借阅等于总副本,操作需原子保持。
事务扣减后违反唯一约束,最终状态?
完整解答
异常离开事务上下文触发回滚,双方修改撤销,保留此前提交状态。
书名索引为何可能使导入变慢?
完整解答
每次插入还维护索引,增加工作与写入。
自测
选择答案查看反馈,重置后可重做。无需 JavaScript 也可阅读答案表。
表保证输出顺序吗?
SQLite 外键应怎样?
事务失败应怎样?
参数占位符保护什么?
索引有何成本?
未知值检查?
答案表
- A — ORDER BY 明确要求。
- B — 依赖检查前启用。
- C — 原子性排除部分应用。
- A — 它们不是权限系统。
- B — 查询收益有取舍。
- C — NULL 不同于空文字。
引导阅读
- SQLite 事务 — 阅读提交回滚与写事务,描述部分更新失败。
- Python sqlite3 教程 — 阅读占位符与连接管理,区分关闭与回滚。
复习与下一步
画作者图书模式,解释失败借阅事务。模块 13 将把程序本身作为结构化输入,并探究计算边界。
关键术语
| 术语 | 含义 |
|---|---|
| 主键 | 唯一行标识。 |
| 事务 | 具有提交回滚语义的组合操作。 |
| 规范化 | 按依赖组织事实以减少异常。 |