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

建视图的顺序:先把 SELECT 单独跑通,确认结果对了,再在前面加一句 CREATE VIEW。 ALTER VIEW 比「删了重建」强的地方是保留权限——DROP 再建,授权就没了。 单表、无聚合、无 DISTINCT 的视图在 SQL Server 上可直接写; 一旦有聚合或 JOIN,就只能读。

📝 本章和前面几章的关系

10.1 讲「视图是什么」,这一节讲「怎么用」——建、改、删,以及通过它写数据。 教材 10.2 分五小节:界面方式、CREATE VIEW、ALTER VIEW、DROP VIEW、通过视图操作数据。 界面那一节跳过(SSMS 里点出来的东西没法在文本里演),其余四节全部实跑。 本沙箱有两处得绕: ALTER VIEW 没有,用 DROP + CREATE 等价; SQLite 的视图一律只读,所以「通过视图写数据」用 INSTEAD OF 触发器复刻语义, 并明确标出这是两家机制不同,不是同一件事。

一、CREATE VIEW:先跑通 SELECT,再套壳

这是最稳的顺序,没有之一。 先把 SELECT 单独执行,看结果对不对、行数对不对; 确认没问题,再在最前面加 CREATE VIEW 视图名 AS。 反过来(先建视图再调试)很痛苦: 视图里出错了,报错信息指向视图名, 不指向具体哪一句——你还得把它展开成 SELECT 再查一遍。

CREATE VIEW v_score_detail
AS
    SELECT st.sno, st.sname, c.cname, sc.subject, sc.score
    FROM tb_score sc
        JOIN tb_student st ON st.id = sc.student_id
        JOIN tb_class   c  ON c.id  = st.class_id;
GO

列名是从 SELECT 自动取的。 想让列名干净(比如 sname 变成「姓名」), 就在 SELECT 里起别名——视图会把这个别名一起存下来。 那个 GO 是客户端的分隔符,不是 SQL 语句。 6.6 说过这个: SSMS 看到 GO 就把前面攒的一批发出去。 用别的工具可能不认它。

二、ALTER VIEW:改定义,保住权限

ALTER VIEW v_score_detail
AS
    SELECT st.sno, st.sname, c.cname, sc.subject, sc.score
    FROM tb_score sc
        JOIN tb_student st ON st.id = sc.student_id
        JOIN tb_class   c  ON c.id  = st.class_id
    WHERE sc.score >= 60;
GO

ALTER VIEW 的好处不只是「少打几个字」。 它保留这个对象上的权限授予—— 别人被授予过查询这个视图,改完定义他还能查。 换成 DROP + CREATE,权限就全没了。 得重新 GRANT 一遍, 而且中间那段时间视图不存在,正在跑的报表会直接报错。 本沙箱没有 ALTER VIEW(SQLite 的 ALTER 只能改表名、加列), 演示里走的是 DROP + CREATE——结果一样,代价就是上面说的那些。

三、DROP VIEW:删视图不动基表

DROP VIEW v_tmp 之后,v_tmp 从数据字典里消失,但 tb_goods 里六行数据一条没少。 反过来要小心:删了基表,视图还在。 但一查就报错—— 「no such table」或者「对象名无效」这类错误,十有八九是基表被人改名或删了。 排查顺序: 先看视图引用的表还在不在,再看列名有没有被改。 视图不存数据,所以它不会「数据丢了」,只会「查不出来」。

四、通过视图操作数据

这是 SQL Server 一个很实用的特性:

UPDATE v_on_sale SET price = 4599 WHERE id = 1001;
GO

这条能跑,改的其实是 tb_goods。 因为 v_on_sale 是单表视图、 没有聚合、没有 DISTINCT——视图里的一行稳稳对应基表的一行,所以改得动。 SQLite 的视图一律只读,直接写会报 cannot modify v_on_sale because it is a view。 要在 SQLite 上复刻这个效果,得给视图挂一个 INSTEAD OF 触发器,把写操作翻译到基表。 这里必须说清楚:这是两件不同的事。 SQL Server 是「视图本身可更新」; SQLite 是「视图只读,但你可以在它上面挂触发器拦截写操作」。效果像,机制完全不同。

五、哪些视图能写,哪些不能

视图里出现了 能不能写 原因
聚合函数(SUM/AVG/COUNT) 不能 一行对应多行,改了不知道改谁
DISTINCT 不能 去重之后行和基表对不上
GROUP BY 不能 同上
UNION 不能 一行可能同时来自两张表
多表 JOIN 里改非保留表的列 不能 缺主键,定位不到具体行
单表 + 无聚合 + 无 DISTINCT 能 一行刚好对一行

判断口诀一句话:能不能做到「视图里的一行,稳稳对应基表的一行」。 能,就可写。 「保留表」这个词要解释一下。 多表 JOIN 里,主表(一的那边)是保留表, 明细表(多的那边)不是。 改保留表的列有时可以, 改明细表的列一定不行——因为一行明细在视图里可能被复制了好几份。 这条规则在 SQL Server 上成立。 具体还要看权限、WITH CHECK OPTION 有没有开。 不确定的时候,别硬写——老老实实改基表最安全。

六、动手 Demo:建、改、删视图,以及通过视图写数据

这一段走全套:先跑 SELECT 再建视图、用 DROP + CREATE 模拟 ALTER、 DROP 之后看基表数据一条没少。 最后一节是本篇重点:直接 UPDATE 视图会报错,挂上 INSTEAD OF 触发器之后能写进去, 而写进去的地方是基表。

import sqlite3

c = sqlite3.connect(u":memory:")
# ... 建六表、装样例数据 ...

# 建视图
c.execute(u"CREATE VIEW v_score_detail AS "
          u"SELECT st.sno, st.sname, c.cname, sc.subject, sc.score "
          u"FROM tb_score sc JOIN tb_student st ON st.id = sc.student_id "
          u"JOIN tb_class c ON c.id = st.class_id")

# 没有 ALTER VIEW,只能 DROP + CREATE
c.execute(u"DROP VIEW v_score_detail")
c.execute(u"CREATE VIEW v_score_detail AS SELECT ... WHERE sc.score >= 60")

# 删视图不影响基表
c.execute(u"DROP VIEW v_tmp")
print(c.execute(u"SELECT COUNT(*) FROM tb_goods").fetchone())

# 视图只读:直接写会报错
c.execute(u"UPDATE v_on_sale SET price = 4599 WHERE id = 1001")   # 报错

# 挂 INSTEAD OF 触发器才能写
c.execute(u"CREATE TRIGGER trg_v_on_sale_upd INSTEAD OF UPDATE ON v_on_sale "
          u"BEGIN UPDATE tb_goods SET price = NEW.price WHERE id = OLD.id; END")
c.execute(u"UPDATE v_on_sale SET price = 4599 WHERE id = 1001")

实跑输出:

--------------------------------------------------------------------
创建、修改、删除视图,以及通过视图写数据
--------------------------------------------------------------------


  ① CREATE VIEW:先写查询,再套壳
  --------------------------------------------------------------------
  顺序建议:先把 SELECT 单独跑通,确认结果对,再前面加一句 CREATE VIEW。

  1> SELECT st.sname, c.cname, sc.subject, sc.score
     FROM tb_score sc JOIN tb_student st ON st.id = sc.student_id JOIN tb_class c ON
         c.id = st.class_id
  sname  cname     subject   score
  --------------------------------
  张伟   计科2301  高等数学  88
  张伟   计科2301  大学英语  76.5
  王芳   计科2301  高等数学  92
  王芳   计科2301  大学英语  85
  李娜   计科2301  高等数学  61
  刘强   计科2302  高等数学  55.5
  陈静   计科2302  大学英语  91
  杨洋   软工2301  高等数学  79
  (8 行受影响)

  1> CREATE VIEW v_score_detail AS
     SELECT st.sno, st.sname, c.cname, sc.subject, sc.score
     FROM tb_score sc JOIN tb_student st ON st.id = sc.student_id JOIN tb_class c ON
         c.id = st.class_id
  视图建好了。列名自动取 SELECT 里的名字——想让列名干净,就在 SELECT 里起别名。

  [教材原码 · 本沙箱不执行]
  | CREATE VIEW v_score_detail
  | AS
  |     SELECT st.sno, st.sname, c.cname, sc.subject, sc.score
  |     FROM tb_score sc
  |         JOIN tb_student st ON st.id = sc.student_id
  |         JOIN tb_class   c  ON c.id  = st.class_id;
  | GO


  ② ALTER VIEW:本沙箱没有,等价做法是先删再建
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | ALTER VIEW v_score_detail
  | AS
  |     SELECT st.sno, st.sname, c.cname, sc.subject, sc.score
  |     FROM tb_score sc
  |         JOIN tb_student st ON st.id = sc.student_id
  |         JOIN tb_class   c  ON c.id  = st.class_id
  |     WHERE sc.score >= 60;
  | GO

  ! SQLite 没有 ALTER VIEW,只能 DROP + CREATE。SQL Server 有,而且不会丢权限。
  这一点差别很实际:ALTER 会保留授予过这个视图的权限,DROP 之后再建,权限就没了。

  1> DROP VIEW v_score_detail

  1> CREATE VIEW v_score_detail AS
     SELECT st.sno, st.sname, c.cname, sc.subject, sc.score
     FROM tb_score sc JOIN tb_student st ON st.id = sc.student_id JOIN tb_class c ON
         c.id = st.class_id
     WHERE sc.score >= 60

  1> SELECT * FROM v_score_detail ORDER BY sno, subject
  sno       sname  cname     subject   score
  ------------------------------------------
  20230101  张伟   计科2301  大学英语  76.5
  20230101  张伟   计科2301  高等数学  88
  20230102  王芳   计科2301  大学英语  85
  20230102  王芳   计科2301  高等数学  92
  20230103  李娜   计科2301  高等数学  61
  20230202  陈静   计科2302  大学英语  91
  20230301  杨洋   软工2301  高等数学  79
  (7 行受影响)
  刘强那条 55.5 不见了——加了 WHERE 之后视图里的行就少了。

  ③ DROP VIEW:删视图不动基表
  --------------------------------------------------------------------

  1> CREATE VIEW v_tmp AS SELECT id, gname FROM tb_goods

  1> DROP VIEW v_tmp
| 删完视图之后 | 还剩几条 |
|---|---:|
| 名为 v_tmp 的视图 | 0 |
| tb_goods 里的数据 | 6 |

  视图没了,数据一条没少。反过来删基表呢?视图还在,但一查就报错——
  「no such table」这类错,十有八九是基表被人改了名或删了。

  ④ 通过视图写数据:SQL Server 能,SQLite 要绕一下
  --------------------------------------------------------------------

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

  [教材原码 · 本沙箱不执行]
  | UPDATE v_on_sale SET price = 4599 WHERE id = 1001;
  | GO

  SQL Server 上这条能跑:v_on_sale 是单表视图、没有聚合、没有 DISTINCT,属于「可更新视图」。
  改的其实是 tb_goods。SQLite 里视图一律只读,直接写会报错:

  1> UPDATE v_on_sale SET price = 4599 WHERE id = 1001
  消息 102,级别 15,状态 1
  cannot modify v_on_sale because it is a view
  要在 SQLite 上复刻这个效果,得给视图挂一个 INSTEAD OF 触发器,把写操作翻译到基表:

  1> CREATE TRIGGER trg_v_on_sale_upd INSTEAD OF
     UPDATE ON v_on_sale BEGIN
     UPDATE tb_goods
     SET price = NEW.price
     WHERE id = OLD.id; END

  1> UPDATE v_on_sale SET price = 4599 WHERE id = 1001
  (0 行受影响)

  1> SELECT id, gname, price FROM tb_goods WHERE id = 1001
  id    gname       price
  -----------------------
  1001  联想笔记本  4599
  (1 行受影响)
  ! 价格真的落到基表了。但注意:这是触发器干的活,不是「视图可更新」。
  上面那句「0 行受影响」也别误会——视图本身一个字节都没写,写的是基表 tb_goods。
  触发器里的 NEW 是「想写进去的新值」,OLD 是「视图里原来那一行」,两个都能用。
  SQL Server 那边不需要触发器,只要视图满足条件就能写;这是两家的机制差异。

  ⑤ 哪些视图不能写
  --------------------------------------------------------------------
| 视图里出现了 | 还能不能写 | 原因 |
|---|---|---|
| 聚合函数(SUM/AVG/COUNT) | 不能 | 一行对应多行数据,改了不知道改谁 |
| DISTINCT | 不能 | 去重之后行和基表对不上 |
| GROUP BY | 不能 | 同上 |
| UNION | 不能 | 一行可能同时来自两张表 |
| 多表 JOIN 里改了非保留表的列 | 不能 | 缺主键,定位不到具体行 |
| 单表 + 无聚合 + 无 DISTINCT | 能 | 一行刚好对一行 |

  判断口诀:能不能做到「视图里的一行,稳稳对应基表的一行」。能,就可写。
  这个判断在 SQL Server 上成立;具体还要看权限和 WITH CHECK OPTION 的设置。

第一段那个 SELECT 返回 8 行(六个学生、八条成绩)。 先看这 8 行对不对,再建视图——这个习惯能省掉很多「视图报错找不到源头」的时间。 加了 WHERE sc.score >= 60 之后只剩 7 行,刘强那条 55.5 不见了。 注意这几行里李娜的 61 分还在——刚好及格,条件写 > 就没了,写 >= 才留。 第三段直接 UPDATE 视图报的是 cannot modify v_on_sale because it is a view。 SQL Server 不会报这个错(单表视图可写),所以这条错误信息本身就是两家差异的证据。 最后 UPDATE 那句返回「0 行受影响」,别误会。 视图自己一个字节都没写, 写的是 tb_goods;查询 tb_goods 能看到价格真变成 4599 了。

30 秒实验

  1. 把 WHERE sc.score >= 60 改成 > 60,重跑,看李娜的 61 分还在不在。 边界条件这种错,只有真跑一次才记得住。
  2. 把视图定义里 SELECT 的列顺序调换一下,重跑,看视图的列顺序变不变。 跟着变——视图的列顺序就是 SELECT 里写的顺序。
  3. 试着 INSERT 一条记录进 v_score_detail(多表视图)。 SQL Server 会报「不可更新」,本沙箱会报「视图」——两种报错,同一个意思。

常见误区

  1. 「改视图用 DROP + CREATE 最省事」。省事,但权限全丢,中间还有一段时间视图不存在。 有 ALTER VIEW 就用它。
  2. 「视图能查就能写」。只有单表、无聚合、无 DISTINCT 的视图可写。 有 JOIN、有 GROUP BY 的,只能读。
  3. 「删了基表,视图也就没了」。视图还在,只是查不了。 报「对象名无效」时先查基表,别急着重建视图。
自测三问(点开对答案) 1. 建视图的正确顺序是什么? —— **先把 `SELECT` 单独跑通,确认结果和行数都对,再在前面加 `CREATE VIEW`。** **反过来调试很痛苦——报错指向视图名,不指向具体哪一句。** 2. `ALTER VIEW` 比 `DROP` + `CREATE` 好在哪? —— **保留权限授予,而且不会出现「视图不存在」的空档。**`DROP` 之后再建, 之前 `GRANT` 的权限全没了,跑着的报表也会中断。 3. 什么样的视图可以写? —— **单表、不含聚合函数、不含 `DISTINCT`、不含 `GROUP BY` 的。** **口诀:视图里的一行能稳稳对应基表的一行。**

下一篇 10.3 讲视图剩下的部分: 从视图取数的实际用法、四条好处、WITH CHECK OPTION 这个防呆开关, 以及 WITH ENCRYPTION 加密定义。

相关笔记