视图
当一个查询经常被重复使用,或者我们不希望每个使用者都直接接触底层表结构时,可以把这个查询保存为一个名字,这就是数据库视图(View)。
视图看起来像一张表,可以被 SELECT 查询;但普通视图通常只保存查询定义,不单独保存一份数据。查询视图时,数据库会根据定义读取底层表。因此可以把它理解成“可复用的查询窗口”,而不是数据的副本。
准备示例数据
下面使用用户表和订单表说明视图的用法:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name VARCHAR(50) NOT NULL,
region VARCHAR(20) NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
);
假设业务经常需要查询“已支付订单及其用户信息”。如果每个报表都重复书写连接和过滤条件,容易出现遗漏或条件不一致。
创建第一个视图
使用 CREATE VIEW 保存查询:
CREATE VIEW paid_orders AS
SELECT
o.id AS order_id,
u.id AS user_id,
u.name AS user_name,
u.region,
o.amount
FROM orders AS o
JOIN users AS u ON u.id = o.user_id
WHERE o.status = 'paid';
之后可以像查询普通表一样查询视图:
SELECT order_id, user_name, amount
FROM paid_orders
WHERE region = '华东'
ORDER BY order_id;
这里的执行过程可以概括为:
查询 paid_orders
-> 展开视图定义
-> 连接 users 和 orders
-> 筛选 status = 'paid'
-> 再应用 region = '华东'
视图可以让调用方只关注业务字段和业务名称,而不必知道订单状态存在哪张表、两张表如何连接。
视图和普通表有什么区别
| 对比项 | 普通表 | 普通视图 |
|---|---|---|
| 是否独立保存数据 | 是 | 通常否,只保存查询定义 |
| 是否可以直接插入数据 | 可以(受约束影响) | 取决于视图定义和数据库规则 |
| 查询时是否读取底层表 | 直接读取自身数据 | 通常读取底层表 |
| 是否能隐藏底层列 | 可以通过权限控制 | 可以只暴露需要的列 |
| 是否会自动反映底层表变化 | 不适用 | 会,查询时看到最新结果 |
普通视图没有自己的数据副本,所以底层表新增或修改记录后,查询视图通常会立即反映变化。这也意味着视图的查询成本通常仍然来自它定义中的连接、过滤和聚合。
修改和删除视图
视图定义需要调整时,优先使用数据库提供的替换语法。不同数据库的语法并不完全相同,例如 PostgreSQL 和 MySQL 都支持常见的 CREATE OR REPLACE VIEW 写法:
CREATE OR REPLACE VIEW paid_orders AS
SELECT
o.id AS order_id,
u.id AS user_id,
u.name AS user_name,
u.region,
o.amount,
o.status
FROM orders AS o
JOIN users AS u ON u.id = o.user_id
WHERE o.status = 'paid';
如果数据库不支持 CREATE OR REPLACE VIEW,需要先确认依赖关系,再使用 DROP VIEW 和 CREATE VIEW。删除视图不会删除底层表中的数据:
DROP VIEW paid_orders;
生产环境中修改或删除视图前,应检查其他视图、存储过程、报表和应用代码是否依赖它。视图的列名和含义一旦被外部使用,就应当把它当作一个需要兼容维护的接口。
视图可以更新吗
有些简单视图允许通过 INSERT、UPDATE 或 DELETE 间接修改底层表,这类视图称为可更新视图。但是否可更新由数据库产品和视图定义共同决定,不能只看视图名字。
通常,包含单张表、没有 GROUP BY、聚合函数、DISTINCT、集合运算或复杂连接的视图,更有可能支持更新。例如:
CREATE VIEW active_users AS
SELECT id, name, region
FROM users
WHERE region = '华东';
下面的语句在支持可更新视图且满足约束的数据库中,可能会修改 users 表:
UPDATE active_users
SET region = '华南'
WHERE id = 1;
但这个结果并不代表用户从视图中“移动”到了另一个区域;更新后该行不再满足 region = '华东',因此下一次查询 active_users 时看不到它。
连接、聚合和计算列会让更新的归属变得不明确。例如,下面的视图把多行订单汇总成一行,通常不能直接更新:
CREATE VIEW user_order_totals AS
SELECT user_id, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id;
如果业务需要修改数据,直接更新底层表通常更清晰。即使数据库允许更新某个视图,也应先查阅对应产品的规则和测试结果。
视图的常见用途
复用复杂查询
把稳定的连接、过滤或字段计算集中在一个地方,避免多个应用各自复制 SQL。修改业务规则时,只需调整视图定义,并统一评估下游影响。
简化使用方式
视图可以把多张表包装成更容易理解的业务对象。例如,报表只需要查询 paid_orders,不必重复记住 orders.user_id = users.id 和支付状态条件。
限制数据暴露
可以只在视图中选择允许访问的列,再把查询视图的权限授予特定账号:
GRANT SELECT ON paid_orders TO report_reader;
权限语法和安全模型因数据库而异。视图不是自动的安全边界,仍需确认调用账号是否同时拥有底层表权限,以及数据库是否支持并启用了相应的安全策略。
兼容表结构变化
应用依赖稳定的视图列名,底层表可以在一定范围内重命名、拆分或合并。只要视图仍能提供相同的列和含义,应用就不必立即修改。不过,删除或重命名视图列仍可能破坏下游使用者。
普通视图和物化视图
普通视图每次查询时重新计算。部分数据库还提供物化视图(Materialized View):它会把查询结果实际保存下来,查询速度可能更快,但数据不会自动实时更新,通常需要刷新:
-- 语法因数据库而异,以下示例适用于支持该功能的数据库
CREATE MATERIALIZED VIEW monthly_sales AS
SELECT
DATE_TRUNC('month', created_at) AS month,
SUM(amount) AS total_amount
FROM orders
GROUP BY DATE_TRUNC('month', created_at);
REFRESH MATERIALIZED VIEW monthly_sales;
两者的选择取决于需求:
| 需求 | 更适合的对象 |
|---|---|
| 结果需要接近实时,数据量可以接受重复计算 | 普通视图 |
| 查询很重,但允许读取稍旧的数据 | 物化视图 |
| 需要频繁写入和逐行修改 | 普通表 |
物化视图不是所有数据库都支持;刷新时机、并发读取和增量刷新能力也需要查看产品文档。
使用视图时的注意事项
- 不要把视图当作性能优化的保证。 普通视图通常不会自动缓存结果,仍需为底层连接列、过滤列建立合适的索引,并使用
EXPLAIN检查执行计划。 - 避免层层嵌套。 一个视图引用另一个视图本身没有问题,但多层嵌套会让执行计划和排查过程变得复杂。应保持命名和依赖关系清晰。
- 明确列名。 不要依赖
SELECT *创建视图。底层表新增列后,视图是否自动包含该列因数据库而异;明确列名可以避免接口悄悄变化。 - 留意过滤条件的含义。
WHERE status = 'paid'会固定业务规则。状态定义变化时,应同步评估视图名称、说明和所有调用方。 - 检查权限链路。 测试实际使用视图的账号,而不是只用管理员账号验证;确认是否能通过其他对象绕过预期的数据限制。
- 把视图当作接口管理。 记录用途、负责人和依赖方,修改列名、类型或语义前先进行兼容性评估。
小结
- 视图是一个有名字的查询定义,通常不单独保存数据,但可以像表一样被查询。
CREATE VIEW用于创建视图,CREATE OR REPLACE VIEW或产品特定语法用于调整定义,DROP VIEW只删除视图本身。- 简单视图有时可以更新底层表;包含连接、聚合、
DISTINCT等复杂逻辑的视图通常不可直接更新,具体以数据库规则为准。 - 视图适合复用查询、简化调用和限制暴露列,但普通视图不会自动带来缓存或性能提升。
- 需要预先保存结果并接受延迟更新时,可以考虑物化视图;需要频繁写入时,应使用普通表。