🐬 第22章 使用视图
📄 一句话概括

视图就是一句存起来的 SELECT,起个名字,之后当表用。 它不存数据、只存那句 SQL,每次查都重跑一遍。 Demo 把三行联结存成 vendor_products,之后一行就能查; order_totals 把每张单的金额固化下来,五张单从 1000.00 到 25.98; 删掉视图之后,customers 那五行一个没少。

📝 本章和前面几章的关系

第 15 到 18 章的联结越写越长,这一章给它们起名字。 视图是把「复杂查询」变成「一个名字」的工具。 它也是第 16 章外联结那套知识的直接产物——你不会联结,也就写不出有用的视图。

一、视图:存起来的查询

CREATE VIEW 名字 AS SELECT ...:把一句查询存进库里,之后 SELECT * FROM 名字 就能用。 视图里不存数据。 它存的只是那句 SQL, 每次查视图,引擎都把 SQL 重跑一遍。 这意味着视图永远反映最新数据, 也意味着它不会让查询变快。 视图和表在清单里长得一样(sqlite_master 里 type 分别是 view 和 table), 所以用它的人甚至不知道自己在查视图。这正是它有价值的地方。

二、视图能干的四件事

一是把复杂的联结藏起来。 三行 SQL 存成 vendor_products,以后谁都只写一行, 也不用担心他写错联结条件。 二是把计算字段固化。 每张单的金额是 SUM(quantity * item_price), 存进 order_totals 之后,一句查询就有一列现成的金额。 三是把筛选条件固定下来。 customer_emails 只含有邮箱的客户, 用它的人不必记得写 IS NOT NULL——忘了写就漏数据,这种错很难被发现。 四是复用与口径统一。 同一个「有效订单」的定义写进视图,十个人查十次都是同一个口径; 写在十份报表里,早晚会有一个不一样。

三、视图的规则与限制

视图可以嵌套(视图里查视图),但别套太深:每套一层就多跑一次子查询,排查问题也难。 教材建议尽量少嵌套。 视图不能带索引(MySQL 的物化视图是另一回事),它的性能完全取决于底层那句 SQL 写得好不好。 ORDER BY 在视图里通常没意义:SQL 标准不保证视图的输出顺序, 要顺序就在查视图时自己写 ORDER BY。 本课这套引擎允许视图里写 ORDER BY,但别依赖它。 视图的另一个用处是「藏列」:只把该给人看的列放进视图, 敏感列(成本价、身份证号)根本不出现在视图里——这比在应用层过滤可靠。

四、视图能不能改数据

两个引擎态度不同。 SQLite 对视图一律不让改,报 cannot modify ... because it is a view; MySQL 松一些:一列对一列、没有聚集的「简单视图」可以直接 UPDATE。 带聚集、GROUP BY、DISTINCT、UNION 的视图,两边都改不了。 原因很直白:一张单的金额是四行算出来的,你改「那一行」到底改哪一行? SQLite 想改视图得挂 INSTEAD OF 触发器(下一章讲),由你自己写「改视图等于改哪张表」。 教材的态度很明确:视图主要用于检索,不用于更新。 能改也别改——你改的是视图,别人看的是表, 出问题很难追。

五、小结

教材小结:视图是存起来的查询,可简化联结、固化计算、限制可见列,主要用于检索,不用于更新。 本课补三条:一是视图不存数据也不提速;二是别嵌套太深; 三是 SQLite 一律不许改视图,MySQL 只许改简单视图。

六、动手 Demo:把三行 SQL 变成一个名字

Demo 六段:先建一个联结视图,再看它在清单里的样子,然后固化计算字段、固定筛选条件、给视图加条件, 最后试改数据、删视图。 注意每次 CREATE VIEW 都是「完成」而不是「影响 N 行」——它改的是结构,不是数据。

import sqlite3
con = sqlite3.connect(":memory:")

# 视图 = 存起来的一句查询
con.execute("CREATE VIEW vendor_products AS SELECT vend_name, prod_name, "
            "prod_price FROM vendors, products "
            "WHERE vendors.vend_id = products.vend_id")
con.execute("SELECT * FROM vendor_products WHERE prod_price <= 5")
# 表和视图都在同一张清单里,但视图里没有数据
con.execute("SELECT name, type FROM sqlite_master WHERE type IN ('table','view')")
# 把计算字段固化进视图
con.execute("CREATE VIEW order_totals AS SELECT order_num, "
            "SUM(quantity * item_price) AS total FROM orderitems "
            "GROUP BY order_num")
con.execute("SELECT * FROM order_totals ORDER BY total DESC")
# 把筛选条件固定下来
con.execute("CREATE VIEW customer_emails AS SELECT cust_id, cust_name, "
            "cust_email FROM customers WHERE cust_email IS NOT NULL")
# 查视图能再加条件
con.execute("SELECT prod_name, prod_price FROM vendor_products "
            "WHERE vend_name = 'ACME' AND prod_price >= 10")
# 视图改数据:SQLite 一律拒绝,MySQL 允许简单视图
con.execute("CREATE VIEW cheap_stuff AS SELECT prod_id, prod_name, "
            "prod_price FROM products WHERE prod_price <= 5")
con.execute("UPDATE cheap_stuff SET prod_price = 4.99 WHERE prod_id = 'FC'")
# 删视图不删数据
con.execute("DROP VIEW customer_emails")

实跑输出:

--------------------------------------------------------------------
第二十二课:使用视图
--------------------------------------------------------------------

  【一】视图就是一个存起来的查询
  SQL> CREATE VIEW vendor_products AS SELECT vend_name, prod_name, prod_price FROM vendors,
       products WHERE vendors.vend_id = products.vend_id
  完成
  SQL> SELECT * FROM vendor_products WHERE prod_price <= 5 ORDER BY prod_price
  vend_name       prod_name         prod_price
  ---------------------------------------------
  ACME            Carrots           2.50
  ACME            TNT (1 stick)     2.50
  LT Supplies     Fuses             3.42
  ACME            Sling             4.49
  (4 行)
  **`CREATE VIEW 名字 AS SELECT ...`** 把这条查询存起来,**之后就能像表一样查它。**
  **视图里不存数据**,只存那句 SQL。**每次查视图,引擎都把那条 SQL 重跑一遍。**

  【二】它和表长得一样,但只是长得一样
  SQL> SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') ORDER BY type, name
  name                type
  --------------------------
  customers           table
  orderitems          table
  orders              table
  productnotes        table
  products            table
  vendors             table
  vendor_products     view
  (7 行)
  **六张表加一个视图,都在这张清单里。** 但表里有数据,视图里只有定义。

  【三】视图能干的三件事
  **第一,把复杂的联结藏起来。** 上面那句三行 SQL,以后只写 `SELECT * FROM vendor_products` 就行。
  SQL> CREATE VIEW order_totals AS SELECT order_num,
       SUM(quantity * item_price) AS total FROM orderitems GROUP BY order_num
  完成
  SQL> SELECT * FROM order_totals ORDER BY total DESC
  order_num     total
  ------------------------
  20007         1000.00
  20005         149.87
  20008         125.00
  20006         55.00
  20009         25.98
  (5 行)
  **第二,把计算字段固化。** 每张单的金额,一句 `SELECT * FROM order_totals` 就有。
  SQL> CREATE VIEW customer_emails AS SELECT cust_id, cust_name,
       cust_email FROM customers WHERE cust_email IS NOT NULL
  完成
  SQL> SELECT * FROM customer_emails ORDER BY cust_id
  cust_id     cust_name          cust_email
  ---------------------------------------------------
  10001       Coyote Inc.        [email protected]
  10003       Wascals            [email protected]
  10004       Yosemite Place     [email protected]
  (3 行)
  **第三,把筛选条件固定下来。** 这个视图只有三个有邮箱的客户,
  **用得着的人不必每次都记得写 `IS NOT NULL`。**

  【四】视图能加条件、能再过滤
  SQL> SELECT prod_name,
       prod_price FROM vendor_products WHERE vend_name = 'ACME' AND prod_price >= 10
  prod_name          prod_price
  ------------------------------
  Detonator          13.00
  Bird seed          10.00
  TNT (5 sticks)     10.00
  (3 行)
  **查视图和查表一样能写 WHERE、ORDER BY、GROUP BY。** 底层的 SQL 会跟你写的条件合在一起执行。

  【五】视图能不能改数据:两个引擎态度不同
  SQL> CREATE VIEW cheap_stuff AS SELECT prod_id, prod_name,
       prod_price FROM products WHERE prod_price <= 5
  完成
  SQL> SELECT * FROM cheap_stuff ORDER BY prod_price
  prod_id     prod_name         prod_price
  -----------------------------------------
  FC          Carrots           2.50
  TNT1        TNT (1 stick)     2.50
  FU1         Fuses             3.42
  SLING       Sling             4.49
  (4 行)
  **改不动,报错:cannot modify cheap_stuff because it is a view**
  **SQLite 对视图一律不让改**,想改得挂一个 INSTEAD OF 触发器(下一章)。
  **MySQL 松一些:像这种「一列对一列、没有聚集」的简单视图是能直接 UPDATE 的。**
  **带聚集的视图在两边都改不了,报错:cannot modify order_totals because it is a view**
  **原因很简单**:一张单的金额是四行算出来的,**改「一行」等于改哪一行?引擎答不上来。**
  **教材的态度很明确:视图主要用于检索,不用于更新。** **能改也别改**——你改的是视图,
  别人看到的是表,出问题很难追。

  【六】删视图不影响数据
  SQL> DROP VIEW customer_emails
  完成
  SQL> SELECT COUNT(*) AS n FROM customers
  n
  --
  5
  (1 行)
  **视图删了,customers 里那五行一个没少。** **删视图删的是那句 SQL,不是数据。**

第一段建视图。 三行联结存成 vendor_products,查它只要一行,结果四行便宜货。 第二段看清单。 六张表加一个视图,type 那列分出谁是表谁是视图。 第三段固化计算。 五张单的金额:1000.00、149.87、125.00、55.00、25.98。 第四段固定筛选。 只剩三个有邮箱的客户,条件焊死在视图里。 第五段给视图加条件。 ACME 里价格不低于 10 的三行。 第六段改与删。 改视图被拒,删视图之后 customers 还是五行。

30 秒实验

  1. 给 vendor_products 再加一个条件建第二个视图,只留价格低于 10 的,看有几行。
  2. 查一下 sqlite_master 里 order_totals 的 sql 字段,看到的是什么?
  3. (先想再跑)把 customer_emails 删掉之后,customers 里那三个邮箱还在吗?

常见误区

  1. 以为视图能提速。它只是把 SQL 存起来,每次查还是重跑一遍。
  2. 以为视图里存了数据。删视图不删数据,改底层表视图立刻跟着变。
  3. 想更新带聚集的视图。两边引擎都不让,因为「改哪一行」无法确定。
自测三问(点开对答案) 1. 视图里存的是什么? —— 存的是那句 SELECT 的定义,不存数据;每次查视图时把这条 SQL 重新执行一遍。 2. 视图能带来什么实际好处? —— 简化复杂联结、固化计算字段、固定筛选条件、限制可见列、统一口径。 3. 为什么带 GROUP BY 的视图不能更新? —— 因为它的每一行是聚合出来的,无法对应到基础表的具体行,引擎不知道改哪里。

视图把查询封装起来了, 但它只能封装一句 SELECT。 下一章封装一整套逻辑——存储过程,连 IF、循环、参数都给你。

相关笔记