PostgreSQL中的查询:1.查询执行阶段

yzsDBA
关注

PostgreSQL中的查询:1.查询执行阶段

开始关于PG内部执行机制的文章系列。这一篇侧重于查询计划和执行机制。

本系列包括:

1、查询执行阶段(本文)

2、统计数据

3、顺序扫描

4、索引扫描

5、嵌套循环连接

6、哈希连接

7、Merge join

本系列针对PG14编写。

简单查询协议

PG客户端-服务协议的基本目的是双重的:将SQL查询发送到服务,接收整个执行结果作为响应。服务接收到查询去执行要经过几个阶段。

解析

首先,需要解析查询文本,这样服务才能准确了解需要执行什么。

词法分析器和解析器。词法解析器负责识别查询字符串中的词位(如SQL关键字、字符串、数字文字等),而解析器确保生成的词位集在语法上是有效的。解析器和词法解析器使用标准工具Bison和Flex实现。解析的查询表示位抽象的语法树。例如:

image.png

在这里,将在后台内存中构建一棵树。下面以高度简化的形式表示该树。树中节点用查询的相应部分标记:

RTE是一个晦涩的缩写,表示“范围表条目”。PG源码中“range table”指表、子查询、连接结果--也就是说SQL语句操作的任何记录集。

语法分析器。语法分析器确定数据库中是否存在查询中引用的表和其他对象,用户是否有访问这些对象的权限。语法分析需要的所有信息都在系统catalog中。

语法分析接收分析器传来的解析树并重新构建它,并用引用的特定数据库对象、数据类型信息等来补充它。如果开启debug_right_parse,则会在服务消息日志中显示完整的树信息,尽管这没什么实际意义。

转换

下一步,对查询进行重写。

系统内核将重写用于多种目的。其中之一是将解析树中的视图名替换为该视图查询相对应的子树。pg_tables是上面例子的一个视图,重写后的解析树将采用以下形式:

解析树对应的查询(经所有操作仅在树上执行,而不是在查询文本上执行):

image.png

解析树反映查询的语法结构,而不是执行操作的顺序。行级安全性在转换阶段实施。

系统核心使用重写的另一个例子是版本14中递归查询的SEARCH和CYCLE子句中实现。

PG支持自定义转换,用户可以使用重写规则系统来实现。规则系统作为PG主要功能之一。这些规则得到了项目基础的支持,并在早期开发过程中反复重新设计。这是一个强大的机制,但难以理解和调试。甚至有人提议将规则从PG中完全删除,但没有得到普遍支持。在大多数情况下,使用触发器而不是规则更安全、更方便。

如果debug_print_rewritten开启,则完整重写的解析树会显示在服务消息日志中。

计划

SQL是一种声明性语言:查询指定要检索什么,但不指定如何检索它。任何查询都可以通过多种方式执行。解析树中的每个操作都有多个执行选项。例如,您可以通过读取整个表并丢弃不需要的行来从表中检索特定记录,或者可以使用索引来查询与您查询匹配的行。数据集总是成对连接。连接顺序的变化会产生大量执行选项。然后有许多方法可以将2组行连接在一起。例如,您可以逐个遍历第一个集合中的行,并在另一个集合中查找匹配的行,或者您可以先对2个集合进行排序,然后将他们合并在一起。不同方法在某些情况下表现更好,在另一些情况下表现更差。

最佳计划的执行速度可能比非最佳计划快几个数量级,这就是为什么优化解析查询的执行计划器是系统最复杂的元素之一。

计划树。执行计划也可以表示为树,但其节点是对数据的物理操作而不是逻辑操作。

开启debug_print_plan,则整个执行计划树会显示在服务消息日志中。这是非常不切实际的,因为日志非常混乱。更方便的选择是使用EXPLAIN命令:

EXPLAIN

SELECT schemaname, tablename

FROM pg_tables

WHERE tableowner = 'postgres'

ORDER BY tablename;

                          QUERY PLAN?????????????????????????????????????????????????????????????????????

Sort  (cost=21.03..21.04 rows=1 width=128)

  Sort Key: c.relname

  ?> Nested Loop Left Join  (cost=0.00..21.02 rows=1 width=128)

     Join Filter: (n.oid = c.relnamespace)

     ?> Seq Scan on pg_class c  (cost=0.00..19.93 rows=1 width=72)

          Filter: ((relkind = ANY ('{r,p}'::"char"[])) AND (pg_g...

      ?> Seq Scan on pg_namespace n  (cost=0.00..1.04 rows=4 wid...

(7 rows)

该图显示了树的主要节点。相同的节点在EXPLAIN输出中使用箭头标记。Seq Scan节点表示读取表操作,而Nested Loop节点表示表连接操作。这里有2个优趣的点需要注意:

1) 其中一个初始化表从执行计划树中消失了,因为执行计划器指出查询处理中不需要它

2) 估算要处理的行数和每个节点处理的代价

计划查询。为找到最佳计划,PG使用基于成本的查询优化器。优化器会检查各种可用的执行计划并估算需要的资源量,例如IO周期和CPU周期。这个计算出的估算值转换成任意单位,被称为计划成本。选择结果成本最低的计划来执行。

问题是,可能的计划数量随着连接数量的增加而呈指数增长,即使对于相对简单的查询,也无法一一筛选所有计划。因此,使用动态规划和启发式限制搜索范围。这允许在合理的时间内精确第解决查询中更多表的问题,但不能保证所选的计划是真正最优的。因为计划其使用简化的数学模型并可能使用不精确的初始化数据。

Ordering joins:可以以特定方式构建查询,以显著缩小搜索范围(有可能错过找到最佳计划的机会):

1) 公共表表达式通常与主查询分开优化。从12开始可以使用MATERIALIZE子句来强制执行此操作。

2) 来自非SQL函数的查询和主查询分开优化。(在某些情况下,SQL函数可以内联到主查询中)

3) join_collapse_limit参数与现式join子句以及from_collapse_limit参数与子查询一起可以定义某些连接顺序,具体取决于查询语法。

最后一个可能需要解释,下面的查询调用FROM子句中的几个表,没有显式连接:

SELECT ...FROM a, b, c, d, eWHERE ...

下面是此查询的解析树:

在这个查询中,规划器将考虑所有可能的连接顺序。在下一个示例中,一些连接由JOIN子句显式定义:

SELECT ...FROM a, b JOIN c ON ..., d, eWHERE ...

解析树反映了这一点:

规划器折叠连接树,有效地将其转换为上一个示例中的树。该算法递归地遍历树并用其组件的平面列表替换每个JOINEXPR节点。

但是只有在生成的屏幕列表包含不超过join_collapse_limit个元素(默认8个)时,才会发生这种“扁平化”。在上面示例中,如果将join_collapse_limit设置5或更少,则不会折叠JOINEXPR节点。对于规划器来说,这意味着两件事:表B必须连接到表C(反之亦然,join对中的join 顺序不受限制);表A、D、E以及B到C的连接可以按任意顺序连接。

如果join_collapse_limit设置为1,则将保留任何显式JOIN顺序。注意,无论该参数如何,操作FULL OUTER JOIN都不会折叠。

参数from_collapse_limit(默认也是8)以类似的方式限制子查询的展平。子查询似乎与连接没有太多共同之处,但当它归结为解析树级别时,相似性显而易见。

例子:

SELECT ...FROM a, b JOIN c ON ..., d, eWHERE ...

下面是对应树:

这里唯一的区别是JOINEXPR节点被替换成FROMEXPR(因此参数名称为FROM)。

声明: 本文由入驻OFweek维科号的作者撰写,观点仅代表作者本人,不代表OFweek立场。如有侵权或其他问题,请联系举报。
侵权投诉

下载OFweek,一手掌握高科技全行业资讯

还不是OFweek会员,马上注册
打开app,查看更多精彩资讯 >
  • 长按识别二维码
  • 进入OFweek阅读全文
长按图片进行保存