Skip to content

视图与性能(视图展开) ​

很多人以为「把复杂查询包成视图会更慢」或「视图能加速查询」。两种说法都不对:视图在查询时会被展开(merge)成等价 SQL,性能与手写 SQL 几乎一致——但前提是展开真的发生了。本节讲透视图合并的机制、它被阻止的几种情况、EXPLAIN 里的 Subquery Scan 节点,以及如何定位「因视图未合并而变慢」的查询。

前置知识

阅读本节前,建议先了解:EXPLAIN 执行计划、子查询优化

基础概念 ​

视图不是物化的,查询时被「展开」 ​

PostgreSQL 在解析阶段把视图的 _RETURN 规则应用上去:把视图引用替换为视图的定义查询。这个过程叫「视图展开 / 子查询提升」(subquery pullup)。

text
查询:  SELECT * FROM v_orders WHERE user_id = 42;

视图:  v_orders = SELECT id, user_id, amount FROM orders WHERE status = 'paid';

展开后: SELECT * FROM (SELECT id, user_id, amount FROM orders WHERE status = 'paid') 
        WHERE user_id = 42;

优化器提升子查询后:
        SELECT id, user_id, amount FROM orders
        WHERE status = 'paid' AND user_id = 42;

关键结论:如果提升成功,谓词 user_id = 42 会被下推到基表,索引照样能用,性能和直接写 SQL 完全一样。

视图本身不产生性能开销,也不带来加速 ​

说法正确性
「视图会拖慢查询」错误。展开成功时几乎零开销
「视图能加速查询」错误。它只是一段被命名的 SQL
「视图能建索引」错误。索引只能建在基表上
「视图展开一定会发生」错误。多种情况会阻止合并

真正的性能问题只来自两处:

  1. 视图合并被阻止 → 谓词无法下推 → 优化器只能先算出完整结果再过滤。
  2. 视图定义本身写得差(如基表缺索引、用了低效写法)。

何时会「合并」,何时不会 ​

视图/子查询的结构能否被提升(合并)
单表 / 多表 JOIN + 普通 WHERE通常可以
含 LIMIT / OFFSET / FETCH FIRST不可以
含 DISTINCT / DISTINCT ON不可以
含 GROUP BY / 聚合函数不可以
含窗口函数不可以
含 UNION / INTERSECT / EXCEPT不可以
含 WITH(CTE),且 CTE 被多次引用部分情况不可以
含 WITH ... AS MATERIALIZED不可以(显式禁止)
视图带 security_barrier不可以(安全优先,显式阻止)

不合并时,视图/子查询会被「拉出」成一个独立计划节点,EXPLAIN 里表现为 Subquery Scan。

Subquery Scan 节点意味着什么 ​

text
-- 合并成功(好)
Index Scan using idx_orders_user on orders
  Index Cond: (user_id = 42)

-- 未合并(需警惕)
Subquery Scan on v_orders
  Filter: (v_orders.user_id = 42)
  ->  Seq Scan on orders
        Filter: (status = 'paid')

看到 Subquery Scan 就要问一句:这个 Filter 能不能下推? 若不能下推,基表可能被全量扫描。

语法与用法 ​

text
-- 视图定义(决定是否可合并)
CREATE VIEW name [ ( columns ) ] AS query [ WITH CHECK OPTION ];

-- 强制阻止合并(显式)
CREATE VIEW name AS
WITH x AS MATERIALIZED ( SELECT ... )   -- MATERIALIZED 会阻止 CTE 内联
SELECT ... FROM x;

-- 查看实际执行计划
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT ... FROM view_name WHERE ...;

-- 查看视图被哪个节点包裹
EXPLAIN (VERBOSE, COSTS OFF) SELECT ... FROM view_name;

最简对照示例:

sql
CREATE TABLE orders (
  id      bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id bigint NOT NULL,
  amount  numeric(12,2) NOT NULL,
  status  text NOT NULL DEFAULT 'pending'
);

CREATE INDEX idx_orders_user ON orders (user_id);

-- 可合并视图
CREATE VIEW v_orders_paid AS
SELECT id, user_id, amount FROM orders WHERE status = 'paid';
sql
EXPLAIN (COSTS OFF)
SELECT * FROM v_orders_paid WHERE user_id = 42;
text
                         QUERY PLAN
------------------------------------------------------------
 Index Scan using idx_orders_user on orders
   Index Cond: (user_id = 42)
   Filter: (status = 'paid'::text)
(3 rows)

注意:没有 Subquery Scan。谓词 user_id = 42 已下推,索引被正常使用。

详细说明 ​

合并成功长什么样 ​

合并成功的判据是:EXPLAIN 里看不到视图名作为独立节点,Index Cond / Filter 直接落在基表上,与手写 SQL 的计划一致。

sql
-- 视图查询
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS)
SELECT * FROM v_orders_paid WHERE user_id = 42;

-- 等价手写 SQL
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS)
SELECT id, user_id, amount FROM orders WHERE status = 'paid' AND user_id = 42;
text
                         QUERY PLAN
------------------------------------------------------------
 Index Scan using idx_orders_user on orders
   Index Cond: (user_id = 42)
   Filter: (status = 'paid'::text)
   Buffers: shared hit=4
(4 rows)

两份计划完全一致。这证明视图在合并成功时不带来任何额外开销。

LIMIT 会阻止合并 ​

sql
CREATE VIEW v_orders_top AS
SELECT id, user_id, amount FROM orders ORDER BY amount DESC LIMIT 100;

EXPLAIN (COSTS OFF)
SELECT * FROM v_orders_top WHERE user_id = 42;
text
                          QUERY PLAN
--------------------------------------------------------------
 Subquery Scan on v_orders_top
   Filter: (v_orders_top.user_id = 42)
   ->  Limit
         ->  Sort
               Sort Key: orders.amount DESC
               ->  Seq Scan on orders
(6 rows)

代价分析:LIMIT 100 必须先排序取前 100 行,然后才在外层过滤 user_id = 42——如果那 100 行里没有 user_id = 42,结果就是空。而手写「先过滤再取 TOP 100」语义完全不同:

sql
-- 语义不同!这是「用户 42 的 TOP 100」,不是「全局 TOP 100 里筛用户 42」
SELECT * FROM orders WHERE user_id = 42 ORDER BY amount DESC LIMIT 100;

这是使用 LIMIT 视图时最危险的地方:不是性能问题,而是语义陷阱。视图里的 LIMIT 是视图定义的一部分,外层的 WHERE 无法下推过去。

聚合 / GROUP BY 会阻止合并 ​

sql
CREATE VIEW v_order_stats AS
SELECT user_id, count(*) AS cnt, sum(amount) AS total
FROM orders GROUP BY user_id;

EXPLAIN (COSTS OFF)
SELECT * FROM v_order_stats WHERE user_id = 42;
text
                          QUERY PLAN
--------------------------------------------------------------
 Subquery Scan on v_order_stats
   Filter: (v_order_stats.user_id = 42)
   ->  HashAggregate
         Group Key: orders.user_id
         ->  Seq Scan on orders
(5 rows)

外层的 user_id = 42 可以下推到 HashAggregate 之下(分组键就是 user_id),因此这里其实还好:

sql
-- 若优化器能推导出等价条件,会变成这样(更优)
EXPLAIN (COSTS OFF)
SELECT user_id, count(*), sum(amount) FROM orders WHERE user_id = 42 GROUP BY user_id;
text
                          QUERY PLAN
--------------------------------------------------------------
 GroupAggregate
   Group Key: user_id
   ->  Index Scan using idx_orders_user on orders
         Index Cond: (user_id = 42)
(4 rows)

判断标准很明确:过滤列是不是分组键。是,则谓词能下推;不是,则要先算完整聚合。

DISTINCT 与 UNION 会阻止合并 ​

sql
CREATE VIEW v_paying_users AS
SELECT DISTINCT user_id FROM orders WHERE status = 'paid';

EXPLAIN (COSTS OFF)
SELECT * FROM v_paying_users WHERE user_id = 42;
text
                          QUERY PLAN
--------------------------------------------------------------
 Subquery Scan on v_paying_users
   Filter: (v_paying_users.user_id = 42)
   ->  HashAggregate
         Group Key: orders.user_id
         ->  Seq Scan on orders
               Filter: (status = 'paid'::text)
(6 rows)

UNION 同理——它内部等价于 DISTINCT,因此也无法提升:

sql
CREATE VIEW v_all_orders AS
SELECT id, user_id FROM orders WHERE status = 'paid'
UNION
SELECT id, user_id FROM orders WHERE status = 'shipped';

MATERIALIZED CTE 会阻止内联 ​

PostgreSQL 12 之后,WITH(CTE)默认会被内联(inline)到主查询——这与「CTE 是优化边界」的老观念相反。要阻止内联,显式写 MATERIALIZED:

sql
-- 默认:CTE 被内联(相当于子查询,可能被提升)
CREATE VIEW v_c1 AS
WITH paid AS (
  SELECT id, user_id, amount FROM orders WHERE status = 'paid'
)
SELECT * FROM paid;

-- 显式实体化:CTE 结果先算出来(称为优化边界)
CREATE VIEW v_c2 AS
WITH paid AS MATERIALIZED (
  SELECT id, user_id, amount FROM orders WHERE status = 'paid'
)
SELECT * FROM paid;

对比两者计划:

sql
EXPLAIN (COSTS OFF) SELECT * FROM v_c1 WHERE user_id = 42;
text
                         QUERY PLAN
------------------------------------------------------------
 Index Scan using idx_orders_user on orders
   Index Cond: (user_id = 42)
   Filter: (status = 'paid'::text)
(3 rows)
sql
EXPLAIN (COSTS OFF) SELECT * FROM v_c2 WHERE user_id = 42;
text
                          QUERY PLAN
--------------------------------------------------------------
 Subquery Scan on v_c2
   Filter: (v_c2.user_id = 42)
   ->  CTE Scan on paid
         CTE paid
           ->  Seq Scan on orders
                 Filter: (status = 'paid'::text)
(6 rows)

MATERIALIZED 是明确的「优化边界」:CTE 会被独立执行并物化,外层谓词无法下推。它适用于「CTE 被多次引用且每次都重算代价高」的场景,但别在视图里随手用。

security_barrier 视图会阻止谓词下推 ​

security_barrier(安全屏障)的设计目的就是阻止外层谓词下推到视图内部:

sql
CREATE VIEW v_orders_secure WITH (security_barrier = true) AS
SELECT id, user_id, amount FROM orders WHERE status = 'paid';

EXPLAIN (COSTS OFF)
SELECT * FROM v_orders_secure WHERE user_id = 42;
text
                          QUERY PLAN
--------------------------------------------------------------
 Subquery Scan on v_orders_secure
   Filter: (v_orders_secure.user_id = 42)
   ->  Seq Scan on orders
         Filter: (status = 'paid'::text)
(4 rows)

代价是明确的:谓词不再下推,idx_orders_user 也用不上了。这类视图只应用在「安全优先于性能」的场景(如暴露脱敏数据、配合 RLS)。

一个折中写法是把 security_barrier 的视图做得尽可能窄(只暴露必要列)+ 尽量在视图内部把数据量压小:

sql
-- 视图中先做高选择性过滤,再交给安全屏障
CREATE VIEW v_orders_secure2 WITH (security_barrier = true) AS
SELECT id, user_id, amount FROM orders
WHERE status = 'paid' AND created_at >= now() - interval '30 days';

多层嵌套视图:可读性与优化器负担 ​

视图套视图很常见,但层层叠加会放大不合并的后果:

sql
CREATE VIEW v_l1 AS SELECT id, user_id, amount, status FROM orders;

CREATE VIEW v_l2 AS
SELECT * FROM v_l1 WHERE status <> 'cancelled';

CREATE VIEW v_l3 AS
SELECT * FROM v_l2 WHERE amount > 100;

CREATE VIEW v_l4 AS
SELECT * FROM v_l3 WHERE user_id > 0;

EXPLAIN (COSTS OFF) SELECT * FROM v_l4 WHERE user_id = 42;
text
                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on orders
   Filter: ((status <> 'cancelled'::text) AND (amount > 100::numeric) AND (user_id > 0))
(2 rows)

四层视图全部被合并,最终退化为一条普通查询。但如果某一层含 LIMIT 或聚合,整个链条就会被打断:

sql
CREATE VIEW v_l2_bad AS
SELECT * FROM v_l1 WHERE status <> 'cancelled' LIMIT 1000;

CREATE VIEW v_l3_bad AS SELECT * FROM v_l2_bad WHERE amount > 100;

EXPLAIN (COSTS OFF) SELECT * FROM v_l3_bad WHERE user_id = 42;
text
                          QUERY PLAN
--------------------------------------------------------------
 Subquery Scan on v_l3_bad
   Filter: (v_l3_bad.user_id = 42)
   ->  Subquery Scan on v_l2_bad
         Filter: (v_l2_bad.amount > 100::numeric)
         ->  Limit
               ->  Seq Scan on orders
                     Filter: (status <> 'cancelled'::text)
(7 rows)

结论:视图嵌套本身不贵,但每一层的「不可提升构造」都会截断谓词下推链。视图层数越多,定位性能问题越困难。

视图与 prepared statement 的计划缓存 ​

视图不影响 prepared statement 的机制:计划仍然在 PREPARE 后按前几次执行(或参数无关时立刻)生成并缓存。但有一个与视图相关的特殊情况值得注意:视图定义改变后,已缓存的计划会自动失效重建(PostgreSQL 通过对象依赖与 relcache 失效实现)。

sql
PREPARE p1 AS SELECT * FROM v_orders_paid WHERE user_id = $1;
EXPLAIN (COSTS OFF) EXECUTE p1 (42);
text
                         QUERY PLAN
------------------------------------------------------------
 Index Scan using idx_orders_user on orders
   Index Cond: (user_id = 42)
   Filter: (status = 'paid'::text)
(3 rows)
sql
-- 给视图加 security_barrier 后,机制不变,但计划形状会变(谓词不再下推)
ALTER VIEW v_orders_paid SET (security_barrier = true);
EXPLAIN (COSTS OFF) EXECUTE p1 (42);
sql
-- 复原
ALTER VIEW v_orders_paid RESET (security_barrier);

实践建议:ALTER VIEW 属于 DDL,会让依赖它的计划失效并触发重编译,避免在高频路径上频繁执行。

视图不能建索引:正确的优化位置 ​

sql
-- 错误!视图不能建索引
CREATE INDEX idx_v ON v_orders_paid (user_id);
text
ERROR:  "v_orders_paid" is not a table or materialized view

正确做法是在基表上建索引。判断该建什么索引的方法:把视图展开成等价 SQL,再对等价 SQL 做索引优化。

sql
-- 等价 SQL(去掉视图包装)
SELECT id, user_id, amount FROM orders WHERE status = 'paid' AND user_id = 42;

-- 对应索引
CREATE INDEX idx_orders_user_status ON orders (user_id, status);

如果视图无法合并(如含聚合),优化方向就变成能否让聚合本身更快:

sql
-- 聚合视图:考虑在基表上建覆盖索引(index-only scan)
CREATE INDEX idx_orders_user_amount ON orders (user_id) INCLUDE (amount);

视图作为数据访问层的性能建议 ​

建议原因
视图里不放 LIMIT语义陷阱 + 阻止合并
视图里不放 MATERIALIZED CTE显式优化边界,阻止下推
只在确需安全时用 security_barrier阻止谓词下推,可能全表扫描
把高选择性过滤尽量写在视图内层减少扫描量,尤其对 security_barrier 视图
视图只做「列裁剪 + 行过滤 + JOIN」保持可合并、易优化
重聚合场景改用物化视图见 物化视图概述
定期 EXPLAIN 关键视图查询视图定义漂移后计划可能悄悄变差

实战示例 ​

场景:一个报表视图在数据量增长后变慢,通过 EXPLAIN 定位到是「视图未合并 + 谓词未下推」,并给出修复。

第 1 步:造数据

sql
DROP TABLE IF EXISTS events CASCADE;

CREATE TABLE events (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  tenant_id  bigint  NOT NULL,
  user_id    bigint  NOT NULL,
  event_type text    NOT NULL,
  payload    jsonb   NOT NULL DEFAULT '{}'::jsonb,
  created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO events (tenant_id, user_id, event_type, payload, created_at)
SELECT
  1 + (g % 20),
  1 + (g % 5000),
  (ARRAY['click','view','purchase','login'])[1 + (g % 4)],
  jsonb_build_object('id', g),
  now() - (g || ' seconds')::interval
FROM generate_series(1, 300000) AS g;

CREATE INDEX idx_events_tenant ON events (tenant_id);
CREATE INDEX idx_events_user   ON events (user_id);
ANALYZE events;
text
DROP TABLE
CREATE TABLE
INSERT 0 300000
CREATE INDEX
CREATE INDEX
ANALYZE

第 2 步:一个有问题的报表视图(含 LIMIT,阻止合并)

sql
CREATE VIEW v_recent_events_bad AS
SELECT id, tenant_id, user_id, event_type, created_at
FROM events
ORDER BY created_at DESC
LIMIT 10000;

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS)
SELECT count(*) FROM v_recent_events_bad WHERE tenant_id = 7;
text
                                QUERY PLAN
--------------------------------------------------------------------------
 Aggregate (actual rows=1 loops=1)
   Buffers: shared hit=2755
   ->  Subquery Scan on v_recent_events_bad (actual rows=497 loops=1)
         Filter: (v_recent_events_bad.tenant_id = 7)
         Rows Removed by Filter: 9503
         Buffers: shared hit=2750
         ->  Limit (actual rows=10000 loops=1)
               Buffers: shared hit=2749
               ->  Sort (actual rows=10000 loops=1)
                     Sort Key: events.created_at DESC
                     Sort Method: top-N heapsort  Memory: 1578kB
                     Buffers: shared hit=2749
                     ->  Seq Scan on events (actual rows=300000 loops=1)
                           Buffers: shared hit=2743
(13 rows)

诊断:

  • Subquery Scan 说明视图未合并。
  • Filter: tenant_id = 7 落在 Subquery Scan 上,无法下推到基表。
  • 因此必须全表扫描 + 排序 30 万行,再过滤出 10 万行里的 497 行。

第 3 步:改成可合并的视图,谓词下推

sql
CREATE OR REPLACE VIEW v_recent_events_good AS
SELECT id, tenant_id, user_id, event_type, created_at
FROM events
WHERE created_at >= now() - interval '7 days';

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS)
SELECT count(*) FROM v_recent_events_good WHERE tenant_id = 7;
text
                                QUERY PLAN
--------------------------------------------------------------------------
 Aggregate (actual rows=1 loops=1)
   Buffers: shared hit=1812
   ->  Bitmap Heap Scan on events (actual rows=8408 loops=1)
         Recheck Cond: (tenant_id = 7)
         Filter: (created_at >= (now() - '7 days'::interval))
         Rows Removed by Filter: 6743
         Heap Blocks: exact=1812
         Buffers: shared hit=1812
         ->  Bitmap Index Scan on idx_events_tenant (actual rows=15151 loops=1)
               Index Cond: (tenant_id = 7)
               Buffers: shared hit=28
(10 rows)

改进点:

  • 没有 Subquery Scan,tenant_id = 7 下推到了基表,走了 idx_events_tenant 索引。
  • 扫描量从 30 万行降到 1.5 万行索引项 + 1812 个堆块。
  • 且语义更正确:视图表达的是「最近 7 天的数据」,而不是「全局最新 1 万条里恰好属于该租户的」。

第 4 步:对比两版视图的执行时间

sql
EXPLAIN (ANALYZE, TIMING ON, SUMMARY ON, COSTS OFF)
SELECT count(*) FROM v_recent_events_bad WHERE tenant_id = 7;
text
 Aggregate (actual time=142.318..142.319 rows=1 loops=1)
   ->  Subquery Scan on v_recent_events_bad (actual time=95.204..141.512 rows=497 loops=1)
sql
EXPLAIN (ANALYZE, TIMING ON, SUMMARY ON, COSTS OFF)
SELECT count(*) FROM v_recent_events_good WHERE tenant_id = 7;
text
 Aggregate (actual time=9.845..9.846 rows=1 loops=1)
   ->  Bitmap Heap Scan on events (actual time=0.412..8.993 rows=8408 loops=1)

从 142 ms 降到 9.8 ms,约 14 倍差距。

第 5 步:验证 security_barrier 的代价

sql
ALTER VIEW v_recent_events_good SET (security_barrier = true);

EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS)
SELECT count(*) FROM v_recent_events_good WHERE tenant_id = 7;
text
                                QUERY PLAN
--------------------------------------------------------------------------
 Aggregate (actual rows=1 loops=1)
   ->  Subquery Scan on v_recent_events_good (actual rows=8408 loops=1)
         Filter: (v_recent_events_good.tenant_id = 7)
         Rows Removed by Filter: 291592
         ->  Seq Scan on events (actual rows=300000 loops=1)
               Filter: (created_at >= (now() - '7 days'::interval))
(6 rows)

安全屏障把谓词下推彻底挡死,重新退化为全表扫描。所以 security_barrier 只应加在确实需要它保护的视图上。

sql
-- 复原
ALTER VIEW v_recent_events_good RESET (security_barrier);

第 6 步:清理

sql
DROP VIEW v_recent_events_bad, v_recent_events_good;
DROP TABLE events;

注意事项 ​

  • 视图不会加速查询,也基本不带来额外开销。 性能问题要么在视图定义里,要么在基表上(缺索引)。

  • LIMIT 视图有语义陷阱,外层的 WHERE 不下推,得到的结果与「先过滤再 LIMIT」完全不同。

    sql
    -- 危险!这不是「用户 42 的最新 10 条」
    SELECT * FROM v_recent_events_bad WHERE user_id = 42;
  • 聚合视图能否下推,取决于过滤列是否是分组键。 不是分组键就要先算完整聚合。

  • WITH ... AS MATERIALIZED 是显式的优化边界,用它就意味着放弃谓词下推。

    sql
    -- 不推荐在视图里随手用
    WITH x AS MATERIALIZED ( SELECT ... ) SELECT * FROM x
  • security_barrier 会让索引失效,因为它按设计阻止谓词下推。只在安全优先场景使用。

  • 视图不能建索引。 报错信息是 is not a table or materialized view:

    sql
    -- 错误!
    CREATE INDEX idx_v ON v_orders_paid (user_id);
  • 多层嵌套视图会截断谓词下推链。 层数越多,定位性能问题越难。

  • ALTER VIEW 会让依赖计划失效,避免在高频路径上频繁执行 DDL。

  • 不要用视图隐藏「基表缺索引」的问题。 Seq Scan 是索引问题,不是视图问题。

  • EXPLAIN 一定要看 Subquery Scan。 它是「未合并」的信号,不一定是坏事,但必须解释清楚。

最佳实践 ​

  • 视图里不写 LIMIT / DISTINCT / 聚合,把这些逻辑留给调用方,保持视图可合并:

    sql
    -- 推荐:纯过滤 + 列裁剪
    CREATE VIEW v_orders_paid AS
    SELECT id, user_id, amount FROM orders WHERE status = 'paid';
  • 把「时间窗口」类条件写成视图内的范围过滤,而不是 ORDER BY ... LIMIT:

    sql
    -- 推荐:语义清晰且可合并
    CREATE VIEW v_recent_orders AS
    SELECT id, user_id, amount, created_at FROM orders
    WHERE created_at >= now() - interval '7 days';
  • 用 EXPLAIN (ANALYZE, BUFFERS) 核对「视图查询」与「展开后等价 SQL」的计划是否一致,这是判断视图是否拖慢查询的最直接手段。

    sql
    EXPLAIN (ANALYZE, COSTS OFF, BUFFERS) SELECT * FROM v_x WHERE k = 1;
    EXPLAIN (ANALYZE, COSTS OFF, BUFFERS) SELECT * FROM base WHERE k = 1 AND <视图条件>;
  • security_barrier 视图尽量做窄 + 在视图内部先做高选择性过滤,把不可下推的代价降到最低。

  • 给基表建索引时,先展开视图再设计索引列顺序:

    sql
    -- 视图展开后常见谓词是 tenant_id + created_at
    CREATE INDEX idx_events_tenant_created ON events (tenant_id, created_at DESC);
  • 控制视图嵌套层数(建议不超过 3 层),超过就考虑合并定义或用物化视图。

  • 重聚合、重 JOIN 的报表场景直接上物化视图,不要指望普通视图能「缓存」结果:

    sql
    CREATE MATERIALIZED VIEW mv_daily_report AS
    SELECT tenant_id, date_trunc('day', created_at) AS day, count(*)
    FROM events GROUP BY 1, 2;
  • 定期审计视图查询的计划,用 auto_explain 或慢查询日志捕获退化的视图查询。

  • 把视图当成「接口定义」而非「性能手段」,性能优化始终落在基表索引与查询写法上。

下一节

继续学习:递归视图

参考链接 ​