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
问题分析
- 行数膨胀严重:原始查询通过左连接关联
decks_cards、deck_reaction、deck_download后,行数从初始的49条暴增至81653条。每个deck对应多张卡片、多个点赞/下载记录,直接连接会产生大量重复行,后续用count(DISTINCT)去重的计算成本极高。 - 磁盘排序拖慢速度:排序步骤使用了外部归并排序(磁盘排序),占用近40GB磁盘空间,这是查询耗时的核心原因——内存不足以容纳所有排序数据,只能写入磁盘处理,速度远慢于内存排序。
- 无效过滤条件:WHERE子句中的
(null is null OR af.field = null)完全多余,null is null恒为true,这个条件不会过滤任何数据,反而增加逻辑判断开销。 - 全表扫描 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
相关产品推荐
相关产品推荐

