Ran Wei/计算机科学系列/12
English
计算机科学基础 — Ran Wei

模块 12: 数据库

用关系模型组织目录,通过 SQL 查询,借助约束与事务保持不变式。

约 5 小时4 个时段2 个实验8 道练习6 道自测题

完成后你能够

  • 设计表与键。
  • 用 SQL 查询与连接。
  • 避免重复事实。
  • 解释索引取舍。
  • 使用原子事务与参数化值。

开始之前

建议先修模块: 03, 10, 11.

了解记录与共享更新。通常 Python 包含 SQLite,实验使用临时库或内存。

目录

学习计划

5 小时

四个 75 分钟时段,包含练习。拓展任务或不熟悉的先修知识可能需要更多时间。进度本地保存,两种语言共享。

时段 275 分钟
构建与探究
时段 375 分钟
应用与拓展
时段 475 分钟
推理与复习
1

关系、键与约束

关系模式描述表、列与约束。主键标识行,外键引用另一表键。作者表及 book.author_id 避免到处重复作者名。NOT NULL 拒绝必需值缺失,UNIQUE 拒绝重复,CHECK 约束局部谓词。表不是有序序列,需 ORDER BY。SQLite 每个连接需开启外键检查才能依赖它。

检查理解

无 ORDER BY 的 SELECT 保证插入顺序吗?

完整解答

不保证,需明确排序。

2

查询、连接与聚合

SELECT 选列,WHERE 筛行,JOIN 按关系组合。图书作者按 ID 连接,而非碰巧相同的名字。COUNT 计行,GROUP BY 聚合前分组。NULL 表示缺失或未知,应用 IS NULL。多匹配可使连接行数增加,需检查键基数。参数占位符绑定数据值,不能任意替代表或列名。

检查理解

为何用作者 ID 而非名字连接?

完整解答

名字未必唯一或稳定。

3

规范化与建模

重复事实导致更新异常,多个图书行的作者名可能不一致。作者事实保存一次并引用。图书与作者多对多需带复合键连接表,而非逗号字段。版本与实体副本身份不同,应分别建模。规范化帮助一致性,实测需要时可反规范化,但更新策略须保持正确。先从事实与依赖出发。

检查理解

每本多作者如何表示?

完整解答

包含书 ID 与作者 ID 对的连接表。

4

索引与查询计划

索引是随表维护的查询结构,能减少精确或范围查询工作,但占空间并增加更新成本。数据库根据结构与统计选择计划,不能假定每个谓词都用索引。EXPLAIN QUERY PLAN 显示策略。书名索引支持精确查询,前导通配子串可能仍扫描。增加索引前需测量负载并计写成本。

检查理解

为何不自动索引每列?

完整解答

索引占空间与维护成本,未必有用。

5

事务与安全查询

事务让修改一起提交或回滚。原子性避免借阅与库存不一致,隔离管理并发交互,持久性关注已提交数据在数据库保证下抗故障。条件 UPDATE 仅在库存正数时扣减,插入借阅属同一事务。用户值用 ? 绑定,保持为数据而非 SQL 语法。输入验证与参数化互补,都不能代替授权。

检查理解

扣减后借阅插入失败应怎样?

完整解答

作为同一事务回滚扣减。

6

常见误解

  • 连接上下文处理事务,不自动关闭连接。
  • 唯一性与索引相关,但要求不同。
7

实验准备

下载脚本,在终端中使用 Python 3.11 或更新版本运行:python m12_database.py. Windows 也可使用 py -3;部分系统使用 python3。实验仅用标准库。先预测结果,再运行并完成变体。不要使用 -O,以保留断言。下方输出由构建器实际运行捕获,两种语言使用相同代码与输出。

8

实验 1 — 持久保存与查询

建作者图书模式、连接、检查索引,重新打开文件确认跨连接保存。

下载 m12_database.py

"""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
  1. 加入同作者另一图书。
  2. 尝试无效作者 ID。
  3. 按作者统计书数。
完整解答

复用作者 ID 一,无效外键抛 IntegrityError。插入后按作者分组 COUNT 为二,包含零书作者需外连接。

9

实验 2 — 原子借阅

重复请求在尝试扣减后失败,扣减须回滚。本实验拒绝重复,模块 14 将实现幂等重放。

下载 m12_transactions.py

"""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
  1. 移除事务分组,指出破坏不变式。
  2. 测试未知图书。
  3. 解释条件更新为何优于未保护先查再扣。
完整解答

插入失败前已提交扣减会无借阅地丢库存。未知书影响零行,拒绝。条件修改合并库存谓词与更新,避免单独陈旧读取。

10

练习与完整解答

先尝试,再展开解答。★ 应用概念;★★ 结合概念;★★★ 进行设计或证明。

练习 1 — 键★

区分主外键。

完整解答

主键标识行,外键引用其他候选键,并在启用时约束关系。

练习 2 — 排序★

如何按书名再 ID 排序?

完整解答

使用 ORDER BY title,id。

练习 3 — 缺失值★★

为何 title=NULL 不是空值检查?

完整解答

NULL 比较产生未知,应使用 IS NULL。

练习 4 — 多对多★★

设计多作者关系。

完整解答

使用两 ID 的连接表、复合主键与双方外键。

练习 5 — 注入★★

带引号书名如何传给 SQL?

完整解答

使用占位符与参数元组,不插入 SQL 文字。

练习 6 — 不变式★★★

陈述借阅库存不变式。

完整解答

无新增退还时,可用副本加有效借阅等于总副本,操作需原子保持。

练习 7 — 失败恢复★★★

事务扣减后违反唯一约束,最终状态?

完整解答

异常离开事务上下文触发回滚,双方修改撤销,保留此前提交状态。

练习 8 — 索引取舍★★

书名索引为何可能使导入变慢?

完整解答

每次插入还维护索引,增加工作与写入。

11

自测

选择答案查看反馈,重置后可重做。无需 JavaScript 也可阅读答案表。

1

表保证输出顺序吗?

2

SQLite 外键应怎样?

3

事务失败应怎样?

4

参数占位符保护什么?

5

索引有何成本?

6

未知值检查?

答案表
  1. A — ORDER BY 明确要求。
  2. B — 依赖检查前启用。
  3. C — 原子性排除部分应用。
  4. A — 它们不是权限系统。
  5. B — 查询收益有取舍。
  6. C — NULL 不同于空文字。
12

引导阅读

13

复习与下一步

画作者图书模式,解释失败借阅事务。模块 13 将把程序本身作为结构化输入,并探究计算边界。

14

关键术语

术语含义
主键唯一行标识。
事务具有提交回滚语义的组合操作。
规范化按依赖组织事实以减少异常。