🪟 第10章 视图的使用
📄 一句话概括

视图能当表用:聚合、排序、再 JOIN 都行,还能在视图上再建视图(别超过两层)。 WITH CHECK OPTION 是个防呆开关:拦住那些「插进去就看不见」的数据。 WITH ENCRYPTION 挡的是同事看你的 SQL,不是安全措施——有 sysadmin 权限的人照样能挖出来。

📝 本章和前面几章的关系

10.1 讲概念,10.2 讲建改删,这一节收尾:把视图真正用起来,以及两个不太常用但很有用的开关。 教材 10.3 分五小节:从视图内调取数据、使用视图的好处、修改视图中的数据、加密视图、小结。 「小结」和「实践与练习」按惯例并进本篇末尾。 本沙箱还是那两处绕法: WITH CHECK OPTION 没有, 用 INSTEAD OF 触发器里的 RAISE(ABORT, ...) 复刻;WITH ENCRYPTION 没有,打原码讲原理。

一、从视图里取数:跟表一样用

视图建好之后,你在 FROM 后面写它,就跟写表名一样。 聚合、排序、再 JOIN,都行:

SELECT uname, SUM(total) AS 消费合计
FROM v_member_order
GROUP BY uname ORDER BY 消费合计 DESC

注意这个视图本身就是 JOIN 出来的(会员表连订单表), 外面的查询照样能对它聚合——数据库会把两层合并成一条语句去执行。 还能在视图上再建视图。 演示里就做了一个「大额消费」视图,但建议别超过两层——三层以上, 改的时候得一层层往下翻,很容易改错地方还以为改对了。

二、用视图的四条好处

好处 具体怎么体现
简化查询 三表 JOIN 包成一个视图,前端只写 SELECT * FROM v
隔离表结构变化 表改列名,只改视图,程序不用动
控制权限 只给视图权限,不给基表——看不到工资列
统一口径 「有效订单」的定义写一次,全公司都用它

第三条最实用,也最容易被忽略。 比如 tb_member 里有手机号, 报表只需要用户名和积分——建一个只含两列的视图给对方,他就看不到手机号。 而且他连改都改不了:只给了视图的 SELECT 权限,没给基表的写权限, 他想 UPDATE 也没门。 这比「叮嘱他别乱改」靠谱得多。 第四条是长期维护的关键。 「有效订单」到底算不算已取消的? 这种口径如果每个报表各写一遍,早晚不一致。 写进视图,全公司查同一个东西。

三、修改视图中的数据:多表视图为什么不行

UPDATE v_member_order SET points = points + 10
WHERE uname = 'member001';
GO

这条在 SQL Server 上会报错,因为 v_member_order 是会员表 JOIN 订单表出来的。 报错的大意是「视图或函数不可更新,因为修改会影响多个基表」。道理不复杂: member001 在视图里有两行(他有两笔订单),points 是会员表上的字段,改它到底改哪一行? 这就是「保留表」概念的用处。 一对多 JOIN 里, 「一」那边是保留表,「多」那边不是——多那边的一行在视图里被复制了好几份。

四、WITH CHECK OPTION:别让数据从窗口溜走

CREATE VIEW v_on_sale2
AS SELECT id, gname, price, is_on FROM tb_goods WHERE is_on = 1
WITH CHECK OPTION;
GO

没有这个子句时会出这么一种怪事: 往视图里 INSERT 一条 is_on = 0 的记录—— 成功了。但你再查视图,看不见它。 数据「从窗口溜走了」。 人还以为没插进去,于是又插一遍,表里就多了一堆看不见的脏数据。 WITH CHECK OPTION 就是拦住这个: 新数据必须满足视图的 WHERE 条件, 否则直接报错。 这是个防呆开关,值得默认加上。 本沙箱不支持这个子句,演示里用 INSTEAD OF 触发器加 RAISE(ABORT, ...) 复刻, 拦住的效果一样,实现方式不同。

五、加密视图:挡人不挡自己

CREATE VIEW v_secret
WITH ENCRYPTION
AS SELECT id, gname, price FROM tb_goods;
GO

加了之后 sp_helptext 'v_secret' 会说「对象已被加密」,看不到定义。 SQL Server 的做法是把定义文本混淆后存进 sys.sql_modules。 但这不是安全措施。 它挡的是「同事顺手看一眼你的 SQL」, 挡不住有 sysadmin 权限的人——那种权限本来就能看到一切。 而且它有个实际代价:加密之后逻辑改不了,只能删了重建。所以别到处加—— 等真有人抱怨「这段代码看不懂又不能改」的时候,代价就出来了。

六、动手 Demo:视图当表用、CHECK OPTION 的效果与加密

这一段把视图真正用起来:在两个表连出来的视图上做聚合、再在视图上建视图。 然后用 INSTEAD OF 触发器复刻 WITH CHECK OPTION——插一条 is_on = 0 的记录,看它怎么被拦。

import sqlite3

c = sqlite3.connect(u":memory:")
# ... 建六表、装样例数据 ...
c.execute(u"CREATE VIEW v_member_order AS "
          u"SELECT m.uname, m.points, o.order_no, o.total, o.state "
          u"FROM tb_member m JOIN tb_order o ON o.member_id = m.id")

# 视图当表用:聚合
print(c.execute(u"SELECT uname, SUM(total) AS 消费合计 FROM v_member_order "
                u"GROUP BY uname ORDER BY 消费合计 DESC").fetchall())

# 视图上再建视图
c.execute(u"CREATE VIEW v_big_spender AS "
          u"SELECT uname, total FROM v_member_order WHERE total > 1000")

# 复刻 WITH CHECK OPTION:触发器里自己判断
c.execute(u"CREATE TRIGGER trg_v_on_sale3_ins INSTEAD OF INSERT ON v_on_sale3 "
          u"BEGIN SELECT RAISE(ABORT, '新行必须满足 is_on = 1') "
          u"WHERE NEW.is_on <> 1; "
          u"INSERT INTO tb_goods (id, gname, price, is_on) "
          u"VALUES (NEW.id, NEW.gname, NEW.price, NEW.is_on); END")
print(c.execute(u"INSERT INTO v_on_sale3 VALUES (2001, '测试下架品', 9.9, 0)"))

实跑输出:

--------------------------------------------------------------------
从视图取数、视图的好处、加密与检查选项
--------------------------------------------------------------------


  ① 从视图里取数:跟表一样用
  --------------------------------------------------------------------

  1> CREATE VIEW v_member_order AS
     SELECT m.uname, m.points, o.order_no, o.total, o.state
     FROM tb_member m JOIN tb_order o ON o.member_id = m.id

  1> SELECT uname, SUM(total) AS 消费合计
     FROM v_member_order
     GROUP BY uname
     ORDER BY 消费合计 DESC
  uname      消费合计
  -------------------
  member001  5487.9
  member002  1299
  member003  977.9
  member004  549
  (4 行受影响)
  视图可以当表用:能聚合、能排序、能再跟别的表 JOIN。
  也可以在视图上再建视图——但别超过两层,改起来会找不到源头。

  1> CREATE VIEW v_big_spender AS SELECT uname, total FROM v_member_order WHERE total > 1000

  1> SELECT * FROM v_big_spender
  uname      total
  -----------------
  member001  5088.9
  member002  1299
  (2 行受影响)

  ② 用视图的好处:四条
  --------------------------------------------------------------------
| 好处 | 具体怎么体现 |
|---|---|
| 简化查询 | 三表 JOIN 包成一个视图,前端只写 SELECT * FROM v |
| 隔离表结构变化 | 表改列名,只改视图,程序不用动 |
| 控制权限 | 只给视图权限,不给基表——看不到工资列 |
| 统一口径 | 「有效订单」的定义写一次,全公司都用它 |

  第三条最实用。比如 tb_member 里有手机号,报表只需要用户名和积分——
  建一个只含两列的视图给对方,他就看不到手机号,也不用担心他改错数据。

  ③ 修改视图中的数据(SQL Server 的写法)
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | UPDATE v_member_order SET points = points + 10
  | WHERE uname = 'member001';
  | GO

  这条在 SQL Server 上会报错——因为 v_member_order 是 JOIN 出来的多表视图。
  报错原话大意是「视图或函数不可更新,因为修改会影响多个基表」。
  多表视图要写得动,得满足更严的条件(比如只改其中一张保留表的列)。

  ④ WITH CHECK OPTION:别让数据从窗口溜走
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | CREATE VIEW v_on_sale2
  | AS SELECT id, gname, price, is_on FROM tb_goods WHERE is_on = 1
  | WITH CHECK OPTION;
  | GO

  没有它的时候,往视图里插一条 is_on = 0 的记录会成功——插完再查视图,看不见它。
  数据「从窗口溜走了」,人还以为没插进去。加了 WITH CHECK OPTION 就拦住:
  新数据必须满足视图的 WHERE 条件,否则报错。这是个防呆开关。
  SQLite 不支持这个子句,等价做法是在 INSTEAD OF 触发器里自己判断:

  1> CREATE VIEW v_on_sale3 AS SELECT id, gname, price, is_on FROM tb_goods WHERE is_on = 1

  1> CREATE TRIGGER trg_v_on_sale3_ins INSTEAD OF INSERT ON v_on_sale3 BEGIN
     SELECT RAISE(ABORT, '新行必须满足 is_on = 1')
     WHERE NEW.is_on <> 1;
     INSERT INTO tb_goods (id, gname, price, is_on)
     VALUES (NEW.id, NEW.gname, NEW.price, NEW.is_on); END

  1> INSERT INTO v_on_sale3 (id, gname, price, is_on) VALUES (2001, '测试下架品', 9.9, 0)
  消息 102,级别 15,状态 1
  新行必须满足 is_on = 1
  被拦住了。虽然实现方式不同(一个是子句,一个是触发器),拦住的效果一样。

  ⑤ 加密视图:让人看不到定义
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | CREATE VIEW v_secret
  | WITH ENCRYPTION
  | AS SELECT id, gname, price FROM tb_goods;
  | GO

  加了 WITH ENCRYPTION 之后,sp_helptext 'v_secret' 会告诉你「对象已被加密」。
  SQL Server 的做法是把定义文本混淆后存进 sys.sql_modules,谁也读不出来。
  ! 这不是「安全」——它挡的是同事看你的 SQL,挡不住有 sysadmin 权限的人。
  真正的权限控制要落在「谁能查哪些列」上,不是「谁能看 SQL 长什么样」。
  还有一条:加密之后视图逻辑没法改,只能删了重建——所以别到处加。

第一段四个数:5487.9、1299、977.9、549。member001 有两笔订单(5088.9 和 399), 所以他的合计是加起来的——这正是「视图当表用」的意思:外层聚合看到的是视图给的行。 第二段那个视图只查出 2 行(member001 的 5088.9 和 member002 的 1299)。 member001 另一笔 399 不够 1000,被筛掉了。 最后那句 RAISE(ABORT, ...) 是关键。 它不是「插了再删」, 而是插之前就中止整个语句——所以基表里不会留下那条脏数据。 换成 SQL Server,这一整段就是 WITH CHECK OPTION 一个子句的事。演示这么绕, 只是为了让本沙箱也能看见「被拦住」这件事。

30 秒实验

  1. 把「大额消费」视图的阈值从 1000 改成 500,重跑。 member001 那笔 399 还是进不来,599 才进得来——阈值这种东西,改一次就记住了。
  2. 把 RAISE 那句的 <> 1 改成 = 1,再插一次。 这次能插进去了——条件反了,防呆开关就变成了「只准插下架品」。
  3. 想一想:如果一个视图只给 SELECT 权限、不给基表权限,对方还能不能绕过视图看到手机号? 不能——权限是落在对象上的,他连表名都查不到。

常见误区

  1. 「视图能当表用,所以可以随便写」。多表视图、带聚合的视图都写不了。能读不等于能写。
  2. 「WITH CHECK OPTION 可有可无」。没有它, 插进去的数据可能从视图里看不见——人以为没插,再插一遍,脏数据就堆起来了。
  3. 「WITH ENCRYPTION 是安全措施」。它只挡顺手一看,sysadmin 照样能看到。 而且加密后改不了定义,代价不小。
自测三问(点开对答案) 1. 多表视图为什么一般不能写? —— **因为「多」的那边一行在视图里被复制了好几份**,改它不知道改哪一行。 **一对多 `JOIN` 里只有「一」那边是保留表。** 2. `WITH CHECK OPTION` 解决什么问题? —— **防止插进去的数据不满足视图的 `WHERE` 条件**——那种数据插成功了, **但查视图看不见,人以为没插,于是重复插,堆出一堆脏数据。** 3. 加密视图算安全措施吗? —— **不算。** 它挡的是同事顺手看你的 SQL,**有 `sysadmin` 权限的人照样能看到。** **真正的权限控制是「谁能查哪些列」,不是「谁能看 SQL」。**

第 10 章到此结束。下一篇进第 11 章 11.1,讲存储过程:把一批 SQL 存进数据库里,起个名字, 以后一句 EXEC 就能跑——这是「把逻辑放进数据库」的第一步。

相关笔记