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

建视图就一句 CREATE VIEW 名字 AS SELECT ...,没有别的必填项。 IF NOT EXISTS 是「已存在就跳过」,不是「覆盖」,这个名字容易误会。 OR REPLACE、ALGORITHM、WITH CHECK OPTION 三个选项沙箱全没有,但都得知道。

📝 本章和前面几章的关系

12.2 讲三件事:权限、怎么建、注意事项。这一篇按「能跑的先跑,跑不了的贴原码」走。 上一篇证明了视图是「存下来的 SELECT」,这一篇就是怎么把这句话存进去。 重点在两个地方:ALGORITHM 影响性能,WITH CHECK OPTION 影响写入安全。

一、最小写法

一句就够,没有必填选项:

CREATE VIEW v1 AS SELECT id, name, price FROM tb_goods WHERE stock < 50;

跑出来四行:电钻、角磨机、交换机、测线仪,库存都低于 50。 WHERE 写在视图里,以后查它就不用再写条件。 IF NOT EXISTS 可以加,但它的意思是「已存在就算了」。 重名时不报错,也不改原视图——它不是「覆盖」,是「跳过」。

二、列名要不要唯一:两边不一样

教材的注意事项里有一条:视图的列名不能重复。 这句话在 MySQL 里是对的,在沙箱里不成立。

环境 两个 id 的视图
沙箱 能建,第二列自动改名 id:1
MySQL 报 Duplicate column name 'id'

沙箱不但不拦,还帮你把第二列改名成 id:1,好让两列区分开。 看起来贴心,其实是在埋雷:按 id 取值的地方只能拿到第一列。 所以这条规矩仍然要守:两个同名列各起一个别名。 视图的列名就是它的接口,接口别留重名。

三、改视图:OR REPLACE 沙箱没有

MySQL 可以一句替换掉旧定义:

CREATE OR REPLACE VIEW v1 AS
SELECT id, name, price, stock FROM tb_goods;

沙箱报 near "OR": syntax error——SQLite 没有这个关键字。 改视图只能两步:先 DROP VIEW,再 CREATE VIEW。 两步之间有一个空档,这时候查这张视图会报「没有这张表」。 MySQL 那一句是原子的,中间不出现空档,这就是它存在的理由。 线上改视图这件事,原子和不原子差别很大:前者用户察觉不到,后者可能有一瞬间的报错。

四、ALGORITHM:合并还是临时表

MySQL 建视图时可以指定算法,这个选项影响性能,教材讲得比较简。

取值 意思
MERGE 视图定义展开进外层 SQL
TEMPTABLE 先算进临时表
UNDEFINED 让优化器自己选

MERGE 等于「把视图那句话原样拼到外层」,所以外层的条件能推进去。 TEMPTABLE 等于「先算完再说」,外层的条件只能筛临时表。 这个选项的价值只有一条:外层条件能不能推进去,决定它到底要读多少行。 沙箱没有这个选项,但它实际上按 MERGE 走,证据就是计划一模一样。 反例也能造出来:子查询里带 ORDER BY 时,计划里多出一步 USE TEMP B-TREE。

SCAN tb_goods
USE TEMP B-TREE FOR ORDER BY

这两行就是「临时表」的影子:优化器没法把带排序的子查询完全展开,得先排一遍。 所以想让 MERGE 生效,视图里别写 ORDER BY——顺序留给外层决定。 这也是教材那条「视图里不要用 ORDER BY」的真正原因。

五、WITH CHECK OPTION:管住能不能改

先想一个问题:往 WHERE price >= 100 的视图里插一条 price = 5 的记录,会怎样? 插得进去,但插完在视图里看不见——因为它不满足条件,等于凭空消失。

CREATE VIEW v3 AS SELECT id, price FROM tb_goods WHERE price >= 100
WITH CHECK OPTION;

加上这个选项,这种插入会被直接拒绝。 它保证「写进去的东西一定还看得见」,视图的可见范围和可写范围就对齐了。 沙箱报 near "WITH": syntax error,没有这个能力。 而且沙箱连视图都不许改,所以这个问题在沙箱里根本不会出现。 要记住的是概念:视图的「可见范围」和「可写范围」是两件可以不一致的事。

六、权限:沙箱没有这一层

建视图本身是一项权限,查视图还要两个权限。

要做什么 MySQL 要的权限
建视图 CREATE VIEW
查视图 视图的 SELECT + 基表的 SELECT
改视图数据 基表的 INSERT/UPDATE/DELETE
删视图 视图的 DROP

第二行值得琢磨:查视图要两个权限。 因为视图只是个壳子,真正的数据在基表里,两边都得放行。 还有一个 DEFINER:视图记着「是谁建的」,默认按建的人的身份去查基表。 所以建视图的账号一被删,视图可能整个失效——这是运维上真实会踩的坑。 沙箱没有账号体系,这一整节都只能贴原码。 能演示的只有一句:没有权限这一层,视图就纯粹是「查询的别名」。

七、动手 Demo:一句建视图,三个沙箱没有的选项

这一段从最小写法开始,依次试 IF NOT EXISTS、重名列、OR REPLACE、 ALGORITHM 与 WITH CHECK OPTION。凡是沙箱没有的,都把报错原文打出来,再贴 MySQL 原码。

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

# ① 最小写法,加 IF NOT EXISTS 不报错也不改原视图
c.execute(u"CREATE VIEW v1 AS SELECT id, name, price "
          u"FROM tb_goods WHERE stock < 50")
c.execute(u"CREATE VIEW IF NOT EXISTS v1 AS SELECT id FROM tb_goods")

# ② 两列同名:沙箱不拦,自动改名
c.execute(u"CREATE VIEW v_join AS SELECT g.id, c.id, g.name "
          u"FROM tb_goods g JOIN tb_cat c ON c.id=g.cat_id")
print(c.execute(u"PRAGMA table_info(v_join)").fetchall())

# ③ CREATE OR REPLACE:沙箱没有
try:
    c.execute(u"CREATE OR REPLACE VIEW v1 AS SELECT id FROM tb_goods")
except sqlite3.OperationalError as e:
    print(e)

# ④ 带 ORDER BY 的子查询会多出一步临时表
print(c.execute(u"EXPLAIN QUERY PLAN SELECT * FROM "
                u"(SELECT id FROM tb_goods ORDER BY price)")
        .fetchall())

# ⑤ WITH CHECK OPTION:沙箱没有
try:
    c.execute(u"CREATE VIEW v3 AS SELECT id, price FROM tb_goods "
              u"WHERE price >= 100 WITH CHECK OPTION")
except sqlite3.OperationalError as e:
    print(e)

实跑输出:

--------------------------------------------------------------------
第五十五课:建视图的规矩,和三个沙箱没有的选项
--------------------------------------------------------------------

  **教材 12.2 讲三件事:权限、怎么建、注意事项。**
  **这一课按「能跑的先跑,跑不了的贴原码」的顺序走。**
  **中间三个选项(`OR REPLACE`、`ALGORITHM`、`WITH CHECK OPTION`)沙箱全不支持,**
  **但它们是教材的重点,所以逐个说清「它解决什么问题」。**

  一、最小写法
  --------------------------------------------------------------------

  mysql> CREATE VIEW v1 AS SELECT id, name, price FROM tb_goods WHERE stock < 50

  mysql> SELECT * FROM v1
  id name   price
  ---------------
  2  电钻   380
  3  角磨机 260
  9  交换机 680
  10 测线仪 150
  4 rows in set
  **一句 `CREATE VIEW 名字 AS SELECT ...` 就完了,没有别的必填项。**
  **`IF NOT EXISTS` 可以加,作用是不让重名报错:**

  mysql> CREATE VIEW IF NOT EXISTS v1 AS SELECT id FROM tb_goods
  **加上之后不报错,也不改原视图——它只是「已存在就算了」。**
  **这个名字容易误会:它不是「覆盖」,是「跳过」。**

  二、列名要不要唯一:两边不一样
  --------------------------------------------------------------------
  **两张表都有 `id`。教材说这样会报「列名重复」,沙箱却建成功了:**
  CREATE VIEW v_join AS
  SELECT g.id, c.id, g.name
  FROM tb_goods g JOIN tb_cat c ON c.id=g.cat_id

  mysql> SELECT * FROM v_join LIMIT 3
  id id:1 name
  --------------
  1  1    螺丝刀
  2  1    电钻
  3  1    角磨机
  3 rows in set

  mysql> PRAGMA table_info(v_join)
  cid name type    notnull dflt_value pk
  --------------------------------------
  0   id   INTEGER 0       NULL       0
  1   id:1 INTEGER 0       NULL       0
  2   name TEXT    0       NULL       0
  3 rows in set
  **不但建成了,还能查——而且它自动把第二列改名成 `id:1`,好让两列区分开。**
  **`PRAGMA table_info` 里也是 `id` 和 `id:1` 两行,一样没拦。**

  [教材原码 · 本沙箱不执行]
  | CREATE VIEW v_join AS
  | SELECT g.id, c.id, g.name FROM tb_goods g
  | JOIN tb_cat c ON c.id = g.cat_id;
  教材原码,MySQL 到这一句直接报 Duplicate column name 'id'

  **所以这条「注意事项」在沙箱里根本不会触发,但它仍然值得守。**
  **因为「按列名取值」的地方遇到重名一定出问题:应用代码取 `row['id']` 只能拿到一个。**
  **解法是给其中一个起别名,或者干脆两个都起:**

  mysql> DROP VIEW v_join
  CREATE VIEW v_join AS
  SELECT g.id AS gid, c.id AS cid, g.name
  FROM tb_goods g JOIN tb_cat c ON c.id=g.cat_id

  mysql> SELECT * FROM v_join LIMIT 3
  gid cid name
  --------------
  1   1   螺丝刀
  2   1   电钻
  3   1   角磨机
  3 rows in set
  **视图的列名就是它的接口,接口别留重名。**
  **沙箱不拦你,是在帮你埋一个到上层才爆的雷。**

  三、改视图:OR REPLACE 沙箱没有
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | CREATE OR REPLACE VIEW v1 AS
  | SELECT id, name, price, stock FROM tb_goods;
  教材原码,MySQL 用一句就替换掉旧定义

  **沙箱报的是 `near "OR": syntax error`。**
  **SQLite 没有 `OR REPLACE`,改视图只能两步:先 `DROP VIEW`,再 `CREATE VIEW`。**
  **两步之间有一个空档,这时候查这张视图会报「没有这个视图」。**
  **MySQL 那一句是原子的,中间不会出现空档,这就是它存在的理由。**
  **教材把它放在「注意事项」里,其实它是个挺实用的差别。**

  四、ALGORITHM:合并还是临时表
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | CREATE ALGORITHM = MERGE VIEW v2 AS
  | SELECT id, name FROM tb_goods WHERE price >= 100;
  教材原码,MERGE 表示把视图展开进外层查询


  取值         意思
  ---------------------------------------
  `MERGE`      把视图的定义展开进外层 SQL
  `TEMPTABLE`  先把结果算进一张临时表
  `UNDEFINED`  让优化器自己选(默认)
  **`MERGE` 等于「把视图那句话原样拼到外层」,所以外层的条件能推进去。**
  **`TEMPTABLE` 等于「先算完再说」,外层条件只能筛临时表,推不进去。**
  **沙箱没有这个选项,但它实际上总是按 `MERGE` 走的。**
  **证据就是上一课那张计划表:查视图和直接查基表,计划一模一样。**

  计划里的两步
  ----------------------------
  SCAN tb_goods
  USE TEMP B-TREE FOR ORDER BY
  **第二行 `USE TEMP B-TREE FOR ORDER BY` 就是「临时表」的影子。**
  **只要子查询里带了 `ORDER BY`,优化器就没法把它完全展开,得先排一遍。**
  **所以想让 `MERGE` 生效,视图里别写 `ORDER BY`——顺序留给外层决定。**
  **这也是教材「注意事项」里那条「视图里不要用 ORDER BY」的真正原因。**

  五、WITH CHECK OPTION:管住能不能改
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | CREATE VIEW v3 AS SELECT id, price FROM tb_goods WHERE price >= 100
  | WITH CHECK OPTION;
  教材原码,MySQL 保证改完的数据还在视图范围内

  **沙箱报的是 `near "WITH": syntax error`。**
  **这个选项解决的问题是:往视图里插一条 `price = 5` 的记录,插进去之后**
  **它在视图里看不见(不满足 `price >= 100`),等于凭空消失。**
  **加上这个选项,这种插入会被直接拒绝。**
  **沙箱连视图都不让改,所以这个问题在沙箱里根本不会出现。**
  **这一条要记住的是概念:视图的「可见范围」和「可写范围」是两件事。**

  六、权限:沙箱没有这一层
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | GRANT CREATE VIEW ON db.* TO 'dev'@'localhost';
  教材原码,建视图本身是一项权限


  要做什么    MySQL 要的权限
  -----------------------------------------------
  建视图      `CREATE VIEW`
  查视图      视图的 `SELECT` + 基表的 `SELECT`
  改视图数据  基表的 `INSERT`/`UPDATE`/`DELETE`
  删视图      视图的 `DROP`
  **第二行值得琢磨:查视图要两个权限,视图自己的和基表的。**
  **因为视图只是个壳子,真正的数据在基表里。**
  **还有一个 `DEFINER` 的概念:视图记着「是谁建的」,默认按建的人的身份去查基表。**
  **所以建视图的账号一旦被删,视图可能整个失效——这是运维上真实会踩的坑。**
  **沙箱没有账号体系,这些一条都演示不了。**
  **能演示的只有一句:没有权限这一层,视图就纯粹是「查询的别名」。**

第一组两条语句都不报错,但 v1 还是原来那句——IF NOT EXISTS 是跳过,不是覆盖。 第二组 PRAGMA table_info 打出三行:id、id:1、name。 重名不但没被拦,还被引擎自动改了个名字。 第三组和第五组都是语法错误:near "OR" 和 near "WITH"。 这两条说明沙箱缺的是关键字,不是能力——它连这个概念都没有。 第四组的计划里有 USE TEMP B-TREE FOR ORDER BY,这就是「临时表」的样子。 ALGORITHM 三种取值的差别,在沙箱里就靠这一行体现。

30 秒实验

  1. 把 IF NOT EXISTS 去掉再跑一次,看重名报什么错。
  2. 把 v_join 里两个 id 都起别名,再看 PRAGMA table_info 的输出。
  3. 把子查询的 ORDER BY 去掉,看 USE TEMP B-TREE 那一行还在不在。

常见误区

  1. 把 IF NOT EXISTS 当成 OR REPLACE 用,改了半天发现视图根本没变。
  2. 在视图里写两个同名列,沙箱能建就以为没问题,上层取值只拿到一个。
  3. 视图里写 ORDER BY 想让结果有序,结果多了一次排序、外层还不一定保序。
自测三问(点开对答案) 1. `CREATE VIEW IF NOT EXISTS` 和 `CREATE OR REPLACE VIEW` 差在哪? —— 前者是「已存在就跳过」,原视图一个字不改;后者是「存在就替换」。 沙箱只有前者,要替换得先 `DROP` 再 `CREATE`。 2. `ALGORITHM=MERGE` 和 `TEMPTABLE` 的实际差别是什么? —— `MERGE` 把视图定义展开进外层 SQL,外层的条件能推进去; `TEMPTABLE` 先把结果算进临时表,外层条件只能筛临时表,通常更慢。 3. `WITH CHECK OPTION` 解决什么问题? —— 防止把「写进去之后在视图里看不见」的行插进去。 它让视图的可写范围和可见范围保持一致,越界的写入直接拒绝。

视图建好了,下一篇讲怎么查定义、怎么改、怎么删,以及为什么沙箱里一个视图都改不了数据。

相关笔记