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

PostgreSQL关联最新日期行并GROUP BY查询过慢优化求助

PostgreSQL查询优化:获取setup_part_setting最新行性能问题解决方案

问题描述

关联setup_part_setting表获取created_at最新行的逻辑导致查询速度极慢,无法正常使用。尝试过DISTINCT ON方法,性能依然很差。以下是完整查询及对应的EXPLAIN ANALYZE结果,寻求优化方案。

原查询代码

SELECT *
FROM setup s
FULL JOIN
  (SELECT part_type AS f_part_type,
          p_m_id AS f_p_m_id,
          YEAR AS f_year,
                  model_name AS f_model_name,
                  brand_name AS f_brand_name
   FROM part_model) pm ON s.p_m_part_id = pm.f_p_m_id
FULL JOIN
  (SELECT p_id AS p_id_1,
          p_setup_id AS p_setup_id_1,
          part_type AS part_type_1
            FROM part) p ON s.s_id = p.p_setup_id_1
AND p.part_type_1 = 'Fork'
-- 以下部分导致查询变慢
  LEFT JOIN(
                    SELECT recent.* FROM setup_part_setting
                    INNER JOIN
                    (SELECT s_p_s_id as s_p_s_id_1, 
                    part_id as part_id_1,
                    s_setup_id as s_setup_id_1, 
                    MAX("created_at") as created_at_1
                    FROM setup_part_setting sps_inner_1 GROUP BY 1) recent
                    on recent.s_p_s_id_1 = setup_part_setting.s_p_s_id
                    WHERE
                      setup_part_setting.created_at = recent.created_at_1
                    ) u
                    ON s.s_id = u.s_setup_id_1 AND p.p_id_1 = u.part_id_1   
-- 以上部分导致查询变慢         
WHERE ((f_brand_name ilike ANY (ARRAY [['%%']])
        AND f_model_name ilike ANY (ARRAY [['%%']])
        AND f_year ilike ANY (ARRAY [['%%']]))
        OR (f_brand_name IS NULL
            OR f_model_name IS NULL
            OR f_year IS NULL))
ORDER BY s.created_at DESC
LIMIT 5;

原查询执行计划(EXPLAIN ANALYZE)

Limit  (cost=694.55..694.57 rows=5 width=277) (actual time=206.186..206.191 rows=5 loops=1)
→  Sort  (cost=694.55..710.39 rows=6334 width=277) (actual time=206.184..206.189 rows=5 loops=1)
Sort Key: s.created_at DESC
Sort Method: top-N heapsort  Memory: 27kB
→  Hash Left Join  (cost=220.19..589.35 rows=6334 width=277) (actual time=150.088..204.156 rows=6478 loops=1)
Hash Cond: ((s.s_id = sps_inner_1.s_setup_id) AND (part.p_id = sps_inner_1.part_id))
→  Hash Full Join  (cost=209.74..531.38 rows=6334 width=237) (actual time=149.736..201.489 rows=6403 loops=1)
Hash Cond: (s.s_id = part.p_setup_id)
Join Filter: (part.part_type = 'Fork'::text)
Rows Removed by Join Filter: 51
Filter: (((part_model.brand_name ~~* ANY ('{{%%}}'::text[])) AND (part_model.model_name ~~* ANY ('{{%%}}'::text[])) AND (part_model.year ~~* ANY ('{{%%}}'::text[]))) OR (part_model.brand_name IS NULL) OR (part_model.model_name IS NULL) OR (part_model.year IS NULL))
→  Hash Full Join  (cost=205.54..207.00 rows=6335 width=216) (actual time=149.685..151.961 rows=6352 loops=1)
Hash Cond: (s.p_m_part_id = part_model.p_m_id)
→  Seq Scan on setup s  (cost=0.00..1.37 rows=37 width=171) (actual time=0.012..0.022 rows=39 loops=1)
→  Hash  (cost=126.35..126.35 rows=6335 width=45) (actual time=149.643..149.644 rows=6316 loops=1)
Buckets: 8192  Batches: 1  Memory Usage: 568kB
→  Seq Scan on part_model  (cost=0.00..126.35 rows=6335 width=45) (actual time=1.005..147.254 rows=6316 loops=1)
→  Hash  (cost=2.98..2.98 rows=98 width=21) (actual time=0.039..0.040 rows=76 loops=1)
Buckets: 1024  Batches: 1  Memory Usage: 12kB
→  Seq Scan on part  (cost=0.00..2.98 rows=98 width=21) (actual time=0.012..0.026 rows=76 loops=1)
→  Hash  (cost=10.44..10.44 rows=1 width=32) (actual time=0.331..0.333 rows=191 loops=1)
Buckets: 1024  Batches: 1  Memory Usage: 20kB
→  Hash Join  (cost=6.40..10.44 rows=1 width=32) (actual time=0.184..0.289 rows=191 loops=1)
Hash Cond: ((setup_part_setting.s_p_s_id = sps_inner_1.s_p_s_id) AND (setup_part_setting.created_at = (max(sps_inner_1.created_at))))
→  Seq Scan on setup_part_setting  (cost=0.00..3.68 rows=68 width=16) (actual time=0.010..0.036 rows=191 loops=1)
→  Hash  (cost=5.38..5.38 rows=68 width=32) (actual time=0.147..0.148 rows=191 loops=1)
Buckets: 1024  Batches: 1  Memory Usage: 20kB
→  HashAggregate  (cost=4.02..4.70 rows=68 width=32) (actual time=0.085..0.114 rows=191 loops=1)
Group Key: sps_inner_1.s_p_s_id
→  Seq Scan on setup_part_setting sps_inner_1  (cost=0.00..3.68 rows=68 width=32) (actual time=0.002..0.028 rows=191 loops=1)
Planning time: 42.701 ms
Execution time: 207.047 ms

优化方案

1. 修正最新行获取逻辑,改用窗口函数

原查询按s_p_s_id分组取最新行不符合业务需求(应按s_setup_id, part_id分组),且需两次扫描setup_part_setting表。改用ROW_NUMBER()窗口函数,仅需一次扫描即可获取目标最新行:

SELECT *
FROM setup s
FULL JOIN part_model pm 
  ON s.p_m_part_id = pm.p_m_id
FULL JOIN part p 
  ON s.s_id = p.p_setup_id 
  AND p.part_type = 'Fork'
LEFT JOIN (
  SELECT 
    sps.*,
    ROW_NUMBER() OVER (PARTITION BY sps.s_setup_id, sps.part_id ORDER BY sps.created_at DESC) AS rn
  FROM setup_part_setting sps
) u ON s.s_id = u.s_setup_id AND p.p_id = u.part_id AND u.rn = 1
WHERE (
  (pm.brand_name IS NOT NULL AND pm.model_name IS NOT NULL AND pm.year IS NOT NULL)
  OR pm.brand_name IS NULL OR pm.model_name IS NULL OR pm.year IS NULL
)
ORDER BY s.created_at DESC
LIMIT 5;

2. 添加针对性索引

为setup_part_setting创建复合索引,让窗口函数快速定位最新行,避免全表扫描:

CREATE INDEX idx_sps_setup_part_created ON setup_part_setting (s_setup_id, part_id, created_at DESC);

同时检查以下索引是否存在:

  • setup表:s_id(主键/索引)、p_m_part_id(索引)
  • part表:p_setup_id(索引)、part_type(索引,或包含p_setup_id的复合索引)
  • part_model表:p_m_id(主键/索引)

3. 简化查询逻辑

  • 去掉不必要的子查询包装,直接使用原表关联,减少查询解析开销。
  • 优化WHERE条件:ilike ANY (ARRAY [['%%']])等价于IS NOT NULL,替换后避免不必要的模式匹配计算。

4. 调整JOIN类型(可选)

如果业务允许,将FULL JOIN改为LEFT JOIN(比如setup表记录大多能匹配到part_model和part),进一步减少关联开销。


内容的提问来源于stack exchange,提问作者Anton Hörl

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 11:20:05