跳到主要内容

视图

当一个查询经常被重复使用,或者我们不希望每个使用者都直接接触底层表结构时,可以把这个查询保存为一个名字,这就是数据库视图(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 等复杂逻辑的视图通常不可直接更新,具体以数据库规则为准。
  • 视图适合复用查询、简化调用和限制暴露列,但普通视图不会自动带来缓存或性能提升。
  • 需要预先保存结果并接受延迟更新时,可以考虑物化视图;需要频繁写入时,应使用普通表。