根据该篇文章做的实验发现个有趣的现象,所以记录下来。

CREATE TABLE t1 (a INT, b INT);
INSERT INTO t1 SELECT i, 1 FROM generate_series(1, 100000) i;
CREATE TABLE t2 AS SELECT * FROM t1;

EXPLAIN (COSTS OFF, ANALYZE)
   SELECT * FROM t1
   WHERE t1.a IN (SELECT a FROM t2 WHERE t2.b = t1.b AND t1.b = 1);

Hash Join (actual time=85.709..122.494 rows=100000 loops=1)
  Hash Cond: (t1.a = t2.a)
  ->  Seq Scan on t1 (actual time=0.025..13.294 rows=100000 loops=1)
        Filter: (b = 1)
  ->  Hash (actual time=85.652..85.654 rows=100000 loops=1)
        Buckets: 131072 (originally 1024)  Batches: 1 (originally 1)  Memory Usage: 4931kB
        ->  HashAggregate (actual time=51.838..67.200 rows=100000 loops=1)
              Group Key: t2.b, t2.a
              Batches: 1  Memory Usage: 7185kB
              ->  Seq Scan on t2 (actual time=0.023..17.255 rows=100000 loops=1)
                    Filter: (b = 1)
Planning Time: 0.501 ms
Execution Time: 127.278 ms

注意看执行计划有个HashAggregate ,为什么会出现这个呢?一般来说,子查询不允许返回重复值,为什么呢?

因为你想一想嘛,返回重复值有啥好处吗? in (1,1,1) 和 in (1) 有什么区别,明显没有。

所以优化器决定使用HashAggregate来消除子查询结果中可能的重复值,如果该值是唯一值,那么就不会有这个操作。

去重操作默认是开启,该配置项由enable_hashagg 控制,一般是采用 group by 和 DISTINCT ,如果禁用,优化器会强制使用其他聚合方法(如排序聚合),可能导致性能下降。该参数是PostgreSQL性能调优的重要选项之一,通常保持默认开启状态。仅在特殊场景(如内存受限时)可能需要临时关闭。

笔者再次执行的时候,计划计划则发生了改变,这个执行计划才符合文档记录。

我们可以看到由Hash Semi Join 替换了 Hash Join ,而且没有了HashAggregate,执行时间大大缩减了 50ms。

可能是统计信息重新收集后,执行器发现了更好的执行路径。

Hash Semi Join (actual time=32.688..69.145 rows=100000 loops=1)
  Hash Cond: (t1.a = t2.a)
  ->  Seq Scan on t1 (actual time=0.022..14.337 rows=100000 loops=1)
        Filter: (b = 1)
  ->  Hash (actual time=32.502..32.503 rows=100000 loops=1)
        Buckets: 131072  Batches: 1  Memory Usage: 4931kB
        ->  Seq Scan on t2 (actual time=0.015..16.514 rows=100000 loops=1)
              Filter: (b = 1)
Planning Time: 0.179 ms
Execution Time: 73.556 ms