Lab 7: 数据库设计与查询优化实战

课程:数据库技术与应用:AI时代的基石数据技能
授课教师:王健楠 (计算机科学与技术系)
学期:2026年(春季)

实验目的与学习目标

完成本实验后,你应能够:

  1. 理解数据库设计的完整流程: 能够从业务需求出发,识别实体、属性、联系以及关键约束,并用 E/R 图表达概念模型。
  2. 掌握从 E/R 模型到关系模式的转换: 能够将概念设计落地为 SQLite 中可执行的建表语句,并合理设置主键、外键和完整性约束。
  3. 理解索引对查询性能的影响: 能够根据查询条件和连接条件选择合适的简单索引,并解释索引为什么可能加速查询。
  4. 学会阅读查询计划并进行实验验证: 能够使用 EXPLAIN QUERY PLAN 和运行时间对比,判断查询是否使用索引以及索引是否真的带来性能收益。

实验背景

你正在参与一个清华大学校园数据平台的原型设计。这个平台一开始要服务教务场景:记录院系、学生、课程以及开课历史,并支持后续基于关系数据库的查询与维护。作为数据库小组的一员,你首先需要把需求整理成清晰的 E/R 模型,再将模型转换为可以在 SQLite 中创建的关系表。

但一个数据库系统仅仅“能存数据”还不够。系统上线后,查询会变多、数据会变大,原本正确的 SQL 也可能因为缺少索引而运行缓慢。为了在更大的数据规模上理解查询优化,本实验的第二部分将使用一个航班数据库 data/flight.db 进行性能诊断:你需要为给定查询选择索引,观察查询计划,并用运行时间截图验证优化效果。

你的工作将分为两个阶段:


评分规则: 共 100 分(2 个任务)。

任务 内容 分值
任务 1 数据库设计:E/R 模型与关系模式 50 分
任务 2 查询优化:索引、查询计划与运行时间对比 50 分

作答要求:

第 0 步:实验材料与环境准备

本实验会用到以下材料:

在第一部分中,你会把校园教务需求转换为 data/campus.db 中的关系表。下面的代码连接到该数据库。


任务 1:数据库设计:E/R 模型与关系模式(50 分)

任务背景

在数据库系统开发中,第一步不是急着写 SQL,而是先理解业务世界。哪些对象需要被记录?对象之间有什么联系?哪些约束必须被数据库表达出来?如果概念模型设计得不清楚,后面的表结构、查询和维护都会变得困难。

在本任务中,你将为清华大学设计一个小型教务数据库。这个数据库需要记录院系、学生、课程以及课程开设历史。你需要先画出 E/R 图,再把它转换为 SQLite 中的关系模式。

业务需求

你需要为清华大学设计一个简化版教务数据库。该数据库面向课程管理和学生选课场景,需要记录院系、学生、课程以及课程开设信息,具体需求如下:

1.1 绘制 E/R 图(25 分)

请根据上面的业务需求,使用课堂中学习过的 E/R 模型表示法,绘制清华大学教务数据库的 E/R 图。你的图需要准确表达所有实体、属性、联系、和约束

请将完成后的图片保存为 assets/ER-diagram.png。下面会显示该文件,你可以用它检查 notebook 中的图片路径是否正确。

E/R diagram

1.2 从 E/R 图转换为关系模式(25 分)

请根据你绘制的 E/R 图,编写 SQL 语句,在 data/campus.db 中创建所需的关系表。

要求:

  1. 为每个关系表选择合适的主键;
  2. 在需要的地方声明外键;
  3. 尽可能用数据库约束表达 E/R 图中的结构要求;
  4. 表名、属性名应清晰、可读,能够反映业务含义。

任务 2:查询优化:索引、查询计划与运行时间对比(50 分)

任务背景

现在,你已经完成了数据库设计的第一步。但在真实系统中,数据库很快会遇到另一个问题:数据量上来以后,查询是否还能跑得动?

为了更直观地观察索引的作用,本任务使用一个规模更大的航班数据库 data/flight.db。数据库中只有一张表 FLIGHTS,其模式如下:

FLIGHTS (fid, year, month_id, day_of_month, day_of_week_id, carrier_id, flight_num, origin_city, origin_state, dest_city, dest_state, departure_delay, taxi_out, arrival_delay, canceled, actual_time, distance)

本任务只需要关注四个属性:carrier_idorigin_cityactual_timedest_city

你需要完成三件事:选择索引、检查查询计划、用运行时间截图验证索引效果。

2.1 为三个选择查询选择一个简单索引(15 分)

请考虑以下三个查询:

sqlite
(a): SELECT DISTINCT carrier_id
     FROM Flights
     WHERE origin_city = 'Seattle WA' AND actual_time <= 180;
sqlite
(b): SELECT DISTINCT carrier_id
     FROM Flights
     WHERE origin_city = 'Gunnison CO' AND actual_time <= 180;
sqlite
(c): SELECT DISTINCT carrier_id
     FROM Flights
     WHERE origin_city = 'Seattle WA' AND actual_time <= 30;

请为这三个查询选择一个最可能同时加速它们的简单索引。简单索引指只建立在一个属性上的索引。

请打开命令行中的 SQLite,使用 sqlite3 data/flight.db 连接提供的数据库,检查 FLIGHTS 表是否已经存在你选择的索引。如果不存在,请把该索引添加到 FLIGHTS 表中。

请使用 EXPLAIN QUERY PLAN ... 检查每个查询是否使用了该索引。

2.2 为连接查询选择第二个简单索引(15 分)

请考虑下面的查询:

sqlite
(d): SELECT DISTINCT F2.origin_city
     FROM Flights F1, Flights F2
     WHERE F1.dest_city = F2.dest_city
         AND F1.origin_city='Gunnison CO'
         AND F1.actual_time <= 30;

请选择一个与 2.1 中不同的简单索引,使它有可能加速这个查询。请写出 CREATE INDEX 语句,并解释你的选择。

请使用 sqlite3 data/flight.db 连接提供的数据库,检查 FLIGHTS 表是否已经存在这个第二个索引。如果不存在,请把该索引添加到 FLIGHTS 表中。

请使用 EXPLAIN QUERY PLAN ... 检查查询 (d) 是否使用了这个第二个索引。

2.3 用运行时间验证索引效果(20 分)

现在我们想知道:这两个索引到底有多有效?请比较查询 (a) 到 (d) 在没有索引有索引两种情况下的运行时间。

提示:在 SQLite 中可以使用 .timer on 打开计时功能。开始计时前,请先用 .schema FlightsPRAGMA index_list(Flights); 确认当前表是否处于你需要的索引状态;如果需要回到无索引状态,可以使用 DROP INDEX 索引名; 删除你创建的索引。

没有索引时的运行时间(10 分)

请在没有这两个索引的 FLIGHTS 表上执行查询 (a) 到 (d),并为每个查询的运行时间截图。下面是示例截图位置,请用你自己的截图替换 runtimes/ 文件夹中的对应图片。

无索引查询 a 无索引查询 b 无索引查询 c 无索引查询 d

有索引时的运行时间(10 分)

请在已经创建两个索引的 FLIGHTS 表上执行查询 (a) 到 (d),并为每个查询的运行时间截图。下面是示例截图位置,请用你自己的截图替换 runtimes/ 文件夹中的对应图片。

有索引查询 a 有索引查询 b 有索引查询 c 有索引查询 d

实验圆满结束!

恭喜你完成了从数据库设计到查询优化的一次完整实践。你已经经历了数据库系统建设中非常典型的两步:先把现实需求抽象成清晰的数据模型,再通过索引和查询计划让系统在更大数据量上跑得更快。

提交前最终检查:

提交文件: 请将以下内容打包为一个压缩文件提交:

你不需要提交 data/flight.db