You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

优化PostgreSQL物化视图查询,将执行时间降至50秒内

优化PostgreSQL物化视图查询至50秒内的方案

背景

我有一个PostgreSQL物化视图,已通过调整连接结构、移除过滤字段类型转换、将外层WHERE子句迁移至CTE等方式优化查询执行时间,现在需要进一步优化,将执行时间降至50秒以内。

原始物化视图代码

-- public.reporting_user_activity_contents source

CREATE MATERIALIZED VIEW public.reporting_user_activity_contents
TABLESPACE pg_default
AS WITH sub_q AS (
         SELECT u.id AS user_id,
            t.id AS tenant_id,
            t.global_tenant_id,
            m.id AS content_id,
            m.title AS content_title,
            um.id AS user_mission_id,
            mk.id AS content_kind_id,
            mk.kind AS content_kind,
            gp.path_id,
            gp.group_id AS assigned_through_group_id,
            COALESCE(ugpr.hidden, false) AS hidden,
                CASE
                    WHEN uv.created_at IS NOT NULL THEN uv.created_at
                    WHEN uv.created_at IS NULL THEN ugpr.viewed_at
                    ELSE NULL::timestamp without time zone
                END AS first_viewed_at,
                CASE
                    WHEN um.completed_at IS NOT NULL THEN um.completed_at
                    WHEN um.completed_at IS NULL THEN ugpr.completed_at
                    ELSE NULL::timestamp without time zone
                END AS first_completed_at,
                CASE
                    WHEN ugpr.discussed_at IS NOT NULL THEN ugpr.discussed_at
                    ELSE NULL::timestamp without time zone
                END AS first_discussed_at,
                CASE
                    WHEN gpr.discussable THEN gpr.discussable
                    ELSE false
                END AS discussible,
            ( SELECT to_jsonb(array_to_json(array_agg(row_to_json(t_1.*)))) AS to_jsonb
                   FROM ( SELECT tags.id,
                            tags.name,
                            tags.taggings_count
                           FROM tags
                             JOIN taggings ON tags.id = taggings.tag_id
                          WHERE taggings.taggable_id = m.id AND taggings.taggable_type::text = 'Mission'::text AND (taggings.context::text = ANY (ARRAY['tags'::character varying::text, 'skills'::character varying::text, 'skill_groups'::character varying::text, 'levels'::character varying::text]))) t_1) AS tags
           FROM missions m
             JOIN user_missions um ON um.mission_id = m.id AND um.assigned_through_type::text = 'Space'::text
             LEFT JOIN user_views uv ON uv.viewable_id = m.id AND uv.user_id = um.user_id AND uv.viewable_type::text = 'Mission'::text
             JOIN mission_kinds mk ON mk.id = m.mission_kind_id
             JOIN users u ON u.id = um.user_id
             JOIN tenants t ON u.tenant_id = t.id
             LEFT JOIN group_path_resources gpr ON gpr.id = uv.assigned_through_id AND uv.assigned_through_type::text = 'GroupPathResource'::text AND gpr.resourceable_type::text = 'Mission'::text AND gpr.resourceable_id = um.mission_id
             LEFT JOIN group_path_sections gps ON gps.id = gpr.group_path_section_id
             LEFT JOIN group_paths gp ON gp.id = gps.group_path_id
             LEFT JOIN user_group_path_resources ugpr ON ugpr.group_path_resource_id = gpr.id AND ugpr.resourceable_id = um.id AND ugpr.resourceable_type::text = 'UserMission'::text
        )
 SELECT sub_q.user_id,
    sub_q.tenant_id,
    sub_q.global_tenant_id,
    sub_q.content_id,
    sub_q.content_title,
    sub_q.user_mission_id,
    sub_q.content_kind_id,
    sub_q.content_kind,
    sub_q.path_id,
    sub_q.assigned_through_group_id,
    sub_q.discussible,
    sub_q.tags,
    min(sub_q.first_viewed_at) AS first_viewed_at,
    min(sub_q.first_completed_at) AS first_completed_at,
    min(sub_q.first_discussed_at) AS first_discussed_at,
    bool_and(sub_q.hidden) AS hidden
   FROM sub_q
  WHERE sub_q.tenant_id IS NOT NULL
  GROUP BY sub_q.user_id, sub_q.tenant_id, sub_q.global_tenant_id, sub_q.content_id, sub_q.content_title, sub_q.user_mission_id, sub_q.content_kind_id, sub_q.content_kind, sub_q.path_id, sub_q.assigned_through_group_id, sub_q.discussible, sub_q.tags
WITH DATA;

视图索引

CREATE INDEX idx_u_t_gt_c_atg ON public.reporting_user_activity_contents USING btree (content_id, user_mission_id, user_id, tenant_id, global_tenant_id, assigned_through_group_id, path_id);
CREATE INDEX ruac_atg_p ON public.reporting_user_activity_contents USING btree (assigned_through_group_id, path_id);
CREATE INDEX ruac_t_gt ON public.reporting_user_activity_contents USING btree (tenant_id, global_tenant_id);
CREATE INDEX ruac_u_atg ON public.reporting_user_activity_contents USING btree (user_id, assigned_through_group_id);

修改后的查询语句

explain analyze
WITH sub_q AS (
         SELECT u.id AS user_id,
            t.id AS tenant_id,
            t.global_tenant_id,
            m.id AS content_id,
            m.title AS content_title,
            um.id AS user_mission_id,
            mk.id AS content_kind_id,
            mk.kind AS content_kind,
            gp.path_id,
            gp.group_id AS assigned_through_group_id,
            COALESCE(ugpr.hidden, false) AS hidden,
            
                COALESCE(uv.created_at, ugpr.viewed_at) AS first_viewed_at,
             
                COALESCE(um.completed_at, ugpr.completed_at) AS first_completed_at,
               
                ugpr.discussed_at AS first_discussed_at,
                
                COALESCE(gpr.discussable, false) AS discussible,
            
           (
                SELECT jsonb_agg(jsonb_build_object('id', tags.id, 'name', tags.name, 'taggings_count', tags.taggings_count))
                FROM taggings
                JOIN tags ON tags.id = taggings.tag_id
                WHERE taggings.taggable_id = m.id
                AND taggings.taggable_type = 'Mission'
                AND taggings.context = ANY (ARRAY['tags', 'skills', 'skill_groups', 'levels'])
            ) AS tags       
            FROM tenants t
            JOIN users u ON u.tenant_id = t.id
            JOIN user_missions um ON um.user_id = u.id AND um.assigned_through_type = 'Space'
            JOIN missions m ON m.id = um.mission_id
            LEFT JOIN user_views uv ON uv.user_id = u.id AND uv.viewable_id = m.id AND uv.viewable_type = 'Mission'
            LEFT JOIN user_group_path_resources ugpr ON ugpr.resourceable_id = um.id AND ugpr.resourceable_type = 'UserMission'
            LEFT JOIN group_path_resources gpr ON gpr.id = uv.assigned_through_id AND uv.assigned_through_type = 'GroupPathResource' AND gpr.resourceable_type = 'Mission' AND gpr.resourceable_id = um.mission_id
            LEFT JOIN group_path_sections gps ON gps.id = gpr.group_path_section_id
            LEFT JOIN group_paths gp ON gp.id = gps.group_path_id
            LEFT JOIN mission_kinds mk ON mk.id = m.mission_kind_id
            WHERE t.id IS NOT NULL 

        )
 SELECT sub_q.user_id,
    sub_q.tenant_id,
    sub_q.global_tenant_id,
    sub_q.content_id,
    sub_q.content_title,
    sub_q.user_mission_id,
    sub_q.content_kind_id,
    sub_q.content_kind,
    sub_q.path_id,
    sub_q.assigned_through_group_id,
    sub_q.discussible,
    sub_q.tags,
    min(sub_q.first_viewed_at) AS first_viewed_at,
    min(sub_q.first_completed_at) AS first_completed_at,
    min(sub_q.first_discussed_at) AS first_discussed_at,
    bool_and(sub_q.hidden) AS hidden
   FROM sub_q
  GROUP BY sub_q.user_id, sub_q.tenant_id, sub_q.global_tenant_id, sub_q.content_id, sub_q.content_title, 
           sub_q.user_mission_id, sub_q.content_kind_id, sub_q.content_kind, sub_q.path_id, sub_q.assigned_through_group_id, sub_q.discussible, sub_q.tags;

执行计划分析

从执行计划来看,主要耗时点集中在:

  • HashAggregate操作处理大量数据时内存不足,导致磁盘IO开销激增
  • 标签聚合的关联子查询逐行执行,重复扫描taggings和tags表
  • 部分JOIN操作未利用合适索引,触发全表扫描

优化方案

1. 重构标签聚合逻辑,避免关联子查询

将逐行执行的标签子查询改为预聚合JOIN方式,减少重复扫描:

WITH tag_agg AS (
    SELECT 
        taggings.taggable_id AS mission_id,
        jsonb_agg(jsonb_build_object('id', tags.id, 'name', tags.name, 'taggings_count', tags.taggings_count)) AS tags
    FROM taggings
    JOIN tags ON tags.id = taggings.tag_id
    WHERE taggings.taggable_type = 'Mission'
      AND taggings.context = ANY (ARRAY['tags', 'skills', 'skill_groups', 'levels'])
    GROUP BY taggings.taggable_id
),
sub_q AS (
    SELECT u.id AS user_id,
           t.id AS tenant_id,
           t.global_tenant_id,
           m.id AS content_id,
           m.title AS content_title,
           um.id AS user_mission_id,
           mk.id AS content_kind_id,
           mk.kind AS content_kind,
           gp.path_id,
           gp.group_id AS assigned_through_group_id,
           COALESCE(ugpr.hidden, false) AS hidden,
           COALESCE(uv.created_at, ugpr.viewed_at) AS first_viewed_at,
           COALESCE(um.completed_at, ugpr.completed_at) AS first_completed_at,
           ugpr.discussed_at AS first_discussed_at,
           COALESCE(gpr.discussable, false) AS discussible,
           tag_agg.tags
    FROM tenants t
    JOIN users u ON u.tenant_id = t.id
    JOIN user_missions um ON um.user_id = u.id AND um.assigned_through_type = 'Space'
    JOIN missions m ON m.id = um.mission_id
    LEFT JOIN tag_agg ON m.id = tag_agg.mission_id
    LEFT JOIN user_views uv ON uv.user_id = u.id AND uv.viewable_id = m.id AND uv.viewable_type = 'Mission'
    LEFT JOIN user_group_path_resources ugpr ON ugpr.resourceable_id = um.id AND ugpr.resourceable_type = 'UserMission'
    LEFT JOIN group_path_resources gpr ON gpr.id = uv.assigned_through_id AND uv.assigned_through_type = 'GroupPathResource' AND gpr.resourceable_type = 'Mission' AND gpr.resourceable_id = um.mission_id
    LEFT JOIN group_path_sections gps ON gps.id = gpr.group_path_section_id
    LEFT JOIN group_paths gp ON gp.id = gps.group_path_id
    LEFT JOIN mission_kinds mk ON mk.id = m.mission_kind_id
    WHERE t.id IS NOT NULL
)
SELECT sub_q.user_id,
       sub_q.tenant_id,
       sub_q.global_tenant_id,
       sub_q.content_id,
       sub_q.content_title,
       sub_q.user_mission_id,
       sub_q.content_kind_id,
       sub_q.content_kind,
       sub_q.path_id,
       sub_q.assigned_through_group_id,
       sub_q.discussible,
       sub_q.tags,
       min(sub_q.first_viewed_at) AS first_viewed_at,
       min(sub_q.first_completed_at) AS first_completed_at,
       min(sub_q.first_discussed_at) AS first_discussed_at,
       bool_and(sub_q.hidden) AS hidden
FROM sub_q
GROUP BY sub_q.user_id, sub_q.tenant_id, sub_q.global_tenant_id, sub_q.content_id, sub_q.content_title, 
         sub_q.user_mission_id, sub_q.content_kind_id, sub_q.content_kind, sub_q.path_id, sub_q.assigned_through_group_id, sub_q.discussible, sub_q.tags;

2. 添加针对性索引,加速JOIN与过滤

为核心关联表创建过滤条件+关联字段的复合索引:

-- 加速user_missions的过滤与连接
CREATE INDEX idx_user_missions_user_id_space ON user_missions(user_id) WHERE assigned_through_type = 'Space';
-- 加速users与tenants的连接
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- 加速missions与user_missions、mission_kinds的连接
CREATE INDEX idx_missions_id_kind ON missions(id, mission_kind_id);
-- 加速user_views的查找
CREATE INDEX idx_user_views_user_mission ON user_views(user_id, viewable_id) WHERE viewable_type = 'Mission';
-- 加速taggings的聚合查询
CREATE INDEX idx_taggings_mission_context ON taggings(taggable_id, taggable_type, context);

3. 优化聚合操作的内存配置

临时提高work_mem参数,让HashAggregate在内存中完成,避免磁盘排序:

-- 根据服务器内存情况调整数值,执行查询前临时设置
SET work_mem = '64MB';

4. 减少聚合前的数据量

  • 检查连接逻辑,确保所有LEFT JOIN条件严谨,避免生成不必要的笛卡尔积
  • 若业务允许,在CTE中提前过滤冗余记录(如添加时间范围过滤)

5. 更新表统计信息

让优化器生成更准确的执行计划:

ANALYZE user_missions, users, missions, taggings, tags, user_views, group_path_resources;

6. 物化视图增量刷新(可选)

若物化视图不需要实时数据,改用增量刷新替代全量重建:

  • 用触发器跟踪源表变化,仅同步新增/修改的数据
  • 借助pg_cron定期执行增量同步,降低每次重建的开销

内容的提问来源于stack exchange,提问作者Ankit Agrawal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 10:57:02