postgresql源码(九)——Executor(Scan节点(上))

从前面介绍的可优化语句处理相关的背景知识、实现思想和执行流程,不难发现可优化语句执行的核心内容是对于各种计划节点的处理,由于使用了节点表示、递归调用、统一接口等设计,计划节点的功能相对独立、代码总体流程相似,下面介绍执行器中各种计划节点的相关执行过程。

在PostgreSQL中,计划节点分为四类,分别是控制节点(Control Node)、扫描节点(ScanNode),物化节点(Materialization Node)、连接节点(Join Node) 。

  • 控制节点:是一类用于处理特殊情况的节点,用于实现特殊的执行流程。例如,Result节点可用来表示INSERT语句中VALUES子句指定的将要插人的元组。

  • 扫描节点:顾名思义,此类节点用于扫描表等对象以从中获取元组。例如,SeqScan节点用于顺序扫描一个表.毎次扫描一个元组。

  • 物化节点:这类节点种类比较复杂,但它们有一个共同特点,即能够缓存执行结果到辅助存储中。物化节点会在第一次被执行时生成其中的所有结果元组,然后将这些结果元组缓存起来,等待其上层节点取用;而非物化节点则是每次被执行时生成一个结果元组并返回给上层节点。例如,Sort节点能够获取下层节点返回的所有元组并根据指定的属性进行排序,并将排序结果全部缓存起来,每次上层节点从Sort节点取元组时就从缓存中按顺序返回下一个元组(见Postgres中的物化节点之sort节点)。

  • 连接节点:此类节点对应于关系代数中的连接操作,可以实现多种连接方式(条件连接、左连接、右连接、全连接、自然连接等),每种节点实现一种连接算法。例如,HashJoin实现了基于Hash的连接箅法。

扫描节点

扫描节点的作用是扫描表,每次获取一条元组作为上层节点的输入。扫描节点普遍存在于查询计划树的叶子节点,它不仅可以扫描表,还可以扫描函数的结果集、链表结构、子查询结果集等。

所有扫描节点都使用Scan作为公共父类,Scan不仅继承了Plan的所有属性,还扩展定义了scanrelid用于记录被扫描的表在范围表中的序号。

typedef struct Scan
{
	Plan		plan;
	Index		scanrelid;		/* relid is index into the range table */
} Scan;

下面是来自源码中的所有的Scan类型:

    T_SeqScanState,
	T_SampleScanState,
	T_IndexScanState,
	T_IndexOnlyScanState,
	T_BitmapIndexScanState,
	T_BitmapHeapScanState,
	T_TidScanState,
	T_SubqueryScanState,
	T_FunctionScanState,
	T_ValuesScanState,
	T_CteScanState,
	T_WorkTableScanState,
	T_ForeignScanState,
	T_CustomScanState,

下面将对其一一说明。

扫描节点有各自的执行函数,但是这些执行函数都由公共的执行函数ExecScan来实现。

TupleTableSlot *
ExecScan(ScanState *node,
		 ExecScanAccessMtd accessMtd,	/* function returning a tuple */
		 ExecScanRecheckMtd recheckMtd)

ExecScan需要三个参数:

  • 状态节点ScanState,
  • 获取扫描元组的函数指针(accessMtd,由于每一种扫描节点扫描的对象不同,因此函数都不同),
  • 判断元组是否满足符合过滤条件的函数指针(recheckMtd)。

(这里要说一下:recheckMtd函数用于并发控制,如果当前元组被其他事物修改并已提交,需要检测该元组是否仍然满足选择条件。如果你有兴趣深入了解,建议查看EvalPlanQual函数

以及src/backend/executor/README文件的EvalPlanQual (READ COMMITTED Update Checking)部分)

ExecScan迭代地扫描对象,每次执行返回一条结果(内部返回元组是通过ExecScanFetch实现的)。ExecScan会使用accessMtd获取元组,然后recheckMtd进行过滤条件判断,最终返回元组。

看例子:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;  
                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=5.07..229.20 rows=101 width=244)
   Recheck Cond: (unique1 < 100)                  <---recheckMtd 的作用
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)
         Index Cond: (unique1 < 100

1.SeqScan 节点

SeqScan是最基本的扫描节点,它用于扫描物理表,完成没有索引辅助的顺序扫描过程。其计划节点SeqScan实际是Scan节点的一个别名,并未定义扩展属性。其执行状态节点SeqScanState也直接使用ScanState。

SeqScan节点的初始化由函数ExecInitSeqScan完成。该函数首先创建一个SeqScanState结构,将SeqScan节点链接在SeqScanState结构的ps字段中。然后调用ExecInitExpr对计划节点的目标属性和査询条件进行初始化,并将它们链接到SeqScanState相应的字段中。接下来还将为计划节点分配用于存储结果元组和扫描元组的数据结构。最后通过计划节点中scanrelid字段的信息获取被扫描对象的RelationData结构,并链接在ss_currentRelation

package Heap; public class Heap<T extends Comparable<T>>{ private T[] items; private int N; public Heap(int capacity) { this.items = (T[])new Comparable[capacity+1]; this.N = 0; } private boolean less(int i, int j){ //判断堆中索引i处的元素是否小于索引i处的元素 return items[i].compareTo(items[j]) < 0; } private void exch(int i, int j){ //交换堆中i索引和i索引处的值 T temp = items[i]; items[i] = items[j]; items[j] = temp; } public T delMax(){ //删除堆中最大的元素,并返回这个最大元素 T max = items[1]; exch(1, N); items[N]=null; N--; sink(1); return max; } public void insert(T t){ //往堆中插入1个元素 items[++N] = t; swim(N); } private void swim(int k){ //使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置 while(k>1){ if(less(k/2, k)){ exch(k/2, k); } k=k/2; } } private void sink(int k){ //使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置 while(2*k<=N){ int max; if(2*k+1<=N){ if(less(2*k,2*k+1)){ max=2*k+1; }else{ max=2*k; } }else{ max=2*k; } if(!less(k,max)){ break; } exch(k,max); k=max; } } } package Heap; public class HeapTest { public static void main(String[] args) { Heap<String> heap= new Heap<String>(7); heap.insert("A"); heap.insert("B"); heap.insert("C"); heap.insert("D"); heap.insert("E"); heap.insert("F"); String s=null; while((s=heap.delMax())!=null){ System.out.print(s+" "); } } } 这个代码运行出来是F E D C B A ,我想知道为什么,解析给我
最新发布
10-24
你提供的代码实现了一个**最大堆(Max Heap)**,使用泛型 `T extends Comparable<T>` 来支持可比较的对象(如字符串、数字等)。最终输出结果是: ``` F E D C B A ``` 我们来详细解析 **为什么是这个顺序?每一步发生了什么?** --- ### ✅ 一、总体逻辑分析 这是一个基于数组的**最大堆**实现,用于维护一个优先队列,其中: - 堆顶(`items[1]`)始终是当前最大的元素。 - 插入时用 `swim`(上浮)保持堆性质。 - 删除最大元素时用 `sink`(下沉)恢复堆结构。 - 使用的是 **1-indexed 数组**(即从索引 1 开始存储数据,`items[0]` 不使用)。 所以每次调用 `delMax()` 都会取出当前最大值,并通过堆的调整保证下一次还能取到新的最大值。 --- ### ✅ 二、插入过程详解:`insert("A"), insert("B") ... insert("F")` 初始:`N = 0`,数组为空(容量为7+1=8,索引0~7) 依次插入:`"A", "B", "C", "D", "E", "F"` > 字符串按字典序比较:`"A"<"B"<"C"<"D"<"E"<"F"` #### 插入过程如下: | 步骤 | 插入值 | N 变为 | 放入位置 | 是否触发 swim | |------|--------|--------|----------|----------------| | 1 | A | 1 | items[1] | 否(k=1,不进入 while) | | 2 | B | 2 | items[2] | 是 → 比较 k=2, k/2=1 → `"B">"A"` → `less(1,2)=true` → 交换 → B 上浮到根 | | 3 | C | 3 | items[3] | 是 → 比较 k=3, 父=1 → `"C">"B"` → 交换 → C 成为根 | | 4 | D | 4 | items[4] | 是 → 父节点是 2 (`k=4 → 4/2=2`) → `"D" > "B"`? 是 → 交换 → D 到 index=2 | | 5 | E | 5 | items[5] | 是 → 父节点是 2 (`5/2=2`) → `"E" > "D"`? 是 → 交换 → E 到 index=2;再看 E 和根 C → `"E">"C"` → 继续上浮!交换 index=1 和 2 → E 成为根 | | 6 | F | 6 | items[6] | 是 → 父节点是 3 (`6/2=3`) → `"F" > "C"` → 交换 → F 到 index=3;再比较 F 和根 E → `"F">"E"` → 交换 → F 成为根 | ✅ 最终堆的状态(逻辑结构): ``` F (1) / \ E (2) C (3) / \ / D(4) B(5) A(6) ``` 对应数组(index 1~6): ``` index: 1 2 3 4 5 6 [F , E , C , D , B , A] ``` ✔️ 根节点是 `F` —— 当前最大。 --- ### ✅ 三、删除过程:`delMax()` 被循环调用直到堆空 ```java while((s=heap.delMax())!=null){ System.out.print(s+" "); } ``` 我们逐次分析每一次 `delMax()` 的行为。 --- #### 第1次:`delMax()` → 返回 F 1. `max = items[1] = "F"` 2. `exch(1, 6)`:把最后一个元素 A 换到根 - 数组变成:`[A, E, C, D, B, null]`,N=5 3. `sink(1)`:对 A 下沉 ##### 执行 `sink(1)`: - k=1,左孩子=2*k=2 → 存在;右孩子=3 → 存在 - 比较 `items[2]="E"` 和 `items[3]="C"` → E > C → max = 2 - 比较 `items[1]="A"` 和 `items[2]="E"` → `less(1,2)=true` → A < E → 需要下沉 - 交换 index 1 和 2 → E 上升,A 下降到 index=2 现在数组: ``` index: 1 2 3 4 5 [E , A , C , D , B] ``` 继续 sink at k=2: - 左孩子=4,右孩子=5 - `items[4]="D"`, `items[5]="B"` → D > B → max=4 - `items[2]="A"` vs `items[4]="D"` → A < D → 交换 → A 到 index=4 继续 sink at k=4: - 左孩子=8 > N=5 → 结束 最终堆结构: ``` E (1) / \ D (2) C (3) / B(4) A(5) ``` 实际数组(index1~5):`[E, D, C, B, A]` 📌 输出:`F` --- #### 第2次:`delMax()` → 返回 E 1. 取出 `E` 2. 把 `A`(最后元素)放到根 → `items[1]=A` 3. `sink(1)` - 左=2(D), 右=3(C) → D > C → max=2 - A < D → 交换 → D 上升,A 到 index=2 - 在 index=2:左=4(B),右=5(A) → B > A → max=4 - A < B → 交换 → B 上升,A 到 index=4 - 在 index=4:无子节点 → 停止 新堆: ``` D (1) / \ B (2) C (3) / A(4) ``` 📌 输出:`F E` --- #### 第3次:`delMax()` → 返回 D - 提取 D - 把 A 移到根 → `items[1]=A` - sink(1) - 左=2(B), 右=3(C) → C > B → max=3 - A < C → 交换 → C 上升 堆变为: ``` C (1) / \ B (2) A (3) ``` 📌 输出:`F E D` --- #### 第4次:`delMax()` → 返回 C - 提取 C - 把 A 移到根(只剩两个元素) - sink(1) - 左=2(B), 无右 - A < B → 交换 堆变为: ``` B (1) / A (2) ``` 📌 输出:`F E D C` --- #### 第5次:`delMax()` → 返回 B - 提取 B - A 移到根,N=1 → 删除后 N=1 → `sink(1)` 不执行(因为 left child=2 > N=1) 堆剩下:`[A]` 📌 输出:`F E D C B` --- #### 第6次:`delMax()` → 返回 A - 提取 A - N 减为 0 - 循环结束 📌 输出:`F E D C B A` --- ### ✅ 四、总结:为什么会输出 `F E D C B A`? 因为你实现的是一个 **最大堆**,它的特性是: > 每次都能快速找到并移除当前集合中的最大元素。 因此,无论插入顺序如何,只要所有元素都插入完成,然后不断调用 `delMax()`,就会得到一个**降序排列的结果**。 这其实就是 **堆排序(Heap Sort)的核心思想**! --- ### ✅ 补充说明:关于字符串比较 Java 中 `String` 实现了 `Comparable<String>`,默认按**字典序**比较: - `"A" < "B" < "C" < "D" < "E" < "F"` 所以在堆中它们被视为数值大小关系,`F` 最大,`A` 最小。 --- ### ✅ 总结回答你的问题: > ❓“这个代码运行出来是 F E D C B A,我想知道为什么” 👉 因为: 1. 你实现的是一个 **基于 1-indexed 数组的最大堆**; 2. 插入时通过 `swim` 上浮确保最大值总能上升到根; 3. 删除时通过 `delMax()` 不断取出当前最大值,并用 `sink` 调整堆; 4. 所有元素 `"A"` 到 `"F"` 按字典序递增,最大堆自然按 `F > E > D > C > B > A` 排序; 5. 因此输出顺序就是这些元素的**降序排列**。 🎯 这正是堆排序的经典应用! ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值