🏺 第10章 其他数据对象
📄 一句话概括

视图不存数据,只存一句 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 秒实验

  1. 把视图改成带 GROUP BY 的版本(按 deptno 统计人数),再试 UPDATE 它。 Oracle 会报 ORA-01732(此视图不可更新)。
  2. 给视图加 WITH READ ONLY(Oracle 原码),再试 INSERT。 报 ORA-42399:无法对只读视图执行 DML。
  3. 用 explain query plan 对比「查视图」和「把视图的 SQL 抄出来直接查」, 两边的计划应该一模一样——这就是「视图不额外花钱」的证据。

常见误区

  1. 「视图是一张缓存表」。 不存数据,每次查都跑一遍 SQL。 想要缓存,那是物化视图(materialized view),另一回事。
  2. 「视图能加快查询」。 一般不会。它的价值在权限控制、屏蔽复杂度、保持接口稳定,不在性能。 个别情况(视图里预先过滤掉大量数据)可能快,但那是「预先过滤」的作用,不是视图的。
  3. 「建视图不用 OR REPLACE」。 视图已存在就报 ORA-00955,得先 DROP, 而 DROP 会让依赖它的对象变 INVALID。
  4. 「可写视图随便写,反正有 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。 重点在「名字解析顺序」,那一条决定了同义词会不会被挡住。

相关笔记