Postgresql 各类查询计划节点

张彤 2022年10月14日 49次浏览

查询计划树plan tree大致分为以下三类

  1. Scan Nodes 扫描节点
  2. Join Nodes 连接节点
  3. Materialization Nodes 物化节点

下面我们逐一进行讲解

3.1 Scan Nodes

以下面的查询为例

SELECT * FROM TBL1, TBL2 where TBL2.ID>1000;

我们假设生成的计划树如下:

flowchart TD A((Nested Loop Join)) B((Sequence Scan on TBL1)) C((Index Scan on TBL2)) A <==> B A <==> C

按照这个计划树,我们将顺序扫描TBL1然后索引扫描TBL2

在介绍扫描类型前,我们需要复习一下常见的知识点。

PostgreSQL Data Layout

  1. HEAP

    堆,用于存储表的全部行,非常普遍的一类表,也叫堆表。

    它被划分为多个页面PAGE,如上图所示。

    每个页面大小默认为8KB,在每个页面中,item指针指向具体的数据。

  2. Index Storage

    索引存储,此类存储只存储键值对key-value,和堆一样,分为多个页面,每个页面的大小默认8KB

  3. Tuple Identifier (TID)

    元组标识符,TID的大小为固定的6bytes.

    它由两部分组成,第一部分是4个字节的页码page number和页内剩余的2字节的元组索引。

    这两个数字的组合唯一地指向特定元组的存储位置。

目前,Postgresql 支持以下扫描办法

  1. Sequential Scan

    顺序扫描顾名思义,是顺序扫描所有页面的所有指针来完成的。

    假设一个表由100个页面组成,每个页面由1000条记录组成,顺序扫描的一部分,它将获取100*1000条记录,并且检查隔离级别和匹配谓词子句。

    因此,即使只有1条记录被选中作为整个表扫描的一部分,它也同样要进行100K次的扫描,才能得到结果。

    虽然没有计算和比较计划开销,但是依旧不能确定将要使用的是哪种扫描。

    顺序扫描至少要符合下面两种条件

    • 键上有没可用的索引,键是谓词的一部分。

    • 大多数行都是作为sql查询的一部分获得的。

  2. Index Scan

    与顺序扫描不同,索引扫描不会顺序的扫描所有页面它使用查询中涉及的索引对应的不同数据结构(取决于索引类型),并定位所需要的数据(按照谓词子句),这些仅仅需要非常少的扫描就可以达到。

    然后,使用索引扫描找到的entry直接指向堆区域中的数据(如上图所示) ,然后根据隔离级别提取该数据,以检查可见性

    索引扫描有两步

    • 从与索引相关的数据结构中获取数据。它返回堆中对应数据的 TID
    • 然后直接访问对应的堆页以获取整个数据。由于以下原因,需要采取这一额外步骤:
      • 查询可能请求获取的列数超过相应索引中可用的列数
      • 可见性信息不与索引数据一起维护。因此,为了按照隔离级别检查数据的可见性,它需要访问堆数据

    既然索引扫描如此有效,那么为什么不都用索引扫描呢?

    任何事情都是要有开销的,这里的开销与I/O类型有关。在索引扫描中,对于索引存储中发现的每一条记录,都要涉及到随机I/O,它必须从堆存储中提取相关的数据,而在顺序扫描中,序列I/O,只占随机I/O时间的25%左右。

    因此,只有当总体增益超过由于随机 I/O 成本而产生的开销时,才应该选择索引扫描。

  3. Index Only Scan

    Index Only Scan 类似于 Index Scan,它只扫描索引数据结构。与索引扫描相比,选择“仅索引扫描”还有两个额外的前提条件:

    1. 查询应该只获取作为索引一部分的键列

    2. 所选堆页上的所有元组(记录)都应该是可见的。

      正如前面部分所讨论的,索引数据结构不维护可见性信息,因此为了只从索引中选择数据,我们应该避免检查可见性,如果认为页面的所有数据都是可见的,就可能发生这种情况

  4. Bitmap Scan

    位图扫描是索引扫描和顺序扫描的混合。该算法尽管克服了索引扫描的缺点,但仍然保持了其最大的优势。

    如上所述,对于在索引数据结构中找到的每个数据,都需要在堆页中找到相应的数据。因此,它需要一次读取索引页,然后再读取堆页,这会导致大量的随机 I/O。因此,位图扫描方法利用了索引扫描的优点,不需要随机 I/O。这在以下两步中实现:

    1. Bitmap Index Scan

      位图索引扫描: 首先,它从索引数据结构中获取所有索引数据,并创建所有 TID 的位图

      为了简单理解,您可以认为这个位图包含所有页面的hash值(基于页号的hash) ,并且每个页面entry包含该页面内所有偏移量的数组

    2. Bitmap Heap Scan

      位图堆扫描: 顾名思义,它通过读取页面的位图,然后从堆中扫描与存储的页面和偏移量相对应的数据。

      最后,它检查可见性和谓词等,并根据所有这些检查的结果返回元组

  5. TID Scan

    如上所述,TID 为6字节数,由4字节的页码和页内剩余的2字节元组索引组成。

    TID 扫描是 PostgreSQL 中一种非常特殊的扫描,只有在查询谓词中有 TID 时才会被选中

    weather=# explain select * from tb_ip_attack where ctid='(115,42)' ;
                              QUERY PLAN                          
    --------------------------------------------------------------
     Tid Scan on tb_ip_attack  (cost=0.00..4.01 rows=1 width=201)
       TID Cond: (ctid = '(115,42)'::tid)
    (2 rows)
    
    

3.2 Join Nodes

举例

SELECT * FROM TBL1, TBL2 where TBL1.ID > TBL2.ID

这里扫描TBL1.ID,然后根据关联条件TBL1.ID>TBL2.ID将它们连接

join方法外,join order也非常重要,举例

SELECT * FROM TBL1, TBL2, TBL3 WHERE TBL1.ID=TBL2.ID AND TBL2.ID=TBL3.ID;

这里假设TBL1,TBL2,TBL3分别由10行,100行,1000行。

条件 TBL1.ID = TBL2.ID 只返回5条记录,而 TBL2.ID = TBL3.ID 返回100条记录

因此最好先jion TBL1和TBL2,这样可以减少与TBL3 join 的数量。

规划树如下

flowchart TD a[NLJ] <--> b[NLJ] b <--> c[TBL1] b <--> d[TBL2] a <--> e[TBL3]

Postgresql以下类型的join方法

  • Nested Loop Join
  • Hash Join
  • Merge Join

让我们为上面的例子中的表填充一些数据,方便举例。

weather=# create table blogtable1(id1 int, id2 int);
CREATE TABLE
weather=# create table blogtable2(id1 int, id2 int);
CREATE TABLE
weather=# insert into blogtable1 values(generate_series(1,10000),3);
INSERT 0 10000
weather=# insert into blogtable2 values(generate_series(1,1000),3);
INSERT 0 1000
weather=#  analyze;
ANALYZE

在后面的所有示例中,除非特别指定,否则我们将使用默认参数

3.2.1 Nested Loop Join

嵌套循环连接(NLJ)是最简单的连接算法其中外部relation的每条记录与内部relation的每条记录相匹配。

  • relation 可以理解为表或者索引

relation A 和 B,条件为 A.ID < B.ID ,可以表示如下:

For each tuple r in A
       	For each tuple s in B
            	If (r.ID < s.ID)
                 	Emit output tuple (r,s)

嵌套循环联接(NLJ)是最常用的联接方法,它几乎可以在任何数据集上使用任何类型的联接子句。

由于该算法可以扫描所有内部和外部关联的元组,因此被认为是开销最大的连接操作。

根据上面的表和数据,下面的查询将导致嵌套循环联接,如下所示:

weather=# explain select * from blogtable1 bt1, blogtable2 bt2 where bt1.id1 < bt2.id1;
                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Nested Loop  (cost=0.00..150162.50 rows=3333333 width=16)
   Join Filter: (bt1.id1 < bt2.id1)
   ->  Seq Scan on blogtable1 bt1  (cost=0.00..145.00 rows=10000 width=8)
   ->  Materialize  (cost=0.00..20.00 rows=1000 width=8)
         ->  Seq Scan on blogtable2 bt2  (cost=0.00..15.00 rows=1000 width=8)
(5 rows)

因为 Join 子句是“ <”,所以唯一可能的 Join 方法是 Nested Loop Join。

值得注意的是,这里有一个新的节点 Matrialize

  • 如果jion子句不是< 而是=,并且在relation之间选择了循环嵌套,那么通过调优配置,例如work_mem,但不限于此。或添加索引等方法来选择更有效的连接方法,如 hash 或 merge 连接,就非常重要了。

有些查询可能没有连接子句,在这种情况下,唯一的连接选择是嵌套循环连接。例如,以下查询:

weather=# explain select * from blogtable1, blogtable2;
                                QUERY PLAN                                
--------------------------------------------------------------------------
 Nested Loop  (cost=0.00..125162.50 rows=10000000 width=16)
   ->  Seq Scan on blogtable1  (cost=0.00..145.00 rows=10000 width=8)
   ->  Materialize  (cost=0.00..20.00 rows=1000 width=8)
         ->  Seq Scan on blogtable2  (cost=0.00..15.00 rows=1000 width=8)
(4 rows)

3.2.2 Hash Join

此方法分为两步

  1. 构建阶段: 使用内部relation记录构建哈希表。哈希键是根据连接子句键计算的
  2. 探测阶段: 基于连接子句键对外部关系记录进行哈希,以在哈希表中查找匹配entry

例如,条件为 A.ID = B.ID 的关系 A 和 B 之间的连接可以表示如下:

  1. Build Phase

    For each tuple r in inner relation B
        Insert r into hash table HashTab with key r.ID
    
  2. Probe Phase

    For each tuple s in outer relation A
        For each tuple r in bucker HashTab[s.ID]
            If (s.ID = r.ID)
                Emit output tuple (r,s)
    

具体举例:

weather=# explain select * from blogtable1 bt1, blogtable2 bt2 where bt1.id1 = bt2.id1;
                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Hash Join  (cost=27.50..220.00 rows=1000 width=16)
   Hash Cond: (bt1.id1 = bt2.id1)
   ->  Seq Scan on blogtable1 bt1  (cost=0.00..145.00 rows=10000 width=8)
   ->  Hash  (cost=15.00..15.00 rows=1000 width=8)
         ->  Seq Scan on blogtable2 bt2  (cost=0.00..15.00 rows=1000 width=8)

  • 在这里,哈希表是在 blogtable2表上创建的,因为它是较小的表,按照哈希表最小化内存的需求,整张哈希表可以放入内存。
3.3.3 Merge Join

合并连接算法,其中外部relation的每条记录与内部relation的每条记录进行匹配,直到存在连接子句匹配的可能性。

只有当两个relation都排序并且连接子句运算符为“ =”时,才使用此连接算法。

relation A 和 B,条件为 A.ID = B.ID,它们之间的连接可以表示如下:

    For each tuple r in A

        For each tuple s in B

             If (r.ID = s.ID)

                  Emit output tuple (r,s)

                  Break;

             If (r.ID > s.ID)

                  Continue;

             Else

                  Break;

如果在两个表上都创建了索引,则可能导致合并连接。这是因为可以按照排序顺序检索表数据,因为索引是合并联接方法的主要标准之一:

weather=# create index idx1 on blogtable1(id1);
CREATE INDEX
weather=# create index idx2 on blogtable2(id1);
CREATE INDEX
weather=# explain select * from blogtable1 bt1, blogtable2 bt2 where bt1.id1 = bt2.id1;
                                      QUERY PLAN                                       
---------------------------------------------------------------------------------------
 Merge Join  (cost=0.56..90.36 rows=1000 width=16)
   Merge Cond: (bt1.id1 = bt2.id1)
   ->  Index Scan using idx1 on blogtable1 bt1  (cost=0.29..318.29 rows=10000 width=8)
   ->  Index Scan using idx2 on blogtable2 bt2  (cost=0.28..43.27 rows=1000 width=8)
(4 rows)

  • 因此,正如我们所看到的,两个表都使用索引扫描,而不是顺序扫描,因为这两个表都将产出排序记录。
3.3.4 Configuration

PostgreSQL 支持各种与规划器相关的配置,这些配置可用于提示查询优化器不要选择某些特定类型的连接方法。

如果优化器选择的连接方法不是最优的,那么可以关闭这些配置参数,以迫使查询优化器选择不同类型的连接方法。

默认情况下,所有这些配置参数都是“打开”的。

有许多计划相关的配置参数用于各种目的,本例中只考虑连接节点相关的参数

weather=# explain select * from blogtable1, blogtable2 where blogtable1.id1 = blogtable2.id1;
                                    QUERY PLAN                                     
-----------------------------------------------------------------------------------
 Merge Join  (cost=0.56..90.36 rows=1000 width=16)
   Merge Cond: (blogtable1.id1 = blogtable2.id1)
   ->  Index Scan using idx1 on blogtable1  (cost=0.29..318.29 rows=10000 width=8)
   ->  Index Scan using idx2 on blogtable2  (cost=0.28..43.27 rows=1000 width=8)
(4 rows)

weather=# set enable_hashjoin to off;
SET
weather=# explain select * from blogtable1, blogtable2 where blogtable1.id1 = blogtable2.id1;
                                    QUERY PLAN                                     
-----------------------------------------------------------------------------------
 Merge Join  (cost=0.56..90.36 rows=1000 width=16)
   Merge Cond: (blogtable1.id1 = blogtable2.id1)
   ->  Index Scan using idx1 on blogtable1  (cost=0.29..318.29 rows=10000 width=8)
   ->  Index Scan using idx2 on blogtable2  (cost=0.28..43.27 rows=1000 width=8)
(4 rows)

3.3 Materialization Nodes

也叫做 辅助节点Auxiliary nodes

一个物化节点,意味着在这个节点之上的所有节点开始运行之前,物化节点下面的输出(可以是一个扫描,也可以是一组连接,或者类似的东西),都必须要具体化到内存中.

前两类节点涉及如何从基表中获取数据以及如何连接从两个表或更多的表中获取的数据。这个类别中的节点应用于检索到的数据之上,以便进一步分析或准备报告等,例如对数据进行排序、数据聚合等。

辅助节点主要分为

  • Sort 排序
  • Aggregate 汇总
  • Group By Aggregate 分组汇总
  • Limit 限制
  • Unique 唯一
  • LockRows 行锁
  • SetOp 设置

我们考虑一个简单的例子

SELECT * FROM TBL1, TBL2 where TBL1.ID > TBL2.ID order by TBL.ID;

假设对应的规划树plan tree如下

flowchart TD a((Sort)) b((Nested loop join)) c((Sequence Scan on TBL1)) d((index Scan on TBL2)) e((Materialize)) style a fill:#ffa,stroke:#333,stroke-width:4px style b fill:#ffa,stroke:#333,stroke-width:4px style c fill:#5af,stroke:#333,stroke-width:4px style d fill:#5af,stroke:#333,stroke-width:4px style e fill:#5af,stroke:#333,stroke-width:4px a --> b b <--> c b <--> e e <--> d

那么,这里在 join 的结果之上添加了一个辅助节点“ Sort”,以便按照所需的顺序对数据进行排序。

下面我们介绍各种物化节点

3.3.1 Sort

顾名思义,这个节点是用作排序的。排序可以以显式或隐式的方式提出。

  1. 用户场景要求将排序后的数据作为输出,显式

    postgres=# CREATE TABLE demotable (num numeric, id int);
    
    CREATE TABLE
    
    postgres=# INSERT INTO demotable SELECT random() * 1000, generate_series(1, 10000);
    
    INSERT 0 10000
    
    postgres=# analyze;
    
    ANALYZE
    
    postgres=# explain select * from demotable order by num;
    
                               QUERY PLAN
    
    ----------------------------------------------------------------------
    
     Sort  (cost=819.39..844.39 rows=10000 width=15)
    
       Sort Key: num
    
       ->  Seq Scan on demotable  (cost=0.00..155.00 rows=10000 width=15)
    
    (3 rows)
    

    注意: 即使用户需要按排序顺序输出最终结果,如果对应的排序列上有索引,则不能在最终计划中添加排序节点。

    postgres=# CREATE INDEX demoidx ON demotable(num);
    
    CREATE INDEX
    
    postgres=# explain select * from demotable order by num;
    
                                    QUERY PLAN
    
    --------------------------------------------------------------------------------
    
     Index Scan using demoidx on demotable  (cost=0.29..534.28 rows=10000 width=15)
    
    (1 row)
    
  2. 合并连接在连接之前需要对两个表数据进行排序,隐式

    postgres=# create table demo1(id int, id2 int);
    
    CREATE TABLE
    
    postgres=# insert into demo1 values(generate_series(1,1000), generate_series(1,1000));
    
    INSERT 0 1000
    
    postgres=# create table demo2(id int, id2 int);
    
    CREATE TABLE
    
    postgres=# create index demoidx2 on demo2(id);
    
    CREATE INDEX
    
    postgres=# insert into demo2 values(generate_series(1,100000), generate_series(1,100000));
    
    INSERT 0 100000
    
    postgres=# analyze;
    
    ANALYZE
    
    postgres=# explain select * from demo1, demo2 where demo1.id=demo2.id;
    
                                      QUERY PLAN
    
    ------------------------------------------------------------------------------------
    
     Merge Join  (cost=65.18..109.82 rows=1000 width=16)
    
       Merge Cond: (demo2.id = demo1.id)
    
       ->  Index Scan using demoidx2 on demo2  (cost=0.29..3050.29 rows=100000 width=8)
    
       ->  Sort  (cost=64.83..67.33 rows=1000 width=8)
    
          Sort Key: demo1.id
    
          ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=8)
    
    (6 rows)
    
3.3.2 Aggregate

如果存在用于从多个输入行计算单个结果的聚合函数,则将聚合节点作为计划树的一部分添加。

所使用的一些聚合函数是 COUNT、 SUM、 AVG (AVERAGE)、 MAX (MAXIMUM)和 MIN (MINIMUM)。

聚合节点可以出现在基本relation扫描的顶部,或者出现在relation的连接上

postgres=# explain select count(*) from demo1;

                       QUERY PLAN

---------------------------------------------------------------

 Aggregate  (cost=17.50..17.51 rows=1 width=8)

   ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=0)

(2 rows)





postgres=# explain select sum(demo1.id) from demo1, demo2 where demo1.id=demo2.id;

                                       QUERY PLAN

-----------------------------------------------------------------------------------------------

 Aggregate  (cost=112.32..112.33 rows=1 width=8)

   ->  Merge Join  (cost=65.18..109.82 rows=1000 width=4)

      Merge Cond: (demo2.id = demo1.id)

      ->  Index Only Scan using demoidx2 on demo2  (cost=0.29..3050.29 rows=100000 width=4)

      ->  Sort  (cost=64.83..67.33 rows=1000 width=4)

            Sort Key: demo1.id

            ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=4)
3.3.3 HashAggregate / GroupAggregate

这两类节点是聚合节点Aggregate Nodes的扩展,如果使用聚合函数按照每个组合并多个输入行,则将这些类型的节点添加到计划树中。因此,如果查询使用了任何聚合函数,并且在查询中有一个 GROUPBY 子句,那么将在计划树中添加哈希聚合节点或 Group聚合节点。

  1. hashAggregate ,散列聚合。

    hashAgg的运行方式是构建数据的哈希表,以便对其进行分组。

    因此,如果聚合发生在未排序的数据集上,则可以使用hash聚合。

    postgres=# explain select count(*) from demo1 group by id2;
    
                           QUERY PLAN
    
    ---------------------------------------------------------------
    
     HashAggregate  (cost=20.00..30.00 rows=1000 width=12)
    
       Group Key: id2
    
       ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=4)
    
    (3 rows)
    

    这里的 demo1表模式数据与前一节中所示的示例一致。因为只有1000行要分组,所以构建哈希表所需的资源比排序的成本要少。查询规划程序决定选择 HashAgge。

  2. groupAggregate 组聚合

    GroupAgge 可以处理已排序的数据,因此不需要任何其他数据结构。

    如果聚合位于已排序的数据集上,则组级聚合可以使用 Group聚合。

    为了对已排序的数据进行分组,它可以显式排序(通过添加“排序”节点) ,也可以对通过索引获取的数据进行排序,在这种情况下,它是隐式排序的。

    postgres=# explain select count(*) from demo2 group by id2;
    
                                QUERY PLAN
    
    -------------------------------------------------------------------------
    
     GroupAggregate  (cost=9747.82..11497.82 rows=100000 width=12)
    
       Group Key: id2
    
       ->  Sort  (cost=9747.82..9997.82 rows=100000 width=4)
    
          Sort Key: id2
    
          ->  Seq Scan on demo2  (cost=0.00..1443.00 rows=100000 width=4)
    
    (5 rows) 
    

    这里的 demo2表模式数据与前一节中所示的示例一致。

    因为这里要对100000行进行分组,所以构建哈希表所需的资源可能比排序的成本更高。

    因此,查询规划程序决定选择 GroupAgge。

    请注意,从“ demo2”表中选择的记录是显式排序的,并且在计划树中为其添加了一个节点。

    请参阅下面的另一个示例,其中已经检索了由于索引扫描而排序的数据:

    postgres=# create index idx1 on demo1(id);
    
    CREATE INDEX
    
    postgres=# explain select sum(id2), id from demo1 where id=1 group by id;
    
                                QUERY PLAN
    
    ------------------------------------------------------------------------
    
     GroupAggregate  (cost=0.28..8.31 rows=1 width=12)
    
       Group Key: id
    
       ->  Index Scan using idx1 on demo1  (cost=0.28..8.29 rows=1 width=8)
    
          Index Cond: (id = 1)
    
    (4 rows) 
    

    请参阅下面的另一个示例,即使它有索引扫描,仍然需要显式排序为索引和分组列不相同的列。因此,它仍然需要按照分组列进行排序。

    postgres=# explain select sum(id), id2 from demo1 where id=1 group by id2;
    
                                   QUERY PLAN
    
    ------------------------------------------------------------------------------
    
     GroupAggregate  (cost=8.30..8.32 rows=1 width=12)
    
       Group Key: id2
    
       ->  Sort  (cost=8.30..8.31 rows=1 width=8)
    
          Sort Key: id2
    
          ->  Index Scan using idx1 on demo1  (cost=0.28..8.29 rows=1 width=8)
    
                Index Cond: (id = 1)
    
    (6 rows)
    

    注意: 组聚合/散列聚合可以用于许多其他的间接查询,即使在查询中不存在与群组的聚合。这取决于规划者如何解释查询。

    例如,假设我们需要从表中获取不同的值,那么它可以被相应的列看作一个组,然后从每个组中获取一个值。

    postgres=# explain select distinct(id) from demo1;
    
                           QUERY PLAN
    
    ---------------------------------------------------------------
    
     HashAggregate  (cost=17.50..27.50 rows=1000 width=4)
    
       Group Key: id
    
       ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=4)
    
    (3 rows)
    

    所以在这里,即使没有涉及到聚合和分组,也会使用 Hash聚合。

3.3.4 Limit

如果在 SELECT 查询中使用“ Limit/offest”子句,则将限制节点添加到计划树中。此子句用于限制行数,并可选地提供开始读取数据的偏移量。

postgres=# explain select * from demo1 offset 10;

                       QUERY PLAN

---------------------------------------------------------------

 Limit  (cost=0.15..15.00 rows=990 width=8)

   ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=8)

(2 rows)





postgres=# explain select * from demo1 limit 10;

                       QUERY PLAN

---------------------------------------------------------------

 Limit  (cost=0.00..0.15 rows=10 width=8)

   ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=8)

(2 rows)





postgres=# explain select * from demo1 offset 5 limit 10;

                       QUERY PLAN

---------------------------------------------------------------

 Limit  (cost=0.07..0.22 rows=10 width=8)

   ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=8)

(2 rows)
3.3.5 Unique

此节点是为了从基础节点中获取一个不同的值。注意,根据查询,选择性和其他资源信息的不同,不同的值也可以使用 HashAgge/GroupAgge 检索,而不需要使用 Unique 节点

postgres=# explain select distinct(id) from demo2 where id<100;

                                 QUERY PLAN

-----------------------------------------------------------------------------------

 Unique  (cost=0.29..10.27 rows=99 width=4)

   ->  Index Only Scan using demoidx2 on demo2  (cost=0.29..10.03 rows=99 width=4)

      Index Cond: (id < 100)

(3 rows)
3.3.6 LockRows

PostgreSQL 提供了锁定所有选定行的功能。可以分别根据“ FORSHARE”和“ FORUPDATE”子句以“ Shared”模式或“ Exsive”模式选择行。一个新的节点“ LockRows”被添加到计划树中以实现此操作。

postgres=# explain select * from demo1 for update;

                        QUERY PLAN

----------------------------------------------------------------

 LockRows  (cost=0.00..25.00 rows=1000 width=14)

   ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=14)

(2 rows)



postgres=# explain select * from demo1 for share;

                        QUERY PLAN

----------------------------------------------------------------

 LockRows  (cost=0.00..25.00 rows=1000 width=14)

   ->  Seq Scan on demo1  (cost=0.00..15.00 rows=1000 width=14)

(2 rows)
3.3.7 SetOp

PostgreSQL 提供了组合两个或多个查询结果的功能。因此,当选择 Join 节点的类型来连接两个表时,将选择类似类型的 SetOP 节点来组合两个或多个查询的结果

例如,考虑一个包含雇员的 id、姓名、年龄和薪水的表,如下所示:

postgres=# create table emp(id int, name char(20), age int, salary int);

CREATE TABLE

postgres=# insert into emp values(1,'a', 30,100);

INSERT 0 1

postgres=# insert into emp values(2,'b', 31,90);

INSERT 0 1

postgres=# insert into emp values(3,'c', 40,105);

INSERT 0 1

postgres=# insert into emp values(4,'d', 20,80);

INSERT 0 1 

现在,为了得到年龄在25岁以上,工资在95M 以上的员工,我们可以写下面的相交查询:

postgres=# explain select * from emp where age>25 intersect select * from emp where salary > 95;

                                QUERY PLAN

---------------------------------------------------------------------------------

 HashSetOp Intersect  (cost=0.00..72.90 rows=185 width=40)

   ->  Append  (cost=0.00..64.44 rows=846 width=40)

      ->  Subquery Scan on "*SELECT* 1"  (cost=0.00..30.11 rows=423 width=40)

            ->  Seq Scan on emp  (cost=0.00..25.88 rows=423 width=36)

                  Filter: (age > 25)

      -> Subquery Scan on "*SELECT* 2"  (cost=0.00..30.11 rows=423 width=40)

            ->  Seq Scan on emp emp_1  (cost=0.00..25.88 rows=423 width=36)

                  Filter: (salary > 95)

(8 rows) 

因此,这里添加了一种新的节点 HashSetOp 来计算这两个单独查询的交集。

注意,这里添加了另外两种新节点:

  1. Append

    添加此节点是为了将多个结果集合并为一个。

  2. Subquery Scan

    添加此节点以计算任何子查询。

    在上述计划中,添加子查询是为了计算一个额外的常量列值,该值指示哪个输入集贡献了一个特定行。

    HashedSetop 使用基础结果的散列,但是可以通过查询优化器生成基于排序的 SetOP 操作。基于排序的 Setop 节点表示为“ Setop”。

    注意: 通过单个查询可以得到与上面结果相同的结果,但是这里使用 intersect 显示,只是为了简单的演示。

PostgreSQL 的所有节点都很有用,可以根据查询、数据等的性质进行选择

许多子句是用节点一对一映射的。对于某些子句,节点有多个选项,这些选项是根据底层数据开销计算确定的。为了更准确的体现explain,一般需要vacuum 相关的表之后进行anlyze,将表信息重新分析统计,更准确的进行执行计划。

至此,查询计划的主要知识点已经全部介绍完毕,另外,大家可以看看俄罗斯团队关于Postgresql查询计划的博客
点击这里跳转