先聊一个很多学数据库的同学都会遇到的困惑:我明明已经把复杂的查询 SQL 写了一长串,为什么还要再花心思把它"存"成一个叫视图(view)的东西?它到底是真的会把数据复制一份存起来,还是只是"披着表皮的查询"?这些问题,正是这一讲要掰开揉碎讲清楚的。
不管你是刚要学、还是已经写过几条 select,只要你吃过"千辛万苦拼好一个大 join,下次还得重新拼一遍"的苦,那视图就是为你准备的。这一讲我们从"视图是什么"讲起,一路走到创建、查看、修改、删除的完整生命周期,再深挖它和基表(被视图引用的成表)之间那种"我改你跟着变、你改我也跟着变"的双向联动,最后落到安全、性能、可更新视图的限制和系统视图这些实战里绕不开的话题。
你应该有的知识准备
在往下读之前,建议你至少对下面这些有概念,它们大多是你已经会的,我只做一句话回顾,看不懂可以回头翻 SQL 基础篇:
- SELECT 基本查询:
select、where、order by、多表连接(from表where关联条件)。视图的本质就是一个封装好的select。 - 别名 (alias):
select e.ename from EMP e里的e,以及结果列别名as 别名。视图的列命名会用到它。 - 增删改(DML):
insert、update、delete。判断"可更新视图"的时候要反复用它们。 - 权限概念:谁有权利建视图、谁能查某张表。视图的一个核心价值恰恰是在这里。
如果你这几样都熟,那太省事了——视图几乎就是把你已经会的 select 换了个地方放。
视图是什么:一张"不存数据"的虚拟表
先给视图下个准确定义。视图(view)是一张虚拟表(virtual table),它的内容不是自己存的数据,而是由一个查询定义出来的。 这句话里两个词都值得拆开:
- 表:视图用起来和普通表几乎一模一样——它有列名、有数据行,你照样能对它
select,能跟别的表join,甚至在某些限制下能update、insert、delete。 - 虚拟(virtual):它是"假的表"。普通表(我们常叫基表,就是真正存数据、占磁盘的那张表)会把每一行物理地存在磁盘上;而视图本身不占一块自己的数据存储,它手里只捏着"一条 SELECT 语句"这张配方。每次你查视图,MySQL 就把这张配方实打实地执行一遍,把结果现算给你看。
打个比方。普通表像一间装满了货物的仓库;而视图像仓库门口贴着的一张"取货清单"——清单上写"要哪些货、从哪个货架拿、怎么拼装"。你拿着清单去柜台,店员就按清单把货拼好端给你。清单本身不占仓库空间,但它决定"端给你的是什么"。这就是视图的本质:它是查询的封装,不是数据的拷贝。
看到这你可能会反问:那视图和我把它里面那条 SQL 直接抄一遍到 select 里有什么区别?功能上几乎没区别,但价值上区别可大了——这正是后面"为什么用视图"那节要展开的。先把"它是配方不是数据"这个最核心的直觉立住,后面全部好懂。
视图与基表的关系:一对"命运共同体"
这是视图最反直觉、也最重要的一条性质,我单独拎出来讲。
从上面的比喻你应该已经能推出:视图自己不存数据,它的每一行都"实时"来源于基表。 于是两者之间就形成了一种双向绑定的联动:
- 改基表 → 视图跟着变:因为视图是"查基表"得到的,基表里新增、修改、删除了数据,你下次再查视图,看到的就是新数据。
- 改视图 → 基表跟着变:反过来,如果你对一个"可更新的视图"执行了更新(比如
update),MySQL 会把这条改动"翻译"成对基表数据的修改,基表就跟着变了。
听起来像是相互影响、其实方向完全对称:视图不保有数据,所以数据只有一份,就是基表里的那份;视图只是它的"另一扇观察窗口"。 窗里窗外观的是同一幅风景,你动窗户也好、动风景也好,另一侧立刻同步。
我们先在脑海里留个印象,下一节用真实案例把它演示出来。
创建视图:CREATE VIEW
创建视图的语法非常直白,就是"给一条 SELECT 起个名字":
-- 语法骨架:把 select 语句存成名为“视图名”的视图
CREATE VIEW 视图名 AS SELECT语句;注意两点:一是 CREATE VIEW 用 OR REPLACE 扩展后的完整写法可以顺带"改名即重建"(稍后讲修改时细说);二是这条 SELECT 里命名的列会成为视图的列。
我们沿用经典的教学库 EMP(员工表)和 DEPT(部门表)来做案例。两张表的关联字段是部门号 deptno。我们先写一条要经常用到的查询——把每位员工的名字和他的部门名放在一起(员工表里只有部门号,部门名叫 dname 在部门表里,所以必须连接两表):
-- 创建一个视图:把“员工名 + 其部门名”这个常用 join 封装起来
CREATE VIEW v_ename_dname AS
SELECT ename, dname
FROM EMP, DEPT
WHERE EMP.deptno = DEPT.deptno;执行完,MySQL 没有报错,它并没有把 14 行结果着急忙慌地算好存起来,而是把这段 SELECT 语句存到了数据字典里,当成一个叫 v_ename_dname 的对象登记在案。从这一刻起,v_ename_dname 就像一张普通表一样对我敞开了。
创建成功后,我们像查普通表一样查询它:
-- 把视图当成一张表来查询,按部门名排序
SELECT * FROM v_ename_dname ORDER BY dname;结果和"手写那条 join"一模一样:
+--------+------------+
| ename | dname |
+--------+------------+
| CLARK | ACCOUNTING |
| KING | ACCOUNTING |
| MILLER | ACCOUNTING |
| SMITH | RESEARCH |
| JONES | RESEARCH |
| SCOTT | RESEARCH |
| ADAMS | RESEARCH |
| FORD | RESEARCH |
| ALLEN | SALES |
| WARD | SALES |
| MARTIN | SALES |
| BLAKE | SALES |
| TURNER | SALES |
| JAMES | SALES |
+--------+------------+请注意,视图把底层的"两表连接"全部藏了起来。以后任何人需要"员工名配部门名",直接 select * from v_ename_dname 就行,再也不用记得关联条件是 emp.deptno = dept.deptno 这一茬了。
这里有个很常用的前提:视图里的列名可以自己指定。 如果不想用 SELECT 里原生的列名,可以这样写:
-- 创建视图时给列起别名,视图对外暴露的是别名这一套名字
CREATE VIEW v_emp_info(emp_name, dept_name) AS
SELECT ename, dname
FROM EMP, DEPT
WHERE EMP.deptno = DEPT.deptno;这样视图 v_emp_info 对外就只叫 emp_name、dept_name 两列,底层的 ename、dname 被"改头换面"了。这个能力在后面讲"安全"时非常有用——可以让你对某些用户只暴露指定的列名。
修改视图 vs 修改基表:双向联动的实证
代码写到这里,我们来亲眼看前面说的"命运共同体"。先看第一方向:改视图,基表跟着变。
我们对视图执行一条更新:把员工 CLARK 的 ename 改成 TEST。注意,我们是"站在视图上"发动的更新:
-- 通过视图去更新数据:把 ename='CLARK' 的行改成 'TEST'
UPDATE v_ename_dname SET ename = 'TEST' WHERE ename = 'CLARK';这条语句执行成功后,MySQL 会把它翻译成对底层基表 EMP 的修改。我们回基表验证:
-- 回到基表看原名字,咦?CLARK 还在吗
SELECT * FROM EMP WHERE ename = 'CLARK';Empty set (0.00 sec) -- 查不到 CLARK 了,它已经被改成 TEST-- 再看 TEST,它真的进了基表 EMP
SELECT * FROM EMP WHERE ename = 'TEST';+-------+-------+---------+------+------------+---------+
| EMPNO | ENAME | JOB | MGR | HIREDATE | SAL |
+-------+-------+---------+------+------------+---------+
| 7782 | TEST | MANAGER | 7839 | 1981-06-09 | 2450.00 |
+-------+-------+---------+------+------------+---------+看到了吗?我们明明改的是视图,原来的员工表 EMP 里 CLARK 却真真切切地变成了 TEST。因为视图脚下踩的就是基表,视图上的更新最终写进了基表。
再看第二方向,改基表,视图跟着变。 这次我们反过来,直接改基表 EMP,把员工 JAMES 的部门号 deptno 从 30(SALES)改成 10(ACCOUNTING):
-- 修改基表:把 JAMES 的 deptno 从 30(SALES)改为 10(ACCOUNTING)
UPDATE EMP SET deptno = 10 WHERE ename = 'JAMES';Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0现在去查视图 v_ename_dname 里 JAMES 那一行,部门名已经跟着翻篇了:
-- 从视图看 JAMES,部门名已经从 SALES 变成了 ACCOUNTING
SELECT * FROM v_ename_dname WHERE ename = 'JAMES';+-------+------------+
| ename | dname |
+-------+------------+
| JAMES | ACCOUNTING | -- 基表一变,视图立刻同步
+-------+------------+这个例子把视图的"虚拟性"展示得淋漓尽致:基表里的数据变了,视图没有"单独的另一份数据"去维护,它本来就是实时从基表查的,于是天然一致。 这也正是视图能保证"各条查询看到同一份数据"的根本原因。顺带说一句,如果你担心刚才把 CLARK 改成了 TEST 影响后续例子,可以把 TEST 再改回 CLARK;视图就是这么张随用随变的纸。
查看视图:确认它到底注册了什么
创建好视图后,我们常常要回过头确认"它到底存的是哪条 SQL"。有几种姿势:
方式一:SHOW CREATE VIEW 直接打印出视图的完整定义语句,最直观。
-- 查看视图的完整定义,能看到它背后那整条 SELECT
SHOW CREATE VIEW v_ename_dname;+---------------+-------------------------------------------------+ ...
| View | Create View |
+---------------+-------------------------------------------------+ ...
| v_ename_dname | CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v_ename_dname` AS
select `EMP`.`ename` AS `ename`,`DEPT`.`dname` AS `dname` from `scott`.`EMP` join `scott`.`DEPT` on((`EMP`.`deptno` = `DEPT`.`deptno`)) |
+---------------+-------------------------------------------------+ ...(实际输出里 CHARACTER_SET_CLIENT、COLLATION_CONNECTION 等列也被打印,这里省略。看到 CREATE ALGORITHM=UNDEFINED、SQL SECURITY DEFINER 这些内部信息了吗?它们正是后面讲"算法"和"权限"时的主角。)
方式二:SHOW TABLES 查看全部表。MySQL 里视图跟表混在一起列示,一眼看不出谁是视图:
-- 列出当前数据库所有“表对象”,视图也在其中
SHOW TABLES;如果你只想看视图,可以查询系统表 information_schema.VIEWS(后面"系统视图"一节会细讲它):
-- 从系统表 information_schema.VIEWS 里挑出视图名
SELECT TABLE_NAME FROM information_schema.VIEWS;方式三:DESC 或 DESCRIBE 看视图的结构(列名、类型),跟看表一样:
-- 看视图的列结构,等价于 DESCRIBE v_ename_dname
DESC v_ename_dname;+-------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------+------+-----+---------+-------+
| ename | char | YES | | NULL | |
| dname | char | YES | | NULL | |
+-------+------+------+-----+---------+-------+注意视图像我们看到的那样没有主键(Key 一列空着),这点在"可更新视图的限制"里还会再提。
修改视图:ALTER VIEW
视图建好之后想改需求(比如换一条 SELECT、加一列)怎么办?两种办法。
方法一:ALTER VIEW,直接重建视图的定义:
-- 修改视图:给 ALTER VIEW 换一条新的 select 语句
ALTER VIEW v_ename_dname AS
SELECT e.ename, d.dname, d.deptno
FROM EMP e, DEPT d
WHERE e.deptno = d.deptno;方法二:CREATE OR REPLACE VIEW。这个名字很形象——"有就替换、没有就新建"。它比 ALTER VIEW 更省心的地方在于:即使这个视图还不存在,它也能直接给建出来,一条语句既当"创建"又当"修改"。所以在日常里,很多人习惯统一用 CREATE OR REPLACE VIEW 来"创建或覆盖"视图:
-- 创建或覆盖视图:视图存在则替换定义,不存在则新建
CREATE OR REPLACE VIEW v_ename_dname AS
SELECT e.ename, d.dname
FROM EMP e, DEPT d
WHERE e.deptno = d.deptno;无论是 ALTER VIEW 还是 CREATE OR REPLACE VIEW,本质上都是"把视图背后的那条 SQL 换掉"。因为视图不存数据,所以"修改视图"永远不会损坏任何业务数据——它只是改了取数配方,这一点和"ALTER TABLE 修改基表结构"有着天壤之别。
删除视图:DROP VIEW
删除视图就是把它从数据字典里注销掉,语法很简单:
-- 语法骨架:删除一个视图
DROP VIEW 视图名;-- 亲手删掉我们建的 v_ename_dname
DROP VIEW v_ename_dname;Query OK, 0 rows affected (0.00 sec)关键的安全语义必须讲清楚:DROP VIEW 只删除视图"这个配方",绝不会动基表里的任何数据。 前面我们说过视图不存数据,所以删视图就像把门口那张取货清单撕掉——仓库里的货一件不少。这一点和 DROP TABLE(会把整张表的物理数据连同结构一起删掉)有天壤之别,务必在脑子里分开。
如果你删一个不存在的视图,会报错 Unknown table 'xxx'。想"能删就删、不存在也不报错"的话,可以加 IF EXISTS:
-- 视图存在才删,不存在也不报错
DROP VIEW IF EXISTS v_ename_dname;为什么用视图:简化、安全、数据一致性
现在回到开头那个问题:视图到底图个什么?我把理由归纳成三条,每一条都能对应到一段实战里摸爬滚打出来的痛。
第一,简化复杂查询(重用与封装)
这是最直觉的一条。你拼过那种动不动就四五个表 join、还带子查询和聚合的大 SQL 吗?这种 SQL 又长又容易写错,每次要用都得重新拼一遍。视图把它封装成一个短名字后,别人(包括未来的你)只需要:
-- 面向视图这位“干将”写查询,背后多表连接全被藏起来
SELECT * FROM v_ename_dname WHERE dname = 'ACCOUNTING';不用再背 EMP.deptno = DEPT.deptno 这种关联条件。这就像把一段高频操作的复杂步骤录成了"宏",要用时一键调用。团队里把通用查询做成视图、让其他人直接引用,能极大降低出错的概率和认知负担。
第二,提高安全性(权限屏蔽敏感列)
视图是数据库安全里非常经典的一道闸门。设想 EMP 表里有一列薪资 SAL,它很敏感,你不想让某些低权限用户看到每个人的工资。但你又想让他们能查"员工名 + 部门名"这种基本信息。怎么办?
做法是:建一个只含非敏感列的视图,然后把视图的查询权限授给那个用户,而基表的权限不给。 用户只能看到视图里暴露的列,碰不到背后的敏感列:
-- 挑出不含薪资的“安全版本”视图,用于给受限用户
CREATE VIEW v_emp_public AS
SELECT ename, job, deptno FROM EMP;
-- 把视图的查询权限授给受限账号,而不是授整张 EMP
GRANT SELECT ON scott.v_emp_public TO 'zhangsan'@'localhost';这样 zhangsan 即便包括 SELECT * FROM EMP 也查不到(因为没权限),但他可以查 v_emp_public 得到非敏感信息。视图像一堵只开了几个窗口的墙,让敏感数据只从指定窗口透出。 甚至像前面说的,你可以用列别名把列名也"包装"成不敏感的名字,连列名都能藏。
第三,保证数据一致性(单一数据源)
这一点往往被忽略,但恰恰是视图最"高级"的价值。因为视图是实时从基表查询的,它天然保证所有用它的人看到的都是同一份最新数据。如果你不用视图、而是各自写各自的查询,万一有人写错条件,各人对同一数据的认知就分叉了;而统一用同一个视图,等于大家从同一扇窗往外看,永远口径一致。加上视图对 update/insert 的限制(后面讲),它还能在"让业务写数据"时做一层统一约束,避免绕过规则的乱写。
一句话收束本节:视图用"查询封装"换来了简化、安全、一致三大好处。它是把"取数的规则"和"取数的人"解耦的一层设计——规则集中管理一处,用户只对视图负责。
视图的实现算法:MERGE 与 TEMPTABLE
前面说视图"每次查询都现算",但"现算"有两种不同的实现策略,MySQL 用一个叫 算法(algorithm) 的属性来控制。这是很多资料一笔带过、但你想真正理解视图性能时必须搞清的底层机制。MySQL 在 CREATE VIEW 时允许你显式指定 ALGORITHM,取值有三个:MERGE、TEMPTABLE、UNDEFINED。
MERGE 算法:把视图 SQL 和你的查询"合成"一条
当算法为 MERGE(合并)时,MySQL 在查询视图时,会把你写的查询和视图底层的 SELECT 文本融合成一条 SELECT 去执行。 也就是说,它不是"先算出视图结果,再在结果上过滤",而是把两边条件揉在一起,直接对基表一次性完成过滤和计算。
比如视图 v_ename_dname 的底层是 SELECT ... FROM EMP, DEPT WHERE EMP.deptno=DEPT.deptno,而你又执行:
SELECT ename FROM v_ename_dname WHERE dname = 'ACCOUNTING';如果是 MERGE,MySQL 会把这条查询"展开"等效成把两个条件并到一条:
-- MERGE 的等效执行:视图条件 + 你的条件被合并进同一条 SELECT
SELECT ename FROM EMP, DEPT
WHERE EMP.deptno = DEPT.deptno AND dname = 'ACCOUNTING';优势很明显:MySQL 优化器能看到整条语句,可以充分利用索引、调整 join 顺序来优化,性能最好。 MERGE 对"可更新视图"也至关重要——正因为视图背后其实就是一条能定位到基表行的 SELECT,update/insert 才能翻译回基表(后面讲可更新视图时要依赖这一点)。
MERGE 不是万能的。当视图的底层 SELECT 含有会让"结果行和基表行无法一一对应"的东西时,就用不了 MERGE,比如:用了 聚合函数(SUM、COUNT 等)、GROUP BY、DISTINCT、UNION 或含 LIMIT 等。这种"汇总压缩过"的视图,没法把一条用户查询干净地合并到底层基表上。
TEMPTABLE 算法:先把结果算进一张"临时表"再查询
当算法为 TEMPTABLE(临时表)时,MySQL 在查询视图时会先把视图底层 SELECT 的结果算出来,物化成一张隐藏的临时表,然后再在这张临时表上执行你的过滤、排序等操作。 相当于"先照配方把货拼好摆到一张临时餐桌上,再从餐桌上去取你要的那几样"。
-- 显式声明用 TEMPTABLE 算法:先物化结果,再在其上查询
CREATE ALGORITHM=TEMPTABLE VIEW v_sum_sal_dept AS
SELECT deptno, SUM(sal) AS total_sal
FROM EMP
GROUP BY deptno;优势:适用范围最广——凡是用聚合、分组、去重、UNION 等不适合 MERGE 的场景,都可以用 TEMPTABLE 兜住,视图照样"能查"。劣势也明显:每次查询都要把整个视图结果物化到临时表里,如果视图底层扫描量很大,这一步本身就有成本;而且物化后临时表上没有索引,你在这张临时表上的过滤往往要全表扫一遍,所以 TEMPTABLE 视图通常比 MERGE 性能差、也更耗内存。另外,TEMPTABLE 算法的视图不可更新(原因后面讲)。
UNDEFINED:让 MySQL 自己看着办(默认)
如果不写 ALGORITHM(也就是默认),算法就是 UNDEFINED(未指定)。此时 MySQL 会自行选择——凡是能用 MERGE 的优先 MERGE,实在不能被合并的再用 TEMPTABLE。所以绝大多数情况下你不需要手动写算法,交给默认的 UNDEFINED 即可,MySQL 比你更清楚该怎么选。
顺带做个对照,方便你记忆:
| 算法 | 执行方式 | 性能 | 可更新? | 典型适用 |
|---|---|---|---|---|
MERGE | 把视图 SQL 与你的查询合并成一条 | 好,可利用索引 | 可以 | 简单映射型视图(不含聚合/分组/去重) |
TEMPTABLE | 先物化结果成临时表再查询 | 较差,需额外物化 | 不可以 | 含聚合、GROUP BY、DISTINCT、UNION 等 |
UNDEFINED | MySQL 自行决定 | 视情况而定 | 视实际采用的算法而定 | 默认选择,日常最常用 |
补充一句准确性说明:ALGORITHM=MERGE 具体能否被采纳,还受到底层 SELECT 是否合法可合并的影响;若你强制声明 MERGE 而底层条件其实不允许,MySQL 会有相应处理。日常使用我们以 UNDEFINED 默认值为主。
可更新视图:视图的 DML 与它的限制
前面我们已经演示过"改视图 → 基表跟着变",说明视图是可以被更新的。但必须把这句话限定得清清楚楚:并不是所有视图都能更新,只有"可更新视图(updatable view)"才可以,而且它能把 DML 翻译回基表,必须满足一系列严格条件。
"可更新视图"的定义:允许通过视图执行 INSERT、UPDATE、DELETE(可更新就是这三种里的至少支持、通常指 UPDATE,含通过视图插入行),且这些操作被 MySQL 翻译成对底层基表的修改,从而真正改变基表数据的视图。
前面 v_ename_dname 可以 update,正是因为它满足可更新视图的条件。但下面这些情况,视图就"只看不动"了,强行增删改会报错:
- 底层 SELECT 里用了聚合函数,如
SUM、COUNT、AVG;或者用了GROUP BY、HAVING——这种"汇总视图"看不清哪一行对应基表的哪一行,没法更新。 - 用了
DISTINCT(去重)或UNION/UNION ALL——行被合并过,无法定位原始行。 - 用了
LIMIT但没有配合WHERE(某些版本下)。 - 视图的列来自表达式或常量,如
SELECT sal*1.1 FROM EMP、SELECT 'a'这样的列没法被写回。 - 底层是
TEMPTABLE算法物化出来的视图,因为已经没有"基表行"的映射关系可循。
MySQL 其实给了一个现成的"体检报告"——information_schema.VIEWS 表的 IS_UPDATABLE 列,或者用 SHOW TABLE STATUS 也能看到。你可以这样一键确认某个视图可不可更新:
-- 查看哪些视图可更新:IS_UPDATABLE 为 YES 才允许 DML
SELECT TABLE_NAME, IS_UPDATABLE
FROM information_schema.VIEWS
WHERE TABLE_SCHEMA = 'scott';+-----------------+-------------+
| TABLE_NAME | IS_UPDATABLE |
+-----------------+-------------+
| v_enum_dname | YES |
| v_sum_sal_dept | NO | -- 用了聚合/GROUP BY,不能更新
+-----------------+-------------+而且,通过视图更新还有一个和"可更新"同样重要的附加条款:如果你想让视图的 INSERT 连"一条可能绕过滤条件的新数据"都不允许插入,就要配合 WITH CHECK OPTION 来拦截。 这是下一个大点。
WITH CHECK OPTION:给视图 DML 把好"合规"的闸
先说一个真实会踩的坑。假设我建了一个"只看部门 ACCOUNTING 的员工"的视图:
-- 建一个只暴露 ACCOUNTING 部门员工的视图
CREATE VIEW v_acct_emp AS
SELECT ename, deptno FROM EMP WHERE deptno = 10;这个视图不含聚合,满足可更新视图条件,所以可以 update。现在问题来了:
-- 通过视图把 TEST 的部门改成 20:TEST 从此不再是 ACCOUNTING 部门
UPDATE v_acct_emp SET deptno = 20 WHERE ename = 'TEST';这条能成功吗?在 MySQL 里,默认能成功——基表里 TEST 确实被改成 deptno=20 了。于是出现一个很"精神分裂"的现象:我明明通过"只看 ACCOUNTING"的视图去改数据,改完之后那条数据却跳出这个视图的视野了。视图的定义是 where deptno=10,可我通过它硬生生把一行改成了不符合这条件的 deptno=20。 这种"改到视图看不到自己"的漏洞,正是 WITH CHECK OPTION 要堵住的。
给视图加上 WITH CHECK OPTION 后,MySQL 会在通过视图执行的 UPDATE(和 INSERT)上进行强制校验:凡是会导致"修改后的行不再满足视图的 WHERE 条件"的操作,一律拒绝执行。 于是上面那条"把 TEST 踢出 ACCOUNTING"的更新就会被拦截并报错。
-- 改造视图:任何通过它进行的修改,必须保证结果行仍满足 deptno=10
ALTER VIEW v_acct_emp AS
SELECT ename, deptno FROM EMP WHERE deptno = 10
WITH CHECK OPTION;-- 这条现在会失败:TEST(或 CLARK) 改到 deptno=20 将不再满足视图条件
UPDATE v_acct_emp SET deptno = 20 WHERE ename = 'TEST';ERROR 1369 (HY000): CHECK OPTION failed 'scott.v_acct_emp'看到 CHECK OPTION failed 这个报错了吗?就是它,拦住了这种"自相矛盾"的修改。同理,如果你通过带 WITH CHECK OPTION 的视图去 INSERT 一条 deptno=30 的新员工,也会被拒——因为它一进去就不在视图的视野里。
(提醒一句:前面演示把 CLARK 改成 TEST 只是为了展示机制,实际生产中改主键列、改会导致行脱离视图的列都是敏感操作,强烈建议对这类筛选型视图统一加 WITH CHECK OPTION 防手滑。)
视图的规则与限制:一条条踩过才有记忆
视图用起来虽然像表,但到底不是表,它有一整套自己的"脾气"。我把要命的几条列全,你写作业或上生产时照着对照:
- 命名必须唯一,且与表共享同一命名空间。视图在数据库里和表是同一个"名册",你不能建一个和已有表同名的视图,也不能建两个同名的视图,否则报错。一句话:视图名和表名不能撞车。
- 创建视图的数目没有硬性限制,但要注意性能。视图不存数据,代表它只是"引用",但如果你把一堆极高成本的大查询封成视图再层层叠加、互相查询,MySQL 每次都得按这条配方从头现算,性能会非常感人——尤其当算法是
TEMPTABLE时。这里有个很实用的经验:只在"依赖度高、复用频繁、且单次查询本身不重"的查询上建视图;别一上来就把几十个视图像套娃一样套起来。 - 视图不能加索引,自然也不能像表那样拥有主键、唯一约束;同时视图也没有自己的触发器,也没有默认值。这些"表的物理属性"都只属于基表。所以想在视图上加约束来"弥补"可更新视图的限制,是做不到的——你能做的只能是
WITH CHECK OPTION。 - ORDER BY 在视图里有"被覆盖"的规则。视图的底层
SELECT里可以写ORDER BY;但如果你从视图检索数据时,自己又写了一个ORDER BY,那么视图里那个ORDER BY会被你外层的ORDER BY覆盖(或者说被忽略/以你的为准)。因为从视图取数时,外层查询的排序生效,视图内部的排序不保证。一个典型反直觉点:
-- 视图内部自己排了序
CREATE VIEW v_sorted AS
SELECT ename, sal FROM EMP ORDER BY sal DESC;
-- 但外层一排序,视图内部的排序就被覆盖,以这个外层排序为准
SELECT ename FROM v_sorted ORDER BY ename;(最终结果按 ename 而非 sal 排列。这一点清晰展示了"视图内部 ORDER BY 不靠谱"。)
- 视图可以和表一起使用。你没有必要只用视图、或者只用表——最常见的用法是"视图 JOIN 表"、"视图 JOIN 视图",完全可以混着来:
-- 视图和普通表一起 join,照常工作
SELECT v.ename, d.loc
FROM v_emp_public v, DEPT d
WHERE v.deptno = d.deptno;- 创建视图需要权限:至少具备底层所有基表的
SELECT权限,以及视图的权限。这是"视图能当安全层"的前提——正因为创建视图本身要触到基表,你才能通过"只授视图权限"来隔离敏感数据。
系统视图:MySQL 自带的"数据字典视图"
你可能已经注意到,前面查"哪些视图可更新"时用到了一个叫 information_schema.VIEWS 的"表"。它本身就是一个 系统视图(system view / 数据字典视图)——由 MySQL 服务器自带的、用来描述数据库自身元数据的特殊视图。
系统视图,是数据库管理工具内置的、查询"数据库自身信息"(元数据)的一组只读视图。 它们不像业务视图那样封装某张业务表,而是封装"这台实例里有哪些库、哪些表、哪些视图、哪些用户、哪些权限"这类关于数据库的"信息的信息"。最常用的就是 information_schema 这个库:
-- 查看库中都有哪些视图对象
SELECT TABLE_NAME FROM information_schema.VIEWS;
-- 查看某视图可否更新、用的什么算法
SELECT TABLE_NAME, ALGORITHM, IS_UPDATABLE
FROM information_schema.VIEWS;
-- 看每个库占多大空间、多少张表
SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_ROWS
FROM information_schema.TABLES;除了 information_schema,MySQL 还提供另一个内置库 mysql(存放用户、权限、事件、存储过程等真正的系统表),以及 performance_schema、sys 等。其中 sys 库里的很多对象也是基于 information_schema 封装好的自定义系统视图,比如:
-- sys 库里的系统视图:当前连接、IO 汇总等,运维排查常用
SELECT * FROM sys.session;
SELECT * FROM sys.io_global_by_wait_by_latency;这条线索的完整链路是:用户建的业务视图 → 也会被登记进 information_schema.VIEWS 这类系统字典;而系统字典本身又以"视图"的形式对外暴露。 所以"视图",既是业务建模的工具(建业务视图),也是 MySQL 自己管理自身的工具(系统视图)。认清了这一点,"视图"这个概念的边界就完全打通了。
自测思考题
到这里,视图这一讲的核心已经讲完。下面给你出几道思考题,先别急着看答案,自己心里答一遍,再对照下面,看看漏了哪一环。
思考题 1:为什么说"视图是一张虚拟表"?它的数据究竟存在哪里?
思考题 2:我用 DROP VIEW v_ename_dname; 删掉视图,EMP、DEPT 两张基表里的数据会受影响吗?为什么?
思考题 3:视图内容性质上允许 update 它来改基表。那么对于"SELECT deptno, SUM(sal) FROM EMP GROUP BY deptno"建出来的视图,能不能 update?为什么?
思考题 4:我给一个"只显示部门 10"的视图加了 WITH CHECK OPTION,然后通过它执行 INSERT INTO 该视图 (ename, deptno) VALUES ('NEW', 30); 插入一个部门 30 的员工,这行会成功吗?为什么?
思考题 5:视图内部的 ORDER BY 和"从视图检索时外层写 ORDER BY"同时存在时,以谁为准?
思考题 6:为什么 TEMPTABLE 算法的视图通常比 MERGE 慢?用一句话解释。
思考题答案详解
答案 1:视图是虚拟表,因为它的数据不是自己存储的,而是由它封装的查询实时从基表算出来的。它手里只有一条 SELECT 配方,每次查询视图都会执行这条配方并现算结果。所以视图的数据存在于它所引用的基表里,视图本身不占独立的物理数据存储。
答案 2:不受影响。视图不存数据,DROP VIEW 只是把"这条查询配方"从数据字典中注销掉。基表 EMP、DEPT 的物理行一条都不会少——相当于撕掉仓库门口的取货清单,仓库里的货一件没动。这正体现了视图的"虚拟、只封装查询"特性。
答案 3:不能更新。因为该视图底层用了聚合函数 SUM 和 GROUP BY,这种"汇总视图"已经把多行基表数据压缩成一行了,MySQL 无法确定"视图里的一行"对应基表里的哪一行,自然无法把视图上的修改翻译回基表。这类视图只能查,不能增删改(IS_UPDATABLE 为 NO)。
答案 4:会失败。视图条件是 deptno=10,而带 WITH CHECK OPTION 后,通过视图执行的所有 INSERT/UPDATE 都被强制校验为"修改后的行必须仍满足视图的 WHERE 条件"。插入的 NEW 员工是 deptno=30,不满足 deptno=10,于是 MySQL 会拒绝这条插入并报 CHECK OPTION failed。所以 WITH CHECK OPTION 保证了"通过视图写入的数据,永远落在视图能看到的范围内"。
答案 5:以外层(从视图检索时)写的 ORDER BY 为准。视图内部的那个 ORDER BY 会被外层查询的排序覆盖,MySQL 并不保证视图内部排序在被外层检索时依然生效。这正是"视图只是查询的封装,外层查询的排序要求优先"的体现。所以在视图定义里写 ORDER BY 意义不大,排序应当在需要排序的查询外层写。
答案 6:因为 TEMPTABLE 算法在查询视图时,会先把视图底层的 SELECT 结果完整物化成一张隐藏临时表,再在这张临时表上做后续过滤;物化本身有额外成本和内存开销,而这张临时表上又没有索引,所以在上面做过滤往往要扫描全部物化结果。相比之下 MERGE 把视图 SQL 和用户查询合并成一条去基表执行,优化器能直接用索引。一句话:TEMPTABLE 多了一步"先全量物化再过滤",MERGE 是一次到位利用索引。
结语:把"配方"和"数据"分开想
走到这里,视图的画像已经很完整了:视图是一张不存数据的虚拟表,它只封装一条查询配方;数据始终活在基表里,视图是实时可变的观察窗口。 记住"配方与数据分离"这个心智模型,你就能顺畅地推演它的所有行为——为什么删视图不删数据、为什么改基表视图立刻变、为什么聚合视图不能更新、为什么加 WITH CHECK OPTION 能拦越界的写。
实际应用上,三条主线你随时带在身上:想复用复杂查询或收口取数口径,用视图做简化与一致性;想屏蔽敏感列、只暴露部分列,用视图做权限控制;想在安全范围内允许部分写操作,用"可更新视图 + WITH CHECK OPTION"做受约束的写入。 至于 MERGE 与 TEMPTABLE,默认的 UNDEFINED 已经替你权衡,遇到性能瓶颈再回头调整就好。
视图是你把 SQL 从"能手写一条 SELECT"推向"能设计出一套取数与权限体系"的关键一跃。把这一讲的命令逐条敲一遍、把每道思考题亲手验证一遍,你对数据库的掌控感会更扎实。下一讲,我们聊聊索引与性能优化——你已经会用视图查询了,是时候看看怎么让这些查询跑得更快了。
还没有评论 — 第一条由你来留。