🛢️ 第12章 视图
📄 一句话概括

视图是一句存下来的 SELECT,它一个字节的数据都不存。 所以「视图能加速查询」这个想法是错的,它省的是打字,不是计算。 它真正的用处有三个:包住长查询、只露一部分列、挡住基表改动。

📝 本章和前面几章的关系

12.1 是视图的导言,教材只讲概念和作用两小节。这一篇把「不存数据」用三种办法证一遍。 前面第 11 章讲索引,那是「让查变快」;这一篇讲视图,是「让写变短」。 两件事常被混在一起,其实一个是性能,一个是可读性。 第 12 章三篇:这一篇讲是什么,12.2 讲怎么建,12.3 讲怎么改和怎么删。

一、改基表,视图跟着变

先看一个会变的事实:改基表的数据,视图里的结果立刻跟着变。 没有谁去「刷新」它,因为每次查视图都在重跑那句 SELECT。

时机 价格 ≥ 100 的条数
建视图后 5 条
把一件从 260 改成 99 后 4 条

这一条就足以证明它没存数据。 如果存了,改基表之后视图里还会是旧值,得等一次刷新才同步。 「物化视图」才是存结果的那种,MySQL 官方版没有,得靠定时任务或第三方工具做。 普通视图和它的区别就一句话:普通视图每次重算,物化视图存着但要刷新。

二、它占多少地方:零

dbstat 能按对象统计占用,视图在它的统计里一条都没有。

dbstat 列出的对象
sqlite_schema
tb_cat
tb_goods

两张表加一份 schema,就这些。视图没有自己的页,占用就是零。 它只在 sqlite_schema 里留一行记录,存着那句 SELECT 的原文。

三、它就是一句存下来的 SELECT

sqlite_master 的 sql 字段里放着的就是定义原文:

CREATE VIEW v_expensive AS
SELECT id, name, price
FROM tb_goods
WHERE price >= 100

所以「查视图」和「把那句 SELECT 抄一遍」在执行层面是一回事。 两边的执行计划完全一样,都是 SCAN tb_goods。

写法 执行计划
直接查基表 SCAN tb_goods
查视图 SCAN tb_goods

计划一样,说明视图没有被提前算好。 要加速还得靠索引,那是第 11 章的事,视图一点忙都帮不上。

四、作用一:把长查询包起来

视图最常见的用法就是这个:一句三表连接太长,包成视图,以后只写一句。 同一个结果,两次查询的行数一模一样,区别只在长短。

CREATE VIEW v_goods_cat AS
SELECT g.id, g.name AS goods, c.name AS cat, g.price
FROM tb_goods g JOIN tb_cat c ON c.id = g.cat_id

注意这里给两个 name 都起了别名:goods 和 cat。 不起的话视图会有两个同名列,上层取值就分不清了。 视图还能套视图:再建一个只留网络类的,底下还是那两句 SELECT 拼在一起。 套得越深越难调,三层以上就该考虑换个写法。

五、作用二:只露一部分列

视图里没放进去的列,外面就查不到。这是最朴素的一种「列级安全」。

写法 结果
SELECT cost FROM v_expensive 报 no such column: cost
SELECT * FROM v_expensive 只有 id/name/price

报的是「没有这一列」,不是「权限不足」——因为这一列根本不在视图的接口里。 想给某个角色只看部分列,建个视图就够了,不用改表结构。 MySQL 还能再往前一步:给视图授权,基表一点不开放。 沙箱没有账号体系,这一层演示不了,只能靠「不放进去」。

六、作用三:挡住基表变化?一半对

教材说视图能「屏蔽基表结构变化」,这句话对一半。 关键在于视图里写没写 SELECT *。

时机 SELECT * 建的视图有几列
加列之前 6 列
基表加一列之后 7 列

沙箱这里视图跟着多了一列,因为 SQLite 每次查询时才展开那个 *。 MySQL 相反:建视图时就把列定死,之后加列视图不变。 两种做法各有麻烦:跟着变,视图的列会悄悄变多;定死,新列永远看不见。 所以两边都有同一条建议:建视图别写 SELECT *,把列名一个个写出来。 写清列名之后,基表加列不影响视图——这才是「挡住变化」的正确用法。

七、动手 Demo:三种办法证明视图不存数据

这一段建一张商品表和一张类别表,然后依次证「视图不存数据」:先改基表看视图跟着变, 再查 dbstat 看它占零字节,最后比执行计划看有没有被提前算好。

import sqlite3
c = sqlite3.connect(u":memory:")

# ① 改基表,视图跟着变
c.execute(u"CREATE VIEW v_expensive AS SELECT id, name, price "
          u"FROM tb_goods WHERE price >= 100")
print(c.execute(u"SELECT * FROM v_expensive").fetchall())
c.execute(u"UPDATE tb_goods SET price = 99 WHERE id = 3")
print(c.execute(u"SELECT * FROM v_expensive").fetchall())

# ② 占多少地方:dbstat 里没有视图
print(c.execute(u"SELECT name FROM dbstat GROUP BY name").fetchall())

# ③ 它就是一句存下来的 SELECT
print(c.execute(u"SELECT sql FROM sqlite_master "
                u"WHERE name = 'v_expensive'").fetchone()[0])
q = u"SELECT id, name FROM tb_goods WHERE price >= 100"
print(c.execute(u"EXPLAIN QUERY PLAN " + q).fetchone()[-1])
print(c.execute(u"EXPLAIN QUERY PLAN SELECT * FROM v_expensive")
        .fetchone()[-1])

# ④ 只露一部分列:视图里没有的列,外面查不到
print(c.execute(u"PRAGMA table_info(v_expensive)").fetchall())

# ⑤ 加一列,看视图的列跟着变不变
c.execute(u"CREATE VIEW v_all AS SELECT * FROM tb_goods")
print(len(c.execute(u"PRAGMA table_info(v_all)").fetchall()))
c.execute(u"ALTER TABLE tb_goods ADD COLUMN remark TEXT")
print(len(c.execute(u"PRAGMA table_info(v_all)").fetchall()))

实跑输出:

--------------------------------------------------------------------
第五十四课:视图里一个字节的数据都没有
--------------------------------------------------------------------

  **教材 12.1 一句话就讲完了:视图是一张虚拟表,它不存数据。**
  **这一课把「不存数据」这四个字用三种办法证一遍。**
  **证完之后再讲三个作用,每个都用真查询跑出来,不靠描述。**

  一、改基表,视图跟着变
  --------------------------------------------------------------------

  mysql> CREATE VIEW v_expensive AS SELECT id, name, price FROM tb_goods WHERE price >= 100

  mysql> SELECT * FROM v_expensive
  id name   price
  ---------------
  2  电钻   380
  3  角磨机 260
  6  安全帽 120
  9  交换机 680
  10 测线仪 150
  5 rows in set
  **这是价格一百以上的 5 条,从 680 元到 120 元。**

  mysql> UPDATE tb_goods SET price = 99 WHERE id = 3

  mysql> SELECT * FROM v_expensive
  id name   price
  ---------------
  2  电钻   380
  6  安全帽 120
  9  交换机 680
  10 测线仪 150
  4 rows in set
  **把角磨机从 260 元改成 99 元,视图里它就消失了。**
  **没有谁去刷新这个视图,它每次查都重跑一遍那句 SELECT。**

  二、它占多少地方:零
  --------------------------------------------------------------------

  库里的对象(dbstat 统计)
  -------------------------
  sqlite_schema
  tb_cat
  tb_goods
  **`dbstat` 只列出两张表和一份 schema,视图一条都没有。**
  **因为它没有自己的页,占的字节数就是零。**
  **这是视图和「物化视图」的根本区别。**
  **物化视图真的把结果存下来,查得快、但要定时刷新;普通视图不存,每次重算。**

  三、它就是一句存下来的 SELECT
  --------------------------------------------------------------------
  **`sqlite_master` 里存的是这句话的原文:**
  CREATE VIEW v_expensive AS
  SELECT id, name, price
  FROM tb_goods
  WHERE price >= 100

  写法        执行计划
  -------------------------
  直接查基表  SCAN tb_goods
  查视图      SCAN tb_goods
  **两个计划一模一样,说明视图没有被「提前算好」,只是把那句 SELECT 换个名字。**
  **所以「视图能不能加速查询」这个问题,答案是:不能。**
  **它省的是你敲键盘的次数,不是数据库的工作量。**
  **要加速还得靠索引,那是上一章的事。**

  四、作用一:把长查询包起来
  --------------------------------------------------------------------

  mysql> SELECT g.name, c.name FROM tb_goods g JOIN tb_cat c ON c.id=g.cat_id WHERE g.price>=100
  name   name
  -----------
  电钻   五金
  安全帽 劳保
  交换机 网络
  测线仪 网络
  4 rows in set
  **把刚才那句包成视图:**
  CREATE VIEW v_goods_cat AS
  SELECT g.id, g.name AS goods, c.name AS cat, g.price
  FROM tb_goods g JOIN tb_cat c ON c.id = g.cat_id

  mysql> SELECT goods, cat, price FROM v_goods_cat WHERE price>=100
  goods  cat  price
  -----------------
  电钻   五金 380
  安全帽 劳保 120
  交换机 网络 680
  测线仪 网络 150
  4 rows in set
  **同样是四行,后者短了一大截。**
  **查询越长,写错的概率越高,视图就是把这个风险一次性消掉。**
  **视图还能套视图:再建一个只留网络类的:**

  mysql> CREATE VIEW v_net AS SELECT * FROM v_goods_cat WHERE cat = '网络'

  mysql> SELECT * FROM v_net
  id goods  cat  price
  --------------------
  7  网线   网络 3
  8  水晶头 网络 1.5
  9  交换机 网络 680
  10 测线仪 网络 150
  4 rows in set
  **两层视图,底下还是那两句 SELECT 拼在一起。**
  **套得越深越难调,三层以上就该考虑换个写法了。**

  五、作用二:只露一部分列
  --------------------------------------------------------------------
  **`v_expensive` 里没有 `cost`(进价)这一列,外面就查不到它。**

  mysql> SELECT cost FROM v_expensive
  ERROR 1064 (42000): no such column: cost
  **报的是 `no such column: cost`——不是权限不足,是这一列根本不在视图里。**
  **这就是「列级安全」最朴素的实现:不给看,就不放进去。**

  [教材原码 · 本沙箱不执行]
  | GRANT SELECT ON db.v_expensive TO 'shop'@'%';
  教材原码,MySQL 用授权把视图开放给指定账号

  **MySQL 这一层做得更细:视图本身可以只给某个账号 SELECT,基表一点都不开放。**
  **沙箱没有账号和授权这套东西,只能靠「不放进去」这一招。**

  六、作用三:挡住基表变化?一半对
  --------------------------------------------------------------------

  mysql> CREATE VIEW v_all AS SELECT * FROM tb_goods

  时机      视图的列
  ------------------------------------------------------
  加列之前  id、name、cat_id、price、cost、stock
  加列之后  id、name、cat_id、price、cost、stock、remark
  **视图的列跟着多了一列——因为 SQLite 每次查询时才展开那个 `*`。**
  **这一点和 MySQL 相反:MySQL 建视图时就把列定死,之后加列视图不会变。**
  **两种做法各有麻烦:跟着变,视图的列会悄悄变多;定死,新列永远看不见。**
  **所以两边都有一条同样的建议:建视图别写 `SELECT *`,把列名一个个写出来。**
  **写清楚列名之后,基表加列不影响视图,这才是「挡住变化」的正确用法。**

第一组两次查询:改基表之前五条,把一件商品的价钱改到 99 之后剩四条。 视图没有刷新动作,它每次查都在重跑那句 SELECT。 第二组只有三个对象名,视图不在里面。 这一组是「零字节」最直接的一条证据,比讲原理管用。 第三组四行:先是视图的定义原文,再是两个执行计划。 两个计划一模一样,说明视图没被物化。 第五组两个数是 6 和 7,说明沙箱的 SELECT * 在视图里每次展开。 这一处和 MySQL 相反,正文专门说了。

30 秒实验

  1. 把视图的定义改成 WHERE price < 50,看条数怎么变。
  2. 把 v_all 的定义改成写全列名,再加一列,看视图的列数还变不变。
  3. 删掉 tb_goods 里的一行,看视图里的行数跟着少不少。

常见误区

  1. 以为建了视图查询就会变快,其实执行计划和直接查基表一模一样。
  2. 以为视图会「缓存」结果,改完基表还在奇怪为什么视图已经变了。
  3. 用 SELECT * 建视图,基表加列之后视图悄悄多一列,应用取列取错位。
自测三问(点开对答案) 1. 怎么用一句话证明视图不存数据? —— 改一行基表,立刻查视图,结果就变了。如果它存了结果,就得等一次刷新才会变。 再补一条:`dbstat` 里查不到视图占的页。 2. 视图能加速查询吗? —— 不能。它只是把一句 SELECT 换个名字,执行计划和直接写那句 SELECT 一模一样。要加速得靠索引。 3. 为什么建视图不建议写 `SELECT *`? —— 因为列会随基表变:沙箱里每次查询展开 `*`,基表加列视图就多一列; MySQL 里建时定死,新列永远看不见。写全列名两种麻烦都没有。

知道视图是什么了,下一篇讲怎么建:一句 CREATE VIEW,外加三个沙箱没有的选项。

相关笔记