视图不存数据,只存一句 SQL。
查视图就是把那句 SQL 展开来跑一遍——所以它省的是「写」,不是「算」。
它能改数据,但要看是不是「键保留表」;
改的时候要小心「插进去就查不到」,这是 WITH CHECK OPTION 要堵的窟窿。
10.1 讲索引,解决「跑得快」。这一篇讲视图,解决「写得省事」。
两者都不存业务数据,但索引改的是物理访问路径,视图改的是逻辑表达。
视图是「权限 + 简化 + 稳定」三件事的工具:
只暴露部分列(权限)、把复杂 JOIN 藏起来(简化)、表结构变了视图不变(稳定)。
一、视图是什么:一句存起来的 SQL
CREATE VIEW v AS SELECT ... 做的事,就是把这段 SELECT 存进数据字典。
字典里存的是纯文本(USER_VIEWS.TEXT,LONG 类型),没有编译、没有执行计划、没有数据。
所以「视图占多少空间」的答案是:几百到几千字节,就是那段 SQL 的长度。
跟它「看起来有几百万行」没有半点关系。
查视图时发生的事:优化器把视图的 SQL 和你的 SQL 拼起来,当成一句来优化。
不是「先算出视图的结果,再在你的结果上过滤」——所以视图基本不额外花钱。
有一个例外:视图里带聚合、distinct、union、rownum 的时候,它就没法被完全展开,
只能先物化出一个中间结果。这时候视图会变慢。
二、创建视图:几个必须知道的选项
基本写法:
CREATE OR REPLACE VIEW v_emp30 AS SELECT ... WHERE deptno = 30;
OR REPLACE 一定要加——不加的话,视图已存在就报 ORA-00955,得先 DROP。
而 DROP 会让依赖它的对象失效,多一道手续。
WITH READ ONLY:把视图钉成只读。
这是最省心的选项——不用担心「谁能改」,也不用想「改了会不会溜出视图范围」。
对外提供的视图,一律加这个。
WITH CHECK OPTION:允许改,但改完必须还在视图范围内。
它要求视图里包含被检查的那一列,否则连条件都没法写。
这是「可写视图」的正确用法。
FORCE:强行建一个「底表还不存在」的视图。
用在「先建视图、后建表」的部署脚本里。
代价是视图状态是 INVALID,得等底表建好才有效。
日常不建议用——它把「引用错误」从建视图时推到了查询时。
三、可更新视图:什么叫「键保留表」
Oracle 判断一个视图能不能 INSERT/UPDATE/DELETE,看它是不是「键保留表」。
通俗的判据三条:
① 底表的主键列在视图里;② 没有聚合函数、GROUP BY、DISTINCT、UNION;
③ 没有表达式列(除非是能反算的)。
为什么要求主键列在?
因为「改一行」得先能唯一定位那一行。
视图里没有主键,就没法把「视图里这一行」映射回「底表哪一行」。
一个常被忽略的点:可更新视图改的是底表。
UPDATE v_emp30 SET sal = 3000 和 UPDATE emp SET sal = 3000 WHERE deptno = 30 是一回事,
包括触发器、约束、审计,全都照样触发。
视图不能改的典型情况:
带 DISTINCT(同一行可能来自多行)、带聚合(一行对应一批行)、
带 UNION(不知道改哪个底表)、ROWNUM(行号是临时的)。
四、管理视图:改、删、查依赖
改视图 = CREATE OR REPLACE。
注意「改」不是「打补丁」,是整段替换——新写的 SQL 里漏掉的列,改完就真没了。
删视图:DROP VIEW v_emp30;
删之前查依赖:USER_DEPENDENCIES 里 REFERENCED_NAME 等于视图名的那些对象。
有别的视图或存储过程依赖它,删完那些对象会变 INVALID。
查视图定义三张视图:
USER_VIEWS(SQL 文本)、USER_OBJECTS(状态)、USER_DEPENDENCIES(谁依赖谁)。
USER_VIEWS.TEXT 是 LONG 类型,SQL*Plus 里要 SET LONG 4000 才看得全——
不然它会被截断,你还以为视图只有半句 SQL。
五、动手 Demo:建视图、查视图、改视图,再看它怎么被拦住
这一篇的 Demo 有四件事:建视图看计划、用 INSTEAD OF 触发器让视图可写、
用 RAISE 模拟 WITH CHECK OPTION、验证「不加 order by 顺序不保证」。
SQLite 的视图是只读的,WITH CHECK OPTION 也不支持——
所以这两件事我用触发器等价实现,并在正文里逐处标出「Oracle 原码长什么样」。
# 视图:展开成 SQL、靠触发器才可写、靠 RAISE 才拦得住
import sqlite3
c = sqlite3.connect(":memory:")
c.executescript("""
create table emp (empno int primary key, ename text, job text, sal real, deptno int);
insert into emp values (7499,'ALLEN','SALESMAN',1600,30),(7521,'WARD','SALESMAN',1250,30),
(7900,'JAMES','CLERK',950,30),(7566,'JONES','MANAGER',2975,20);
create view v_emp30 as select empno, ename, job, sal from emp where deptno = 30;
""")
print("视图行数:", c.execute("select count(*) from v_emp30").fetchone()[0])
print("计划:", " / ".join(r[3] for r in c.execute("explain query plan select * from v_emp30")))
c.execute("create trigger tr_ins instead of insert on v_emp30 "
"begin insert into emp (empno, ename, job, sal, deptno) "
"values (new.empno, new.ename, new.job, new.sal, 30); end")
c.execute("insert into v_emp30 (empno, ename, job, sal) values (7999, 'TONY', 'ANALYST', 3000)")
print("插进去的 deptno:", c.execute("select deptno from emp where empno = 7999").fetchone()[0])
c.execute("create view v_emp30b as select empno, ename, job, sal, deptno "
"from emp where deptno = 30")
c.execute("create trigger tr_ins_b instead of insert on v_emp30b "
"begin select case when new.deptno <> 30 "
"then raise(abort, 'CHECK OPTION violated') end; "
"insert into emp (empno, ename, job, sal, deptno) "
"values (new.empno, new.ename, new.job, new.sal, new.deptno); end")
try:
c.execute("insert into v_emp30b values (7998, 'KATE', 'CLERK', 950, 20)")
print("没拦住")
except sqlite3.Error as e:
print("被拦住:", e)
实跑输出:
--------------------------------------------------------------------
第四十四课:视图对象——一张会变形的表
--------------------------------------------------------------------
**视图不是表,它连数据都不存。**
**它存的是一句 SQL,每次查它就把那句 SQL 展开来跑一遍。**
**沙箱里能真跑,因为 SQLite 也有视图,语义基本一致。**
一、建一个视图,然后像表一样查它
--------------------------------------------------------------------
SQL> create view v_emp30 as select empno, ename, job, sal from emp where deptno = 30
SQL> select * from v_emp30
EMPNO ENAME JOB SAL
--------------------------
7499 ALLEN SALESMAN 1600
7521 WARD SALESMAN 1250
7654 MARTIN SALESMAN 1250
7698 BLAKE MANAGER 2850
7844 TURNER SALESMAN 1500
7900 JAMES CLERK 950
已选择 6 行
**注意视图里没有 `deptno` 这一列**——它被 `where` 用掉了,但没被选出来。
**这是视图最常见的用法:**
**把「某个部门」「某个时间段」「某个状态」这类固定条件固化下来,让调用方少写一句 `where`。**
二、视图的定义存在哪:数据字典里的一行文本
--------------------------------------------------------------------
SQL> select name as 视图名, sql as 定义 from sqlite_master where type = 'view'
视图名 定义
---------------------------------------------------------------------------------------
v_emp30 CREATE VIEW v_emp30 as select empno, ename, job, sal from emp where deptno = 30
已选择 1 行
**Oracle 里这句存在 `USER_VIEWS.TEXT`(`LONG` 类型,得用 `SET LONG` 才看得全)。**
**它是纯文本,没有编译过,所以「改视图」本质就是「改这段文本」。**
三、视图上面还能再建视图
--------------------------------------------------------------------
SQL> create view v_emp30_clerk as select empno, ename from v_emp30 where job = 'CLERK'
SQL> select * from v_emp30_clerk
EMPNO ENAME
-----------
7900 JAMES
已选择 1 行
**两层视图,实际执行时被展开成一条 SQL:**
SQL> explain query plan select * from v_emp30_clerk
SCAN emp
**计划里只有一句 `SCAN emp`,没有「先算子视图再过滤」的痕迹。**
**说明两层视图被揉成了一条 SQL,落到底表上一次扫完。**
**优化器不是「先算视图再过滤」,而是「把视图的 SQL 和你的 SQL 拼起来再优化」。**
**展开之后,视图基本不额外花钱。**
四、加索引,看视图查询会不会用上
--------------------------------------------------------------------
SQL> create index idx_emp_dept on emp (deptno)
SQL> explain query plan select * from v_emp30 where sal > 1000
SEARCH emp USING INDEX idx_emp_dept (deptno=?)
**`SEARCH emp USING INDEX idx_emp_dept (deptno=?)` 出现了。**
**注意这个索引是视图里那句 `where deptno = 30` 用上的,不是 `sal > 1000` 用上的**——
**`sal` 上没有索引,那一部分只能拿索引取到的行逐条比。**
**但它足以说明一件事:视图没有把底表挡住。**
**视图里的条件照样落到 `emp` 上,索引照样能用。**
**真正会挡住索引的是「在列上算东西」**,比如 `where sal * 12 > 12000`;
**这时候索引就废了,除非建函数索引。**
五、视图能不能改数据:键保留表
--------------------------------------------------------------------
**Oracle 的规矩是:视图能不能 `insert`/`update`,看它是不是「键保留表」。**
**简单说,视图里必须保留底表的主键列,而且不能有聚合、`distinct`、`union`。**
**`v_emp30` 保留了 `empno`(主键),所以它能改。**
[Oracle 原码 · 本沙箱不执行]
| UPDATE v_emp30 SET sal = sal + 100 WHERE ename = 'ALLEN'; -- 可以
| INSERT INTO v_emp30 (empno, ename, job, sal)
| VALUES (7902, 'FORD', 'ANALYST', 3000);
| -- 插得进去,但插完查不到:deptno 是 NULL,不满足 where deptno = 30
**能改,但改的范围受视图的 `where` 限制**——**插进去的行不在 `deptno = 30` 里,插完就查不到了。**
**这就是下一节 `WITH CHECK OPTION` 要堵的窟窿。**
**SQLite 的视图是只读的,所以要真跑就得靠 `INSTEAD OF` 触发器:**
SQL> create trigger tr_v_emp30_ins instead of insert on v_emp30 begin insert into emp (empno, ename, job, sal, deptno) values (new.empno, new.ename, new.job, new.sal, 30); end
SQL> insert into v_emp30 (empno, ename, job, sal) values (7999, 'TONY', 'ANALYST', 3000)
SQL> select empno, ename, job, sal, deptno from emp where empno = 7999
EMPNO ENAME JOB SAL DEPTNO
-------------------------------
7999 TONY ANALYST 3000 30
已选择 1 行
**触发器替视图干了「补上 `deptno = 30`」这件事。**
**Oracle 的 `INSTEAD OF` 触发器是同一个用途**——**专门给「改不了的视图」加一层翻译。**
六、WITH CHECK OPTION:别让数据从视图里溜走
--------------------------------------------------------------------
**上面那个问题(插进去就查不到)Oracle 用 `WITH CHECK OPTION` 解决:**
[Oracle 原码 · 本沙箱不执行]
| CREATE OR REPLACE VIEW v_emp30 AS
| SELECT empno, ename, job, sal, deptno FROM emp WHERE deptno = 30
| WITH CHECK OPTION;
**加了这个选项,任何「插完不满足 `deptno = 30`」的语句直接报错,不会静悄悄溜进去。**
**注意它要求视图里必须带着 `deptno` 这一列**——
**因为要检查的条件就在这一列上,没有它连检查都没法写。**
**所以我另建一个带 `deptno` 的视图:**
SQL> create view v_emp30b as select empno, ename, job, sal, deptno from emp where deptno = 30
**SQLite 没有 `WITH CHECK OPTION`,只能在 `INSTEAD OF` 触发器里手写同样的判断:**
SQL> create trigger tr_v_emp30b_ins instead of insert on v_emp30b begin select case when new.deptno <> 30 then raise(abort, 'CHECK OPTION violated: deptno must be 30') end; insert into emp (empno, ename, job, sal, deptno) values (new.empno, new.ename, new.job, new.sal, new.deptno); end
SQL> insert into v_emp30b (empno, ename, job, sal, deptno) values (7998, 'KATE', 'CLERK', 950, 20)
ERROR 位于第 1 行:
CHECK OPTION violated: deptno must be 30
**被拦住了,一行都没写进去。**
**`RAISE(ABORT, ...)` 就是把整条语句回滚掉**,**比「插完再报警」干净得多。**
SQL> select count(*) as 溜进去的行数 from emp where empno = 7998
溜进去的行数
------------
0
已选择 1 行
七、一个常被问的问题:不写 order by 顺序固定吗
--------------------------------------------------------------------
**连查两次,顺序一样。**
**这次没测出差异,但结论不能反过来说「顺序固定」。**
**SQL 标准里没有 `order by` 就没有顺序保证**,
**两万行的表、并行的执行计划、换个索引,顺序都可能变。**
**所以凡是「要按顺序显示」的地方,`order by` 必须写。**
第一段:建视图、查视图、看定义。
注意 deptno 没出现在视图的列里——它被 where 用掉了。
这带来一个后果:往这个视图里 INSERT 时你没法指定部门,插进去的行 deptno 是空值,
然后因为不满足 where deptno = 30,插完就查不到了。
第二段:两层视图被展开成一句。
计划里只有 SCAN emp,没有「先算子视图」的痕迹——
说明优化器把两层视图和你的 SQL 拼成了一句。
第三段:SQLite 的视图只读,所以真跑要靠 INSTEAD OF 触发器。
触发器替视图补上了 deptno = 30,插进去的行带着部门 30 落到底表。
Oracle 的 INSTEAD OF 触发器是同一个用途——给改不了的视图加一层翻译。
第四段:把判断写进触发器,模拟 WITH CHECK OPTION。
RAISE(ABORT, ...) 的效果是「整条语句回滚」,
不是「插完再报警」——所以最后数一下 empno = 7998 的行数,是 0,一行都没溜进去。
最后一段是诚实记录:连查两次顺序一样,这次没测出差异。
但「没测出差异」不等于「顺序固定」——SQL 标准里没有 order by 就没有顺序保证。
30 秒实验
- 把视图改成带
GROUP BY的版本(按deptno统计人数),再试UPDATE它。 Oracle 会报ORA-01732(此视图不可更新)。 - 给视图加
WITH READ ONLY(Oracle 原码),再试INSERT。 报ORA-42399:无法对只读视图执行 DML。 - 用
explain query plan对比「查视图」和「把视图的 SQL 抄出来直接查」, 两边的计划应该一模一样——这就是「视图不额外花钱」的证据。
常见误区
- 「视图是一张缓存表」。 不存数据,每次查都跑一遍 SQL。 想要缓存,那是物化视图(materialized view),另一回事。
- 「视图能加快查询」。 一般不会。它的价值在权限控制、屏蔽复杂度、保持接口稳定,不在性能。 个别情况(视图里预先过滤掉大量数据)可能快,但那是「预先过滤」的作用,不是视图的。
- 「建视图不用
OR REPLACE」。 视图已存在就报ORA-00955,得先DROP, 而DROP会让依赖它的对象变INVALID。 - 「可写视图随便写,反正有
where挡着」。 挡不住。where deptno = 30只影响「查」,不影响「写」—— 写进去的行可以不属于部门 30,然后你就查不到它了。 要挡住得用WITH CHECK OPTION。
自测三问(点开对答案)
1. 视图占多少存储空间? —— **就是那段 SQL 文本的长度**,存在 `USER_VIEWS.TEXT` 里。不存数据。 2. 什么叫「键保留表」? —— **视图里保留了底表的主键列,且没有聚合、`DISTINCT`、`UNION`**, 这样的视图可以直接 `INSERT`/`UPDATE`/`DELETE`。 3. `WITH CHECK OPTION` 解决什么问题? —— **防止「改了之后不满足视图的 `where`」**—— 比如往 `deptno = 30` 的视图里插一个 `deptno = 20` 的行,加了这个选项直接报错。 4. 删视图之前要查什么? —— **`USER_DEPENDENCIES`。** 有别的对象依赖它,删完那些对象会变 `INVALID`。下一篇讲同义词:给对象起个外号。 它比视图更轻——字典里只存一行名字映射,不存 SQL。 重点在「名字解析顺序」,那一条决定了同义词会不会被挡住。
- ☐ (本篇完,下一篇 10.3 同义词对象)
相关笔记
- 索引对象
- 同义词对象
- 数据表概述
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引