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

为何关联transaction表时采用Seq Scan而非Index Scan?

为何PostgreSQL关联查询时对transaction表执行全表扫描而非索引扫描?

我有两张需执行全文检索的表:transaction表和transaction_line表。检索始终通过transaction_line表的日期范围进行过滤,随后希望利用主键索引将过滤后的transaction_line记录与transaction表关联,再对关联结果执行全文检索。但当前查询在关联transaction表时使用了Sequential Scan(全表扫描)而非Index Scan(索引扫描)。

给定查询语句

explain analyse with
    t as (select transaction.id, transaction.document as t_doc, transaction_line.document as tl_doc                          from transaction_line
left join transaction on transaction_id = transaction.id
where '2023-12-30' <= date and date <= '2023-12-31')
select id from t where t.t_doc @@ to_tsquery('norwegian', '2400') or
t.tl_doc @@ to_tsquery('norwegian', '2400');

当前查询执行计划

+-------------------------------------------------------------------------------------------------------------------------------+
|QUERY PLAN                                                                                                                     |
+-------------------------------------------------------------------------------------------------------------------------------+
|Hash Left Join  (cost=1301.31..2576.17 rows=36 width=8) (actual time=26.248..28.903 rows=23 loops=1)                           |
|  Hash Cond: (transaction_line.transaction_id = transaction.id)                                                                |
|  Filter: ((transaction.document @@ '''2400'''::tsquery) OR (transaction_line.document @@ '''2400'''::tsquery))                |
|  Rows Removed by Filter: 305                                                                                                  |
|  ->  Bitmap Heap Scan on transaction_line  (cost=12.79..1286.50 rows=439 width=55) (actual time=0.193..2.598 rows=328 loops=1)|
|        Recheck Cond: (('2023-12-30'::date <= date) AND (date <= '2023-12-31'::date))                                          |
|        Heap Blocks: exact=93                                                                                                  |
|        ->  Bitmap Index Scan on date_idx  (cost=0.00..12.68 rows=439 width=0) (actual time=0.135..0.135 rows=328 loops=1)     |
|              Index Cond: ((date >= '2023-12-30'::date) AND (date <= '2023-12-31'::date))                                      |
|  ->  Hash  (cost=1101.01..1101.01 rows=15001 width=63) (actual time=25.853..25.854 rows=15001 loops=1)                        |
|        Buckets: 16384  Batches: 1  Memory Usage: 1531kB                                                                       |
|        ->  Seq Scan on transaction  (cost=0.00..1101.01 rows=15001 width=63) (actual time=0.025..19.925 rows=15001 loops=1)   |
|Planning Time: 5.917 ms                                                                                                        |
|Execution Time: 30.389 ms                                                                                                      |
+-------------------------------------------------------------------------------------------------------------------------------+

预期查询执行计划

+-------------------------------------------------------------------------------------------------------------------------------------+
|QUERY PLAN                                                                                                                           |
+-------------------------------------------------------------------------------------------------------------------------------------+
|Nested Loop Left Join  (cost=13.07..3293.88 rows=36 width=8) (actual time=0.399..6.457 rows=23 loops=1)                              |
|  Filter: ((transaction.document @@ '''2400'''::tsquery) OR (transaction_line.document @@ '''2400'''::tsquery))                      |
|  Rows Removed by Filter: 305                                                                                                        |
|  ->  Bitmap Heap Scan on transaction_line  (cost=12.79..1286.50 rows=439 width=55) (actual time=0.145..2.198 rows=328 loops=1)      |
|        Recheck Cond: (('2023-12-30'::date <= date) AND (date <= '2023-12-31'::date))                                                |
|        Heap Blocks: exact=93                                                                                                        |
|        ->  Bitmap Index Scan on date_idx  (cost=0.00..12.68 rows=439 width=0) (actual time=0.094..0.094 rows=328 loops=1)           |
|              Index Cond: ((date >= '2023-12-30'::date) AND (date <= '2023-12-31'::date))                                            |
|  ->  Index Scan using transaction_pkey on transaction  (cost=0.29..4.56 rows=1 width=63) (actual time=0.012..0.012 rows=1 loops=328)|
|        Index Cond: (id = transaction_line.transaction_id)                                                                           |
|Planning Time: 1.905 ms                                                                                                              |
|Execution Time: 6.625 ms                                                                                                             |
+-------------------------------------------------------------------------------------------------------------------------------------+

补充说明

补充说明(09.11.23-10:20):
目前暂无生产数据,现有15001条transaction记录,预计每个schema每年会产生约1000万条transaction记录,以及2000-3000万条transaction_line记录。由于需生成无间隙序列数据,生成这些量级数据耗时较长。因此,为找到不足10%的transaction记录而执行千万级Seq Scan,成本过高。


解答

核心原因:优化器的成本估算选择了哈希连接

PostgreSQL查询优化器会基于数据量、统计信息和内置成本模型选择连接策略:

  • 当前测试数据中transaction表仅15001条记录,优化器认为全表扫描后构建哈希表的总成本(1101.01)比328次索引扫描+嵌套循环的总成本(估算3293.88)更低,因此选择了Hash Left Join + 全表扫描的方案。
  • 优化器默认认为内存哈希操作的效率高于多次索引查找,尤其是小表场景下。

引导优化器使用嵌套循环+索引扫描的方法

1. 调整查询结构,提前过滤数据

将全文检索条件提前到CTE内部,减少后续连接的数据量,让优化器更倾向于嵌套循环:

explain analyse with
    filtered_lines as (
        -- 先筛选transaction_line中命中全文检索的记录
        select transaction_id, document as tl_doc 
        from transaction_line
        where date between '2023-12-30' and '2023-12-31'
          and document @@ to_tsquery('norwegian', '2400')
        union all
        -- 再筛选需要关联transaction表做检索的记录
        select transaction_id, null as tl_doc
        from transaction_line
        where date between '2023-12-30' and '2023-12-31'
          and not document @@ to_tsquery('norwegian', '2400')
    )
select t.id
from filtered_lines fl
left join transaction t on fl.transaction_id = t.id
where fl.tl_doc is not null or t.document @@ to_tsquery('norwegian', '2400');

2. 临时调整优化器参数(会话级别)

通过参数降低哈希连接优先级,或提升索引扫描的竞争力:

-- 临时禁用哈希连接,仅当前会话生效
set enable_hashjoin = off;

-- 或降低随机页读取成本(默认4),让索引扫描成本更具优势
set random_page_cost = 1.1;

注意:参数调整仅建议用于测试,生产环境需结合实际数据量评估。

3. 更新统计信息

确保优化器掌握准确的数据分布:

analyse transaction;
analyse transaction_line;

4. 生产量级数据的自动优化

当transaction表达到千万级规模时,全表扫描的成本会急剧上升,此时328次索引扫描的总成本会远低于全表扫描,优化器会自动选择嵌套循环+索引扫描的方案,无需手动干预。


内容的提问来源于stack exchange,提问作者regine.urtegard

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 15:24:54