🦏 第11章 视图
📄 一句话概括

CREATE VIEW 视图名 AS 查询,视图里不存数据,存的是一条 select 语句, 每次查它都重新跑一遍。 所以基表一改,视图立刻跟着变——不会过期、也没有同步延迟, 但也缓存不了任何东西,复杂视图用多了会慢。 本沙箱的视图一律只读,写会报 cannot modify … because it is a view; MySQL 的「简单视图」是可以写的,复杂视图只读。 要在这边写,只能挂 INSTEAD OF 触发器。

📝 本章和前面几章的关系

前面十章都在「写查询」,这一篇开始「把查询存起来」。 视图的价值不是省打字,是「把口径固定下来」: 一段五张表的连接、一堆条件,写成视图之后只有一处定义, 十个人写十份报表,用的都是同一个口径。

一、视图是什么:存起来的一条查询

create view v_boy as select id, name, height from students where gender = '男', 建完就能像表一样查。 但 v_boy 里没有存任何数据—— 它存的是「那条 select 语句」,每次查它都重新跑一遍。 所以视图也叫「虚拟表」。 students 是真表(数据在磁盘上),v_boy 是视图(只有一条规则)。 这个区别决定了很多行为:能不能写、能不能建索引、能不能存数据,全都跟着它。

二、视图不存数据,基表一变它就变

张伟的身高在 students 里改成 199.9,视图里立刻跟着变,因为它每次都在重新查 students。 这一点很重要:视图不会「过期」,也不会「同步延迟」——它压根没有自己的数据。 反过来说,视图也「缓存」不了任何东西: 查一次跑一次,复杂视图用多了会慢。 所以「用视图优化性能」是错的—— 它可能比直接写查询还慢一点,因为它多了一层解析。

三、视图最实用的两个好处

一是「把复杂的查询收起来」。 一段五张表的连接、一堆 where,写成视图之后,业务代码只写 select * from v_xxx。 改逻辑时只改视图定义一处,不用翻遍所有调用点。 二是「按行授权」。 MySQL 可以只给某个账号视图的权限、不给基表的权限, 于是「他只能看男生」这件事就由视图的 where 保证, 而不是靠每个查询自己记得加条件。 本沙箱没有权限系统,这一条只能在 MySQL 上用。 还有一个隐性好处:口径统一。 「活跃用户」怎么算、「平均身高」要不要排除空值, 这些定义写进视图之后就不会各算各的。 这是视图在数据团队里最大的价值,比省打字重要得多。

四、能不能通过视图改数据:这里一律只读

insert/update/delete 三条全报 cannot modify v_boy because it is a view。 本沙箱的视图是严格只读的,再简单的视图也不许写。 MySQL 不一样: 「简单视图」是可以写的——一对一映射、没有聚合、没有 distinct、没有 union, 写进去会落到基表上。 复杂视图(带聚合、group by、union)一样只读。 所以同一句 update 视图,换个数据库行为完全不同。 写跨库代码时不能假设「视图能写」或「视图不能写」,要么统一走基表,要么在目标库上先试一次。

五、要让视图可写:INSTEAD OF 触发器

本沙箱的解法是「用触发器接管」: INSTEAD OF INSERT ON 视图 的意思是「本来要插视图,改成干这件事」。 Demo 里插视图成功了,数据落在 students 里。 触发器里的 NEW 指「你往视图里塞的那一行」。 这条路能走通,但代价是「每加一条写入路径就多一个触发器」: 插入、更新、删除各一个,还要自己保证条件对得上。 所以实际项目里更常见的做法是「视图只读,写直接写基表」——少一层间接,少一类 bug。

六、建视图时给列改个名字

create view 视图名 (列名, 列名, …) as 查询,列名按顺序对应,个数必须一致。 这个写法在「查询里有重复列名」或「计算列没名字」时几乎是必须的: select a.id, b.id 两个都叫 id,不起名根本没法引用。 所以多表视图里起别名是常规动作,不是可选项。

七、动手 Demo:建一个视图,看它跟着基表变,再撞三次只读

Demo 六段:建视图并查它、改基表看视图跟着变、连撞三条写操作、用 INSTEAD OF 触发器让它可写、 最后给列改名。 第二段和第三段是全篇的核心对照——「读的时候是活的」和「写的时候不行」是视图的一体两面。

import _q46 as Q
c = Q.con()

# 给一条查询起个名字
Q.run(c, "CREATE VIEW v_boy AS "
          "SELECT id, name, height FROM students WHERE gender = '男'")
Q.show(c, "SELECT id, name, height FROM v_boy")

# 视图不存数据:改基表,视图跟着变
Q.run(c, "UPDATE students SET height = 199.9 WHERE id = 1")
Q.show(c, "SELECT id, name, height FROM v_boy WHERE id = 1")

# 视图一律只读
c.execute("INSERT INTO v_boy (id, name) VALUES (99, '甲')")
c.execute("UPDATE v_boy SET name = '改过' WHERE id = 1")
c.execute("DELETE FROM v_boy WHERE id = 1")

# 让它可写:INSTEAD OF 触发器接管
c2 = Q.con()
c2.execute("CREATE VIEW v_boy AS SELECT id, name, gender, height "
           "FROM students WHERE gender = '男'")
c2.execute("CREATE TRIGGER tr_ins INSTEAD OF INSERT ON v_boy "
           "BEGIN INSERT INTO students (id, name, gender) "
           "VALUES (NEW.id, NEW.name, NEW.gender); END")
Q.run(c2, "INSERT INTO v_boy (id, name, gender) VALUES (99, '甲', '男')")

# 建视图时给列改名
c.execute("CREATE VIEW v_c (编号, 姓名, 身高) AS "
          "SELECT id, name, height FROM students WHERE gender = '男'")

实跑输出:

--------------------------------------------------------------------
第五十八课:视图——给一条查询起个名字
--------------------------------------------------------------------

  视图是什么:存起来的一条查询
  SQL> CREATE VIEW v_boy AS SELECT id, name, height FROM students WHERE gender = '男'
  完成
  SQL> SELECT id, name, height FROM v_boy
  id     name     height
  -----------------------
  1      张伟     172.50
  4      刘洋     178.30
  6      杨帆     175.10
  8      黄磊     180.20
  9      周涛     174.00
  11     徐明     169.70
  (6 行)
  **建完就能像表一样查它。** **但 `v_boy` 里没有存任何数据**——
  **它存的是「那条 `select` 语句」,每次查它都重新跑一遍。**
  **所以视图也叫「虚拟表」。**
  **`students` 是真表(数据在磁盘上),`v_boy` 是视图(只有一条规则)。**

  视图不存数据,基表一变它就变
  SQL> UPDATE students SET height = 199.9 WHERE id = 1
  影响 1 行
  SQL> SELECT id, name, height FROM v_boy WHERE id = 1
  id     name     height
  -----------------------
  1      张伟     199.90
  (1 行)
  **张伟的身高在 `students` 里改成 199.9,视图里立刻跟着变。**
  **因为它每次都在重新查 `students`。**
  **这一点很重要**:
  **视图不会「过期」,也不会「同步延迟」**——
  **它压根没有自己的数据。** **反过来说,视图也「缓存」不了任何东西**,
  **查一次跑一次,复杂视图用多了会慢。**

  视图最实用的两个好处
  **一是「把复杂的查询收起来」。**
  **一段五张表的连接、一堆 `where`,写成视图之后,业务代码只写 `select * from v_xxx`**——
  **改逻辑时只改视图定义一处,不用翻遍所有调用点。**
  **二是「按行授权」。** **MySQL 可以只给某个账号视图的权限、不给基表的权限**,
  **于是「他只能看男生」这件事就由视图 `where gender = '男'` 保证**,
  **而不是靠每个查询自己记得加条件。** **本沙箱没有权限系统,这一条只能在 MySQL 上用。**

  能不能通过视图改数据:这里一律只读
  SQL> INSERT INTO v_boy (id, name) VALUES (99, '甲')
  报错:cannot modify v_boy because it is a view
  SQL> UPDATE v_boy SET name = '改过' WHERE id = 1
  报错:cannot modify v_boy because it is a view
  SQL> DELETE FROM v_boy WHERE id = 1
  报错:cannot modify v_boy because it is a view
  **三条全报 `cannot modify v_boy because it is a view`。**
  **本沙箱的视图是严格只读的,再简单的视图也不许写。**
  **MySQL 不一样**:
  **「简单视图」(一对一映射、没有聚合、没有 `distinct`、没有 `union`)是可以写的**,
  写进去会落到基表上。
  **复杂视图(带聚合、`group by`、`union`)一样只读。**
  **所以同一句 `update 视图`,换个数据库行为完全不同。**

  要让视图可写:INSTEAD OF 触发器
  **本沙箱的解法是「用触发器接管」**:
  **`INSTEAD OF INSERT ON 视图` 的意思是「本来要插视图,改成干这件事」。**
  SQL> INSERT INTO v_boy (id, name, gender) VALUES (99, '甲', '男')
  影响 0 行
  SQL> SELECT id, name, gender FROM students WHERE id = 99
  id     name     gender
  -----------------------
  99     甲       男
  (1 行)
  **插视图成功了,数据落在 `students` 里。** **触发器里的 `NEW` 指「你往视图里塞的那一行」。**
  **这条路能走通,但代价是「每加一条写入路径就多一个触发器」**:
  **插入、更新、删除各一个,还要自己保证 `where` 条件对得上。**
  **所以实际项目里更常见的做法是「视图只读,写直接写基表」**——**少一层间接,少一类 bug。**

  建视图时给列改个名字
  SQL> SELECT 编号, 姓名 FROM v_c LIMIT 2
  编号     姓名
  --------------
  1        张伟
  4        刘洋
  (2 行)
  **`create view 视图名 (列名, 列名, …) as 查询`**——**列名按顺序对应,个数必须一致。**
  **这个写法在「查询里有重复列名」或「计算列没名字」时几乎是必须的**:
  **`select a.id, b.id` 两个都叫 `id`,不起名根本没法引用。**
  **所以多表视图里起别名是常规动作,不是可选项。**

第一段六行:男生六个。 第二段改基表,视图里张伟身高立刻变 199.90。 第三段三条写操作全报错,cannot modify … because it is a view。 第四段挂触发器之后插入成功,数据在 students 里能查到。 第五段给列改名,select 编号, 姓名 能直接用。

30 秒实验

  1. CREATE VIEW v AS SELECT * FROM students 之后改了一个学生的名字, v 里看到的是新名字还是旧的?
  2. INSERT INTO 视图 在本沙箱会怎样?MySQL 呢?
  3. 为什么「用视图优化性能」这个想法是错的?

常见误区

  1. 把视图当表改。 本沙箱一律只读,MySQL 只有简单视图能写。写之前先确认目标库的行为。
  2. 指望视图缓存结果、加速查询。 它每次重新跑,复杂视图只会更慢。要加速得建索引或物化。
  3. 多表视图不起列别名。 两个 id 撞在一起,外面引用时根本分不清哪个是哪个。
自测三问(点开对答案) 1. 视图里存的是什么? —— 一条 `select` 语句。视图本身不存数据,每次查它都重新执行那条查询。 2. 为什么改基表之后视图立刻变了? —— 因为视图没有自己的数据,每次查询都在重新读基表。 3. 本沙箱的视图能写吗?MySQL 呢? —— 本沙箱一律只读,要写只能挂 `INSTEAD OF` 触发器。 MySQL 的简单视图(无聚合、无 distinct、无 union)可写,复杂视图只读。

视图建好了,下一篇看怎么查它:11.2 查看视图——它也只是系统表里的一条记录。

相关笔记