视图是一句存下来的 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 秒实验
- 把视图的定义改成
WHERE price < 50,看条数怎么变。 - 把
v_all的定义改成写全列名,再加一列,看视图的列数还变不变。 - 删掉
tb_goods里的一行,看视图里的行数跟着少不少。
常见误区
- 以为建了视图查询就会变快,其实执行计划和直接查基表一模一样。
- 以为视图会「缓存」结果,改完基表还在奇怪为什么视图已经变了。
- 用
SELECT *建视图,基表加列之后视图悄悄多一列,应用取列取错位。
自测三问(点开对答案)
1. 怎么用一句话证明视图不存数据? —— 改一行基表,立刻查视图,结果就变了。如果它存了结果,就得等一次刷新才会变。 再补一条:`dbstat` 里查不到视图占的页。 2. 视图能加速查询吗? —— 不能。它只是把一句 SELECT 换个名字,执行计划和直接写那句 SELECT 一模一样。要加速得靠索引。 3. 为什么建视图不建议写 `SELECT *`? —— 因为列会随基表变:沙箱里每次查询展开 `*`,基表加列视图就多一列; MySQL 里建时定死,新列永远看不见。写全列名两种麻烦都没有。知道视图是什么了,下一篇讲怎么建:一句 CREATE VIEW,外加三个沙箱没有的选项。
- ☐ (本篇完,下一篇 12.2 创建视图)