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)
- [**示例 1:普通 `DISTINCT`**](#示例 1:普通
- [2.1.3 UniquePath 详解(Semi Join 优化场景)](#2.1.3 UniquePath 详解(Semi Join 优化场景))
-
- [**示例 1:显式 `DISTINCT + JOIN`**](#示例 1:显式
DISTINCT + JOIN)
- [**示例 1:显式 `DISTINCT + JOIN`**](#示例 1:显式
- [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 针对 DISTINCT、UNION 等去重语句,优化器会根据数据量、排序成本、内存配置,自动选择 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 算子出现的核心判定规则
SQL语义需要全局去重(DISTINCT、UNION、DISTINCT ON等);- 优化器评估排序成本低于哈希去重成本(或强制关闭
HashAggregate); - 可以通过
Sort节点或索引扫描提供有序输入流。
满足以上条件时,PostgreSQL 就会生成 Sort + Unique 的经典执行计划组合。
1.5 参考链接
-
官方文档 - 执行计划基础 :https://www.postgresql.org/docs/current/using-explain.html (解释
Unique节点在计划中的位置) -
Unique vs HashAggregate :
Percona文章(PostgreSQL 15 DISTINCT优化)很好地展示了两种路径的性能差异:https://www.percona.com/blog/introducing-postgresql-15-working-with-distinct/ -
UNION去重 :官方文档明确UNION会消除重复:https://www.postgresql.org/docs/current/queries-union.html -
源码层面 :
-
Unique节点定义 :https://github.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h (搜索Unique) -
执行器实现 :https://doxygen.postgresql.org/nodeUnique_8h.html (
ExecInitUnique、ExecUnique等函数) -
规划器相关 :搜索
planner中的create_unique_plan或distinct_path。
-
-
Stack Overflow讨论Unique含义 :https://stackoverflow.com/questions/75249883/what-does-unique-mean-in-explain-analyze-postgresql (实用解释)
2. Unique 算子解读
2.1 规划器中的路径决策流程
PostgreSQL 优化器采用基于代价(Cost-based) 的优化策略。在生成去重计划时,会同时考虑 UniquePath 和 GroupingPath(对应 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 类型?
很多人会误以为 UniquePath 和 UpperUniquePath 只是同一事物的两种写法,实际上它们代表了两种完全不同的优化场景。在 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 BY、DISTINCT、DISTINCT ON、ORDER 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?
这是非常重要的优化器决策细节:
Hash Semi Join的特性 :
Hash表本身对重复值有天然的"存在性"处理能力(使用哈希桶)。- 构建
Hash表时,即使右表有重复,也不会影响Semi Join的正确性(只关心"是否存在")。- 因此,额外插入
Unique + Sort的成本可能高于直接Hash全量数据。Merge Join vs Hash Join的差异 :
Merge Join:需要有序输入,因此优化器倾向插入Unique + Sort,生成UniquePath。Hash Join / Hash Semi Join:无需排序,更倾向直接Hash全量数据,只有在重复率极高、Unique收益显著时,才会插入Unique。- 优化器权衡:
- 当使用
Hash方式时,如果Unique + Sort的总代价高于直接Hash,优化器就会放弃Unique。- 这也说明:
UniquePath是否真正生成,最终取决于代价估算,而非固定规则。
验证 UniquePath 的推荐方法 :如果希望更稳定地看到 UniquePath 在 IN 查询中的出现,可以采用如下方式:
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结构)。
主要逻辑:
- 调用
create_plan_recurse递归生成子计划(通常是Sort节点或底层扫描节点),并传递CP_LABEL_TLIST标志,确保去重列在目标列表中被正确标记。 - 调用
make_unique_from_pathkeys函数,根据pathkeys构造真正的Unique节点,填充去重所需的列索引 、等值操作符 和排序规则。 - 调用
copy_generic_path_info将路径的代价信息(启动代价、总代价、行数等)复制到计划节点中 ,供EXPLAIN显示使用。 - 返回构造完成的
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 等关键字段。
主要逻辑:
- 创建
Unique节点,并将子计划挂载到lefttree上,继承子节点的targetlist作为自己的输出列。 - 为去重列 分配三个关键数组:
uniqColIdx(列在元组中的位置 )、uniqOperators(等值比较操作符 )、uniqCollations(排序规则)。 - 遍历
pathkeys列表,对每一列进行处理:- 如果是
volatile(不稳定)表达式(如ORDER BY中的函数),则精确匹配对应的目标列。 - 如果是非
volatile表达式,则从等价类(EquivalenceClass)中查找匹配的目标列。
- 如果是
- 为每一列查找对应的等值操作符(
=操作符),并填充到数组中。 - 最终将数组指针和列数量挂载到
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 是什么?
PathKey 是 PostgreSQL 规划器用来描述路径(Path)排序顺序 的核心数据结构。
一条
Path(路径)可能是有序的,也可能是无序的。如果是有序的,就用一个List *pathkeys(PathKey链表)来精确描述它的排序规则。
官方注释核心含义:
- 一个空的
pathkeys列表表示该路径无已知排序顺序。 - 非空列表中,第
1个PathKey是主要排序键 (Primary Sort Key),第2个是次要排序键 (Secondary Sort Key),以此类推。 - 每个
PathKey通过链接到一个 EquivalenceClass(等价类)来表示"被排序的值"。
PathKey 的根本作用:
- 描述排序顺序:让规划器知道某条路径当前按哪些列、什么方向排序。
- 去重决策 :
Unique算子需要有序输入,而PathKey正是"有序"这一属性的正式表达。 - 排序匹配与复用 :规划器可以轻松判断两个路径的排序是否兼容(例如是否能省略
Sort节点)。 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;
字段详细解释:
-
pk_eclass(EquivalenceClass*)- 最核心字段。
- 指向一个等价类 ,表示"这个排序键对应的值"。
- 等价类可以包含多个表达式(如
a、b+1、lower(c)),只要它们在规划器看来是等价的。 Unique去重时最终依赖的就是这个等价类中的表达式。
-
pk_opfamily(Oid)- 操作符族(
Operator Family)。 - 定义了该列使用哪一套比较规则进行排序(例如
text_ops、integer_ops等)。 - 后续生成等值比较操作符(
=)时需要用到。
- 操作符族(
-
pk_cmptype(CompareType)- 排序方向:
COMPARE_LT表示升序(ASC),COMPARE_GT表示降序(DESC)。
- 排序方向:
-
pk_nulls_first(bool)- 控制
NULL值的排序位置(NULLS FIRST或NULLS LAST)。 - 这也是
PostgreSQL支持的排序特性之一。
- 控制
2.2.4 PathKey 在 Unique 算子中的作用
在 make_unique_from_pathkeys 函数中,规划器会遍历 PathKey 列表:
- 把每个
PathKey转换成执行器需要的uniqColIdx、uniqOperators、uniqCollations。 - 最终让
ExecUnique知道应该按哪些列、用什么比较规则来进行相邻去重。 - 这也是为什么
Unique必须依赖有序输入(即必须有有效的pathkeys)的原因。
举例:
sql
SELECT DISTINCT city, age FROM test_user ORDER BY city, age;
规划器会生成包含两个
PathKey的列表:
- 第1个
PathKey:city列,ASC- 第2个
PathKey:age列,ASC
总结 :PathKey 把"排序需求 "形式化成 (等价类 + 排序方向 + NULLS 规则),让优化器能在不同路径之间比较排序属性。
2.2.5 copy_generic_path_info
函数功能:
这是一个通用的辅助函数,用于将规划器 Path 节点中的代价和统计信息复制到对应的 Plan 执行节点中。
入参:
Plan *dest:目标执行计划节点(需要填充信息的节点)。Path *src:源路径节点(包含规划器估算的各种代价信息)。
主要逻辑:
- 复制节点禁用信息(
disabled_nodes)。 - 复制启动代价(
startup_cost)、总代价(total_cost)、预计行数(plan_rows)、行宽度(plan_width)等核心统计信息。 - 复制并行相关标志(
parallel_aware、parallel_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 执行计划节点(可能是 Unique、Agg 或直接返回子计划)。 |
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 优化器中,存在两种看似相似但用途完全不同的路径节点:UniquePath 和 UpperUniquePath。它们并非简单按照查询层次(上层/下层)划分,而是按照去重的目的和语义来区分,这是 PostgreSQL 优化器设计中一个容易被误解的细节。
UpperUniquePath 服务于用户查询语义,UniquePath 服务于查询优化内部 。这种区分让优化器能够针对不同场景选择最合适的去重策略,同时保证输入数据有序(这是优化器与执行器之间的关键契约)。
无论优化器选择哪种 Path,最终生成的执行计划节点在运行时通常都是 Unique 节点,由执行器统一处理。
2.3.2 主要数据结构
c
/* ----------------
* UniqueState 状态信息说明
*
* Unique 节点通常挂载在 Sort 排序节点的上层,用于剔除排序阶段产出的重复元组。
* 其核心工作逻辑非常简单:将从子计划读取的当前元组,与从上一轮缓存的历史元组
* (存储在自身结果槽中)进行对比。
* 如果两条元组的所有关键字段完全一致,则判定为重复,继续从下层排序节点读取
* 下一条元组,反复尝试比对,直至找到不重复的元组或数据读取完毕。
* ----------------
*/
typedef struct UniqueState
{
PlanState ps; /* 算子基础执行状态,首个字段为节点类型标记 */
ExprState *eqfunction; /* 预编译完成的元组等值比对表达式状态 */
} UniqueState;
2.3.3 初始化逻辑(ExecInitUnique)
ExecInitUnique 是 Unique 算子的生命周期入口,核心作用是加载优化器生成的静态计划模板,完成所有执行资源初始化,构建可运行的执行状态机。整体执行流程清晰、层层递进:
首先函数通过断言拦截非法扫描模式,禁止反向扫描与标记扫描。由于 Unique 依赖有序相邻比对,反向遍历会彻底打乱去重逻辑,内核从初始化阶段就做了严格防护。
随后创建 UniqueState 状态节点,绑定计划模板与执行上下文,注册核心迭代执行函数 ExecUnique,完成状态与执行逻辑的绑定。
接着递归初始化下层子计划节点,也就是为 Unique 提供有序数据的 Sort 排序节点 或有序索引扫描节点 ,保证后续迭代可正常拉取元组。
最后初始化结果缓存元组槽,关闭无用的投影能力,同时调用 execTuplesMatchPrepare 预编译元组比对函数。Unique 仅做过滤、不修改元组数据,无需投影计算,这也是它比 Group、HashAggregate 更轻量、高效的核心原因。
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)
ExecUnique 是 Unique 算子的运行时核心函数,承担整个去重过程的实际执行工作。它属于典型的 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)
ExecEndUnique 是 Unique 算子的资源回收入口,用于结束节点执行并释放相关资源 。
由于 Unique 本身不维护哈希表、排序器、磁盘临时文件等复杂运行状态,因此其清理过程极为简单。绝大多数资源(ExprContext、TupleSlot 等)都由 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);
}