查询计划
PostgreSQL为每个收到的查询产生一个查询计划。查询计划的结构是一个计划结点的树。最底层的结点是扫描结点:它们从表中返回未经处理的行。不同的表访问模式有不同的扫描结点类型:顺序扫描、索引扫描、位图索引扫描。也还有不是表的行来源,例如VALUES
子句和FROM
中返回集合的函数,它们有自己的结点类型。如果查询需要连接、聚集、排序、或者在未经处理的行上的其它操作,那么就会在扫描结点之上有其它额外的结点来执行这些操作。并且,做这些操作通常都有多种方法,因此在这些位置也有可能出现不同的结点类型。
本节列出了 PostgreSQL 中的不同扫描结点。请注意,有些结点是通用的,在特定查询中可能有几个替代结点可用,实际结点由上面概述的规划算法确定(例如,索引扫描和顺序扫描),而有些结点仅适用于特定情况,没有替代项(例如,外表扫描)。
对于访问表数据的扫描结点,大致了解数据在磁盘上的组织方式非常有用:每个表由一个或多个页组成,每个页包含一个小的页头,然后是与表记录对应的数据行。由于 Redrock Postgres 中的 多版本并发控制 机制,一个页面实际上可能在共享缓冲区中存在多个版本,每个事务只有一个版本是“可见”的。
索引是指向特定页面,然后指向该页面中的值的独立结构。不同类型的索引支持不同的索引扫描功能:哈希索引只能查找特定值,但其他索引类型(如 btree)按特定顺序维护值,并支持按该顺序(甚至向后扫描)扫描数据!这为索引扫描提供了一些有趣的属性:对于不等式谓词(例如,WHERE value > 10
或WHERE value < 100
),它们可以在正确的位置开始或结束扫描,忽略任何不匹配的值。它们还可用于通过查看索引的开头或结尾(并忽略任何 NULL 值)来满足 max 和 min 等聚合,并且可以按索引顺序生成数据(有时对ORDER BY
或 Merge 结点很有用)。
顺序扫描(Seq Scan),是从表中获取数据的最简单方法:它按顺序扫描每一页数据。与大多数其他扫描一样,它可以在读取数据时应用过滤器,但它需要先读取数据,然后再丢弃它。顺序扫描无法仅对您想要的数据进行归零:它始终读取表中的所有内容。这通常是低效的,除非您需要很大一部分表来回答您的查询,但始终可用,有时可能是唯一的选择。
相关属性:
索引扫描(Index Scan),使用索引来查找特定行或与谓词匹配的所有行。索引扫描将一次查找一行(对于像 WHERE id = 1234
这样的查询,或者作为嵌套循环中的内部表,查找与当前外行匹配的行),或者按顺序扫描表的某个部分。索引扫描必须首先查找索引中的每一行,然后检查该索引条目的实际表数据。必须检查表数据,以确保它找到的行对当前事务实际可见,并且还要提取查询中包含的索引中不存在的任何列。因此,索引扫描实际上比顺序扫描具有更高的每行开销:它的真正优点是它只允许您读取表中的某些行。如果查询谓词不是很有选择性(即,如果筛选出的行很少),则顺序扫描可能仍比索引扫描更有效。
如果查询谓词与索引完全匹配,则扫描将仅检索匹配的行。如果查询中有其他谓词,则索引扫描可以在读取行时筛选行,就像顺序扫描一样。
相关属性:
只用索引的扫描(Index Only Scan),与索引扫描非常相似,但数据直接来自索引,并且可见性检查是专门处理的,因此可以避免完全查看表数据。只用索引的扫描 速度更快,但并不总是可以作为常规索引扫描的替代方法。它有两个限制:索引类型必须支持仅索引扫描(常见的 btree 索引类型始终支持),并且(有些明显)查询必须仅投影索引中包含的列。如果您有 SELECT * 查询,但实际上并不需要所有列,则只需更改列列表即可使用仅索引扫描。
相关属性:
位图堆扫描(Bitmap Heap Scan),采用由 位图索引扫描(直接使用,或者通过 位图与 和 位图或 结点生成的一系列位图集操作)生成的行位置位图,并查找相关数据。位图的每个区块可以是精确的(直接指向行),也可以是有损的(指向包含至少一个与谓词匹配的行的页面)。
PostgreSQL 更倾向使用精确的区块,但如果配置的工作内存区(由参数work_mem
决定)空间有限,它也将开始使用有损区块。这些区块实际上是由位图堆扫描的子结点以有损或精确的形式生成的,不过在处理这些区块以获取行时,更能合理有效地选择有损和精确的形式,因此这些属性会反映在位图堆扫描中。如果位图区块是有损的,则结点将需要读取整个页面,并重新检查里面的行是否匹配指定的索引条件(因为它不知道页面上需要哪些行)。
相关属性:
- Exact Heap Blocks
- Filter
- Lossy Heap Blocks
- Recheck Cond
- Rows Removed by Filter
- Rows Removed by Index Recheck
位图索引扫描(Bitmap Index Scan),可以被视为顺序扫描和索引扫描之间的中间地带。与索引扫描一样,它扫描索引以确定需要获取的确切数据,但与顺序扫描一样,它利用了更易于批量读取的数据。普通的索引扫描一次从索引中获取一个行位置,并立即访问表中的该行。位图扫描一次性从索引中获取所有行位置,使用内存中的“位图”数据结构对它们进行排序,然后按物理行位置顺序访问表元组。
位图索引扫描实际上与位图堆扫描协同运行:它不会提取数据本身。位图索引扫描不是直接生成行,而是构造潜在行位置的位图。它将此数据馈送到父位图堆扫描,该扫描可以解码位图以提取基础数据,逐页抓取数据。
位图堆扫描是位图索引扫描最常见的父结点,但计划也可能在实际获取基础数据之前将几个不同的位图索引扫描与 位图与 或 位图或 结点组合在一起。这允许 PostgreSQL 一次使用两个不同的索引来执行查询。
相关属性:
公共表表达式的扫描(CTE Scan),用于扫描 公共表表达式 的结果。
相关属性:
- CTE Name
- Filter
- Rows Removed by Filter
自定义扫描(Custom Scan)实现的扫描,可以作为单独的模块添加并插入到标准 PostgreSQL 查询规划和执行中。
外表扫描(Foreign Scan),用于扫描 外表。
函数结果扫描(Function Scan),扫描 集合返回函数 (比如 unnest
或 regexp_split_to_table
) 的结果。
相关属性:
- Filter
- Function Call
- Function Name
- Rows Removed by Filter
子查询扫描(Subquery Scan),用于扫描范围表中子查询的输出。我们经常需要在子查询的计划之上有一个额外的计划结点来执行表达式计算(我们不能在不改变其语义的情况下将其推送到子查询中)。
相关属性:
表样本扫描(Table Sample Scan),在使用 表样本 功能时扫描表。请注意,此子句确实会更改查询的语义,但如果您希望收集有关大型表中数据的一些统计信息,则它可能比完全顺序扫描更有效。
行地址扫描(Tid Scan),与索引扫描类似,但只能使用内部和不稳定的 ctid 标识符查找行。您不太可能在查询中使用这种类型的扫描。
行集合扫描(Values Scan),扫描字面的 VALUES 子句。
工作表扫描(Work Table Scan),扫描用于计算递归 公共表表达式 时使用的工作表。
本节介绍 PostgreSQL 中的三种类型的连接机制。
一次在两个表上执行连接;如果将多个表连接在一起,则一个连接的输出将被视为后续连接的输入。联接大量表时,遗传查询优化器设置可能会影响所考虑的联接组合。
哈希连接(Hash Join),从内部表生成一个哈希表,采用连接键进行关联映射。然后扫描外部表,检查是否存在相应的值。如果哈希表超过work_mem
,则此过程需要分几个批次进行,将临时文件写入磁盘,这会变得非常慢。
相关属性:
- Filter
- Hash Cond
- Inner Unique
- Join Filter
- Join Type
- Rows Removed by Filter
- Rows Removed by Join Filter
合并连接(Merge Join),加入已按其共享连接密钥排序的两个子项。这只需要扫描每个关系一次,但两个输入都需要首先按连接键进行排序(或以生成已排序输出的方式进行扫描,例如与所需排序顺序匹配的索引扫描)。
相关属性:
- Filter
- Inner Unique
- Join Filter
- Join Type
- Merge Cond
- Rows Removed by Filter
- Rows Removed by Join Filter
嵌套连接(Nested Loop Join),对于外部表中的每一行,循环访问内部表中的所有行,并查看它们是否与连接条件匹配。如果可以使用索引扫描内部关系,则可以提高嵌套循环联接的性能。这通常是处理联接的低效方法,但始终可用,有时可能是唯一的选择。
相关属性:
本节列出了 PostgreSQL 中的不同物化结点。这些结点在内存中物化其子结点的结果(以避免重新计算值)。
聚合结点(Aggregate)实现普通聚合或分组聚合。对于分组聚合,PostgreSQL 可以使用预排序输入或未排序输入;后一种策略使用内部哈希表。
相关属性:
- Filter
- Group Key
- Partial Mode
- Rows Removed by Filter
- Strategy
位图与结点(Bitmap And),生成两个物理行位置位图相交的位图(即,仅在两个位图中出现的位置)。位图可以来自位图索引扫描或其他 位图与 或 位图或 子结点。
请注意,由于内部实现限制,位图与结点不会跟踪它们生成的行数。它们的行计数将始终列为“未知”,并且不会被标记为错误估计。
位图或结点(Bitmap Or),生成两个物理行位置位图(即任一位图中出现的位置)并集的位图。位图可以来自位图索引扫描或其他 位图与 或 位图或 子结点。
请注意,由于内部实现限制,位图或结点不会跟踪它们生成的行数。它们的行计数将始终列为“未知”,并且不会被标记为错误估计。
分组结点(Group),用于指定了GROUP BY
(但未聚合)的查询。输入必须根据分组列进行预排序。
相关属性:
- Group Key
哈希结点(Hash),将数据读取到哈希表中,通过哈希键可以轻松查找数据。这用于哈希连接和哈希聚合。
相关属性:
物化结点(Materialize),在内存中物化其子结点的结果(以避免重新计算值)。
如果没有外部计划,计算出一个无变量的目标列表。如果是外部计划,则从外部计划返回元组(根据目标列表所示的投影层次之后)。如果带有用于过滤的常量条件表达式,则它表示一次性限定测试(即,不依赖于外部计划中的任何变量,因此只需计算一次)。
集合运算结点(SetOp),合并 集合运算 的两个数据集,如UNION
、INTERSECT
和EXCEPT
。请注意,集合运算结点的查询结构可能与你预期不同:集合运算结点不是其操作的集合的直接父级,而是只有一个追加子结点,该子结点具有一个子查询扫描,用于组合每个结点。
相关属性:
排序结点(Sort),从子结点获取数据并生成排序输出,内存足够的时候使用内存(取决于work_mem
设置),内存不足的时候“溢出”到磁盘。如果需要对输出进行排序,这显然是必要的,尽管有时可以以已排序的方式扫描输入 - 例如,如果扫描与所需排序顺序兼容的 btree 索引。
相关属性:
去重结点(Unique),与UNIX命令uniq
一样,它接受排序的输入并消除相邻的重复项。如果输入已排序(例如,查询还包括 ORDER BY
),则对于DISTINCT
子句很有用。
窗口聚合结点(Window Aggregate),在 窗口函数 中实现聚合。
本节列出了 PostgreSQL 中的不同控制结点。这些结点控制其子结点的行为。
追加结点(Append),生成子计划结果的串联。例如,这可以用于UNION ALL
查询。
聚集合并结点(Gather Merge),合并预先排序的工作进程输出的结果,类似于合并追加结点。
相关属性:
聚集结点(Gather),从多个工作进程收集数据 - 类似于追加结点。
相关属性:
从子结点获取数据并生成排序输出,内存足够的时候使用内存(取决于work_mem
设置),内存不足的时候“溢出”到磁盘。如果需要对输出进行排序,这显然是必要的,尽管有时可以以已排序的方式扫描输入 - 例如,如果扫描与所需排序顺序兼容的 btree 索引。
相关属性:
锁定行结点(Lock Rows),执行 FOR UPDATE
、 FOR NO KEY UPDATE
、 FOR SHARE
或者FOR KEY SHARE
子句的锁定行为。
合并追加结点(Merge Append),合并预先排序的子计划的结果以保留排序。
相关属性:
改表结点(Modify Table),通过插入、更新或删除与输入对应的行,将子计划生成的行应用于结果表。
相关属性:
- Conflict Arbiter Indexes
- Conflict Filter
- Conflicting Tuples
- Conflict Resolution
- Operation
- Rows Removed by Conflict Filter
- Tuples Inserted
集合投影结点(Project Set),将包含集合返回函数的投影应用于外部计划的输出元组。
递归并集结点(Recursive Union),生成两个子计划的递归并集。这用于计算递归 公共表表达式。
本节列出了计划结点中的各种属性。
过滤器(Filter),如果存在,这是一个用于删除行的过滤器。
需要注意的是,这是传统意义上的过滤器:这些行被读取(从数据源或计划中的其他操作)、检查,然后根据过滤器输出或删除。
虽然在目的上与您在索引扫描或仅索引扫描上看到的 索引条件 相似,但实现完全不同。在“索引条件”中,索引用于根据其索引值选择行,而根本不检查行本身。事实上,在某些情况下,您可能会在同一操作上看到“索引条件”和“过滤器”。
如果您注意到查询计划中较晚筛选了许多行,这可能是由于操作(如GROUP BY或排序)使用的列超过了必要的列,要求在丢弃行之前进行连接。有时也是不必要或不明智使用DISTINCT的结果。
索引条件(Index Cond),用于从索引中查找行位置的条件。
Postgres使用索引的结构化性质快速跳转到它要查找的行。不同的索引类型使用不同的策略。
虽然在用途上与 过滤器 相似,但实现完全不同。在“过滤器”中,检索出行,然后根据其值丢弃行。因此,您可以在同一操作上找到“索引条件”和“过滤器”。
过滤器删除的行数(Rows Removed by Filter)。这是一个计划结点每次执行的平均值。
如果删除的行比例很高,您可能需要调查是否有更具选择性的索引,帮助优化执行性能。
索引出的不满足条件的行数(Rows Removed by Index Recheck),索引扫描返回的不满足条件并随后被删除的行数。
这要么是有损位图扫描的结果,要么是不保证行匹配条件的索引类型。
扫描方向(Scan Direction)。Postgres能够对(b-tree)索引执行向前或向后扫描,以按排序顺序获取数据。在默认(文本)格式中,除非另有说明,否则方向是向前的。
您还可以在其他索引类型的扫描中看到“NoMovement”的扫描方向。
堆表访问行数(Heap Fetches),在只用索引的扫描期间,Postgres必须在堆表中而不是索引中查找的行数。
位图堆扫描精确块数(Exact Heap Blocks),位图堆扫描访问的精确块的数量。
如果行位图太大,无法放入工作内存(work_mem
),则其某些部分将被设置为“有损”——即它们引用整个页面而不是特定的行。
位图堆扫描有损块数(Lossy Heap Blocks),位图堆扫描访问的有损块数。
如果行位图太大,无法放入工作内存(work_mem
),则其某些部分将被设置为“有损”——即它们引用整个页面而不是特定的行。
位图扫描过滤条件(Recheck Cond),是位图扫描可能用于在提取行后过滤行的条件。
只有当位图扫描有损,或者它使用了任何不保证行匹配条件的有损索引类型(BRIN索引就是一个例子)时,才需要它。
有关是否实际需要重新检查的更多信息,请查看非零的 位图堆扫描有损块数 和非零的 索引出的不满足条件的行数。
哈希条件(Hash Cond),用于在哈希连接中匹配从外部计划到内部计划的行的条件。
内行唯一(Inner Unique),如果不超过一个内行可以匹配任何给定的外行,则此为true。这允许执行器跳过搜索其他匹配项。
连接过滤器(Join Filter),一个可以在连接之前发生的过滤器,允许Postgres避免查找行并在之后过滤它们。
连接类型(Join Type),执行了哪种类型的连接:内部(Inner)、完整(Full)、左(Left)、右(Right)、半(Semi)或反(Anti)。
在文本格式计划中,除非另有说明,否则可以假定它是内部连接。
连接过滤器删除行数(Rows Removed by Join Filter),连接过滤器删除的行数。
如果删除的行比例很高,您可能需要调查是否有更具选择性的索引,帮助优化执行性能。
合并条件(Merge Cond),用于在合并连接中匹配从外部计划到内部计划的行的条件。
部分模式(Partial Mode),如果这是Simple,则操作在一步中进行。
否则,Partial操作并行执行操作的块,而单独的Finalize操作将不同的部分连接在一起。
来自文档:“支持部分模式的聚合函数有资格参与各种优化,如并行聚合。” [来源]
执行了哪种聚合策略(Strategy):普通(Plain)、散列(Hashed)、排序(Sorted)或混合(Mixed)。
在非文本格式查询计划中,这使您可以查看Postgres是在执行HashAggregate (Hashed)、GroupAggregate (Sorted)还是MixedAggregate (Mixed)。
哈希批次(Hash Batches),如果哈希是在内存中完成的,则只有一个批次。
与 原始哈希批次 的区别是由于Postgres选择增加批处理数量以减少内存消耗。
哈希桶数(Hash Buckets),哈希数据被分配给哈希桶。
当哈希桶不够时,桶的数量会翻倍增长,直到有足够的桶,所以它们总是2的幂。
有时Postgres会选择增加原始桶数,以减少每个桶的元组数。
原始哈希批次(Original Hash Batches),如果哈希是在内存中完成的,则只有一个批次。
与 哈希批次 的区别是由于Postgres选择增加批数以减少内存消耗。
原始哈希桶数(Original Hash Buckets),哈希数据被分配给哈希桶。
当哈希桶不够时,桶的数量会翻倍增长,直到有足够的桶,所以它们总是2的幂。
与 哈希桶数 的区别是由于Postgres选择增加原始桶数,以减少每个桶的元组数。
内存使用峰值(Peak Memory Usage),在数据量最大的批次中使用的内存,以kB为单位。
集合操作(Command):Intersect(相交)或Except(除外)。
联合(UNION )操作由追加结点(Append)处理。
排序方式(Sort Method)。如果它所需的内存低于work_mem
设置,Postgres可以使用quicksort(快速排序),或(如果带了LIMIT)top-N heapsort(top-N堆排序)。
否则,它将在磁盘上执行external(外部)排序,这很慢,但对于大排序可能是必要的。如果排序是按单列或同一表的多列排序,则可以通过添加具有所需顺序的索引来完全避免排序。
外部排序的占用空间比快速排序小,因此可以看到 排序使用空间 低于work_mem
的外部排序。
排序键(Sort Key),按条件排序。
如果排序是按单列或同一表的多列排序,则可以通过添加具有所需顺序的索引来完全避免排序。
排序空间类型(Sort Space Type),排序是在memory(内存)还是在disk(磁盘)上完成的。
磁盘上排序的占用空间比内存中排序的占用空间小,因此可以看到磁盘上排序的 排序使用空间 低于work_mem
。
排序使用空间(Sort Space Used),用于排序的内存或磁盘空间量,以kB为单位。
外部排序的占用空间比快速排序小,因此可以看到使用的排序空间低于work_mem
的外部排序。
启动工作者数(Workers Launched),额外工作进程的数量。这不包括主进程。
Postgres如何确定此数字,以及与计划工作者(Workers Planned)的任何差异,请参考 并行查询如何工作。
计划工作者数(Workers Planned),规划器请求的额外工作进程数。这不包括主进程。
Postgres如何确定此数字,以及与启动的工作者(Workers Launched)的任何差异,请参考 并行查询如何工作。
单次执行计划(Single Copy),如果Postgres不会多次执行计划,则这是true。
因此,如果您在“Workers Planned: 1”旁边看到这一点,那么主进程不会(或没有)并行执行任何事情。
如果您在文本格式查询计划中没有看到它,则可以假定它为false。