【PostgreSQL内核学习:Unique 算子源码深度解读学习】

PostgreSQL内核学习:Unique 算子源码深度解读学习

  • [1. 什么是 Unique 算子?](#1. 什么是 Unique 算子?)
    • [1.1 Unique 算子的核心作用](#1.1 Unique 算子的核心作用)
      • [1.1.1 Unique 算子核心定义](#1.1.1 Unique 算子核心定义)
      • [1.1.2 Unique 与 HashAggregate 核心区别](#1.1.2 Unique 与 HashAggregate 核心区别)
    • [1.2 测试环境准备](#1.2 测试环境准备)
    • [1.3 哪些 SQL 会触发 Unique 算子?](#1.3 哪些 SQL 会触发 Unique 算子?)
      • [1.3.1 SELECT DISTINCT](#1.3.1 SELECT DISTINCT)
      • [1.3.2 UNION(默认去重)](#1.3.2 UNION(默认去重))
      • [1.3.3 DISTINCT ON](#1.3.3 DISTINCT ON)
      • [1.3.4 其他常见场景](#1.3.4 其他常见场景)
    • [1.4 Unique 算子出现的核心判定规则](#1.4 Unique 算子出现的核心判定规则)
    • [1.5 参考链接](#1.5 参考链接)
  • [2. Unique 算子解读](#2. Unique 算子解读)
    • [2.1 规划器中的路径决策流程](#2.1 规划器中的路径决策流程)
      • [2.1.1 两种 Path 的核心区别](#2.1.1 两种 Path 的核心区别)
      • [2.1.2 UpperUniquePath 详解(最常见的场景)](#2.1.2 UpperUniquePath 详解(最常见的场景))
        • [**示例 1:普通 `DISTINCT`**](#示例 1:普通 DISTINCT)
        • [**示例 2:`DISTINCT ON`**](#示例 2:DISTINCT ON)
      • [2.1.3 UniquePath 详解(Semi Join 优化场景)](#2.1.3 UniquePath 详解(Semi Join 优化场景))
        • [**示例 1:显式 `DISTINCT + JOIN`**](#示例 1:显式 DISTINCT + JOIN)
    • [2.2 优化器对应源码解析](#2.2 优化器对应源码解析)
      • [2.2.1 create_upper_unique_plan](#2.2.1 create_upper_unique_plan)
      • [2.2.2 make_unique_from_pathkeys](#2.2.2 make_unique_from_pathkeys)
        • [PathKey 是什么?](#PathKey 是什么?)
      • [2.2.3 PathKey 结构体字段详解](#2.2.3 PathKey 结构体字段详解)
      • [2.2.4 PathKey 在 Unique 算子中的作用](#2.2.4 PathKey 在 Unique 算子中的作用)
      • [2.2.5 copy_generic_path_info](#2.2.5 copy_generic_path_info)
      • [2.2.6 create_unique_plan](#2.2.6 create_unique_plan)
    • [2.3 Unique 算子执行器源码解读](#2.3 Unique 算子执行器源码解读)
      • [2.3.1 概述与优化器衔接](#2.3.1 概述与优化器衔接)
      • [2.3.2 主要数据结构](#2.3.2 主要数据结构)
      • [2.3.3 初始化逻辑(`ExecInitUnique`)](#2.3.3 初始化逻辑(ExecInitUnique))
      • [2.3.4 算子核心执行逻辑(`ExecUnique`)](#2.3.4 算子核心执行逻辑(ExecUnique))
      • [2.3.5 资源回收(`ExecEndUnique`)](#2.3.5 资源回收(ExecEndUnique))
      • [2.3.5 重扫描机制(`ExecReScanUnique`)](#2.3.5 重扫描机制(ExecReScanUnique))

声明 :本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。

本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书

1. 什么是 Unique 算子?

  在 PostgreSQL 数据库的 SQL 执行计划中,去重是极其高频的核心能力,主要由两种内核算子实现:Unique 有序去重算子HashAggregate 哈希聚合去重算子 。其中 Unique 算子是 PostgreSQL 传统、轻量的去重实现,也是理解数据库执行器迭代逻辑、有序数据处理的核心切入点。本章将从功能定义、核心特性、适用 SQL 场景、执行计划实例四个维度完成入门铺垫,为后续源码层级的深度解读打下基础。

1.1 Unique 算子的核心作用

1.1.1 Unique 算子核心定义

  Unique 算子是 PostgreSQL 执行器中专门用于有序数据集相邻去重 的核心节点,其核心工作机制可以概括为一句话:仅针对有序输入的元组流,对比相邻元组,剔除重复数据,保留唯一数据。

  不同于全局全量数据比对的去重逻辑,Unique 算子不存储整条数据流的所有元组,仅在内存中缓存上一条已经输出的唯一元组,将当前读取的新元组与缓存元组做等值比对:

  • 若两者完全一致:判定为重复元组,直接丢弃,不向上层节点返回;
  • 若两者不一致:判定为新唯一元组,向上层返回该元组,并更新内存缓存为当前新元组。

  核心强制约束Unique 算子的正常工作完全依赖有序输入。只有输入数据流按照去重字段完成排序,重复元组才会集中相邻,算子才能精准完成去重;若输入数据无序,重复元组分散在数据流不同位置,仅对比相邻元组会导致去重失效,产生错误结果。这也是 Unique 算子最核心、最容易被忽略的底层特性。

1.1.2 Unique 与 HashAggregate 核心区别

  PostgreSQL 针对 DISTINCTUNION 等去重语句,优化器会根据数据量、排序成本、内存配置,自动选择 Unique 有序去重 或 HashAggregate 无序哈希去重 两种方案,二者底层原理、性能特性、适用场景完全不同。下面是两者的详细对比:

对比维度 Unique 算子(有序去重) HashAggregate 算子(无序去重)
输入数据要求 必须有序 (依赖 Sort 或索引有序扫描) 无需有序,支持任意数据流
去重原理 相邻元组比对,仅缓存上一条去重键 构建哈希表,全局存储所有已出现键值
内存开销 极低(仅保存单条元组的键值) 较高(需存储所有唯一键值,数据量大时内存敏感)
主要性能开销 前置排序开销,去重阶段几乎无额外开销 哈希计算、表读写、冲突处理,无排序开销
大数据量表现 排序开销随数据量显著增长 更稳定,但内存占用可能导致 HashAggregate 溢出
典型执行计划 Sort + Unique 直接扫描 + HashAggregate
适用场景 数据量中等、有序索引、排序成本较低 大数据量、无序数据、内存充足

  优化器选型逻辑 :当排序成本较低(数据量不大、存在索引等)时,优先选择 Unique;当排序成本过高或数据量极大时,倾向于选择 HashAggregate

提示 :在实际测试中,如果希望强制让优化器走 Unique 路径,可执行 SET enable_hashagg = off;。演示完成后可使用 RESET enable_hashagg; 恢复默认设置。

1.2 测试环境准备

  以下是完整的建表和数据插入脚本,可直接复制执行:

sql 复制代码
-- 创建测试表
DROP TABLE IF EXISTS test_user;
DROP TABLE IF EXISTS test_user_bak;

CREATE TABLE test_user (
    id          SERIAL PRIMARY KEY,
    user_name   VARCHAR(50),
    email       VARCHAR(100),
    age         INT,
    city        VARCHAR(50),
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE test_user_bak (LIKE test_user);

-- 创建索引(有助于触发有序扫描)
CREATE INDEX idx_test_user_email ON test_user(email);
CREATE INDEX idx_test_user_city_age ON test_user(city, age);

-- 插入测试数据(含重复数据)
INSERT INTO test_user (user_name, email, age, city)
SELECT 
    'user_' || (i % 500 + 1),
    'user_' || (i % 300 + 1) || '@example.com',
    20 + (i % 40),
    CASE 
        WHEN i % 5 = 0 THEN 'Singapore'
        WHEN i % 5 = 1 THEN 'Beijing'
        WHEN i % 5 = 2 THEN 'Shanghai'
        WHEN i % 5 = 3 THEN 'Tokyo'
        ELSE 'Seoul'
    END
FROM generate_series(1, 10000) AS s(i);

INSERT INTO test_user_bak 
SELECT * FROM test_user LIMIT 6000;

ANALYZE test_user;
ANALYZE test_user_bak;

1.3 哪些 SQL 会触发 Unique 算子?

  以下是典型触发场景(建议配合 SET enable_hashagg = off; 演示以更稳定地看到 Unique):

1.3.1 SELECT DISTINCT

sql 复制代码
SET enable_hashagg = off;

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) 
SELECT DISTINCT city FROM test_user;

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) 
SELECT DISTINCT city, age FROM test_user 
ORDER BY city, age;

执行计划

sql 复制代码
postgres=# SET enable_hashagg = off;
SET
postgres=#
postgres=# EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
postgres-# SELECT DISTINCT city FROM test_user;
                                                    QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Unique (actual time=0.089..5.571 rows=5.00 loops=1)
   Buffers: shared hit=11
   ->  Index Only Scan using idx_test_user_city_age on test_user (actual time=0.086..3.857 rows=10000.00 loops=1)
         Heap Fetches: 0
         Index Searches: 1
         Buffers: shared hit=11
 Planning Time: 0.129 ms
 Execution Time: 5.617 ms
(8 rows)

postgres=#
postgres=# EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
postgres-# SELECT DISTINCT city, age FROM test_user
postgres-# ORDER BY city, age;
                                                    QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Unique (actual time=0.057..5.737 rows=40.00 loops=1)
   Buffers: shared hit=11
   ->  Index Only Scan using idx_test_user_city_age on test_user (actual time=0.054..3.500 rows=10000.00 loops=1)
         Heap Fetches: 0
         Index Searches: 1
         Buffers: shared hit=11
 Planning:
   Buffers: shared hit=3
 Planning Time: 0.224 ms
 Execution Time: 5.780 ms
(10 rows)

1.3.2 UNION(默认去重)

sql 复制代码
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) 
SELECT id, email, city FROM test_user
UNION
SELECT id, email, city FROM test_user_bak;

执行计划

sql 复制代码
postgres=# EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
postgres-# SELECT id, email, city FROM test_user
postgres-# UNION
postgres-# SELECT id, email, city FROM test_user_bak;
                                         QUERY PLAN
---------------------------------------------------------------------------------------------
 Unique (actual time=28.090..38.535 rows=10000.00 loops=1)
   Buffers: shared hit=172
   ->  Sort (actual time=28.087..29.645 rows=16000.00 loops=1)
         Sort Key: test_user.id, test_user.email, test_user.city
         Sort Method: quicksort  Memory: 1200kB
         Buffers: shared hit=172
         ->  Append (actual time=0.026..9.803 rows=16000.00 loops=1)
               Buffers: shared hit=172
               ->  Seq Scan on test_user (actual time=0.025..4.755 rows=10000.00 loops=1)
                     Buffers: shared hit=107
               ->  Seq Scan on test_user_bak (actual time=0.028..2.860 rows=6000.00 loops=1)
                     Buffers: shared hit=65
 Planning Time: 0.237 ms
 Execution Time: 40.369 ms
(14 rows)

1.3.3 DISTINCT ON

sql 复制代码
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) 
SELECT id, email, city FROM test_user
UNION
SELECT id, email, city FROM test_user_bak;

执行计划

sql 复制代码
postgres=# EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
postgres-# SELECT id, email, city FROM test_user
postgres-# UNION
postgres-# SELECT id, email, city FROM test_user_bak;
                                         QUERY PLAN
---------------------------------------------------------------------------------------------
 Unique (actual time=28.011..38.473 rows=10000.00 loops=1)
   Buffers: shared hit=172
   ->  Sort (actual time=28.008..29.558 rows=16000.00 loops=1)
         Sort Key: test_user.id, test_user.email, test_user.city
         Sort Method: quicksort  Memory: 1200kB
         Buffers: shared hit=172
         ->  Append (actual time=0.027..9.778 rows=16000.00 loops=1)
               Buffers: shared hit=172
               ->  Seq Scan on test_user (actual time=0.027..4.785 rows=10000.00 loops=1)
                     Buffers: shared hit=107
               ->  Seq Scan on test_user_bak (actual time=0.020..2.797 rows=6000.00 loops=1)
                     Buffers: shared hit=65
 Planning Time: 0.224 ms
 Execution Time: 40.295 ms
(14 rows)

1.3.4 其他常见场景

sql 复制代码
-- 带有 ORDER BY 的 DISTINCT
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) 
SELECT DISTINCT city, age FROM test_user ORDER BY city, age;

-- CTE 中的 DISTINCT
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) 
WITH distinct_cities AS (
    SELECT DISTINCT city FROM test_user
)
SELECT * FROM distinct_cities;

执行计划

sql 复制代码
postgres=# -- 带有 ORDER BY 的 DISTINCT
postgres=# EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
postgres-# SELECT DISTINCT city, age FROM test_user ORDER BY city, age;
-- CTE 中的 DISTINCT
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
WITH distinct_cities AS (
    SELECT DISTINCT city FROM test_user
)
SELECT * FROM distinct_cities;                                                    QUERY PLAN                                            
------------------------------------------------------------------------------------------------------------------
 Unique (actual time=0.084..5.372 rows=40.00 loops=1)
   Buffers: shared hit=11
   ->  Index Only Scan using idx_test_user_city_age on test_user (actual time=0.080..3.278 rows=10000.00 loops=1)
         Heap Fetches: 0
         Index Searches: 1
         Buffers: shared hit=11
 Planning Time: 0.124 ms
 Execution Time: 5.411 ms
(8 rows)

postgres=#
postgres=# -- CTE 中的 DISTINCT
postgres=# EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
postgres-# WITH distinct_cities AS (
postgres(#     SELECT DISTINCT city FROM test_user
postgres(# )
postgres-# SELECT * FROM distinct_cities;
                                                    QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Unique (actual time=0.079..5.922 rows=5.00 loops=1)
   Buffers: shared hit=11
   ->  Index Only Scan using idx_test_user_city_age on test_user (actual time=0.076..4.133 rows=10000.00 loops=1)
         Heap Fetches: 0
         Index Searches: 1
         Buffers: shared hit=11
 Planning Time: 0.143 ms
 Execution Time: 5.960 ms
(8 rows)

1.4 Unique 算子出现的核心判定规则

  1. SQL 语义需要全局去重(DISTINCTUNIONDISTINCT ON 等);
  2. 优化器评估排序成本低于哈希去重成本(或强制关闭 HashAggregate);
  3. 可以通过 Sort 节点或索引扫描提供有序输入流。

  满足以上条件时,PostgreSQL 就会生成 Sort + Unique 的经典执行计划组合。

1.5 参考链接

2. Unique 算子解读

2.1 规划器中的路径决策流程

  PostgreSQL 优化器采用基于代价(Cost-based) 的优化策略。在生成去重计划时,会同时考虑 UniquePathGroupingPath(对应 HashAggregate)两种路径,最终选择代价更低的那个。

  以下是 create_plan_recurse 函数中处理 Unique 路径的核心代码片段:

c 复制代码
/*
 * create_plan_recurse
 *	  Recursive guts of create_plan().
 */
static Plan *
create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
{
	Plan	   *plan;

	/* Guard against stack overflow due to overly complex plans */
	check_stack_depth();

	switch (best_path->pathtype)
	{
		......
		case T_Unique:
			if (IsA(best_path, UpperUniquePath))
			{
				plan = (Plan *) create_upper_unique_plan(root,
														 (UpperUniquePath *) best_path,
														 flags);
			}
			else
			{
				Assert(IsA(best_path, UniquePath));
				plan = create_unique_plan(root,
										  (UniquePath *) best_path,
										  flags);
			}
			break;
		......
		default:
			elog(ERROR, "unrecognized node type: %d",
				 (int) best_path->pathtype);
			plan = NULL;		/* keep compiler quiet */
			break;
	}

	return plan;
}

  在 create_plan_recurse 函数中,我们看到对 T_Unique 的处理分支:

c 复制代码
case T_Unique:
    if (IsA(best_path, UpperUniquePath))
    {
        plan = (Plan *) create_upper_unique_plan(root,
                                                 (UpperUniquePath *) best_path,
                                                 flags);
    }
    else
    {
        Assert(IsA(best_path, UniquePath));
        plan = create_unique_plan(root,
                                  (UniquePath *) best_path,
                                  flags);
    }
    break;

为什么 PostgreSQL 要设计两种不同的 Unique Path 类型?

  很多人会误以为 UniquePathUpperUniquePath 只是同一事物的两种写法,实际上它们代表了两种完全不同的优化场景。在 PostgreSQL Optimizer 中,存在两种看起来非常相似的路径节点:

c 复制代码
UniquePath
UpperUniquePath

  很多人容易认为:

c 复制代码
UniquePath     = 普通 DISTINCT
UpperUniquePath = 高级 DISTINCT

  或者:

c 复制代码
UniquePath     = 查询下层去重
UpperUniquePath = 查询上层去重

  实际上这些理解都不准确。PostgreSQL 设计这两种 Path 的真正原因是:它们服务于两类完全不同的优化场景。并不是按照"去重发生在哪一层"划分,而是按照:去重是不是用户SQL显式要求的结果来划分。

2.1.1 两种 Path 的核心区别

Path 类型 用途场景 典型 SQL 示例 最终优化目标 调用函数
UpperUniquePath 上层去重(用户可见的 DISTINCT SELECT DISTINCT、DISTINCT ON 减少最终结果集行数 create_upper_unique_plan()
UniquePath 内部去重优化(Semi Join 场景) IN、EXISTS、Semi Join、Anti Join 减少参与 Join 的中间数据量 create_unique_plan()

一句话总结:

  • UpperUniquePath:服务于用户查询语义中的去重,是最终结果的一部分。
  • UniquePath:服务于查询优化 ,去重只是中间手段 ,用户 SQL 中通常不出现 DISTINCT

2.1.2 UpperUniquePath 详解(最常见的场景)

  UpperUniquePath 对应的是"Upper Relation"层面的去重,即处理 GROUP BYDISTINCTDISTINCT ONORDER BY 等上层操作。

示例 1:普通 DISTINCT

  这是用户明确要求的结果,因此 Planner 必须生成去重路径,如下所示:

sql 复制代码
EXPLAIN (COSTS OFF) 
SELECT DISTINCT city FROM test_user;
                           QUERY PLAN
-----------------------------------------------------------------
 Unique
   ->  Index Only Scan using idx_test_user_city_age on test_user
(2 rows)

规划器处理流程:

  • 构造 UpperUniquePath
  • 最终调用 create_upper_unique_plan()
  • 执行计划通常为:Unique + Sort(或有序索引扫描)
示例 2:DISTINCT ON
sql 复制代码
EXPLAIN (COSTS OFF) 
SELECT DISTINCT ON (city) city, age 
FROM test_user 
ORDER BY city, age DESC;
                              QUERY PLAN
-----------------------------------------------------------------------
 Unique
   ->  Incremental Sort
         Sort Key: city, age DESC
         Presorted Key: city
         ->  Index Only Scan using idx_test_user_city_age on test_user
(5 rows)

  同样生成 UpperUniquePath,因为 DISTINCT ON 本质是"分组内取第一条 "的上层去重语义。

  共同特点 :去重是查询的最终语义,结果行数会显著减少(例如 10000 行 → 5 个城市)。

2.1.3 UniquePath 详解(Semi Join 优化场景)

  UniquePath 主要用于子查询去重优化,尤其是在 Semi Join(半连接)场景中。

示例 1:显式 DISTINCT + JOIN
sql 复制代码
EXPLAIN 
SELECT * 
FROM test_user u
JOIN (
    SELECT DISTINCT email FROM test_user_bak
) t ON u.email = t.email;
                                        QUERY PLAN
-------------------------------------------------------------------------------------------
 Hash Join  (cost=535.27..768.89 rows=10000 width=71)
   Hash Cond: ((u.email)::text = (test_user_bak.email)::text)
   ->  Seq Scan on test_user u  (cost=0.00..207.00 rows=10000 width=51)
   ->  Hash  (cost=531.52..531.52 rows=300 width=20)
         ->  Unique  (cost=501.52..531.52 rows=300 width=20)         ← 显式 DISTINCT 强制生成
               ->  Sort  (cost=501.52..516.52 rows=6000 width=20)
                     Sort Key: test_user_bak.email
                     ->  Seq Scan on test_user_bak  (cost=0.00..125.00 rows=6000 width=20)
(8 rows)

  这里出现了 Unique,因为子查询中明确写了 DISTINCT,规划器生成了 UpperUniquePath。那么修改为 IN 子查询呢?(相同语义)

sql 复制代码
EXPLAIN 
SELECT * 
FROM test_user u 
WHERE u.email IN (SELECT email FROM test_user_bak);
                                  QUERY PLAN
-------------------------------------------------------------------------------
 Hash Semi Join  (cost=200.00..557.00 rows=10000 width=51)
   Hash Cond: ((u.email)::text = (test_user_bak.email)::text)
   ->  Seq Scan on test_user u  (cost=0.00..207.00 rows=10000 width=51)
   ->  Hash  (cost=125.00..125.00 rows=6000 width=20)
         ->  Seq Scan on test_user_bak  (cost=0.00..125.00 rows=6000 width=20)          ← 注意:没有 Unique!
(5 rows)

为什么 IN 查询没有使用 Unique

  这是非常重要的优化器决策细节:

  1. Hash Semi Join 的特性
    • Hash 表本身对重复值有天然的"存在性"处理能力(使用哈希桶)。
    • 构建 Hash 表时,即使右表有重复,也不会影响 Semi Join 的正确性(只关心"是否存在")。
    • 因此,额外插入 Unique + Sort 的成本可能高于直接 Hash 全量数据
  2. Merge Join vs Hash Join 的差异
    • Merge Join:需要有序输入,因此优化器倾向插入 Unique + Sort,生成 UniquePath
    • Hash Join / Hash Semi Join:无需排序,更倾向直接 Hash 全量数据,只有在重复率极高、Unique 收益显著时,才会插入 Unique
  3. 优化器权衡:
    • 当使用 Hash 方式时,如果 Unique + Sort 的总代价高于直接 Hash,优化器就会放弃 Unique
    • 这也说明:UniquePath 是否真正生成,最终取决于代价估算,而非固定规则。

  验证 UniquePath 的推荐方法 :如果希望更稳定地看到 UniquePathIN 查询中的出现,可以采用如下方式:

sql 复制代码
SET enable_hashjoin = off;
SET enable_hashagg = off;     -- 同时关闭哈希去重

EXPLAIN 
SELECT * FROM test_user u 
WHERE u.email IN (SELECT email FROM test_user_bak);
                                             QUERY PLAN
----------------------------------------------------------------------------------------------------
 Merge Join  (cost=544.15..1308.32 rows=10000 width=51)
   Merge Cond: ((u.email)::text = ((test_user_bak.email)::text))
   ->  Index Scan using idx_test_user_email on test_user u  (cost=0.29..638.22 rows=10000 width=51)
   ->  Sort  (cost=543.87..544.62 rows=300 width=20)
         Sort Key: test_user_bak.email
         ->  Unique  (cost=501.52..531.52 rows=300 width=20)
               ->  Sort  (cost=501.52..516.52 rows=6000 width=20)
                     Sort Key: ((test_user_bak.email)::text)
                     ->  Seq Scan on test_user_bak  (cost=0.00..125.00 rows=6000 width=20)
(9 rows)

  此时,执行计划变为 Merge Join + Unique 的组合,从而清晰看到 Unique Path

2.2 优化器对应源码解析

2.2.1 create_upper_unique_plan

函数功能:

  create_upper_unique_plan 是规划器中将 UpperUniquePath 路径转换为 Unique 执行计划节点的核心入口函数。它负责递归构建子计划,并最终生成可供执行器使用的 Unique 计划节点。

入参:

  • PlannerInfo *root:规划器全局信息结构,包含查询的各种上下文(如参数、约束、统计信息等)。
  • UpperUniquePath *best_path:规划器选择的最优 UpperUnique 路径,包含去重列数量(numkeys)、排序路径键(pathkeys)等关键信息。
  • int flags:计划创建标记(如是否需要标签目标列表等)。

出参:

  • 返回一个 Unique * 类型的执行计划节点(包含完整的 Plan 结构)。

主要逻辑:

  1. 调用 create_plan_recurse 递归生成子计划(通常是 Sort 节点或底层扫描节点),并传递 CP_LABEL_TLIST 标志,确保去重列在目标列表中被正确标记。
  2. 调用 make_unique_from_pathkeys 函数,根据 pathkeys 构造真正的 Unique 节点,填充去重所需的列索引等值操作符排序规则
  3. 调用 copy_generic_path_info 将路径的代价信息(启动代价、总代价、行数等)复制到计划节点中 ,供 EXPLAIN 显示使用。
  4. 返回构造完成的 Unique 计划节点。
c 复制代码
/*
 * create_upper_unique_plan
 *
 *	  Create a Unique plan for 'best_path' and (recursively) plans
 *	  for its subpaths.
 */
static Unique *
create_upper_unique_plan(PlannerInfo *root, UpperUniquePath *best_path, int flags)
{
	Unique	   *plan;          /* 将要创建的 Unique 执行计划节点 */
	Plan	   *subplan;       /* 子计划节点(通常是 Sort 或 Scan) */

	/*
	 * Unique doesn't project, so tlist requirements pass through; moreover we
	 * need grouping columns to be labeled.
	 */
	/* Unique 节点自身不做投影,因此目标列列表(tlist)需求直接透传给子节点;
	   同时需要为分组列(去重列)添加标签(CP_LABEL_TLIST) */
	subplan = create_plan_recurse(root, best_path->subpath,
								  flags | CP_LABEL_TLIST);

	/* 使用 pathkeys 信息构造 Unique 节点 */
	plan = make_unique_from_pathkeys(subplan,
									 best_path->path.pathkeys,
									 best_path->numkeys);

	/* 复制路径的代价、行数等信息到计划节点(供 EXPLAIN 使用) */
	copy_generic_path_info(&plan->plan, (Path *) best_path);

	return plan;
}

2.2.2 make_unique_from_pathkeys

函数功能:

  这是构造 Unique 计划节点最核心的函数。它负责将规划器中的 PathKey(路径键)信息转换为执行器所需的去重列数组(列索引、等值操作符、排序规则),从而让执行器能够进行相邻元组比较去重。

入参:

  • Plan *lefttree子计划节点Unique 的输入来源,通常是 Sort 节点)。
  • List *pathkeys排序路径键列表,包含去重所依赖的列及其排序语义。
  • int numCols需要去重的列数量 (即 DISTINCT 涉及的列数)。

出参:

  返回一个初始化完成的 Unique * 计划节点,其中已填充 numCols、uniqColIdx、uniqOperators、uniqCollations 等关键字段。

主要逻辑:

  1. 创建 Unique 节点,并将子计划挂载到 lefttree 上,继承子节点的 targetlist 作为自己的输出列。
  2. 去重列 分配三个关键数组:uniqColIdx列在元组中的位置 )、uniqOperators等值比较操作符 )、uniqCollations排序规则)。
  3. 遍历 pathkeys 列表,对每一列进行处理:
    • 如果是 volatile(不稳定)表达式(如 ORDER BY 中的函数),则精确匹配对应的目标列。
    • 如果是非 volatile 表达式,则从等价类(EquivalenceClass)中查找匹配的目标列。
  4. 为每一列查找对应的等值操作符(= 操作符),并填充到数组中。
  5. 最终将数组指针和列数量挂载到 Unique 节点上,返回给上层调用。

  该函数是规划器与执行器之间的关键桥梁。它把"逻辑上的排序键 "转换成了"物理上可执行的去重配置",直接决定了执行器 ExecUnique 中相邻比较的准确性。

c 复制代码
/*
 * as above, but use pathkeys to identify the sort columns and semantics
 */
static Unique *
make_unique_from_pathkeys(Plan *lefttree, List *pathkeys, int numCols)
{
	Unique	   *node = makeNode(Unique);   /* 创建 Unique 执行节点 */
	Plan	   *plan = &node->plan;        /* 指向节点的 Plan 部分 */
	int			keyno = 0;                 /* 当前处理的去重列序号 */
	AttrNumber *uniqColIdx;                /* 去重列在 Tuple 中的序号数组 */
	Oid		   *uniqOperators;             /* 去重使用的等值比较操作符数组 */
	Oid		   *uniqCollations;            /* 排序规则(Collation)数组 */
	ListCell   *lc;                        /* 遍历 pathkeys 的链表游标 */

	/* 继承子节点的输出列作为自己的输出 */
	plan->targetlist = lefttree->targetlist;
	plan->qual = NIL;                      /* Unique 没有额外的过滤条件 */
	plan->lefttree = lefttree;             /* 挂载子计划 */
	plan->righttree = NULL;

	/*
	 * Convert pathkeys list into arrays of attr indexes and equality
	 * operators, as wanted by executor.  This has a lot in common with
	 * prepare_sort_from_pathkeys ... maybe unify sometime?
	 */
	/* 将 PathKey 列表转换为执行器需要的列索引和等值操作符数组。
	   该逻辑与排序的 prepare_sort_from_pathkeys 高度相似,后续可能重构统一。 */
	Assert(numCols >= 0 && numCols <= list_length(pathkeys));

	/* 为去重列分配内存 */
	uniqColIdx = (AttrNumber *) palloc(sizeof(AttrNumber) * numCols);
	uniqOperators = (Oid *) palloc(sizeof(Oid) * numCols);
	uniqCollations = (Oid *) palloc(sizeof(Oid) * numCols);

	/* 遍历 pathkeys,逐一填充去重所需的信息 */
	foreach(lc, pathkeys)
	{
		PathKey    *pathkey = (PathKey *) lfirst(lc);
		EquivalenceClass *ec = pathkey->pk_eclass;      /* 等价类 */
		EquivalenceMember *em;
		TargetEntry *tle = NULL;                        /* 目标列入口 */
		Oid			pk_datatype = InvalidOid;           /* 数据类型 */
		Oid			eqop;                               /* 等值操作符 */
		ListCell   *j;

		/* 超出指定去重列数量时停止 */
		if (keyno >= numCols)
			break;

		if (ec->ec_has_volatile)
		{
			/*
			 * If the pathkey's EquivalenceClass is volatile, then it must
			 * have come from an ORDER BY clause, and we have to match it to
			 * that same targetlist entry.
			 */
			/* 如果等价类是 volatile(不稳定,如函数、volatile 表达式),
			   则必然来自 ORDER BY,必须精确匹配同一个 targetlist 项 */
			if (ec->ec_sortref == 0)	/* can't happen */
				elog(ERROR, "volatile EquivalenceClass has no sortref");
			tle = get_sortgroupref_tle(ec->ec_sortref, plan->targetlist);
			Assert(tle);
			Assert(list_length(ec->ec_members) == 1);
			pk_datatype = ((EquivalenceMember *) linitial(ec->ec_members))->em_datatype;
		}
		else
		{
			/*
			 * Otherwise, we can use any non-constant expression listed in the
			 * pathkey's EquivalenceClass.  For now, we take the first tlist
			 * item found in the EC.
			 */
			/* 非 volatile 情况:从等价类中找到第一个出现在 targetlist 中的表达式 */
			foreach(j, plan->targetlist)
			{
				tle = (TargetEntry *) lfirst(j);
				em = find_ec_member_matching_expr(ec, tle->expr, NULL);
				if (em)
				{
					/* found expr already in tlist */
					pk_datatype = em->em_datatype;
					break;
				}
				tle = NULL;
			}
		}

		if (!tle)
			elog(ERROR, "could not find pathkey item to sort");

		/*
		 * Look up the correct equality operator from the PathKey's slightly
		 * abstracted representation.
		 */
		/* 根据 PathKey 中的操作符族,查找对应的等值(=)比较操作符 */
		eqop = get_opfamily_member_for_cmptype(pathkey->pk_opfamily,
											   pk_datatype,
											   pk_datatype,
											   COMPARE_EQ);
		if (!OidIsValid(eqop))	/* should not happen */
			elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
				 COMPARE_EQ, pk_datatype, pk_datatype,
				 pathkey->pk_opfamily);

		/* 填充数组 */
		uniqColIdx[keyno] = tle->resno;           /* 列在 tuple 中的位置 */
		uniqOperators[keyno] = eqop;              /* 等值比较操作符 */
		uniqCollations[keyno] = ec->ec_collation; /* 排序规则 */

		keyno++;
	}

	/* 最终设置节点属性 */
	node->numCols = numCols;
	node->uniqColIdx = uniqColIdx;
	node->uniqOperators = uniqOperators;
	node->uniqCollations = uniqCollations;

	return node;
}
PathKey 是什么?

  PathKeyPostgreSQL 规划器用来描述路径(Path)排序顺序 的核心数据结构。

一条 Path(路径)可能是有序的,也可能是无序的。如果是有序的,就用一个 List *pathkeysPathKey 链表)来精确描述它的排序规则。

官方注释核心含义

  • 一个空的 pathkeys 列表表示该路径无已知排序顺序
  • 非空列表中,第 1PathKey主要排序键Primary Sort Key),第 2 个是次要排序键Secondary Sort Key),以此类推。
  • 每个 PathKey 通过链接到一个 EquivalenceClass(等价类)来表示"被排序的值"。

PathKey 的根本作用

  1. 描述排序顺序:让规划器知道某条路径当前按哪些列、什么方向排序。
  2. 去重决策Unique 算子需要有序输入,而 PathKey 正是"有序"这一属性的正式表达。
  3. 排序匹配与复用 :规划器可以轻松判断两个路径的排序是否兼容(例如是否能省略 Sort 节点)。
  4. DISTINCT ON 支持DISTINCT ON 需要按指定列排序后取第一条,PathKey 是实现这一语义的基础。

2.2.3 PathKey 结构体字段详解

c 复制代码
typedef struct PathKey
{
    NodeTag		type;

    /* the value that is ordered */
    EquivalenceClass *pk_eclass;     /* 指向等价类,代表被排序的值 */
    Oid			pk_opfamily;         /* 索引操作符族,定义排序规则 */
    CompareType pk_cmptype;          /* 排序方向:ASC 或 DESC */
    bool		pk_nulls_first;      /* NULL 值是否排在最前面 */
} PathKey;

字段详细解释

  1. pk_eclassEquivalenceClass *)

    • 最核心字段。
    • 指向一个等价类 ,表示"这个排序键对应的值"。
    • 等价类可以包含多个表达式(如 ab+1lower(c)),只要它们在规划器看来是等价的。
    • Unique 去重时最终依赖的就是这个等价类中的表达式。
  2. pk_opfamilyOid

    • 操作符族(Operator Family)。
    • 定义了该列使用哪一套比较规则进行排序(例如 text_opsinteger_ops 等)。
    • 后续生成等值比较操作符(=)时需要用到。
  3. pk_cmptypeCompareType

    • 排序方向:COMPARE_LT 表示升序(ASC),COMPARE_GT 表示降序(DESC)。
  4. pk_nulls_firstbool

    • 控制 NULL 值的排序位置(NULLS FIRSTNULLS LAST)。
    • 这也是 PostgreSQL 支持的排序特性之一。

2.2.4 PathKey 在 Unique 算子中的作用

  在 make_unique_from_pathkeys 函数中,规划器会遍历 PathKey 列表:

  • 把每个 PathKey 转换成执行器需要的 uniqColIdxuniqOperatorsuniqCollations
  • 最终让 ExecUnique 知道应该按哪些列、用什么比较规则来进行相邻去重
  • 这也是为什么 Unique 必须依赖有序输入(即必须有有效的 pathkeys)的原因。

举例

sql 复制代码
SELECT DISTINCT city, age FROM test_user ORDER BY city, age;

规划器会生成包含两个 PathKey 的列表:

  • 第1个 PathKeycity 列,ASC
  • 第2个 PathKeyage 列,ASC

  总结PathKey 把"排序需求 "形式化成 (等价类 + 排序方向 + NULLS 规则),让优化器能在不同路径之间比较排序属性。

2.2.5 copy_generic_path_info

函数功能:

  这是一个通用的辅助函数,用于将规划器 Path 节点中的代价和统计信息复制到对应的 Plan 执行节点中。

入参:

  • Plan *dest:目标执行计划节点(需要填充信息的节点)。
  • Path *src:源路径节点(包含规划器估算的各种代价信息)。

主要逻辑:

  1. 复制节点禁用信息(disabled_nodes)。
  2. 复制启动代价(startup_cost)、总代价(total_cost)、预计行数(plan_rows)、行宽度(plan_width)等核心统计信息。
  3. 复制并行相关标志(parallel_awareparallel_safe),供执行器决定是否启用并行执行。
c 复制代码
/*
 * Copy cost and size info from a Path node to the Plan node created from it.
 * The executor usually won't use this info, but it's needed by EXPLAIN.
 * Also copy the parallel-related flags, which the executor *will* use.
 */
static void
copy_generic_path_info(Plan *dest, Path *src)
{
	dest->disabled_nodes = src->disabled_nodes;     /* 禁用节点信息 */
	dest->startup_cost = src->startup_cost;         /* 启动代价 */
	dest->total_cost = src->total_cost;             /* 总代价 */
	dest->plan_rows = src->rows;                    /* 预计返回行数 */
	dest->plan_width = src->pathtarget->width;      /* 每行宽度(字节) */
	dest->parallel_aware = src->parallel_aware;     /* 并行感知标志 */
	dest->parallel_safe = src->parallel_safe;       /* 并行安全标志 */
}

2.2.6 create_unique_plan

函数功能:

  create_unique_plan 的作用是:根据 UniquePath 生成对应的执行计划节点(Unique Plan)。它支持两种去重方式:

  • Hash 去重UNIQUE_PATH_HASH):使用 Agg 节点(AGG_HASHED)实现,适合数据量较大、无序输入的情况。
  • Sort 去重UNIQUE_PATH_SORT):先排序,再用 Unique 节点去重,适合已经接近有序或需要有序输出的场景。

入参:

参数 类型 含义
root PlannerInfo * 全局规划器信息,包含查询上下文、参数、统计信息等
best_path UniquePath * 优化器选出的最优 Unique 路径,包含去重方法、表达式、代价等关键信息
flags int 计划创建标志(如是否支持并行),传递给子计划创建函数

出参:

参数 类型 含义
Plan plan * 返回一个完整的 Unique 执行计划节点(可能是 UniqueAgg 或直接返回子计划)。
c 复制代码
/*
 * create_unique_plan
 *    为指定的 UniquePath 生成对应的 Unique 执行计划节点,
 *    并递归为其子路径生成执行计划。
 *
 *    返回一个 Plan 类型的执行计划节点。
 */
static Plan *
create_unique_plan(PlannerInfo *root, UniquePath *best_path, int flags)
{
	Plan	   *plan;					// 最终生成的 Unique 执行计划节点
	Plan	   *subplan;				// 下层子计划节点
	List	   *in_operators;			// 用于去重的相等操作符列表,通常来自 DISTINCT 或 IN 子句
	List	   *uniq_exprs;				// 需要进行唯一化处理的表达式列表
	List	   *newtlist;				// 调整后的目标列列表(targetlist)
	int			nextresno;				// 下一个可分配的目标列序号(resno)
	bool		newitems;				// 标记是否向子计划的 targetlist 中新增了表达式
	int			numGroupCols;			// 需要去重的列(或表达式)的数量
	AttrNumber *groupColIdx;			// 记录每个去重列在子计划 targetlist 中的位置数组
	Oid		   *groupCollations;		// 每个去重列对应的排序规则(collation)
	int			groupColPos;			// 当前正在处理的分组列的索引位置
	ListCell   *l;						// 用于遍历链表的临时指针

	/* 
	 * Unique 节点本身不进行投影操作,因此子路径的目标列需求可以直接透传下去。
	 * 调用递归函数生成子计划。
	 */
	subplan = create_plan_recurse(root, best_path->subpath, flags);

	/* 
	 * 如果当前 UniquePath 被标记为不需要实际执行去重(例如输入数据已经唯一),
	 * 则直接返回子计划,无需额外开销。
	 */
	if (best_path->umethod == UNIQUE_PATH_NOOP)
		return subplan;

	/*
	 * 子计划当前输出的 targetlist 是一个精简的"扁平"列表,只包含当前层级和上层需要的列。
	 * 但需要去重的值可能是基于这些列的复杂表达式,因此必须确保这些表达式出现在子计划的输出中。
	 *
	 * 如果后续采用 Sort 方式去重,应该尽量减少排序的数据量;
	 * 如果采用 Hash 方式,在需要新增表达式时也会产生投影,因此可以提前清理多余列。
	 */
	in_operators = best_path->in_operators;   // 获取去重使用的操作符列表
	uniq_exprs = best_path->uniq_exprs;       // 获取需要去重的表达式列表

	/* 
	 * 使用 build_path_tlist 从 Path 中构建初始 targetlist,只包含必要列,
	 * 避免携带不相关的数据。
	 */
	newtlist = build_path_tlist(root, &best_path->path);
	nextresno = list_length(newtlist) + 1;    // 计算下一个可用的目标列编号
	newitems = false;                         // 标记是否新增了表达式

	/* 
	 * 遍历所有需要去重的表达式,如果当前 targetlist 中不存在,则添加进去。
	 */
	foreach(l, uniq_exprs)
	{
		Expr	   *uniqexpr = lfirst(l);      // 当前需要唯一化的表达式
		TargetEntry *tle;

		/* 检查该表达式是否已经在 targetlist 中 */
		tle = tlist_member(uniqexpr, newtlist);
		if (!tle)
		{
			/* 创建新的 TargetEntry 并添加到列表末尾 */
			tle = makeTargetEntry((Expr *) uniqexpr,
								  nextresno,
								  NULL,
								  false);
			newtlist = lappend(newtlist, tle);
			nextresno++;
			newitems = true;                  // 记录已新增表达式
		}
	}

	/* 
	 * 如果新增了表达式,或者采用 Sort 方式去重,则需要调整子计划的 targetlist。
	 * change_plan_targetlist 可能会在必要时插入 Result 节点进行投影。
	 */
	if (newitems || best_path->umethod == UNIQUE_PATH_SORT)
		subplan = change_plan_targetlist(subplan, newtlist,
										 best_path->path.parallel_safe);

	/*
	 * 构建去重所需的分组列控制信息,包括列在 targetlist 中的位置和排序规则。
	 * 这一步必须在 targetlist 最终确定之后进行,因此不能和上一个循环合并。
	 */
	newtlist = subplan->targetlist;           // 使用最终版本的 targetlist
	numGroupCols = list_length(uniq_exprs);   // 去重列数量

	groupColIdx = (AttrNumber *) palloc(numGroupCols * sizeof(AttrNumber));
	groupCollations = (Oid *) palloc(numGroupCols * sizeof(Oid));

	groupColPos = 0;
	foreach(l, uniq_exprs)
	{
		Expr	   *uniqexpr = lfirst(l);
		TargetEntry *tle;

		tle = tlist_member(uniqexpr, newtlist);
		if (!tle)
			elog(ERROR, "failed to find unique expression in subplan tlist");

		groupColIdx[groupColPos] = tle->resno;                    // 保存列位置
		groupCollations[groupColPos] = exprCollation((Node *) tle->expr); // 保存排序规则
		groupColPos++;
	}

	/* 根据不同的唯一化策略生成执行计划 */
	if (best_path->umethod == UNIQUE_PATH_HASH)
	{
		Oid		   *groupOperators;

		/*
		 * 为 Hash 聚合准备可用于哈希的相等操作符。
		 * 如果原始操作符是跨类型比较,则需要获取右侧数据类型的相等操作符。
		 */
		groupOperators = (Oid *) palloc(numGroupCols * sizeof(Oid));
		groupColPos = 0;
		foreach(l, in_operators)
		{
			Oid			in_oper = lfirst_oid(l);
			Oid			eq_oper;

			if (!get_compatible_hash_operators(in_oper, NULL, &eq_oper))
				elog(ERROR, "could not find compatible hash operator for operator %u",
					 in_oper);

			groupOperators[groupColPos++] = eq_oper;
		}

		/* 
		 * 使用 Agg 节点实现基于哈希的去重。
		 * 因为 Agg 节点本身会进行投影,所以直接传入最小化的 targetlist。
		 */
		plan = (Plan *) make_agg(build_path_tlist(root, &best_path->path),
								 NIL,
								 AGG_HASHED,
								 AGGSPLIT_SIMPLE,
								 numGroupCols,
								 groupColIdx,
								 groupOperators,
								 groupCollations,
								 NIL,
								 NIL,
								 best_path->path.rows,
								 0,
								 subplan);
	}
	else  /* UNIQUE_PATH_SORT 方式 */
	{
		List	   *sortList = NIL;
		Sort	   *sort;

		/* 
		 * 为 Sort 节点构造排序子句列表,确保排序顺序与去重要求一致。
		 */
		groupColPos = 0;
		foreach(l, in_operators)
		{
			Oid			in_oper = lfirst_oid(l);
			Oid			sortop;
			Oid			eqop;
			TargetEntry *tle;
			SortGroupClause *sortcl;

			/* 获取用于排序的操作符 */
			sortop = get_ordering_op_for_equality_op(in_oper, false);
			if (!OidIsValid(sortop))
				elog(ERROR, "could not find ordering operator for equality operator %u",
					 in_oper);

			/* 获取对应的相等操作符 */
			eqop = get_equality_op_for_ordering_op(sortop, NULL);
			if (!OidIsValid(eqop))
				elog(ERROR, "could not find equality operator for ordering operator %u",
					 sortop);

			tle = get_tle_by_resno(subplan->targetlist, groupColIdx[groupColPos]);
			Assert(tle != NULL);

			sortcl = makeNode(SortGroupClause);
			sortcl->tleSortGroupRef = assignSortGroupRef(tle, subplan->targetlist);
			sortcl->eqop = eqop;
			sortcl->sortop = sortop;
			sortcl->reverse_sort = false;
			sortcl->nulls_first = false;
			sortcl->hashable = false;
			sortList = lappend(sortList, sortcl);
			groupColPos++;
		}

		/* 创建 Sort 节点并计算其代价 */
		sort = make_sort_from_sortclauses(sortList, subplan);
		label_sort_with_costsize(root, sort, -1.0);

		/* 创建基于排序的 Unique 节点 */
		plan = (Plan *) make_unique_from_sortclauses((Plan *) sort, sortList);
	}

	/* 
	 * 将 Path 中的代价估算信息(启动代价、总代价、行数等)复制到 Plan 节点中,
	 * 供上层计划使用。
	 */
	copy_generic_path_info(plan, &best_path->path);

	return plan;		// 返回最终构建好的 Unique 执行计划
}

2.3 Unique 算子执行器源码解读

  前文已经完成 Unique 算子的业务定义、使用场景与优化器路径生成逻辑。PostgreSQL 内核将去重逻辑分为优化器路径决策执行器物理执行 两个阶段。优化器负责选出最优有序去重路径并生成计划模板,执行器负责真正的元组过滤去重。本章基于官方nodeUnique.c 源码,以「优化器衔接 → 数据结构 → 生命周期函数 → 闭环总结」的逻辑,清晰拆解 Unique 算子完整执行原理,同时区分内核中两类 Unique 路径分支的差异与复用关系。

2.3.1 概述与优化器衔接

  要真正理解 Unique 算子,必须先澄清它与优化器层的衔接关系。在 PostgreSQL 优化器中,存在两种看似相似但用途完全不同的路径节点:UniquePathUpperUniquePath。它们并非简单按照查询层次(上层/下层)划分,而是按照去重的目的和语义来区分,这是 PostgreSQL 优化器设计中一个容易被误解的细节。

  UpperUniquePath 服务于用户查询语义,UniquePath 服务于查询优化内部 。这种区分让优化器能够针对不同场景选择最合适的去重策略,同时保证输入数据有序(这是优化器与执行器之间的关键契约)。

  无论优化器选择哪种 Path,最终生成的执行计划节点在运行时通常都是 Unique 节点,由执行器统一处理。

2.3.2 主要数据结构

c 复制代码
/* ----------------
 *	 UniqueState 状态信息说明
 *
 *		Unique 节点通常挂载在 Sort 排序节点的上层,用于剔除排序阶段产出的重复元组。
 *		其核心工作逻辑非常简单:将从子计划读取的当前元组,与从上一轮缓存的历史元组
 *		(存储在自身结果槽中)进行对比。
 *		如果两条元组的所有关键字段完全一致,则判定为重复,继续从下层排序节点读取
 *		下一条元组,反复尝试比对,直至找到不重复的元组或数据读取完毕。
 * ----------------
 */
typedef struct UniqueState
{
	PlanState	ps;				/* 算子基础执行状态,首个字段为节点类型标记 */
	ExprState  *eqfunction;		/* 预编译完成的元组等值比对表达式状态 */
} UniqueState;

2.3.3 初始化逻辑(ExecInitUnique

  ExecInitUniqueUnique 算子的生命周期入口,核心作用是加载优化器生成的静态计划模板,完成所有执行资源初始化,构建可运行的执行状态机。整体执行流程清晰、层层递进:

  首先函数通过断言拦截非法扫描模式,禁止反向扫描与标记扫描。由于 Unique 依赖有序相邻比对,反向遍历会彻底打乱去重逻辑,内核从初始化阶段就做了严格防护。

  随后创建 UniqueState 状态节点,绑定计划模板与执行上下文,注册核心迭代执行函数 ExecUnique,完成状态与执行逻辑的绑定。

  接着递归初始化下层子计划节点,也就是为 Unique 提供有序数据的 Sort 排序节点有序索引扫描节点 ,保证后续迭代可正常拉取元组。

  最后初始化结果缓存元组槽,关闭无用的投影能力,同时调用 execTuplesMatchPrepare 预编译元组比对函数。Unique 仅做过滤、不修改元组数据,无需投影计算,这也是它比 GroupHashAggregate 更轻量、高效的核心原因。

c 复制代码
/* ----------------------------------------------------------------
 *		ExecInitUnique
 *
 *		初始化 Unique 节点的状态结构,并递归初始化其子计划。
 * ----------------------------------------------------------------
 */
UniqueState *
ExecInitUnique(Unique *node, EState *estate, int eflags)
{
	UniqueState *uniquestate;

	/* 检查不支持的执行标志:Unique 不支持反向扫描和标记恢复 */
	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));

	/*
	 * 创建 Unique 节点的状态结构
	 */
	uniquestate = makeNode(UniqueState);                        // 分配并初始化 UniqueState 内存
	uniquestate->ps.plan = (Plan *) node;                       // 关联对应的 Plan 节点
	uniquestate->ps.state = estate;                             // 关联执行状态
	uniquestate->ps.ExecProcNode = ExecUnique;                  // 绑定核心执行函数

	/*
	 * 创建表达式执行上下文
	 */
	ExecAssignExprContext(estate, &uniquestate->ps);

	/*
	 * 初始化外层(子)计划
	 */
	outerPlanState(uniquestate) = ExecInitNode(outerPlan(node), estate, eflags);

	/*
	 * 初始化结果槽和元组类型。
	 * Unique 节点不做投影,因此投影信息设为空。
	 */
	ExecInitResultTupleSlotTL(&uniquestate->ps, &TTSOpsMinimalTuple);  // 使用最小元组格式
	uniquestate->ps.ps_ProjInfo = NULL;                         // 无需投影

	/*
	 * 为内层循环预先计算函数管理器(fmgr)查找数据,这是重要的性能优化。
	 * 避免在 ExecUnique 的每一次循环中都重复查找比较函数。
	 */
	uniquestate->eqfunction =
		execTuplesMatchPrepare(ExecGetResultType(outerPlanState(uniquestate)),
							   node->numCols,                   // 去重列数量
							   node->uniqColIdx,                // 去重列在元组中的索引数组
							   node->uniqOperators,             // 去重使用的相等操作符
							   node->uniqCollations,            // 每列对应的排序规则
							   &uniquestate->ps);

	return uniquestate;
}

2.3.4 算子核心执行逻辑(ExecUnique

  ExecUniqueUnique 算子的运行时核心函数,承担整个去重过程的实际执行工作。它属于典型的 Volcano Iterator(火山模型)执行节点,每次被上层节点调用时,仅返回一条满足唯一性的元组,因此本质上是一个流式去重算子

  Unique 的设计建立在一个重要前提之上:输入数据已经按照唯一键完成排序 。因此它无需像 HashAggregate 那样构建哈希表,也无需维护整组数据,仅需保存上一条已经输出的记录即可完成去重判断

  执行过程中,ExecUnique 持续从子计划拉取元组。当读取到第一条记录时直接输出;之后每获取一条新记录,就与上一次已经输出的记录进行比较。如果两条记录在所有唯一键列上均相等,则说明当前记录属于重复数据,直接丢弃;如果比较结果不同,则说明遇到了新的唯一值组,更新缓存并向上层返回该记录。

  整个执行过程中,Unique 始终只维护一个历史输出元组作为比较基准,因此其内存消耗几乎恒定,不随数据规模增长而增加。

c 复制代码
/* ----------------------------------------------------------------
 *		ExecUnique
 *      Unique 算子的核心执行函数:负责实际的去重逻辑
 * ----------------------------------------------------------------
 */
static TupleTableSlot *			/* 返回:一个元组或 NULL */
ExecUnique(PlanState *pstate)
{
	UniqueState *node = castNode(UniqueState, pstate);          // 将 PlanState 转换为 UniqueState
	ExprContext *econtext = node->ps.ps_ExprContext;            // 获取表达式执行上下文
	TupleTableSlot *resultTupleSlot;                            // 保存上一次输出的元组,用于比较
	TupleTableSlot *slot;                                       // 当前从子计划获取的元组
	PlanState  *outerPlan;                                      // 子计划的 PlanState

	CHECK_FOR_INTERRUPTS();                                     // 检查查询是否被用户中断

	/*
	 * 从节点中获取必要的信息
	 */
	outerPlan = outerPlanState(node);                           // 获取子计划节点
	resultTupleSlot = node->ps.ps_ResultTupleSlot;              // 获取本节点的结果槽

	/*
	 * 主循环:持续处理输入,只返回非重复的元组。
	 * 我们假设输入元组已经按去重键排序,因此可以通过比较相邻元组快速检测重复。
	 * 每组重复元组中只返回第一条。
	 */
	for (;;)
	{
		/*
		 * 从子计划中获取下一条元组
		 */
		slot = ExecProcNode(outerPlan);
		if (TupIsNull(slot))
		{
			/* 子计划已结束,清空结果槽并返回 NULL 表示结束 */
			ExecClearTuple(resultTupleSlot);
			return NULL;
		}

		/*
		 * 第一次执行时(resultTupleSlot 为空),直接返回第一条元组
		 */
		if (TupIsNull(resultTupleSlot))
			break;

		/*
		 * 否则,比较当前新元组与之前已返回的元组是否在去重键上相同。
		 * 如果相同,则继续循环,丢弃当前元组并取下一条。
		 */
		econtext->ecxt_innertuple = slot;                       // 当前新获取的元组
		econtext->ecxt_outertuple = resultTupleSlot;            // 上一条已输出的元组
		
		if (!ExecQualAndReset(node->eqfunction, econtext))
			break;                                              // 发现不同,跳出循环准备返回
	}

	/*
	 * 此时我们拿到了一个与之前不同的新元组(或第一条元组)。
	 * 保存它并返回。由于子计划不能保证当前 slot 在下次调用时仍然有效,
	 * 因此必须进行一次拷贝。
	 */
	return ExecCopySlot(resultTupleSlot, slot);
}

#mermaid-svg-9kUuM2krZHaFayCa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9kUuM2krZHaFayCa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9kUuM2krZHaFayCa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9kUuM2krZHaFayCa .error-icon{fill:#552222;}#mermaid-svg-9kUuM2krZHaFayCa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9kUuM2krZHaFayCa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9kUuM2krZHaFayCa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9kUuM2krZHaFayCa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9kUuM2krZHaFayCa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9kUuM2krZHaFayCa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9kUuM2krZHaFayCa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9kUuM2krZHaFayCa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9kUuM2krZHaFayCa .marker.cross{stroke:#333333;}#mermaid-svg-9kUuM2krZHaFayCa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9kUuM2krZHaFayCa p{margin:0;}#mermaid-svg-9kUuM2krZHaFayCa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9kUuM2krZHaFayCa .cluster-label text{fill:#333;}#mermaid-svg-9kUuM2krZHaFayCa .cluster-label span{color:#333;}#mermaid-svg-9kUuM2krZHaFayCa .cluster-label span p{background-color:transparent;}#mermaid-svg-9kUuM2krZHaFayCa .label text,#mermaid-svg-9kUuM2krZHaFayCa span{fill:#333;color:#333;}#mermaid-svg-9kUuM2krZHaFayCa .node rect,#mermaid-svg-9kUuM2krZHaFayCa .node circle,#mermaid-svg-9kUuM2krZHaFayCa .node ellipse,#mermaid-svg-9kUuM2krZHaFayCa .node polygon,#mermaid-svg-9kUuM2krZHaFayCa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9kUuM2krZHaFayCa .rough-node .label text,#mermaid-svg-9kUuM2krZHaFayCa .node .label text,#mermaid-svg-9kUuM2krZHaFayCa .image-shape .label,#mermaid-svg-9kUuM2krZHaFayCa .icon-shape .label{text-anchor:middle;}#mermaid-svg-9kUuM2krZHaFayCa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9kUuM2krZHaFayCa .rough-node .label,#mermaid-svg-9kUuM2krZHaFayCa .node .label,#mermaid-svg-9kUuM2krZHaFayCa .image-shape .label,#mermaid-svg-9kUuM2krZHaFayCa .icon-shape .label{text-align:center;}#mermaid-svg-9kUuM2krZHaFayCa .node.clickable{cursor:pointer;}#mermaid-svg-9kUuM2krZHaFayCa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9kUuM2krZHaFayCa .arrowheadPath{fill:#333333;}#mermaid-svg-9kUuM2krZHaFayCa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9kUuM2krZHaFayCa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9kUuM2krZHaFayCa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9kUuM2krZHaFayCa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9kUuM2krZHaFayCa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9kUuM2krZHaFayCa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9kUuM2krZHaFayCa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9kUuM2krZHaFayCa .cluster text{fill:#333;}#mermaid-svg-9kUuM2krZHaFayCa .cluster span{color:#333;}#mermaid-svg-9kUuM2krZHaFayCa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9kUuM2krZHaFayCa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9kUuM2krZHaFayCa rect.text{fill:none;stroke-width:0;}#mermaid-svg-9kUuM2krZHaFayCa .icon-shape,#mermaid-svg-9kUuM2krZHaFayCa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9kUuM2krZHaFayCa .icon-shape p,#mermaid-svg-9kUuM2krZHaFayCa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9kUuM2krZHaFayCa .icon-shape .label rect,#mermaid-svg-9kUuM2krZHaFayCa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9kUuM2krZHaFayCa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9kUuM2krZHaFayCa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9kUuM2krZHaFayCa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

第一次输出



ExecUnique() 被调用
获取 outerPlan 和 ResultTupleSlot
从子计划读取下一条 Tuple

ExecProcNode(outerPlan)
slot 是否为空?
ExecClearTuple(ResultTupleSlot)
返回 NULL
ResultTupleSlot 是否为空?
当前 Tuple 为首条记录
复制到 ResultTupleSlot
设置比较上下文

outertuple=上次输出

innertuple=当前Tuple
执行 eqfunction 比较
两条记录是否相等?
ExecCopySlot(ResultTupleSlot, slot)
返回当前唯一 Tuple

2.3.5 资源回收(ExecEndUnique

  ExecEndUniqueUnique 算子的资源回收入口,用于结束节点执行并释放相关资源

  由于 Unique 本身不维护哈希表、排序器、磁盘临时文件等复杂运行状态,因此其清理过程极为简单。绝大多数资源(ExprContextTupleSlot 等)都由 PostgreSQL 执行器框架统一管理,无需节点显式释放。

  函数主要工作是递归调用 ExecEndNode() 关闭其子计划节点,使整个执行树能够自底向上完成资源回收。

c 复制代码
/* ----------------------------------------------------------------
 *		ExecEndUnique
 *
 *		关闭子计划,并释放本节点分配的资源。
 * ----------------------------------------------------------------
 */
void
ExecEndUnique(UniqueState *node)
{
	/* 递归结束子计划即可,Unique 自身没有额外资源需要释放 */
	ExecEndNode(outerPlanState(node));
}

2.3.5 重扫描机制(ExecReScanUnique

  ExecReScanUnique 用于重新启动 Unique 节点的执行过程,通常出现在 Nested Loop 参数变化、Cursor 重定位、子查询重复执行等场景中。

  Unique 的去重逻辑依赖于 ps_ResultTupleSlot 中保存的"上一条已输出元组 ",因此重新扫描时首先必须清空该缓存状态。否则新的扫描结果可能会与旧扫描遗留的数据进行比较,从而导致错误的去重行为。

  完成状态清理后,函数会检查子计划的 chgParam 标记。如果子节点已经收到参数变化通知,则 PostgreSQL 会在下一次执行时自动触发重扫描;否则直接调用 ExecReScan() 主动重置子计划。

c 复制代码
/* ----------------------------------------------------------------
 *		ExecReScanUnique
 *
 *		重新扫描 Unique 节点,通常用于参数改变或游标重新执行等场景。
 * ----------------------------------------------------------------
 */
void
ExecReScanUnique(UniqueState *node)
{
	PlanState  *outerPlan = outerPlanState(node);

	/* 必须清空之前保存的结果元组,这样下一次会重新返回第一条输入元组 */
	ExecClearTuple(node->ps.ps_ResultTupleSlot);

	/*
	 * 如果子计划的 chgParam 不为空,则在下一次 ExecProcNode 时会自动重新扫描。
	 * 否则手动触发子计划的重新扫描。
	 */
	if (outerPlan->chgParam == NULL)
		ExecReScan(outerPlan);
}
相关推荐
Je1lyfish1 小时前
CMU15-445 (2025 Fall/2026 Spring) Project#4 - Concurrency Control
开发语言·数据库·c++·笔记·后端·算法·系统架构
我是一颗柠檬1 小时前
【Redis】Cluster集群Day11(2026年)
数据库·redis·后端·缓存
一只fish1 小时前
Oracle官方文档翻译《Database Concepts 26ai》第21章-Oracle AI 数据库中的AI
数据库·人工智能·oracle
踏着七彩祥云的小丑2 小时前
嵌入式测试学习第 28 天:网络调试助手使用、TCP服务端客户端实操
单片机·嵌入式硬件·学习
imDwAaY2 小时前
从感知机到 Attention:我用 PyTorch 打穿 CS188 机器学习终章 CS188 Proj5 学习笔记
人工智能·pytorch·笔记·python·学习·机器学习
Database_Cool_4 小时前
云原生多租户隔离 + 近实时分析怎么选型?阿里云 AnalyticDB MySQL 资源隔离方案
数据库·mysql·阿里云
小马爱打代码10 小时前
Redis 集群方案详解:主从复制、哨兵、脑裂、分片集群和哈希槽
数据库·redis·哈希算法
马***41110 小时前
适配成人英语学习痛点,打造落地性强的学习辅助方式
人工智能·学习
暴躁小师兄数据学院11 小时前
【AI大数据工程师特训笔记】第12讲:表分区与索引
大数据·笔记·sql·postgresql