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

视图不存数据,只存一句 SELECT——它是「看数据的一个窗口」,不是表的副本。 所以改基表,视图立刻变;它没有「刷新」这一步,每次查都去问基表。 视图在编译时就被展开成基表查询,实测查询耗时跟直接查基表一样,不会因为多一层而变慢。

📝 本章和前面几章的关系

第 8、9 章讲的是「怎么查」, 从这一章开始讲「怎么把查询存起来复用」——这就是视图。 它属于第三篇「高级应用」的第一站。 教材 10.1 分三小节:视图的概念、视图的分类、视图与表的区别。 三节都在讲「它是什么」,没有一句 SQL,但这一节想明白了,后面两节会顺很多。 演示里建了一个「在售商品」视图,然后改了基表的数据,看视图跟着变; 最后在 20 万行的表上比了一次「查视图」和「查基表」的耗时——两个数基本一样。

一、视图的概念:一句存下来的查询

报表要反复看「在售商品的名字、单价、库存」。 每次都写一遍 WHERE is_on = 1 很啰嗦, 写错了还没人发现——那就把它存下来,起个名字。

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

建完之后,v_on_sale 用起来跟表一模一样: 能 SELECT、能 WHERE、能 ORDER BY, 还能跟别的表 JOIN。 但库里存的不是数据,是那句 SELECT 的原文。 演示里查了数据字典:type 是 view, sql 那列里躺着完整的 CREATE VIEW 语句,一行数据都没有。 所以「视图」这个名字有点误导。 它不是「另一个视角的表」, 它是「一条存起来备用的查询」——每次查它,数据库就把那句 SQL 现展开一遍。

二、视图不存数据:改基表,视图立刻变

这一点最能说明视图的本质。 演示里把 1006 号打印机下架(is_on 从 1 改成 0), 再数一次视图里的行数:从 5 变成 4。 没有「刷新」这一步,也不需要。 因为视图本来就没有自己的一份数据可刷新—— 它每次被查,都去问基表要。 反过来说,视图也保护不了数据。 有人直接改基表,视图里的结果立刻就变了, 视图挡不住——它只是个窗口,窗口挡不住屋里的人搬东西。 这一点跟「物化视图」正好相反。 物化视图(SQL Server 里叫索引视图)是真把结果存下来的, 查得快,但要手动或定期刷新,而且刷新之前看到的是旧数据。 两个东西别混。

三、视图与表的区别

对比项 表 视图
存什么 存数据(行和列) 只存一句 SELECT
占多少空间 随数据量涨 几乎为 0
数据从哪来 自己就是源头 全部来自基表
基表变了 跟它无关 下次查立刻反映
能不能加索引 能 普通视图不能(索引视图另算)

第一行是根,后面四行都是从它推出来的。 不存数据,所以不占空间、所以数据只能来自基表、 所以基表一变它就变、所以没法在自己的数据上建索引。 「能不能加索引」这一条最容易被忽略。 有人以为给视图建索引能加速, 结果发现普通视图根本建不了——要建只能建成索引视图,而那是另一回事(真存数据、要维护)。

四、视图的分类

分法 类型 说明
按用途 常规视图 一条 SELECT,最常见
索引视图 真存数据、能建索引,要手动维护
分区视图 把多张结构相同的表 UNION 起来当一张看
按来源 单表视图 只查一张表,通常可更新
多表视图 JOIN 出来的,一般只读

日常用的 99% 是常规视图。 索引视图和分区视图属于「有特殊性能或规模需求时才考虑」的东西, 而且各有各的维护成本——索引视图要随基表变化重建,分区视图要把 UNION 写全。 「可更新」是 SQL Server 的一个特性。 单表、不含聚合和 DISTINCT 的视图, 对它 INSERT/UPDATE,会真的落到基表上。 10.2 会演示这回事。

五、查视图会不会更慢:实测

很多人担心「多套一层会不会变慢」。 在 20 万行的表上测一次就知道了:

写法(20 万行) 最快一次
直接查基表,条件写全 19.68 ms
查视图,条件藏在视图里 18.33 ms

两个数基本一样,差的那 1 毫秒在这个量级上就是噪声(后台进程来一下就是这个数)。 原因是视图在编译时就被展开成基表查询了。 它是个「文本替换」, 不是一层真的中间结果——数据库不会先算完视图再查它。 所以别怕用视图。 该担心的是「视图里那句 SQL 本身写得差不差」, 而不是「多了一层」。 视图里的 WHERE 没走索引,直接查基表一样慢。

六、动手 Demo:看视图的定义、改基表看视图跟着变、比一次耗时

这一段先建一个在售商品视图,然后去数据字典里翻它的定义——看看到库存了什么。 接着把一件商品下架,再数视图里的行数;最后在 20 万行的表上比「查视图」和「查基表」的耗时。

import sqlite3, time

c = sqlite3.connect(u":memory:")
# ... 建六表、装样例数据 ...
c.execute(u"CREATE VIEW v_on_sale AS SELECT id, gname, price, stock "
          u"FROM tb_goods WHERE is_on = 1")

# 视图里到底存了什么
print(c.execute(u"SELECT type, name, substr(sql, 1, 34) "
                u"FROM sqlite_master WHERE name = 'v_on_sale'").fetchall())

# 改基表,视图立刻变
print(c.execute(u"SELECT COUNT(*) FROM v_on_sale").fetchone())
c.execute(u"UPDATE tb_goods SET is_on = 0 WHERE id = 1006")
print(c.execute(u"SELECT COUNT(*) FROM v_on_sale").fetchone())

# 20 万行上比一次耗时
c.execute(u"CREATE VIEW v_big AS SELECT id, gname, price "
          u"FROM big WHERE is_on = 1")
print(clock(u"SELECT COUNT(*) FROM big WHERE is_on = 1"))
print(clock(u"SELECT COUNT(*) FROM v_big"))

实跑输出:

--------------------------------------------------------------------
视图是什么:不存数据的一条查询
--------------------------------------------------------------------


  ① 一个视图,两副面孔
  --------------------------------------------------------------------
  需求:报表要反复看「在售商品的名字、单价、库存」,但不想每次都写一遍条件。

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

  1> SELECT * FROM v_on_sale
  id    gname       price  stock
  ------------------------------
  1001  联想笔记本  4999   30
  1002  戴尔显示器  1299   50
  1003  罗技鼠标    89.9   200
  1004  机械键盘    399    80
  1006  打印机      899    12
  (5 行受影响)
  用起来跟表一模一样:能 SELECT、能 WHERE、能 ORDER BY,还能跟别的表 JOIN。
  但库里到底存了什么?看数据字典:

  1> SELECT type AS 类型, name AS 名字, substr(sql, 1, 34) || ' ...' AS 定义的前半截
     FROM sqlite_master
     WHERE name = 'v_on_sale'
  类型  名字       定义的前半截
  -------------------------------------------------------
  view  v_on_sale  CREATE VIEW v_on_sale AS SELECT id ...
  (1 行受影响)
  ! 存的是那句 SELECT 的原文,一行数据都没有。视图是个「存下来的查询」。
  SQL Server 里这句查的是 sys.sql_modules 或者 OBJECT_DEFINITION(),同样只有定义。

  ② 视图不存数据:改基表,视图立刻变
  --------------------------------------------------------------------

  1> SELECT COUNT(*) AS 行数 FROM v_on_sale
  行数
  ----
  5
  (1 行受影响)

  1> UPDATE tb_goods SET is_on = 0 WHERE id = 1006
  把 1006 号打印机下架(is_on 从 1 改成 0),再数一次视图里还有几行:

  1> SELECT COUNT(*) AS 行数 FROM v_on_sale
  行数
  ----
  4
  (1 行受影响)
  少了一行。视图没有「刷新」这一步——它每次都去问基表。
  这一点跟物化视图(索引视图)正好相反:那个是真存数据,要刷新,但查得快。

  ③ 视图与表的区别
  --------------------------------------------------------------------
| 对比项 | 表 | 视图 |
|---|---|---|
| 存什么 | 存数据(行和列) | 只存一句 SELECT |
| 占多少空间 | 随数据量涨 | 几乎为 0 |
| 数据从哪来 | 自己就是源头 | 全部来自基表 |
| 基表变了 | 跟它无关 | 下次查立刻反映 |
| 能不能加索引 | 能 | 普通视图不能(索引视图另算) |

  最重要的一条是第一行:表是数据的家,视图只是看数据的一个窗口。

  ④ 视图的分类
  --------------------------------------------------------------------
| 分法 | 类型 | 说明 |
|---|---|---|
| 按用途 | 常规视图 | 一条 SELECT,最常见 |
|  | 索引视图 | 真存数据、能建索引,要手动维护 |
|  | 分区视图 | 把多张结构相同的表 UNION 起来当一张看 |
| 按来源 | 单表视图 | 只查一张表,通常可更新 |
|  | 多表视图 | JOIN 出来的,一般只读 |

  「可更新」是 SQL Server 的一个特性:单表、不含聚合和 DISTINCT 的视图,
  对它 INSERT/UPDATE,会真的落到基表上。下一节演示这回事。

  ⑤ 视图查询有没有额外开销:实测
  --------------------------------------------------------------------
| 写法(20 万行) | 最快一次(毫秒) |
|---|---:|
| 直接查基表,条件写全 | 19.23 |
| 查视图,条件藏在视图里 | 18.60 |

  两个数基本一样,差的那点在这个量级上就是噪声。
  原因是视图在编译时就被展开成基表查询了——它只是个「文本替换」,不是一层真的中间结果。
  所以别怕用视图,它不会因为「多套一层」就变慢。

第一段返回 ('view', 'v_on_sale', 'CREATE VIEW v_on_sale AS SELECT id ...')。 看清楚了:存的是那句 SQL,不是数据。 这是「视图」二字最该记住的一点。 第二段两个数:5 和 4。 中间那句 UPDATE 改的是 tb_goods, 视图一动没动,但查出来的结果变了——因为它每次都去问基表。 第三段 19.68 和 18.33,基本并列。 如果有人告诉你「视图会慢一倍」, 让他先在自己库上测一次——多数情况下那是视图里那句 SQL 的问题,不是视图的问题。

30 秒实验

  1. 给视图加一句 WHERE price > 100,再查一次。视图可以随便改,改完用起来跟新表一样。
  2. 把视图 DROP 掉,看基表数据少不少。一条都不会少——视图本来就没存数据。
  3. 试着给普通视图 CREATE INDEX。会报错。要建索引只能建成索引视图,那要另一套语法。

常见误区

  1. 「视图就是把查询结果存下来了」。存的是查询语句,不是结果。 改基表,视图立刻变;它没有「刷新」这一步。
  2. 「给视图建索引能加速」。普通视图建不了索引。 要建索引得建索引视图——那是真存数据的,代价是要维护。
  3. 「查视图会多一层开销」。视图在编译时就展开成基表查询了,实测耗时跟直接查一样。 慢的是视图里那句 SQL,不是视图这层壳。
自测三问(点开对答案) 1. 视图里存的是什么? —— **一句 `SELECT` 的原文。** 一行数据都没有——**数据字典里那列 `sql` 放着完整定义。** 2. 改了基表的数据,视图要「刷新」吗? —— **不用,也没有这个操作。** 视图每次被查都去问基表,**所以改完立刻就能看到新结果。** 3. 视图查询会不会比直接查基表慢? —— **不会。** 实测 19.68 对 18.33,基本并列。 **因为视图在编译时就被展开成基表查询了,它只是一个文本替换。**

下一篇 10.2 讲怎么建、怎么改、怎么删视图, 还有一件很多人不知道的事:单表视图在 SQL Server 上是可以直接 INSERT/UPDATE 的, 改的其实是基表。

相关笔记