Lab 6:银行数据库SQL查询与分析实战

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

实验目的与学习目标

本实验以银行数据库为背景,引导你完成由浅入深的 20 道 SQL 实战任务。完成本实验后,你应能够:

  1. 排序与过滤:使用 ORDER BY 按单列或多列对结果排序,使用 WHERE 筛选满足条件的记录。
  2. 多表连接:使用 INNER JOIN 关联多张表获取复合信息,使用 LEFT OUTER JOIN 保留左表中不匹配的记录。
  3. 聚合与分组:使用 COUNTSUMAVGMAXMIN 对数据进行统计,结合 GROUP BY 分组汇总,并用 HAVING 对分组结果进行过滤。
  4. 子查询:在 WHEREFROM 子句中嵌套子查询,将复杂问题拆解为多个步骤逐层求解。
  5. 集合运算:使用 UNIONINTERSECTEXCEPT 对多个查询的结果集进行合并、取交集和求差集。

实验背景

你刚刚收到了一家跨国银行数据分析部门的实习录取通知!该银行为全球数以万计的客户提供金融服务,在纽约、伦敦、柏林等多个城市设有分行。

入职第一天,主管把你带到工位前,指着屏幕上的数据库架构图说:

银行每天产生大量业务数据,我们的工作就是通过 SQL 查询,从这些数据中提炼出管理层所需的信息。今天,你就来体验一下真实的数据分析工作吧!

你打开电脑,准备迎接挑战 :)


数据库结构

数据库文件 bank.db 包含以下 6 张表,下划线标注主键,上标标注外键:

字段说明:


评分规则: 共 100 分,20 道题,每题 5 分。

任务 主题 题数 分值
任务 1 排序、过滤与基础聚合 4 题 20 分
任务 2 连接查询 5 题 25 分
任务 3 聚合与分组 6 题 30 分
任务 4 子查询与集合运算 5 题 25 分

作答要求:

第 0 步:环境准备

安装 ipython-sql

ipython-sql 是一个 Jupyter Notebook 扩展,安装后可以在 Notebook 中直接执行 SQL 语句,结果以表格形式呈现。运行下面的代码完成安装(若已安装则会跳过):

如何在 Notebook 中编写 SQL?

安装完成后,按以下三步使用 SQL 魔法命令:

第 1 步:加载扩展(每次启动 Notebook 执行一次)

%load_ext sql

第 2 步:连接数据库

%sql sqlite:///bank.db

连接成功后会显示 Connected: @bank.db

第 3 步:编写 SQL 查询

写法 格式 适用场景
单行查询 %sql SELECT ... 简短语句
多行查询 单元格首行写 %%sql,SQL 从第二行开始 多行 SQL
存入变量 单元格首行写 %%sql 变量名 <<,SQL 从第二行开始 需在 Python 中处理结果

示例——将查询结果存入变量,再转为 pandas DataFrame:

%%sql my_result <<
SELECT firstName, lastName
FROM Customer LIMIT 5
df = my_result.DataFrame()
print(df)

本实验约定: 每道题的 SQL 单元格首行为 %%sql qX_Y <<(如 %%sql q1_1 <<), 结果将被保存到对应变量,供下方检验单元格自动验证。

运行下面的代码,验证数据库连接是否正常,并查看数据库中的所有表:

运行以下代码,快速浏览 CustomerBranch 表的部分数据,熟悉数据内容:


任务 1:排序、过滤与基础聚合(20 分)

任务背景

"好,我们先从最基础的查询开始," 你的主管说,"在实际分析中,WHERE 帮你筛选感兴趣的行,ORDER BY 让结果整洁有序,聚合函数则把大量数据压缩成一个关键数字——这三件工具是所有分析的起点。"

他递给你四份数据需求单:

1.1 中等收入客户名单(5 分)

财务部门需要一份中等收入客户的排序名单,用于市场细分分析。

请查询收入(income)在 6 万至 7 万之间(含两端)的客户的 firstName(名)、lastName(姓)和 income(收入)。

排序:income 降序;收入相同时按 lastName 升序,再按 firstName 升序。

1.2 重点分行员工薪资差距(5 分)

人力资源部门想了解几个重点分行中员工与其经理的薪资差距。

请查询在纽约(New York)、伦敦(London)或柏林(Berlin)分行工作的全体员工的以下信息: sin(员工号)、branchName(分行名)、salary(员工薪资)、以及经理薪资 − 员工薪资(命名为 boss_diff)。

排序:boss_diff 升序

提示: 需同时关联 Employee(员工)、Branch(分行)、以及再次关联 Employee(经理)三张表,通过 managerSIN 找到经理薪资。

1.3 纽约分行人员构成统计(5 分)

纽约分行需要了解当前员工的规模与姓氏多样性。

请查询纽约(New York)分行的员工总人数(命名为 count_employees)和不同姓氏数量(命名为 count_distinct_lastnames),结果为一行两列

提示: 使用 COUNT(DISTINCT 列名) 统计不重复值。

1.4 纽约分行薪资总额(5 分)

财务部门需要汇总纽约分行的人力成本。

请查询纽约(New York)分行所有员工薪资之和(命名为 sum_salaries),结果为一行一列


任务 2:连接查询(25 分)

任务背景

"一张表的信息总是有限的," 主管接着跟你说,"比如 Account 表只有分行号,没有分行名;Employee 表只有员工号,没有分行详情。要把散落在多张表的信息整合到一起,就需要用到 JOIN——这是 SQL 中最强大的工具之一。"

他打开了五个新的需求工单,每个工单都需要查询不止一张表……

2.1 特定经理分行的大额账户(5 分)

财务审计部门需要核查由 Phillip Edwards 担任经理的分行中,余额超过 11 万元的账户。

请查询这些账户的 branchName(分行名)、accNumber(账号)和 balance(余额)。

排序:accNumber 升序。

提示: 关联 AccountBranchEmployee 三张表,通过 managerSIN 定位经理。

2.2 跨国 VIP 客户识别(5 分)

营销部门希望识别同时在伦敦(London)和拉脱维亚(Latveria)两地都持有账户、且收入超过 9 万的客户,并列出他们的所有账户信息。

请返回 customerID(客户编号)、income(收入)、accNumber(账号)、branchNumber(分行号)。

排序: 先按 customerID 升序,再按 accNumber 升序。

注意: 结果应包含满足条件客户的所有账户,即使某账户并不在伦敦或拉脱维亚分行。

提示: 先用 INTERSECT 取"在伦敦持有账户的客户编号"与"在拉脱维亚持有账户的客户编号"的交集,再作为子查询筛选。

2.3 伦敦分行薪资最低员工(5 分)

伦敦分行计划对薪资最低的员工进行福利补贴,首先需要确定这些员工的信息(薪资并列最低时全部列出)。

请查询伦敦(London)分行中薪资最低的员工的 sinfirstNamelastNamesalary

排序:sin 升序。

提示: 子查询先求 MIN(salary),外层再筛选等于该值的员工。

2.4 高薪员工与经理标注(外连接)(5 分)

人力资源部门想要一份薪资超过 7 万元的员工名单,并标注其中担任分行经理的人员。

请查询薪资超过 7 万的所有员工的 sinfirstNamelastNamesalary,以及 branchName(若该员工担任分行经理,则显示分行名;否则为 NULL)。

排序:branchName 升序(NULL 排最前面)。

强制要求:必须使用 LEFT OUTER JOIN 实现。

提示: Employee LEFT OUTER JOIN Branch,连接条件为 e.sin = b.managerSIN

2.5 同题重做:不用 JOIN(5 分)

上一题要求用外连接实现,本题要求达到完全相同的结果,但不允许使用任何 JOIN 操作

提示:可以用 UNION 将两个子查询合并:

通过对比两种实现,你会发现外连接和 UNION 在某些场景下可以相互替代。


任务 3:聚合与分组(30 分)

任务背景

季度末,各部门纷纷向数据团队提交汇总报表需求。你的主管把几份需求单推到你面前:

"光有原始数据还不够,管理层更需要统计汇总——每个分行的薪资水平如何?哪些客户账户分布集中?哪些账户交易频繁?这些问题都要靠 GROUP BYHAVING 和聚合函数来回答。"

你翻开第一份需求单……

3.1 各分行薪资统计(5 分)

总部人力资源部需要一张汇总各分行薪资水平的报表。

请查询每个分行的 branchName(分行名)、salary_gap(薪资极差 = 最高 − 最低)和 avg_salary(平均薪资)。

排序:branchName 升序。

3.2 账户分布集中的客户(5 分)

客户关系部门希望识别账户分布集中的客户:他们的账户涉及最多 2 个不同分行,便于针对性推广本地化服务。

请查询满足条件的客户的 customerID(客户编号)、firstName(名)和 lastName(姓)。

排序: 先按 lastName 升序,再按 firstName 升序。

提示: 子查询中关联 OwnsAccount,按 customerID 分组,用 COUNT(DISTINCT branchNumber) <= 2 筛选,外层再取姓名。

3.3 不同年龄段的平均收入(5 分)

市场研究部门需要对比年长与年轻客户群体的平均收入水平。

请查询年龄超过 60 岁的客户平均收入(命名为 avg_income_over60)和年龄不足 20 岁的客户平均收入(命名为 avg_income_under20),结果为一行两列

提示: 为避免不同运行日期导致结果变化,本题统一以 2026-05-10 作为参考日期。使用 SQLite 的 date('2026-05-10', '-N years') 计算精确年龄分界日期:

  • 年龄超过 60 岁 等价于出生日期满足 birthDate <= date('2026-05-10', '-60 years')
  • 年龄不足 20 岁 等价于出生日期满足 birthDate > date('2026-05-10', '-20 years')

结合 CASE WHEN ... THEN income END 在同一行计算两组平均值。详见 SQLite 日期时间函数文档

3.4 特定姓名规律的多账户客户(5 分)

VIP 服务部门需要一批拥有多个账户、且名字有特定规律的客户名单。

请查询同时满足以下两个条件的客户的 customerIDlastNamefirstNameincome 和平均账户余额(命名为 avg_balance):

  1. 持有至少 2 个账户
  2. 满足以下姓名条件之一(或同时满足):
    • 姓(lastName)以 S 开头且包含字母 e(如 Steve)
    • 名(firstName)以 A 开头,且倒数第 3 位为字母 n(如 Amanda:A-m-a-[n]-d-a,方括号标注倒数第3位)

排序:customerID 升序。

提示: LIKE 模式:lastName LIKE 'S%e%'firstName LIKE 'A%n__'_ 匹配任意单个字符,A%n__ 表示以 A 开头,且倒数第 3 位为 n,即末尾还有任意两个字符)。 使用 HAVING COUNT(*) >= 2 筛选持有至少 2 个账户的客户。

3.5 伦敦分行高频交易账户分析(5 分)

风控部门需要了解伦敦分行中交易次数较多的账户情况。

请查询伦敦(London)分行中交易次数不少于 15 次的账户的 accNumber(账号)、balance(余额)、sum_amount(交易金额之和)和 balance_minus_sum(余额 − 交易总额)。

排序:sum_amount 升序。

3.6 大型分行各类账户平均交易金额(5 分)

业务分析部门需要了解规模较大的分行中,各类账户的平均交易情况。

请查询账户总数不少于 50 个的分行中,各账户类型的 branchName(分行名)、type(账户类型)和 avg_amount(平均交易金额)。

排序: 先按 branchName 升序,再按 type 升序。


任务 4:子查询与集合运算(25 分)

任务背景

"最后一组任务来了!" 你的主管推来一叠需求单,"这些问题比较复杂,一层查询解决不了,你需要把问题分解,借助子查询集合运算把各步骤串联起来。"

他补充道:"遇到困难时,先在纸上把逻辑理清楚,再动手写 SQL——这永远是最有效的方法。"

4.1 超高收入客户筛选(5 分)

市场部门需要识别超高收入客户,定义为:收入不低于所有姓 Butler 的客户中最高收入的两倍

请查询满足条件的客户的 firstName(名)、lastName(姓)和 income(收入)。

排序: 先按 lastName 升序,再按 firstName 升序。

提示: 子查询求 Butler 客户中 MAX(income),外层筛选收入 >= MAX(income) * 2 的客户。

4.2 储蓄与企业账户持有人分析(5 分)

产品部门希望了解持有储蓄(SAV)或企业(BUS)账户的客户,以及他们名下所有此类账户。

请查询至少持有一个 BUS 账户或至少一个 SAV 账户的客户的所有 BUS 和 SAV 账户信息,返回 customerIDtypeaccNumberbalance

排序: 先按 customerID,再按 type,最后按 accNumber,全部升序。

提示:UNION 将"持有 SAV 账户的客户编号"与"持有 BUS 账户的客户编号"合并,作为 IN 子查询条件。

4.3 "纯纽约"客户识别(5 分)

安全合规部门需要识别与伦敦分行完全无关联的纽约客户,用于一次专项排查。

请查询同时满足以下全部条件的客户的 customerID(不含重复):

  1. 纽约(New York)分行持有账户;
  2. 伦敦(London)分行持有任何账户;
  3. 与任何在伦敦分行持有账户的客户共同署名持有同一账户。

排序:customerID 升序。

三步法:

  1. 找出"伦敦客户"集合(在伦敦分行持有账户的客户编号);
  2. 找出"与伦敦客户共同持有账户的所有客户":先在 Owns 中取第1步客户持有的全部账号(含非伦敦分行账户),再找 Owns 中持有这些账号的所有客户编号(双层子查询);
  3. 从纽约客户中用 NOT IN 排除第1步(对应条件2)和第2步(对应条件3)中的客户。

注意: 条件3排查的是"账户共享",而非"在伦敦持有账户"——两者的差别在于,某个非伦敦账户也可能由纽约客户与伦敦客户共同持有。

4.4 覆盖 Helen Morgan 所有分行的客户(5 分)

VIP 服务部门想找出与某位重要客户"涉猎面"相当的客户(含本人)。

请查询收入超过 5000在 Helen Morgan 所持有账户涉及的所有分行都持有账户的客户的 customerIDfirstNamelastNameincome

排序:income 降序

这是经典的"关系除法"查询——相当于"找出满足全称量词条件的客户"。 提示: 使用 NOT EXISTS (... EXCEPT ...) 结构:先找出 Helen Morgan 持有账户的分行集合,再检查当前客户是否缺少其中任何一个分行。 说明: Helen Morgan 本人也满足条件(她自身覆盖了自己的所有分行),因此结果中会包含她。

4.5 异常高额交易账户的全部交易记录(5 分)

风控部门需要审查交易金额异常偏高的账户。

筛选标准:某账户的平均交易金额超过同类型账户整体平均交易金额的 3 倍时,该账户被认为异常。

这里的“同类型账户整体平均交易金额”是指:将该类型账户对应的所有交易记录合并在一起后,对 amount 直接求一次 AVG不是先分别求每个账户的平均交易金额,再对这些账户均值求平均。

例如:若所有企业账户(BUS)的整体平均交易金额为 2000 元,则某 BUS 账户的平均交易金额超过 6000 元时,该账户的所有交易记录均需返回(即使其中某笔交易金额低于 6000)。

请返回这些异常账户的 branchName(分行名)、type(账户类型)、accNumber(账号)、transNumber(交易号)和 amount(金额)。

排序: 先按 branchName,再按 type,再按 accNumber,最后按 transNumber,全部升序。

提示: 先用嵌套子查询找出平均交易金额超过 3 倍类型整体均值的账户编号;其中“类型整体均值”应按该类型的全部交易记录直接计算 AVG(amount)。再关联 BranchAccountTransactions 三张表获取完整记录。


实验圆满结束!

恭喜你完成了全部任务!你从一份真实的关系型数据库出发,系统地运用了 SQL 进阶查询的核心技能:


提交前最终检查: