如何从0深入PostgreSQL内核写一个执行器算子?
大家好,我叫光城,昨天分享了一个主题:如何从0深入PostgreSQL内核写一个执行器算子?今天来总结一下,本篇文章的直播回放可以在b站观看,点击原文或者识别下方二维码即可!
1.执行器概论
执行器作为连接查询计划和存储引擎的桥梁,负责从存储引擎读取数据,并基于查询计划树执行对应的算子,并得到最终的查询结果。
执行器的处理模型主要分为两大类:基于拉操作的模型和基于推操作的模型。
1.1 Pull模型
也被称为火山模型,是指从最顶层的输出节点开始,不断从下层节点拉取数据,因此是一种自顶向下的执行方式。
优点
通用性:拉模型不受数据及规模的限制,可以处理任意规模的数据集。
灵活性:拉模型可以灵活控制输出的数量,比如Limit算子及时短路。
不足
阻塞节点:对于排序节点,需要首先读取下层节点所有数据,并根据数据量,选择合适的算法进行内排序或者外排序。
函数调用开销:每条元组在节点之间流动的过程中都会涉及大量的函数调用。
缓存不友好:过多的控制语句、函数调用容易导致缓存失效。
并行不友好。
1.2 Push模型
Push模型:从最底层的节点开始,不断生成数据,并向上层节点进行数据推送,因此是一种自底向上的执行方式。推模型本质是一种基于物化的操作,每一个节点处理所有的输入数据,并将处理后的数据进行物化,并传递给上层节点。
优点
并行友好。
推模型解决了拉模型中函数调用过多和缓存切换过多的问题。由于每个节点内部的处理逻辑相同,缓存使用率也更高。
不足
内存占用大
1.3 向量化执行引擎
除了拉模型和推模型两大基础模型之外,还引入向量化执行引擎。
每次一个 batch数据而非一行数据,减少函数调用。
配合列式存储 + SIMD指令,提升性能。
2.执行器执行流程
2.1 执行器与上下游的关联关系
1.执行器与算子如何关联?
通过三部曲:ExecutorStart、ExecutorRun、ExecutorEnd。
2.查询计划与执行器如何关联?
通过Portal。
Portal记录了与执行相关的所有信息,例如查询树、计划树和执行状态。对于用户提交的普通查询语句,执行器会创建一个匿名的Portal对象。游标语句,执行器会创建一个对应的命名Portal对象。
3.执行器与存储层如何关联?
通过table am与scan/modifyTable算子进行关联。
2.2 表达式与投影
SQL语句中除了SELECT、FROM、WHERE、GROUP BY等关键字之外的部分,都可以被认为是某种表达式。
例如:a列,a + 1,a * b等等。
表达式名 | 示例 |
---|---|
常量表达式 | 10 |
列引用 | i, j |
位置参数引用 | $1 |
下标 | arr[i] |
域选择表达式 | table.column |
运算符表达式 | a > b, x and y, x or y |
函数表达式 | upper(name), now() |
聚集表达式 | coumt(*), avg(salary) |
窗口函数 | sum(salary) over (partition by department) |
标量子查询 | select (select max(age) from students) from student; |
数组表达式 | select array[1, 2, 3] |
Row表达式 | select row(1, 'John Doe') |
2.3 表达式实现原理
ExprContext
记录下每次表达式评估所需要的tuple,可能来自scan、outer、inner。
TupleTableSlot *ecxt_scantuple;
TupleTableSlot *ecxt_innertuple;
TupleTableSlot *ecxt_outertuple;
ExprState
ExprState 是表达式求值的顶级节点,它包含:
计算表达式的指令(steps)
存储评估的结果slot
存储空值结果
存储scalar表达式评估的结果
实际计算表达式的函数
对于一个表达式树,每个node初始化为ExprEvalOp,ExprEvalStep 存储每一步表达式评估的结果。
3.如何写一个执行器算子?
假设有一个数据库需求,需要添加一个数据检查的功能,会检查其输入的数据,并对数据进行验证,如果发现数据不符合条件,则会抛出错误或者警告。
例如:plan如下
-> AssertAssert Cond: (i = 1)-> Seq Scan
我们如何为数据库新增一个AssertOp算子呢?(不考虑优化器,只考虑执行器)
以上就是本次的分享,欢迎转发与收藏。