ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据库实验六:视图与关系图,从虚拟表到可视化外键设计

数据库实验六:视图与关系图,从虚拟表到可视化外键设计

数据库课程做到实验六,基本就进入了一个分水岭:前面玩的是单表,后面玩的是表和表之间的关系。这个实验叫“视图和关系图定义及使用”,很多人第一反应是“视图我知道,就是虚拟表,但关系图是什么?”,还有人做实验时觉得视图可有可无,关系图更是打开画两下就算完事,结果一查成绩,扣分点全在细节上。

这个实验看起来只有两个任务:定义视图、使用视图,外加画一个关系图。但实际做下来你就会发现,它考的不是你能不能敲出CREATE VIEW,也不是你会不会拖拽几个表框,而是你有没有理解“视图是数据库对外的接口,关系图是数据库设计落地的可视化表达”这两件事。

我下面按自己实操和帮人调实验的顺序,把这个实验完整拆一遍。每个步骤都给出可复现的命令和操作思路,顺带把容易踩的坑提前标出来。

1. 实验目标与整体设计思路

1.1 视图到底解决什么问题

在开始敲命令之前,得先想清楚一个问题:表里面明明有数据,为什么还要造一个“虚表”出来?我见过不少初学者把视图当成“简化版查询”,建了个视图之后发现查询结果和原来一样,就觉得这实验没有意义。这是对视图定位的误解。

视图最核心的价值有两个。第一个是逻辑独立性。应用程序面向视图编程,不直接面向物理表。以后基表结构改了,只要视图定义不变,应用层代码就不用动。比如说,选课表里原来存的成绩是百分制整数,后来系统要求改成小数,如果所有报表程序都直接查选课表,那到处都要改;但是如果你对外提供一个“成绩视图”,视图负责把整数转成小数,应用层就感受不到变化。

第二个价值是安全控制。你可以把表的某些列隐藏掉,只给外部用户开放视图。比如学生表里有身份证号、手机号、家庭住址,这些字段不应该让普通任课老师看到。那就建一个视图,只包含学号、姓名、班级、专业,让任课老师只能查这个视图,而不是直接查学生表。视图在数据库里就相当于“按需裁剪后的表”,权限控制粒度可以做到列级别,这是单靠GRANT语句很难做到的。

所以实验六里的“定义视图”,不是让你体验一下语法,而是要建立一种思维:视图是表之上的“外模式”,是数据库对外开放的窗口。你写CREATE VIEW的时候,要想清楚这个视图给谁用、要隐藏什么、要暴露什么。

1.2 关系图在实验中的定位

关系图这个名字,在不同数据库管理工具里叫法不完全一样,有的叫“数据库关系图”,有的叫“EER图”,有的干脆就叫“模型”。不管名字怎么变,本质都一样:用图形方式展示表、字段,以及表之间的主外键关系。

这个实验把视图和关系图放在一起,其实是刻意安排的。视图解决的是“对外暴露什么样的数据形态”,关系图解决的是“表之间的关联关系是否清晰”。你在试验里用SQL建过外键,但那只是命令行里一句ALTER TABLE,看不见全局。关系图把三张表、五张表摆在一个画布上,字段和连线一眼就能看明白,数据从哪张表流向哪张表,哪个表是主表,哪个表是从表,全部可视化。

我更愿意把关系图当成数据库设计的“施工图”。你建表的时候可能清楚A表和B表的关系,等过两个月表多了,你再看DDL脚本,很难快速回忆起当初怎么设计的。打开关系图,一切关系都在线上。所以实验要求画关系图,不是让你练鼠标操作,而是让你养成“用图形表达关系”的习惯。

整体实验思路可以分两条线走:

  • 第一条线是“视图线”:建单表视图、多表连接视图、带条件限制的视图,然后基于视图做查询和更新。
  • 第二条线是“关系图线”:把已经建好的表导入关系图,拖拽建立外键关系,保存并验证约束。

2. 视图的定义与查询实操

2.1 创建单表视图:别忽略列别名和选择列

先准备一个最基础的三张表场景,后面所有例子都用它。

学生表student,字段包括student_id、student_name、major、phone。课程表course,字段包括course_id、course_name、credit。选课成绩表score,字段包括student_id、course_id、score,其中student_id和course_id联合作为主键,同时分别引用student和course。

最简单的视图,就是把单表的某些行列拿出来:

CREATE VIEW v_student_info AS SELECT student_id, student_name, major FROM student;

这个视图只保留学号、姓名、专业,隐藏了电话字段。创建之后,可以像查表一样查询:

SELECT * FROM v_student_info WHERE major = '计算机';

很多实验指导书上还会要求给视图列重新起名字。这个点很容易被忽略,但非常重要,因为视图可以暴露给外部程序,列名应该更友好,甚至需要隐藏物理字段名。写法有两种,一种是在SELECT里直接写别名,另一种是跟在视图名后面加列名列表:

CREATE VIEW v_student_alias(id, name, specialty) AS SELECT student_id, student_name, major FROM student;

两种写法效果类似,但第二种更适合统一外部接口命名。注意一点:如果视图定义中包含了计算列、函数处理后的列,或者多表连接时有同名列,就必须使用别名,否则数据库会报“列名不唯一”之类的错误。

2.2 创建多表连接视图:三表连接的正确姿势

视图真正有意思的地方在于多表连接。比如要做一个“学生成绩明细视图”,显示学生姓名、课程名称、成绩,就必须连接student、course、score三张表:

CREATE VIEW v_score_detail AS SELECT s.student_name, c.course_name, sc.score FROM score sc JOIN student s ON sc.student_id = s.student_id JOIN course c ON sc.course_id = c.course_id;

这里有个初学者容易写反的点:JOIN的顺序建议从事实表出发。score是事实表,它通过student_id和course_id关联两张维度表。先写score,再往外关联学生和课程,逻辑上更顺,查询优化器执行起来也更自然。

建完这个视图后,查询就变得非常简洁了:

SELECT * FROM v_score_detail WHERE score >= 90;

如果不建视图,这条查询要写一大段JOIN。视图的价值在这里体现得最直接:把复杂的连接逻辑封装起来,使用者不需要理解底层表结构。这就像你不需要知道电视机内部电路,只需要按遥控器一样。

2.3 修改、删除与验证视图

实验里通常还会要求修改视图。修改视图用ALTER VIEW,而不是先DROP再CREATE。标准SQL写法如下:

ALTER VIEW v_student_info AS SELECT student_id, student_name, major, phone FROM student;

这里相当于把之前隐藏的phone列又放出来了,业务上可能不太合理,但实验要演示语法,逻辑只要自洽就行。真正常见的场景是修改视图的过滤条件,比如原来只显示计算机专业,现在要显示所有专业。

删除视图用:

DROP VIEW v_student_info;

删除视图不会影响基表数据,因为视图本身不存储数据。这一点做实验时容易紧张,担心DROP VIEW把数据删了。放心删,视图只是数据字典里的一条定义记录。

验证视图是否创建成功,除了直接SELECT之外,还可以查看数据库的系统视图或信息模式,类似下面的查询:

SELECT * FROM information_schema.views WHERE table_name = 'v_score_detail';

这类查询能帮你看清视图的创建时间和定义文本,做实验报告时截个图也非常有说服力。

2.4 带CHECK OPTION的视图:过滤条件的“铁闸”

很多实验指导书会专门要求创建“带有WITH CHECK OPTION的视图”。这个东西单独做一个小结,是因为它太容易被忽略,又太容易在考试和实验中被追问。

先看定义:

CREATE VIEW v_computer_student AS SELECT student_id, student_name, major FROM student WHERE major = '计算机' WITH CHECK OPTION;

这个视图只显示计算机专业的学生。问题来了:你通过这个视图往student表里插入一条“软件工程专业”的记录,会发生什么?

如果不加WITH CHECK OPTION,数据库只判断基表约束,插入可能成功,但这条记录在视图里看不见。这就会形成一种诡异的数据状态:视图说“我只管计算机专业”,结果里面塞了一条软件工程的数据,只是刷不出来。加了WITH CHECK OPTION之后,数据库会强制校验:通过视图插入或更新的数据,必须满足视图定义的WHERE条件。插入软件工程专业,直接报错,拒绝执行。

这个特性在工作里非常实用。比如你给某个教务人员开放了一个“本学院学生视图”,限定WHERE辅导员属于他的学院,同时加了WITH CHECK OPTION,那他就无法通过这个视图往库里插入别的学院的学生数据,因为根本插不进去。这是一种在应用层之外的数据库级防线。

3. 视图的更新操作与限制

3.1 什么样的视图可以更新

前面一直在说查询,但实验要求里通常还有“使用视图”中的“更新”部分。视图能不能INSERT、UPDATE、DELETE,并不是看心情,而是有硬性规则。

先说说我的实操结论:最有把握可以更新的视图,是“行列子集视图”。什么是行列子集视图?就是视图的数据源只来自一张基表,只做了行过滤和列过滤,没有GROUP BY、没有DISTINCT、没有聚合函数、没有表达式列,还保留了主键。比如:

CREATE VIEW v_student_basic AS SELECT student_id, student_name, major FROM student WHERE major = '计算机';

这种视图直接映射到student表,通过它执行UPDATE,数据库可以准确地定位到基表的行。例如:

UPDATE v_student_basic SET student_name = '张三' WHERE student_id = '20230001';

执行之后,视图中看到的姓名变了,基表student里的姓名也变了。本质就是数据库把这条UPDATE改写成了对student表的更新。

反过来,遇到以下几种情况,视图基本不能直接更新:

  • 视图定义里包含聚合函数,例如“每个专业的学生人数”这种视图,没有对应基表行可以更新。
  • 视图定义里包含GROUP BY或DISTINCT。
  • 视图定义来自多表连接,尤其是一个UPDATE只更新一张表的字段,可能有DBMS支持,但风险很高,而且多数实验平台会拒绝。
  • 视图定义中包含计算列,比如“总分 = 平时分 * 0.3 + 考试分 * 0.7”。

其实判断依据很简单:视图的每一行必须能反向映射到基表的唯一一行。映射不上,就不能更新。

我在实验报告里建议做一个对比测试:先对行列子集视图执行更新,成功;再对分组统计视图执行更新,看报错。然后把两个结果都写进报告,这个实验的“更新限制”部分就撑起来了。

3.2 通过视图更新数据时,最容易犯的错

在实际操作中,最容易犯的错有两个。

第一个是作用范围没搞清。比如v_computer_student这个视图只显示计算机专业,但你执行:

UPDATE v_computer_student SET major = '计算机' WHERE student_id = '20230002';

如果20230002原来不是计算机专业,正常情况下视图里根本看不到这条记录,所以这个UPDATE是多余的。可是新手会认为“我更新视图,数据库会自动把学生改成计算机专业”。不对,视图更新不会因为“你在视图里执行”就改变WHERE范围,它只对视图可见范围内的行生效。

第二个错是违反WITH CHECK OPTION不提醒。有了WITH CHECK OPTION,你执行:

UPDATE v_computer_student SET major = '软件工程' WHERE student_id = '20230001';

数据库会拒绝,错误信息里会明确提到WITH CHECK OPTION约束。这不是数据库坏了,而是保护机制生效了。做实验时千万别为了看到“成功”而把WITH CHECK OPTION去掉,那等于把实验的考点删掉了。

还有一个特别隐蔽的问题:视图更新后,原来SELECT出来的结果会变。比如先查询视图,看到一条记录;再更新这条记录的某个辨识字段;再查询视图,发现记录不见了,因为它已经不在视图的WHERE条件范围内。这个现象以前很多同学遇到后以为是数据丢了,吓得赶紧翻基表,其实数据还在,只是视图的过滤条件把它挡在门外了。

3.3 用视图做权限控制:隐藏敏感列

更新部分做完后,我建议顺手做一个“权限控制”小实验,因为它能体现视图在真实系统中的用途。

假设有一个普通用户,你不想让他看到学生表里的phone字段。那你创建一个不含phone的视图,并把查询权限授予这个用户:

CREATE VIEW v_teacher_view AS SELECT student_id, student_name, major FROM student; GRANT SELECT ON v_teacher_view TO teacher_role;

然后拒绝他对student表的直接访问,或者保持默认不授权。这样他查student表会提示权限不足,但是查v_teacher_view正常。这个实验点如果写进报告,老师会觉得你真的理解视图了。

使用视图做权限控制的一个关键提醒:视图的权限和表的权限是分开管理的。对象是视图,授权时对象就是视图名,不是表名。很多时候权限查不出来,就是因为授权语句写错了对象名。

4. 关系图定义实操:从表结构到可视化关系

4.1 关系图和ER图不是一回事

这里先厘清一个概念。你在“数据库系统概论”课里可能画过ER图,实体-联系图。那是设计阶段的产物,用矩形、菱形、连线表示实体和联系,画完不一定要落进数据库。

实验六里的“关系图”不一样,它是数据库管理系统提供的一个图形化对象,展示的是数据库中已经存在的表和已经建立(或将要建立)的主外键关系。它就是数据库内部结构的“实时地图”。你可以在关系图里新增表、删除表、建立表之间的外键关系;保存关系图的时候,数据库会执行对应的DDL语句,把外键约束真正加到表上。

所以关系图实验通常有两种做法:

  • 先在命令行或者SQL脚本里建好表和外键,再在关系图里“添加表”,此时关系自动显示出来。
  • 先只建表不加外键,然后在关系图里通过拖拽建立外键连线,保存时由数据库自动生成外键约束。

我建议两种都试一遍,尤其第二种能帮你理解“关系图不只是画图工具,它还会写数据定义语句”。

4.2 新建关系图并把表加入画布

不同数据库管理工具的操作菜单名称有差异,但流程逻辑差不多。第一步,在数据库对象树里找到“数据库关系图”这个节点,右键选择“新建关系图”。如果数据库里已有关系图,会提示选择要添加的表;如果是空库,会先让你添加表。

这时候会出现一个“添加表”对话框,把student、course、score三张表全加进去。画布上就会出现三个表框,每个表框里列出字段,字段旁边带一个小钥匙图标的就是主键。

这一步有几个注意点:

  • 确保三张表都已经有主键。关系图里建立外键,前提是被引用的表要有主键或唯一约束。
  • 如果表数量比较多,画布会很乱,建议先手动把有关联的表摆近一点,再建连线。
  • 有些数据库工具在“添加表”时只会显示当前用户有权限看的表,如果发现找不到表,先检查登录账号是否对这张表有权限。

4.3 拖拽建立外键关系:核心操作

关系图的核心操作就是拖拽。比如score表需要引用student表和course表,做法是:

在score表框上选中student_id字段(鼠标按住),拖到student表框的student_id字段上松开。数据库会弹出一个“关系和键”之类的设置对话框,显示外键名、主键表、外键表、对应字段,确认无误后保存即可。

再用同样的方式,把score表的course_id拖到course表的course_id上。完成后,画布上会出现三条连线,一条连接student和score,一条连接course和score。连线的两端会有小图标,通常是钥匙和无穷符号,用来标识“一”端和“多”端。

这里有个细节很多人不注意:外键字段的字段名不需要和主键字段名一致,但数据类型必须一致,或者能隐式转换。比如student表的student_id是整数,score表的student_id也必须是整数,不能一个是字符串一个是整数。如果类型不一致,保存关系图时会直接报错。

建立外键时,对话框里通常还有“更新规则”和“删除规则”选项。常见值包括:

规则含义
不执行任何操作(NO ACTION)如果存在关联子记录,禁止修改或删除父记录
级联(CASCADE)父记录修改或删除时,子记录同步修改或删除
设为空(SET NULL)父记录删除时,子记录的外键字段置为NULL

课程实验里一般选“不执行任何操作”或“级联”,具体看指导书要求。但我提醒一句:级联删除很危险。你删除一个学生,选课成绩全没了。实际业务里很少对核心业务数据用级联删除,宁可先归档再处理。

4.4 保存关系图:其实是一次数据定义操作

画完关系,点击保存,输入关系图的名字,比如diagram_student_course。这一步不是简单存个图片文件,数据库会在后台执行一系列ALTER TABLE语句,把外键约束正式写入表结构。

保存成功以后,展开三张表下面的“键”或“约束”节点,就能看到刚才生成的外键约束名。有些工具还会自动给约束起名,比如FK_score_student,或者含GUID的随机名。为了规范性,可以在设置外键时手动把约束名改成易读的名字,比如fk_score_student_id。

这时候你再去看原先表结构脚本,会发现和画关系图之前不一样了,多出了外键约束。这就是关系图的真实作用:它在图形界面和底层DDL之间建立了一座桥。

如果保存关系图时遇到“保存失败,外键约束冲突”之类的错误,不要强行保存,原因通常不是关系图画得不对,而是表里已有数据不满足约束条件。你要先检查score表里是否有student表中不存在的student_id,处理掉脏数据后再保存。

5. 常见问题与排查技巧实录

这一节是实验现场最实用的部分。我按照出现频率从高到低列一个速查表,每一条都是实际动手时容易被卡住的地方。

问题现象常见原因解决思路
创建视图报错:列名无效或列名不明确多表连接时两张表有同名字段,没有加别名在SELECT中显式使用表别名限定,比如写成s.student_id
创建视图报错:ORDER BY不允许视图定义中直接写了ORDER BY,标准SQL默认视图无序把ORDER BY放到查询视图的SELECT语句里;如果实验平台支持TOP或LIMIT,再结合使用
查询视图为空,但基表有数据视图WHERE条件写错,或者字段名大小写不匹配单独先查SELECT条件,确认能查出数据,再套进视图定义
通过视图更新时报错:对象不可更新视图包含聚合、GROUP BY、DISTINCT或多表连接改为直接更新基表;若业务需要,考虑使用触发器类机制实现
插入数据时被WITH CHECK OPTION拦截插入的数据不满足视图WHERE条件确认业务语义:如果数据确实不该插入,则是正常拦截;如果该插入,应该改为使用基表
关系图保存失败表里已有违反外键约束的脏数据,或字段类型不一致先清理数据,再核对主外键字段的数据类型、精度
删除基表时被拒绝有其他表的外键引用了它,或有视图依赖于它先删除依赖它的子表外键或视图,再删除该表
关系图里看不到某张表当前账号没有权限,或表在另外的数据库/模式下检查连接数据库和模式,再检查用户权限
ALTER VIEW修改字段列表后,查询视图报列名不匹配视图的列名列表和外层查询的列名列表没有同步保持视图定义中的列名一一对应,必要时先DROP再重建

再补充一个比较隐蔽的问题:视图改名。有人为了省事,用ALTER VIEW把视图重命名,结果报错。ALTER VIEW是用来修改视图定义的,不是改视图名的。改名的正确做法是DROP VIEW之后重建,或者使用数据库提供的专门改名工具命令。而且要注意:改掉视图名之后,原来引用了这个视图的其他存储过程或视图会失效,需要同步更新。

还有一个跟关系图相关的排查思路。如果你建完外键之后,发现插入数据时报错“外键约束冲突”,但明明数据看起来没问题,那就要检查外键字段的排序规则和字符集。比如两张表的字段都是字符串,但一张表字段排序规则是case-sensitive,另一张是case-insensitive,外键创建或数据操作时就可能莫名其妙失败。解决办法是让两边字段的字符集和排序规则保持一致。

排查时我习惯先从数据字典入手。查看所有外键约束的语句可以这样写(不同数据库实现有差异,但思路通用):

SELECT constraint_name, table_name, constraint_type FROM information_schema.table_constraints WHERE table_name IN ('student', 'course', 'score');

这类查询能快速列出所有约束,帮助判断约束是否真的创建成功。

6. 手把手完整实验流程:从空库到视图和关系图

最后给一套可以直接照着做的完整流程,适合做实验当天晚上赶报告的同学。假设数据库里还没有任何表,我们从零开始。

第一步,创建三张基础表。这里用最简洁的结构,突出主键和外键:

CREATE TABLE student ( student_id INT PRIMARY KEY, student_name VARCHAR(20), major VARCHAR(20), phone VARCHAR(11) ); CREATE TABLE course ( course_id INT PRIMARY KEY, course_name VARCHAR(50), credit INT ); CREATE TABLE score ( student_id INT, course_id INT, score DECIMAL(5,2), PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) );

建议先不手动写外键,因为后面要体验关系图生成外键。但如果你已经写了,也没关系,关系图会自动显示出来。

第二步,插入少量测试数据。数据量不用多,但一定包含一个“违反外键约束”的反例测试数据,方便后面验证。正常数据三到五行,另外准备一条“成绩表里有课程ID但课程表里没有”的数据,先不插入。

第三步,按实验要求逐个创建视图:

CREATE VIEW v_student_simple AS SELECT student_id, student_name, major FROM student; CREATE VIEW v_student_computer AS SELECT student_id, student_name, major FROM student WHERE major = '计算机' WITH CHECK OPTION; CREATE VIEW v_score_detail AS SELECT s.student_name, c.course_name, sc.score FROM score sc JOIN student s ON sc.student_id = s.student_id JOIN course c ON sc.course_id = c.course_id;

第四步,测试视图查询:

SELECT * FROM v_student_simple; SELECT * FROM v_student_computer; SELECT * FROM v_score_detail WHERE course_name = '数据库原理';

第五步,测试视图更新。先更新v_student_simple里的姓名,观察基表变化。再尝试向v_student_computer插入一条非计算机专业的记录,记录报错。

第六步,新建关系图。把student、course、score三张表添加到画布,拖拽创建两个外键关系。注意级联删除规则不要乱设,一般选不执行任何操作。保存关系图后,再用SQL验证外键约束:

SELECT constraint_name, constraint_type FROM information_schema.table_constraints WHERE table_name = 'score';

第七步,做一个“外键冲突”反向验证。往score表插入一条不存在的student_id,确认数据库拒绝;再删除student表中已被score引用的记录,确认数据库拒绝。把两次报错信息截图,作为实验报告里“数据完整性约束”的证据。

第八步,清理实验对象。按依赖顺序先删除视图,再删除外键约束,最后删除表。不要先删表再删视图,那样会留下一些挂在系统目录里的无效依赖信息,虽然大多数情况下不影响,但实验环境是要交还给老师检查的,保持干净最稳妥。

一些实战后的个人体会

做完这个实验,我有一个很深的感受:视图和关系图这两个东西,直接体现了数据库设计的“逻辑层”和“物理层”之间的衔接。

视图是我在真实项目里用得最多的对象之一。接口文档不稳定的时候,我经常先定义一个视图,让前端拿着视图字段去开发,后端再慢慢改物理表。视图起了一个缓冲作用,两边都不耽误。关系图则更像数据库的“户型图”,新人接手项目,我第一件事不是甩给他几百行建表脚本,而是打开关系图,让他顺着连线把表结构过一遍。

实验里有一个点我建议你额外验证一下:把一张表的多列分别做成多个视图,或者把不同过滤条件的视图叠加,看看它们之间的数据是相互独立的还是联动的。这个验证做完,你对视图的“虚”会有更直观的理解——它不存数据,所有视图都是基表的实时投影。理解了这一点,后面学视图性能优化、物化视图,就有基础了。

顺手分享一个实验报告的小技巧:把所有SQL语句都按编号整理,并在每条语句后面写上执行结果说明。比如“该语句执行成功,影响行数为X”“该语句执行失败,错误信息为Y”,这一行说明的价值,往往比满屏的SQL截图更让看得人信服。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进