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

PostgreSQL 13.2多JOIN函数性能优化方案咨询

PostgreSQL 13.2 查询性能优化分析

问题背景

生产环境中用于获取deck列表的复杂查询响应缓慢,已将函数参数硬编码以生成详细执行计划。

原始查询

SELECT community_deck.approved_at,
        community_deck.approval_status,
        community_deck.is_public,
        community_deck.owner_sub,
        decks.id,
        decks.title,
        decks.owner,
        decks.share_id,
        decks.objective,
        decks.description,
        decks.updated_at,
        decks.frontend_id,
        af.field,
        users.avatar,
        users.quote,
        users.nickname,
        users.picture,
        users.profile_picture,
        ud.background_color,
        ud.text_color,
        count(DISTINCT dc.card_id) as number_of_cards,
        count(DISTINCT dr.user_id) as number_of_likes,
        count(DISTINCT dd.user_id) as number_of_unique_downloads
 from community_deck
          left join decks on decks.id = community_deck.deck_id
          left join decks_cards dc on decks.id = dc.deck_id
          left join deck_reaction dr on decks.id = dr.deck_id
          left join academic_fields af on af.id = decks.category
          left join users on users.sub = decks.owner
          left join user_deck ud on decks.id = ud.deck_id AND ud.user_id = decks.owner
          left join deck_download dd on decks.id = dd.deck_id
 WHERE community_deck.is_public = true
   AND community_deck.approval_status = 'approved'
   AND (null is null OR af.field = null)
 group by decks.id, af.field, users.avatar, users.quote, users.nickname, users.picture,
          users.profile_picture,
          ud.background_color, ud.text_color, decks.created_at, community_deck.approved_at,
          community_deck.approval_status, community_deck.is_public, community_deck.owner_sub
 ORDER BY approved_at DESC;

执行计划(已翻译)

GroupAggregate  (预估成本: 314.33..396.91, 预估行数: 1573, 宽度: 410) (实际耗时: 517.521..945.153, 实际行数: 49, 循环次数: 1)
  分组键: community_deck.approved_at, decks.id, af.field, users.avatar, users.quote, users.nickname, users.picture, users.profile_picture, ud.background_color, ud.text_color, community_deck.approval_status, community_deck.is_public, community_deck.owner_sub
  缓存命中: 共享缓存命中=14643, 临时缓存读取=5777, 临时缓存写入=5793
  ->  Sort  (预估成本: 314.33..318.26, 预估行数: 1573, 宽度: 474) (实际耗时: 517.373..770.206, 实际行数: 81653, 循环次数: 1)
        排序键: community_deck.approved_at DESC, decks.id, af.field, users.avatar, users.quote, users.nickname, users.picture, users.profile_picture, ud.background_color, ud.text_color, community_deck.is_public, community_deck.owner_sub
        排序方式: 外部归并排序 磁盘占用: 39152kB
        缓存命中: 共享缓存命中=14643, 临时缓存读取=5777, 临时缓存写入=5793
        ->  Nested Loop Left Join  (预估成本: 102.01..230.81, 预估行数: 1573, 宽度: 474) (实际耗时: 3.146..78.871, 实际行数: 81653, 循环次数: 1)
              缓存命中: 共享缓存命中=14629
              ->  Nested Loop Left Join  (预估成本: 101.72..140.56, 预估行数: 42, 宽度: 470) (实际耗时: 3.111..19.517, 实际行数: 801, 循环次数: 1)
                    缓存命中: 共享缓存命中=4874
                    ->  Nested Loop Left Join  (预估成本: 101.44..124.22, 预估行数: 42, 宽度: 457) (实际耗时: 3.058..15.053, 实际行数: 801, 循环次数: 1)
                          缓存命中: 共享缓存命中=2471
                          ->  Hash Left Join  (预估成本: 101.16..106.63, 预估行数: 42, 宽度: 303) (实际耗时: 2.998..5.190, 实际行数: 801, 循环次数: 1)
                                哈希条件: (decks.category = af.id)
                                缓存命中: 共享缓存命中=68
                                ->  Hash Right Join  (预估成本: 99.56..104.91, 预估行数: 42, 宽度: 307) (实际耗时: 2.873..4.179, 实际行数: 801, 循环次数: 1)
                                      哈希条件: (dd.deck_id = decks.id)
                                      缓存命中: 共享缓存命中=64
                                      ->  全表扫描 deck_download dd  (预估成本: 0.00..4.69, 预估行数: 169, 宽度: 46) (实际耗时: 0.011..0.163, 实际行数: 194, 循环次数: 1)
                                            缓存命中: 共享缓存命中=3
                                      ->  哈希表  (预估成本: 99.03..99.03, 预估行数: 42, 宽度: 265) (实际耗时: 2.844..2.851, 实际行数: 139, 循环次数: 1)
                                            桶数: 1024  批次: 1  内存占用: 50kB
                                            缓存命中: 共享缓存命中=61
                                            ->  Hash Right Join  (预估成本: 95.29..99.03, 预估行数: 42, 宽度: 265) (实际耗时: 2.502..2.712, 实际行数: 139, 循环次数: 1)
                                                  哈希条件: (dr.deck_id = decks.id)
                                                  缓存命中: 共享缓存命中=61
                                                  ->  全表扫描 deck_reaction dr  (预估成本: 0.00..3.25, 预估行数: 125, 宽度: 46) (实际耗时: 0.011..0.108, 实际行数: 138, 循环次数: 1)
                                                        缓存命中: 共享缓存命中=2
                                                  ->  哈希表  (预估成本: 94.77..94.77, 预估行数: 42, 宽度: 223) (实际耗时: 2.473..2.477, 实际行数: 49, 循环次数: 1)
                                                        桶数: 1024  批次: 1  内存占用: 21kB
                                                        缓存命中: 共享缓存命中=59
                                                        ->  Hash Right Join  (预估成本: 2.12..94.77, 预估行数: 42, 宽度: 223) (实际耗时: 0.271..2.404, 实际行数: 49, 循环次数: 1)
                                                              哈希条件: (decks.id = community_deck.deck_id)
                                                              缓存命中: 共享缓存命中=59
                                                              ->  全表扫描 decks  (预估成本: 0.00..85.43, 预估行数: 2743, 宽度: 162) (实际耗时: 0.010..1.763, 实际行数: 2780, 循环次数: 1)
                                                                    缓存命中: 共享缓存命中=58
                                                              ->  哈希表  (预估成本: 1.60..1.60, 预估行数: 42, 宽度: 65) (实际耗时: 0.070..0.072, 实际行数: 49, 循环次数: 1)
                                                                    桶数: 1024  批次: 1  内存占用: 13kB
                                                                    缓存命中: 共享缓存命中=1
                                                                    ->  全表扫描 community_deck  (预估成本: 0.00..1.60, 预估行数: 42, 宽度: 65) (实际耗时: 0.019..0.041, 实际行数: 49, 循环次数: 1)
                                                                          过滤条件: (is_public AND (approval_status = 'approved'::text))
                                                                          过滤掉的行数: 4
                                                                          缓存命中: 共享缓存命中=1
                                ->  哈希表  (预估成本: 1.27..1.27, 预估行数: 27, 宽度: 28) (实际耗时: 0.042..0.043, 实际行数: 27, 循环次数: 1)
                                      桶数: 1024  批次: 1  内存占用: 10kB
                                      缓存命中: 共享缓存命中=1
                                      ->  全表扫描 academic_fields af  (预估成本: 0.00..1.27, 预估行数: 27, 宽度: 28) (实际耗时: 0.009..0.015, 实际行数: 27, 循环次数: 1)
                                            缓存命中: 共享缓存命中=1
                          ->  索引扫描 使用 users_sub_uindex 表 users  (预估成本: 0.28..0.42, 预估行数: 1, 宽度: 196) (实际耗时: 0.011..0.011, 实际行数: 1, 循环次数: 801)
                                索引条件: (sub = decks.owner)
                                缓存命中: 共享缓存命中=2403
                    ->  索引扫描 使用 user_deck_pkey 表 user_deck ud  (预估成本: 0.28..0.39, 预估行数: 1, 宽度: 58) (实际耗时: 0.004..0.004, 实际行数: 1, 循环次数: 801)
                          索引条件: ((deck_id = decks.id) AND (user_id = decks.owner))
                          缓存命中: 共享缓存命中=2403
              ->  仅索引扫描 使用 decks_cards_pkey 表 decks_cards dc  (预估成本: 0.29..1.72, 预估行数: 43, 宽度: 8) (实际耗时: 0.006..0.035, 实际行数: 102, 循环次数: 801)
                    索引条件: (deck_id = decks.id)
                    堆读取次数: 8672
                    缓存命中: 共享缓存命中=9755
规划阶段:
  缓存命中: 共享缓存命中=589
规划耗时: 12.864 ms
执行耗时: 955.913 ms

问题分析

  1. 行数膨胀严重:原始查询通过左连接关联decks_cards、deck_reaction、deck_download后,行数从初始的49条暴增至81653条。每个deck对应多张卡片、多个点赞/下载记录,直接连接会产生大量重复行,后续用count(DISTINCT)去重的计算成本极高。
  2. 磁盘排序拖慢速度:排序步骤使用了外部归并排序(磁盘排序),占用近40GB磁盘空间,这是查询耗时的核心原因——内存不足以容纳所有排序数据,只能写入磁盘处理,速度远慢于内存排序。
  3. 无效过滤条件:WHERE子句中的(null is null OR af.field = null)完全多余,null is null恒为true,这个条件不会过滤任何数据,反而增加逻辑判断开销。
  4. 全表扫描 decks:当前对decks表做全表扫描(2780行),虽然数据量不大,但随着业务增长,全表扫描的开销会越来越大,应该基于community_deck.deck_id做精准索引扫描。

优化方案

1. 预聚合统计数据

将卡片数、点赞数、下载数的统计提前在子查询中完成,避免连接时的行数膨胀。主查询只连接每个deck的聚合结果,而非原始明细行,能大幅减少数据量。

2. 避免磁盘排序

临时调整work_mem参数,让排序操作在内存中完成。当前排序用了39MB磁盘,可设置SET work_mem = '64MB';(会话级临时生效),后续可根据实际情况调整全局配置。

3. 删除无效条件

直接去掉WHERE中的(null is null OR af.field = null),简化查询逻辑。

4. 添加针对性索引

在community_deck表上创建复合索引:

CREATE INDEX idx_community_deck_public_approved ON community_deck (is_public, approval_status, deck_id, approved_at);

该索引可直接过滤符合条件的行,同时包含后续排序和连接需要的字段,避免回表查询。

5. 简化GROUP BY

PostgreSQL 10及以上版本支持主键覆盖的GROUP BY简化——decks.id是主键,GROUP BY中只需包含decks.id,其他关联表字段会通过函数依赖自动推导,减少分组计算开销。

优化后查询示例

SELECT 
    cd.approved_at,
    cd.approval_status,
    cd.is_public,
    cd.owner_sub,
    d.id,
    d.title,
    d.owner,
    d.share_id,
    d.objective,
    d.description,
    d.updated_at,
    d.frontend_id,
    af.field,
    u.avatar,
    u.quote,
    u.nickname,
    u.picture,
    u.profile_picture,
    ud.background_color,
    ud.text_color,
    COALESCE(c.card_count, 0) AS number_of_cards,
    COALESCE(l.like_count, 0) AS number_of_likes,
    COALESCE(dl.download_count, 0) AS number_of_unique_downloads
FROM community_deck cd
LEFT JOIN decks d ON d.id = cd.deck_id
LEFT JOIN academic_fields af ON af.id = d.category
LEFT JOIN users u ON u.sub = d.owner
LEFT JOIN user_deck ud ON ud.deck_id = d.id AND ud.user_id = d.owner
-- 预聚合卡片数量
LEFT JOIN (
    SELECT deck_id, COUNT(DISTINCT card_id) AS card_count
    FROM decks_cards
    GROUP BY deck_id
) c ON c.deck_id = d.id
-- 预聚合点赞数量
LEFT JOIN (
    SELECT deck_id, COUNT(DISTINCT user_id) AS like_count
    FROM deck_reaction
    GROUP BY deck_id
) l ON l.deck_id = d.id
-- 预聚合下载数量
LEFT JOIN (
    SELECT deck_id, COUNT(DISTINCT user_id) AS download_count
    FROM deck_download
    GROUP BY deck_id
) dl ON dl.deck_id = d.id
WHERE cd.is_public = true
  AND cd.approval_status = 'approved'
GROUP BY d.id, cd.approved_at, cd.approval_status, cd.is_public, cd.owner_sub,
         af.field, u.avatar, u.quote, u.nickname, u.picture, u.profile_picture,
         ud.background_color, ud.text_color
ORDER BY cd.approved_at DESC;

内容的提问来源于stack exchange,提问作者Kevin Amiranoff

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 14:24:59