数据库视图(View)是一个虚拟的表。它不像普通表那样存储实际的数据,而是存储一个查询(SELECT 语句)的定义。当你查询视图时,数据库会执行这个查询,并将其结果作为虚拟的表返回。从用户的角度看,视图就像一张真实的表一样,可以对其进行查询操作。

视图的设计初衷是为了简化复杂的查询、限制数据访问、以及提供一定的数据抽象层。例如,你可以创建一个视图来隐藏多个表的复杂联接过程,或者只暴露表中部分敏感列给特定用户。

SQL

-- 示例:创建一个视图来显示活跃用户的订单总金额
CREATE VIEW ActiveUserOrderSummary AS
SELECT
    u.user_id,
    u.username,
    COUNT(o.order_id) AS total_orders,
    SUM(o.total_amount) AS total_spent
FROM
    users u
JOIN
    orders o ON u.user_id = o.user_id
WHERE
    u.status = 'active'
GROUP BY
    u.user_id, u.username;

-- 查询视图
SELECT user_id, username, total_spent FROM ActiveUserOrderSummary WHERE total_spent > 1000;

上面的例子展示了视图如何将一个涉及联接、过滤、分组和聚合的复杂查询封装起来,使得后续查询(SELECT user_id, username, total_spent FROM ActiveUserOrderSummary WHERE total_spent > 1000;)变得非常简洁。

然而,在实际的程序开发和数据库管理中,我们常常会听到一种声音:“不建议过度使用数据库视图”,甚至有些激进的观点认为“尽量不要使用视图”。这又是为什么呢?视图明明看起来很方便,为什么会被一些人“嫌弃”?

这并非因为视图本身是错误的设计,而是因为视图在使用不当或过度使用时,会引入一些潜在的问题和风险,而这些问题往往隐藏在视图简洁的外表之下。主要的担忧集中在以下几个方面:

1. 性能杀手?隐藏的复杂性与优化器困境

这是许多人对视图持保留意见的最主要原因。视图本身不存储数据,每次查询视图时,数据库都需要解析并执行视图定义中的底层查询。如果视图定义很复杂(例如涉及多个联接、子查询、聚合、排序),或者视图是基于其他视图(视图的视图),那么查询视图的性能问题就会凸显:

  • 隐藏的查询成本: 对视图进行简单的 SELECT * FROM my_view; 操作,其背后可能隐藏着巨大的计算和 I/O 开销。视图的使用者(尤其是应用程序的开发者)如果不查看视图的定义,很难意识到这个查询实际上有多么昂贵。 SQL

    -- 假设 views 基于 tables A, B, C 的复杂 JOIN 和 GROUP BY
    
    -- 应用程序代码或 Ad-hoc 查询
    SELECT user_id, total_spent FROM ActiveUserOrderSummary; -- 看起来简单,但每次执行都要重新计算联接和聚合
    
    -- 如果直接写底层复杂查询,开发者更清楚其成本
    SELECT u.user_id, SUM(o.total_amount) AS total_spent
    FROM users u JOIN orders o ON u.user_id = o.user_id
    WHERE u.status = 'active'
    GROUP BY u.user_id;
    
  • 优化器的困境: 数据库的查询优化器非常智能,它会尝试找到执行查询的最优路径。然而,当查询是针对一个复杂视图时,不同的数据库系统和不同版本优化器处理方式可能有所不同。
    • 查询合并 (Query Merging): 理想情况下,优化器会将针对视图的查询与视图本身的定义合并成一个更复杂的查询,然后对合并后的查询进行优化。例如 SELECT ... FROM my_view WHERE condition; 可能被合并为 SELECT ... FROM (视图定义) AS temp WHERE condition; 然后再优化。如果优化器能有效地将外部查询的条件 (predicates) 下推 (push down) 到视图定义的底层查询中,性能通常不错。
    • 视图物化 (View Materialization): 在某些情况下(取决于视图的复杂性、数据库系统的策略),优化器可能选择先完全计算出视图的结果集(将其物化为一个临时表),然后再对这个临时表执行外部查询的过滤、排序等操作。如果视图的结果集非常大,而外部查询只需要其中的一小部分数据,那么物化整个视图会造成巨大的不必要的开销。
    • 优化器无法穿透视图: 对于过于复杂的视图(例如包含 UNION, DISTINCT, GROUP BY 的视图),优化器可能无法有效地将外部查询的条件和操作“穿透”视图定义下推到底层表,导致无法充分利用底层表的索引,或者执行次优的联接顺序。
    SQL

    -- 示例:对一个包含聚合的视图进行过滤
    SELECT user_id, total_spent FROM ActiveUserOrderSummary WHERE total_spent > 1000;
    
    -- 优化器理想的处理方式 (Predicate Pushdown):
    -- 尝试将过滤条件推到聚合之前或过程中,可能基于索引过滤更少的数据再聚合
    -- 这依赖于优化器的能力,对于复杂的聚合视图可能难以实现
    
    -- 优化器可能的低效处理方式 (Materialization then Filter):
    -- 1. 先完全计算出所有活跃用户的订单总金额结果集 (物化视图)
    -- 2. 在物化后的结果集中过滤 total_spent > 1000 的记录
    -- 如果活跃用户很多,物化所有总金额再过滤的开销可能很大
    
  • 索引问题: 标准视图本身是没有索引的。针对视图的查询完全依赖于底层表的索引。如果视图的复杂性(如多表联接)导致查询无法有效地利用底层表的现有索引,或者需要的索引无法在视图层创建,那么性能会很差。虽然一些数据库支持物化视图 (Materialized View),物化视图存储实际数据并可以创建索引,但它需要额外的存储空间和定期刷新(维护数据一致性)的开销,与标准视图概念不同。

2. 维护噩梦?隐藏的依赖关系与 Schema 变更

随着项目的发展,数据库 Schema 会不断变化。视图引入了额外的抽象层和依赖关系,使得维护变得更加复杂和脆弱:

  • 复杂性隐藏: 视图将底层复杂性封装起来,短期内提高了易用性,但长期来看,当需要修改视图定义、排查视图相关的性能问题或理解基于视图的应用逻辑时,开发者必须深入理解视图的定义及其依赖的所有底层表结构。这增加了理解系统的门槛。
  • 依赖关系蔓延 (Dependency Sprawl): 一个视图可以基于多个表,另一个视图又可以基于第一个视图,应用程序的代码又依赖于这些视图。形成复杂的依赖链。 SQL

    -- 假设有 tables A, B, C
    CREATE VIEW View1 AS SELECT ... FROM A JOIN B ...;
    CREATE VIEW View2 AS SELECT ... FROM View1 JOIN C ...;
    -- 应用程序代码 SELECT ... FROM View2 ...;
    
    -- 如果 table A 的 schema 发生变化 (例如删除一个列)
    ALTER TABLE A DROP COLUMN problematic_col;
    
    -- 此时 View1 会失效
    -- 接着 View2 也会失效 (因为它依赖于 View1)
    -- 最后应用程序中所有查询 View2 的地方都会报错
    
    当底层表的 Schema 发生变化时,需要检查所有依赖于该表的视图、依赖于这些视图的视图,以及所有依赖这些视图的应用代码。依赖链越长,维护越困难,出错的风险也越高。在大型项目中,视图之间的依赖关系可能会变得非常混乱,形成“视图地狱”。
  • “视图蔓延” (View Sprawl): 不同开发者可能为了略微不同的需求创建功能相似但定义不同的视图,导致视图数量爆炸式增长,管理和理解这些视图变得困难。哪些视图是活跃的?哪些是废弃的?它们之间有什么细微差异?这些都成为维护负担。

3. 写入受限:视图的不可更新性

视图通常用于查询数据,但大部分视图是不支持直接进行数据修改(INSERT, UPDATE, DELETE)操作的。

  • 不可更新的视图类型: 视图是否可更新取决于其定义。通常,以下情况会导致视图不可更新:
    • 包含联接 (JOIN) 的视图。
    • 包含聚合函数 (COUNT, SUM, AVG, MIN, MAX) 的视图。
    • 包含 DISTINCT 关键字的视图。
    • 包含 UNIONUNION ALL 的视图。
    • 包含子查询的视图。
    • 包含计算列(如 column1 + column2)的视图。
    • 包含 GROUP BYHAVING 子句的视图。
    • 从多个表中选择列的视图。
  • 影响: 如果你的应用需要通过视图来修改数据,很可能无法实现,最终还是需要直接操作底层表或使用存储过程/函数。这限制了视图在业务逻辑层面的应用。

4. 安全与抽象的误区

  • 安全: 视图可以用来隐藏敏感列或行(通过 WHERE 条件),为不同用户提供不同的数据视图。但这通常不足以作为主要的数据库安全手段。更强大的安全控制应该在用户权限管理层面进行,限制用户对底层表的直接读写权限。如果攻击者能够绕过应用直接访问数据库,而底层表的权限设置不当,视图提供的安全屏障是有限的。
  • 抽象: 视图提供的是一种数据库层面的抽象。在现代应用程序开发中,更多地依赖应用层面的抽象(如使用 ORM 将表映射为对象、在 Service 层封装业务逻辑、使用 DTOs 数据传输对象)可能更加灵活和强大,也更容易与应用程序的架构模式(如 MVC、微服务)结合。过度依赖数据库视图作为主要的抽象层,可能会导致数据库 Schema 的微小变化就需要修改大量的视图,或者需要在数据库层创建大量仅仅服务于应用特定数据结构的视图,将应用逻辑泄露到了数据库层。

5. 跨数据库系统的差异

尽管 CREATE VIEW 是标准 SQL,但不同数据库系统对视图特性的支持和实现细节有所差异。例如,视图的可更新性规则、优化器对视图的处理能力、对物化视图的支持程度等都可能不同。在一个数据库系统中表现良好的视图,迁移到另一个系统时可能会遇到性能或功能问题。

视图的合理使用场景

认识到视图的潜在问题后,并不是说就完全不能使用视图了。在一些特定且受控的场景下,视图仍然是非常有用的工具:

  • 简化频繁使用的复杂查询: 如果一个涉及 3-4 个表联接,并且包含特定过滤条件的 SELECT 查询在系统中被频繁地使用,将其封装成一个视图可以极大地简化应用代码或 Ad-hoc 查询的编写,降低出错率。这里的关键是“频繁”和“简化”。
  • 限制特定用户或角色的数据访问: 为特定的数据库用户或应用程序组件创建视图,只暴露它们需要访问的列和行。这是一种额外的安全层,尽管不应是唯一的安全手段。
  • 作为遗留系统的兼容层: 在重构底层数据库 Schema 时,可以创建视图来模拟旧的表结构,让依赖旧 Schema 的遗留应用暂时继续工作,为应用迁移争取时间。但这是一种临时方案,需要谨慎管理。
  • 提供特定视角的报表数据(配合物化视图): 在支持物化视图的数据库系统中,可以创建物化视图来预计算聚合数据或复杂查询结果,用于加速报表查询。这是以空间换时间,并需要考虑数据一致性和刷新策略。

替代方案

在很多可以使用视图的场景下,往往有其他替代方案:

  • 在应用代码中直接编写 SQL: 结合 ORM 或 SQL Builder,直接在应用代码中编写和维护复杂的查询。开发者对查询的实际执行更清楚,易于结合应用逻辑进行优化。
  • 使用存储过程或函数: 将复杂的查询或数据处理逻辑封装在数据库的存储过程或函数中。这对于需要频繁在数据库层执行的复杂操作(特别是包含业务逻辑、数据校验甚至写入操作的场景)可能更合适。
  • 在应用层进行数据组合和处理: 在应用代码中分别查询所需的底层数据,然后在内存中进行联接、过滤、聚合等操作。适用于数据量不大或业务逻辑在应用层实现更自然的场景。
  • 物化视图 (Materialized View): 如果数据库支持,并且需求是加速固定复杂查询的读取性能,可以考虑物化视图。

总结

回到最初的问题:“为什么说不建议使用视图”。这种说法并非绝对的禁令,而是对过度使用、不恰当使用或使用复杂视图所带来潜在问题的一种警示。

视图的简洁性是一把双刃剑:它简化了外在的查询语法,但可能隐藏了内在的复杂性、性能问题、维护负担和依赖风险。特别是复杂的、嵌套的、包含聚合或联接的视图,更容易成为性能瓶颈和维护噩梦的源头。视图的不可更新性也限制了其应用范围。

因此,“不建议使用视图”更准确的理解应该是:

  • 谨慎使用视图,尤其避免创建过于复杂或多层嵌套的视图。
  • 在使用视图时,务必了解其底层定义和潜在的查询成本。
  • 不要将视图作为主要的应用程序抽象层,应用层面的抽象往往更灵活。
  • 将视图限制在简化简单查询、有限的数据访问控制等少数合理场景。

视图是一个有用的数据库工具,但就像任何工具一样,需要理解其原理和局限性,并在合适的场景下正确使用。避免将视图变成隐藏问题的“糖衣炮弹”。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐