执行计划

每个查询都会产生一个查询计划。
选择正确的计划来匹配查询结构和数据的属性对于好的性能来说绝对是最关键的,因此系统包含了一个复杂的规划器来尝试选择好的计划。使用 EXPLAIN 命令可以察看规划器生成的查询计划。

EXPLAIN

EXPLAIN 显示计划器为提供的语句所生成的查询计划。查询计划是一颗节点计划树。在计划中的每个节点代表了一个操作,例如表扫描、连接、聚集或者是一个排序操作。 因为每个节点直接向它上面的节点提供行结果,所以计划应该从下往上进行阅读。最底层的节点通常是一些表扫描操作(顺序扫描、索引扫描或者是位图扫描)。 如果查询要求连接、聚集或者排序(或者其他在原始行上的操作),那么需要在扫描节点增加这些操作的节点。计划最顶层的节点通常是 motion 节点(重分布、显式重分布、广播或者聚集 motion)。这些操作符代表了在查询处理期间在分片示例之间移动行数据。

EXPLAIN 的输出是树中每个节点有一行,显示基本的节点类型,紧接着是由计划器为执行该计划节点时的代价评估:

  • cost:通过用取磁盘页面的次数来度量。即1.0代表一个连续磁盘页面的读取。首先是启动代价(获取第一行的代价),第二个代价是总的代价(获取所有行的代价)。注意总的代价假设所有的行都将要被取回,但并不是总是这种情况(例如使用 LIMIT 语句)。

  • rows:该计划节点总的输出行数。这通常是小于实际由计划节点处理或者扫描的行数,主要是由于任何一个 WHERE 条件语句的评估选择性。理想情况下,最高层的节点估计近似等于实际由该查询返回、更新或者删除的行数。

  • width:该计划节点所有行输出的总的字节数。 值得注意的是更上一层的节点的代价包括了所有它孩子节点的代价。计划中最顶层节点是估计执行整个计划的代价。该数字是计划器寻求最小化的地方。同时也需要意识到代价仅仅反应了查询优化器关心的部分。特别是,代价没有考虑将结果行传输到客户端所花费的时间。

EXPLAIN ANALYZE 导致该语句被实际执行,而不仅是被计划。EXPLAIN ANALYZE 计划显示实际的结果以及计划器的评估。这个对于看是否计划器评估接近实际的情况非常有用。除了显示在 EXPLAIN 计划中的信息,EXPLAIN ANALYZE 还要外加显示下面的信息:

总的花费在执行该查询的时间间隔(以毫秒为单位)。 在一个计划节点操作中涉及到的 workers(Segment)的数量。只有返回行的 Segment 被计入。 一个操作中输出最多行的 Segment 返回的最大行数。如果多个 Segment 输出了相同数量的行数,取 time to end 最长的那个 Segment。 在一个操作中输出最多行的 Segment 的 ID。 对于相关的操作,该操作使用的 work_mem。如果 work_mem 不足以在内存中执行操作,计划将显示有多少数据溢出到磁盘上以及对于使用工作内存最少的执行 Segment 要求了多少趟对数据的处理。

最底层的结点是扫描结点:它们从表中返回未经处理的行。
不同的表访问模式有不同的扫描结点类型:顺序扫描、索引扫描、位图索引扫描。
也还有不是表的行来源,例如VALUES子句和FROM中返回集合的函数,它们有自己的结点类型。
如果查询需要连接、聚集、排序、或者在未经处理的行上的其它操作,那么就会在扫描结点之上有其它额外的结点来执行这些操作。
并且,做这些操作通常都有多种方法,因此在这些位置也有可能出现不同的结点类型。
EXPLAIN 给计划树中每个结点都输出一行,显示基本的结点类型和结点信息。
可能会出现从结点摘要行缩进的其他行,以显示结点的其他属性。

举例

test1=# explain select * from tb1 where id=2;
                      QUERY PLAN                      
------------------------------------------------------
 Seq Scan on tb1  (cost=0.00..11.38 rows=1 width=664)
   Filter: (id = 2)
(2 rows)
test1=# select * from tb1;
 id |                                                 time                                                 | con_id | ins_id |      
     select_ip            |           server_ip            
----+------------------------------------------------------------------------------------------------------+--------+--------+------
--------------------------+--------------------------------
  1 | 2022-09-12 14:05:28.995906+08                                                                        |      1 |      1 | aa   
                          | ::1/128                       
(1 row)
test1=# explain ANALYZE INSERT INTO  tb1(time,con_id,ins_id,select_ip,server_ip)values(now(),1,1,'aa',inet_server_addr());
                                         QUERY PLAN                                         
--------------------------------------------------------------------------------------------
 Insert on tb1  (cost=0.00..0.03 rows=0 width=0) (actual time=0.054..0.054 rows=0 loops=1)
   ->  Result  (cost=0.00..0.03 rows=1 width=664) (actual time=0.043..0.043 rows=1 loops=1)
 Planning Time: 0.022 ms
 Execution Time: 0.066 ms
(4 rows)
test1=# select * from tb1;
 id |                                                 time                                                 | con_id | ins_id |      
     select_ip            |           server_ip            
----+------------------------------------------------------------------------------------------------------+--------+--------+------
--------------------------+--------------------------------
  1 | 2022-09-12 14:05:28.995906+08                                                                        |      1 |      1 | aa   
                          | ::1/128                       
  2 | 2022-09-12 14:07:58.953339+08                                                                        |      1 |      1 | aa   
                          | ::1/128                       
(2 rows)
explain (settings,costs,analyze,buffers) select * from people where age <18 or user_name like 'Wu%' order by user_name limit 10;

输出以下结果

Limit  (cost=62.72..62.74 rows=10 width=133) (actual time=0.599..0.602 rows=10 loops=1)
  Buffers: shared hit=44
  ->  Sort  (cost=62.72..63.15 rows=172 width=133) (actual time=0.597..0.599 rows=10 loops=1)
        Sort Key: user_name
        Sort Method: top-N heapsort  Memory: 29kB
        Buffers: shared hit=44
        ->  Seq Scan on people  (cost=0.00..59.00 rows=172 width=133) (actual time=0.112..0.278 rows=172 loops=1)
              Filter: ((age < 18) OR ((user_name)::text ~~ 'Wu%'::text))
              Rows Removed by Filter: 828
              Buffers: shared hit=44
Settings: effective_cache_size = '1330MB', work_mem = '16MB'
Planning Time: 0.142 ms
Execution Time: 0.644 ms

settings 参数

控制在输出中显示当前服务器配置参数值。
输出的格式为:Settings: effective_cache_size = ‘1330MB’, work_mem = ‘16MB’

costs 参数

控制在输出中显示查询计划结点的代价估计值。
可以输出包括每一个计划结点的估计启动和总代价,以及估计的行数和每行的宽度。默认被设置为TRUE 输出的格式为:计划结点 (cost=启动开销..总开销 rows=计划结点输出行数 width=计划结点输出行宽)。

  • 启动开销
    返回第一行的估计成本。
    这个数值的单位是任意的,目的是与启动时间相关。

  • 总开销
    从该计划结点及其子结点返回所有行的估计总成本。
    数据库查询规划器通常会有几种不同的路径可以执行同一查询。它为每个潜在计划计算成本–希望与所花费的时间相关–然后选择成本最小的计划。需要注意的是,这些成本是无单位的–它们不是为了转换为时间或磁盘读取而设计的。对于较慢的操作,它们应该更大,对于较快的操作,它们应该更小。
    如果查询规划器认为它可以由于上层的LIMIT操作而提前结束,则LIMIT操作的总开销将反映这一点,但其下面的子结点操作的总开销不会反映这一点。

  • 计划结点输出行数
    查询规划器预计会从该计划结点返回的行数,该数值是按单次执行计算。
    糟糕的行数估计可能导致次优的查询规划。通常可以通过运行ANALYZE(不是EXPLAIN参数,是数据库本身的自动或手动分析)、增加统计信息、或者允许数据库使用多元统计信息了解列之间的相关性,来改进它们。
    如果查询规划器认为它可以由于上层的LIMIT操作而提前结束,则LIMIT操作的估计输出行数将反映这一点,但其下面的子结点操作的估计输出行数不会反映这一点。

  • 计划结点输出行宽
    操作返回每行估计的平均大小,以字节为单位。

analyze 参数

EXPLAIN有一个参数ANALYZE,可以实际执行命令并且显示实际的运行时间和其他统计信息。默认被设置为FALSE。

通过实际运行命令,我们可以看到查询的哪些部分是真正耗时的,并可以将这些部分与它们的估计值进行比较。 输出的格式为:(actual time=实际启动开销..实际总开销 rows=实际输出行数 loops=实际执行次数)。

  • 实际启动开销
    从计划结点返回第一行所需的开销时间,以毫秒为单位。
    有时,这非常接近计划结点的设置时间。例如,在返回表中所有行的顺序扫描中。 不过,其他时候,这或多或少是总时间。例如,为了返回排序的第一行,您必须对所有行进行排序,以计算哪一行先返回。

  • 实际总开销
    此操作及其下面的子结点操作上花费的实际总开销时间(以毫秒为单位)。它是每次执行的平均值,四舍五入到最接近的千分之一毫秒。
    确定单个操作时间,特别是在涉及CTE和子计划的情况下,可能会很棘手。

  • 实际输出行数 每次执行该计划结点返回的行数。
    需要重点注意的是,它是所有执行中单次执行的平均值,四舍五入到最接近的整数。这意味着,在大多数时候,虽然“实际执行次数”乘以“实际输出行数”是返回的总行数的一个相当好的近似值,但实际总行数可能会减少到计算值的一半。通常,当您在操作之间看到一两行出现或消失时,它只是有点混乱,但在反复循环执行的计划结点操作中,可能会出现严重的计算错误。
    糟糕的行数估计(实际行数与估计行数有很大差异)可能导致次优的查询规划。通常可以通过运行ANALYZE(不是EXPLAIN参数)、增加统计信息、或者允许数据库使用多元统计信息了解列之间的相关性,来改进它们。

  • 实际执行次数
    执行计划结点的次数。对于许多结点,它的值将为1,但当它不是时,有三种不同的情况:

    • 某些计划结点可以多次执行。例如,“嵌套循环”为其“外部”的子结点返回的每一行运行一次其“内部”的子结点。

    • 当通常只由一次执行完成的操作跨多个进程拆分执行时,每个部分的操作都被计算为一次执行。

    • 当计划结点根本不需要执行时,执行的次数可以为零。例如,如果计划结点读取表为内部连接提供候选项,但结果发现连接的另一侧没有行,则可以有效地消除该结点。

buffers 参数

EXPLAIN 具有一个参数BUFFERS,用于控制要在输出中显示的查询计划节点的缓冲区使用情况。仅当同时启用了ANALYZE时,才能使用此参数。默认为FALSE。

输出的格式为:
/只有当其中的统计数据不为零时才会显示对应的指标。/
Buffers:
shared hit=共享块命中数 read=共享块读取数 dirtied=共享块修改数 written=共享块刷写数
local hit=本地块命中数 read=本地块读取数 dirtied=本地块修改数 written=本地块刷写数
temp read=临时块读取数 written=临时块刷写数
I/O Timings: read=读入块耗时 write=写出块耗时

  • 共享块命中数
    共享块命中数(shared hit),在共享缓冲区中找到的索引/表中的块数。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

    减去子计划结点的值后,共享块命中数与共享块读取数的比例,可用于度量共享缓冲区的缓存效果。

    共享块命中和读取数,与处理的行数(包括输出和过滤掉的行)的比率也很有意义:高比率意味着数据库正在读取大量数据,这可能是膨胀的迹象。

  • 共享块读取数
    共享块读取数(shared read),从磁盘(或操作系统缓存)读取的表/索引中的块数。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

    减去子计划结点的值后,共享块命中数与共享块读取数的比例,可用于度量共享缓冲区的缓存效果。

    共享块命中和读取数,与处理的行数(包括输出和过滤掉的行)的比率也很有意义:高比率意味着数据库正在读取大量数据,这可能是膨胀的迹象。

  • 共享块修改数 共享块修改数(shared dirtied),由该计划结点修改的表/索引中的块数。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

    SELECT查询可能在最近被修改过的页面上设置事务状态标记,在共享缓冲区中修改块。

  • 共享块刷写数 共享块刷写数(shared written),从共享缓冲区中逐出的表/索引中的块数。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

  • 本地块命中数 本地块命中数(local hit),在本地缓冲区中找到的临时表和索引中的块数。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

  • 本地块读取数 本地块读取数(local read),从临时表和索引读取的块数。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

  • 本地块修改数 本地块修改数(local dirtied),由该计划结点修改的临时表和索引中的块数。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

  • 本地块刷写数 本地块刷写数(local written),从本地缓冲区中逐出的临时表和索引中的块数。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

  • 临时块读取数 临时块读取数(temp read),从临时数据中读取的块数,用于计算哈希、排序、物化操作等。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

    work_mem 的值决定了每个计划结点有多少内存可供数据库使用。

  • 临时块刷写数 临时块刷写数(temp written),从工作内存区中逐出的临时数据(用于计算哈希、排序、物化操作等)中的块数。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

    您的设置work_mem决定了每个计划结点有多少内存可供数据库使用。

  • 读入块耗时 读入块耗时(I/O Read Time),读入块所花费的实际时间,以毫秒为单位。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

    需要打开track_io_timing和BUFFERS,但不受EXPLAIN的TIMING参数的影响。

  • 写出块耗时 写出块耗时(I/O Write Time),写出块所花费的实际时间,以毫秒为单位。

    它包括了子计划结点的值,并且是总计数值(即不是每次执行)。

    需要打开track_io_timing和BUFFERS,但不受EXPLAIN的TIMING参数的影响。

结点属性

列出了计划结点中的各种属性。

过滤器(Filter)

如果存在,这是一个用于删除行的过滤器。

需要注意的是,这是传统意义上的过滤器:这些行被读取(从数据源或计划中的其他操作)、检查,然后根据过滤器输出或删除。

虽然在目的上与您在索引扫描或仅索引扫描上看到的 索引条件 相似,但实现完全不同。在“索引条件”中,索引用于根据其索引值选择行,而根本不检查行本身。事实上,在某些情况下,您可能会在同一操作上看到“索引条件”和“过滤器”。

如果您注意到查询计划中较晚筛选了许多行,这可能是由于操作(如GROUP BY或排序)使用的列超过了必要的列,要求在丢弃行之前进行连接。有时也是不必要或不明智使用DISTINCT的结果。

索引条件(Index Cond)

用于从索引中查找行位置的条件。

数据库使用索引的结构化性质快速跳转到它要查找的行。不同的索引类型使用不同的策略。

虽然在用途上与 过滤器 相似,但实现完全不同。在“过滤器”中,检索出行,然后根据其值丢弃行。因此,您可以在同一操作上找到“索引条件”和“过滤器”。

过滤器删除的行数(Rows Removed by Filter)

这是一个计划结点每次执行的平均值。

如果删除的行比例很高,您可能需要调查是否有更具选择性的索引,帮助优化执行性能。

索引出的不满足条件的行数(Rows Removed by Index Recheck)

索引扫描返回的不满足条件并随后被删除的行数。

这要么是有损位图扫描的结果,要么是不保证行匹配条件的索引类型。

扫描方向(Scan Direction)

数据库能够对(b-tree)索引执行向前或向后扫描,以按排序顺序获取数据。在默认(文本)格式中,除非另有说明,否则方向是向前的。

您还可以在其他索引类型的扫描中看到“NoMovement”的扫描方向。

堆表访问行数(Heap Fetches)

在只用索引的扫描期间,数据库必须在堆表中而不是索引中查找的行数。

位图堆扫描精确块数(Exact Heap Blocks)

位图堆扫描访问的精确块的数量。

如果行位图太大,无法放入工作内存(work_mem),则其某些部分将被设置为“有损”——即它们引用整个页面而不是特定的行。

位图堆扫描有损块数(Lossy Heap Blocks)

位图堆扫描访问的有损块数。

如果行位图太大,无法放入工作内存(work_mem),则其某些部分将被设置为“有损”——即它们引用整个页面而不是特定的行。

位图扫描过滤条件(Recheck Cond)

是位图扫描可能用于在提取行后过滤行的条件。

只有当位图扫描有损,或者它使用了任何不保证行匹配条件的有损索引类型(BRIN索引就是一个例子)时,才需要它。

有关是否实际需要重新检查的更多信息,请查看非零的 位图堆扫描有损块数 和非零的 索引出的不满足条件的行数。

哈希条件(Hash Cond)

用于在哈希连接中匹配从外部计划到内部计划的行的条件。

内行唯一(Inner Unique)

如果不超过一个内行可以匹配任何给定的外行,则此为true。这允许执行器跳过搜索其他匹配项。

连接过滤器(Join Filter)

一个可以在连接之前发生的过滤器,允许数据库避免查找行并在之后过滤它们。

连接类型(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)。

在非文本格式查询计划中,这使您可以查看数据库是在执行HashAggregate (Hashed)、GroupAggregate (Sorted)还是MixedAggregate (Mixed)。

哈希批次(Hash Batches)

如果哈希是在内存中完成的,则只有一个批次。

与 原始哈希批次 的区别是由于数据库选择增加批处理数量以减少内存消耗。

哈希桶数(Hash Buckets)

哈希数据被分配给哈希桶。

当哈希桶不够时,桶的数量会翻倍增长,直到有足够的桶,所以它们总是2的幂。

有时数据库会选择增加原始桶数,以减少每个桶的元组数。

原始哈希批次(Original Hash Batches)

如果哈希是在内存中完成的,则只有一个批次。

与 哈希批次 的区别是由于数据库选择增加批数以减少内存消耗。

原始哈希桶数(Original Hash Buckets)

哈希数据被分配给哈希桶。

当哈希桶不够时,桶的数量会翻倍增长,直到有足够的桶,所以它们总是2的幂。

与 哈希桶数 的区别是由于数据库选择增加原始桶数,以减少每个桶的元组数。

内存使用峰值(Peak Memory Usage)

在数据量最大的批次中使用的内存,以kB为单位。

集合操作(Command)

Intersect(相交)或Except(除外)。

联合(UNION )操作由追加结点(Append)处理。

排序方式(Sort Method)

如果它所需的内存低于work_mem设置,数据库可以使用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)

额外工作进程的数量。这不包括主进程。

数据库如何确定此数字,以及与计划工作者(Workers Planned)的任何差异,请参考 并行查询如何工作。

计划工作者数(Workers Planned)

规划器请求的额外工作进程数。这不包括主进程。

数据库如何确定此数字,以及与启动的工作者(Workers Launched)的任何差异,请参考 并行查询如何工作。

单次执行计划(Single Copy)

如果数据库不会多次执行计划,则这是true。

因此,如果您在“Workers Planned: 1”旁边看到这一点,那么主进程不会(或没有)并行执行任何事情。

如果您在文本格式查询计划中没有看到它,则可以假定它为false。

扫描结点

本节列出了 数据库 中的不同扫描结点。
有些结点是通用的,在特定查询中可能有几个替代结点可用,实际结点由上面概述的规划算法确定(例如,索引扫描和顺序扫描)
而有些结点仅适用于特定情况,没有替代项(例如,外表扫描)。

对于访问表数据的扫描结点,大致了解数据在磁盘上的组织方式非常有用:每个表由一个或多个页组成,每个页包含一个小的页头,然后是与表记录对应的数据行。由于数据库多版本并发控制机制,一个页面实际上可能在共享缓冲区中存在多个版本,每个事务只有一个版本是“可见”的。

索引是指向特定页面,然后指向该页面中的值的独立结构。
不同类型的索引支持不同的索引扫描功能:哈希索引只能查找特定值,但其他索引类型(如 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)

采用由 位图索引扫描(直接使用,或者通过 位图与 和 位图或 结点生成的一系列位图集操作)生成的行位置位图,并查找相关数据。
位图的每个区块可以是精确的(直接指向行),也可以是有损的(指向包含至少一个与谓词匹配的行的页面)。

数据库 更倾向使用精确的区块,但如果配置的工作内存区(由参数work_mem决定)空间有限,它也将开始使用有损区块。这些区块实际上是由位图堆扫描的子结点以有损或精确的形式生成的,不过在处理这些区块以获取行时,更能合理有效地选择有损和精确的形式,因此这些属性会反映在位图堆扫描中。如果位图区块是有损的,则结点将需要读取整个页面,并重新检查里面的行是否匹配指定的索引条件(因为它不知道页面上需要哪些行)。

位图索引扫描(Bitmap Index Scan)

可以被视为顺序扫描和索引扫描之间的中间地带。与索引扫描一样,它扫描索引以确定需要获取的确切数据,但与顺序扫描一样,它利用了更易于批量读取的数据。普通的索引扫描一次从索引中获取一个行位置,并立即访问表中的该行。位图扫描一次性从索引中获取所有行位置,使用内存中的“位图”数据结构对它们进行排序,然后按物理行位置顺序访问表元组。

位图索引扫描实际上与位图堆扫描协同运行:它不会提取数据本身。位图索引扫描不是直接生成行,而是构造潜在行位置的位图。它将此数据馈送到父位图堆扫描,该扫描可以解码位图以提取基础数据,逐页抓取数据。

位图堆扫描是位图索引扫描最常见的父结点,但计划也可能在实际获取基础数据之前将几个不同的位图索引扫描与 位图与 或 位图或 结点组合在一起。这允许 数据库 一次使用两个不同的索引来执行查询。

公共表表达式的扫描(CTE Scan)

用于扫描 公共表表达式 的结果。

自定义扫描(Custom Scan)

可以作为单独的模块添加并插入到标准 数据库 查询规划和执行中。

外表扫描(Foreign Scan)

用于扫描 外表。

函数结果扫描(Function Scan)

扫描 集合返回函数 (比如 unnest 或 regexp_split_to_table) 的结果。

子查询扫描(Subquery Scan)

用于扫描范围表中子查询的输出。
我们经常需要在子查询的计划之上有一个额外的计划结点来执行表达式计算(我们不能在不改变其语义的情况下将其推送到子查询中)。

表样本扫描(Table Sample Scan)

在使用 表样本 功能时扫描表。请注意,此子句确实会更改查询的语义,但如果您希望收集有关大型表中数据的一些统计信息,则它可能比完全顺序扫描更有效。

行地址扫描(Tid Scan)

与索引扫描类似,但只能使用内部和不稳定的 ctid 标识符查找行。您不太可能在查询中使用这种类型的扫描。

行集合扫描(Values Scan)

扫描字面的 VALUES 子句。

工作表扫描(Work Table Scan)

扫描用于计算递归 公共表表达式 时使用的工作表。

连接结点

本节介绍 数据库 中的三种类型的连接机制。

一次在两个表上执行连接;如果将多个表连接在一起,则一个连接的输出将被视为后续连接的输入。
联接大量表时,遗传查询优化器设置可能会影响所考虑的联接组合。

哈希连接(Hash Join)

从内部表生成一个哈希表,采用连接键进行关联映射。然后扫描外部表,检查是否存在相应的值。如果哈希表超过work_mem,则此过程需要分几个批次进行,将临时文件写入磁盘,这会变得非常慢。

嵌套连接(Nested Loop Join)

对于外部表中的每一行,循环访问内部表中的所有行,并查看它们是否与连接条件匹配。如果可以使用索引扫描内部关系,则可以提高嵌套循环联接的性能。这通常是处理联接的低效方法,但始终可用,有时可能是唯一的选择。