[译]理解PG如何执行一个查询-2

Limit

Limit算子用于限制结果集的大小。PG使用limit算子进行limit和offset处理。Limit算子将输入集前x行去掉,返回接着的y行,再将剩下的丢弃。如果查询中包括offset,x表示offset的数量,否则x为0.如果查询中包含Limit,y表示limit数量,否则y是输入集的大小。

输入集的顺序对LIMIT运算符并不重要,但它通常对整个查询计划很重要。例如,此查询的查询计划:

perf=# EXPLAIN SELECT * FROM recalls LIMIT 5;

NOTICE: QUERY PLAN:

Limit (cost=0.00..0.10 rows=5 width=1917)

  -> Seq Scan on recalls (cost=0.00..9217.41 rows=39241 width=1917)

Limit算子仅返回Seq Scan的前5条记录。另外下面这个查询:

perf=# EXPLAIN ANALYZE SELECT * FROM recalls ORDER BY yeartxt LIMIT 5;

NOTICE:  QUERY PLAN:

Limit (cost=0.00..0.10 rows=5 width=1917)

  ->Sort (cost=145321.51..145321.51 rows=39241 width=1911)

    ->Seq Scan on recalls (cost=0.00..9217.41 rows=39241 width=1917)

这个limit算子返回的是Sort的结果集,也就是一个有序的输入集和的前5条。Limit算子不会删除结果集中的列,但是显然他会删除行,实际上并不是从表中真正删除。

如果一个查询中包含limit或offset或者2者,那么计划器/优化器会使用一个limit算子。如果查询中仅包含limit,limit算子在处理整个集合前会先返回第一行记录。

Aggregate

当查询中包含聚合函数时计划器/优化器会产生一个Aggregate算子。下面是聚合函数:AVG(),COUNT(),MAX(),MIN(),STDDEV(),SUM()和VARIANCE()。

Aggregate通过读取输入集中的所有行,然后计算出聚合值。如果输入集没有分组,那么就产生一个结果行。例如:

movies=# EXPLAIN SELECT COUNT(*) FROM customers;

Aggregate (cost=22.50..22.50 rows=1 width=0)

  -> Seq Scan on customers  (cost=0.00..20.00 rows=1000 width=0)

如果输入集分组了,就对每个组产生一个结果行:

movies=# EXPLAIN

movies-#   SELECT COUNT(*), EXTRACT( DECADE FROM birth_date )

movies-#     FROM customers

movies-#     GROUP BY EXTRACT( DECADE FROM birth_date );

NOTICE:  QUERY PLAN:

Aggregate (cost=69.83..74.83 rows=100 width=4)

  -> Group (cost=69.83..72.33 rows=1000 width=4)

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

       ->  Seq Scan on customers  (cost=0.00..20.00 rows=1000 width=4)

注意,未分组的聚合值始终估计是1个,分组的聚合值个数估计是输入集大小的1/10.

Append

Append算子用于实现union。该算子将有2个或多个输入集。Append返回第一个输入集中的所有行,然后返回第2个输入集的所有行,以此类推,直到所有输入集中的所有行都处理。这是一个显示Append算子的查询计划:

perf=# EXPLAIN

perf-#   SELECT * FROM recalls WHERE mfgname = 'FORD'

perf-#     UNION

perf=#   SELECT * FROM recalls WHERE yeartxt = '1983';

Unique

  ->Sort

    ->Append

      ->Subquery Scan *SELECT* 1

        ->Seq Scan on recalls

      ->Subquery Scan *SELECT* 2

        ->Seq Scan on recalls

Append算子的成本估算只是所有输入集的成本估算的总和。Append算子可以在处理所有输入行之前返回其第一行。每当遇到UNION子句时,规划器/优化器都会使用Append算子。当您从涉及继承层次结构的表中进行选择时,也会使用Append。

63f38f941ebfb886fa29e7d681387f2a.png

dvds表继承自video,tapes表也是。如果您从dvds或video中选择,PostgreSQL 将使用一个简单的查询计划进行响应:

movies=# EXPLAIN SELECT * FROM dvds;

  Seq Scan on dvds (cost=0.00..20.00 rows=1000 width=122)

movies=# EXPLAIN SELECT * FROM tapes;

  Seq Scan on tapes (cost=0.00..20.00 rows=1000 width=86)

请记住,由于继承层次结构。dvd是一个video,tape也是一个video。如果从video中select,预期看到素有dvds和所有tapes,所有videos。下面的查询计划反应了整个继承层次:

movies=# EXPLAIN SELECT * FROM video;

Result(cost=0.00..60.00 rows=3000 width=86)

  ->Append(cost=0.00..60.00 rows=3000 width=86)

    ->Seq Scan on video  (cost=0.00..20.00 rows=1000 width=86)

    ->Seq Scan on tapes video  (cost=0.00..20.00 rows=1000 width=86)

->Seq Scan on dvds video  (cost=0.00..20.00 rows=1000 width=86)

仔细查看前面成本估算中的width。如果从dvds表中select,width是每行122个字节。如果从tapes表select,每行是86字节。如果从video,所有行都预期是86字节。下面是创建tapes和dvds表的命令:

movies=# CREATE TABLE tapes ( ) INHERITS( video );

movies=# CREATE TABLE dvds

movies-# (

movies(#   region_id    INTEGER,

movies(#   audio_tracks VARCHAR[]

movies(# ) INHERITS ( video );

可以看到tapes表中的行和video中一样,期望他们都是相同的86字节。dvds表中包video所有列,再加上额外列,因此期望比video行大。当从video表select时,你想要所有videos。PG丢弃没有从video表继承的所有列。

Result

Result算子在3种情况下使用。

1)首先result算子用于执行不需要从表种检索数据的查询:

movies=# EXPLAIN SELECT timeofday();

  Result

在这种形式种,Result算子只计算给定的表达式并返回结果。

2)WHERE子句不依赖表中任何数据,用于评估这样的WHERE:

movies=# EXPLAIN SELECT * FROM tapes WHERE 1 <> 1;

  Result

->Seq Scan on tapes

这看起来像也给愚蠢的查询,但一些客户端应用程序会生成这种形式的查询,作为检索表的元数据的简单方法。在这种形式种,Result算子先计算WHERE子句的常量部分。如果表达式计算结果是false,则不需要进一步处理,并且算子运算完成。如果表达式计算结果true,则result算子将返回其输入集。

3)如果查询计划种顶部节点是Append算子,则计划器/优化器还会生成一个Reuslt算子。这是一个相当模糊的规则,对性能没有影响;它恰好使得PG开发任意维护查询计划器和执行器更简单些。

Nested Loop

Nexted loop算子用于在2个表之间执行连接。需要2个输入集(鉴于连接2个表,这很有意义)。工作原理是从一个输入集(外表)种获取每个元组,对于外表的每一行,在另一个输入(内表)种搜索满足连接条件的行。这是一个例子:

perf=# EXPLAIN

perf-#   SELECT * FROM customers, rentals

perf=#   WHERE customers.customer_id = rentals.customer_id;

Nested Loop

  -> Seq Scan on rentals

  -> Index Scan using customer_id on customers

外表在执行计划种总是列在最前面(rentals是外表)。为了执行这个执行计划,nested loop算子将读取rentals表中每一行,对于每个rentals 行,该算子使用一个索引customer_id读取customers种对应的行。实际上嵌套循环只读取那些满足查询条件的行。嵌套循环算子可用于执行内连接、左外连接和联合。因为不处理整个内表,所以它不能用于其他连接类型:全连接和右连接。

Merge Join

Merge Join算子也是连接2个表。需要2个输入集:一个外表和一个内表。每个输入集必须按连接列排序。看下前面的查询,这次作为Merge Join执行:

perf=# EXPLAIN

perf-#   SELECT * FROM customers, rentals

perf=#   WHERE customers.customer_id = rentals.customer_id;

Merge Join

  -> Sort

     -> Seq Scan on rentals

  -> Index Scan using customer_id on customers

Merge Join开始从每个表种读取第一行:

b17584f6091cefa98d23e214e274917e.png

如果连接列相当,如本例所示,Merge join会创建一个新行,其中包含来自每个输入表的必要列并返回新行。然后,merge join移动外表种的下一行,并将其与内表相应行连接:

40acb44a3aebc06ec0e36fc406425b81.png

接下来,merge join读取外表第3行:

a92755ef0e088e8924305822600579a1.png

现在merge join必须将内表推进2次,才能创建另一个结果行:

2ab7cc738b6aab45ec40282520984e01.png

在为customer_id=3生成结果行后,merge join移动外表到最后一行,然后将内表推进到匹配行:

dda635781281fb7c286ba7be062bfac7.png

Merge join通过生成最终结果行(customer_id=4)来完成。您可以看到merge join的工作原理是遍历2个已排好序的表并找到匹配项。诀窍在于保持指针同步。此示例显示了一个内连接,但merge join算子可以通过以不同方式遍历排序的输入集来用于其他连接类型。Merge join可以做内连接、外连接、联合。

Hash和Hash Join

Hash和Hash Join算子一起工作。需要2个输入集,也称为外表和内表。这是一个使用hash join算子的查询计划:

movies=# EXPLAIN

movies-#   SELECT * FROM customers, rentals

movies-#     WHERE rentals.customer_id = customers.customer_id;

Hash Join

  -> Seq Scan on customers

  -> Hash

       -> Seq Scan on rentals

与其他算子不同,hash join不需要任何一个输入集按连接列排序。相反,内表始终是hash表,外表的顺序不重要。首先使用Hash算法创建内表。Hash算子创建一个临时hash索引,该索引覆盖内表的连接列。创建hash表后,hash join会读取外表每一行,对连接列(从外表)进行hash,并在临时hash索引种搜索匹配值。Hash join算子可用于执行内连接、左外连接和联合。

Group

Group算子用于满足group by子句。Group算子需要一个输入集。并且必须按分组列排序。Group可以在两种不同模式下工作。如果正在计算分组聚合,group将返回其输入集种每一行,每个分组后面都右一个NULL行以指示该组结束(NULL不会显示在最终结果集种,仅用于内部标记):

movies=# EXPLAIN

movies-#   SELECT COUNT(*), EXTRACT( DECADE FROM birth_date )

movies-#     FROM customers

movies-#     GROUP BY EXTRACT( DECADE FROM birth_date );

NOTICE:  QUERY PLAN:

Aggregate (cost=69.83..74.83 rows=100 width=4)

  -> Group (cost=69.83..72.33 rows=1000 width=4)

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

       ->  Seq Scan on customers  (cost=0.00..20.00 rows=1000 width=4)

注意,group算子的成本估算种的行数和输入集相同。如果不计算分组聚合,则group将为其输入集种每个组返回一行:

movies=# EXPLAIN

movies-#   SELECT EXTRACT( DECADE FROM birth_date ) FROM customers

movies-#     GROUP BY EXTRACT( DECADE FROM birth_date );

Group (cost=69.83..69,83 rows=100 width=4)

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

          -> Seq Scan on customers  (cost=0.00..20.00 rows=1000 width=4)

Group算子的评估行数是输入集的1/10.

Subquery Scan和Subplan

Subquery Scan算子用于union子句;subplan用于子选择。这些算子扫描他们的输入集,将每一行添加到结果集种。这些算子用于内部标记目的,不会影响整个查询计划,可以忽略。

为了让您知道何时可能使用它们,这里有两个示例查询计划:

perf=# EXPLAIN

perf-#   SELECT * FROM recalls WHERE mfgname = 'FORD'

perf-#     UNION

perf=#   SELECT * FROM recalls WHERE yeartxt = '1983';

Unique

  ->Sort

    ->Append

      ->Subquery Scan *SELECT* 1

        ->Seq Scan on recalls

      ->Subquery Scan *SELECT* 2

        ->Seq Scan on recalls

movies=# EXPLAIN

movies-#   SELECT * FROM customers

movies-#     WHERE customer_id IN

movies-#       (

movies(#          SELECT customer_id FROM rentals

movies(#       );

NOTICE:  QUERY PLAN:

Seq Scan on customers  (cost=0.00..3.66 rows=2 width=47)

  SubPlan

->  Seq Scan on rentals  (cost=0.00..1.04 rows=4 width=4)

Tid Scan

Tid Scan算子很少使用。一个元组大致相当于一行。每个元组都有一个在表中的唯一标识,元组ID。当选择一行时,可以询问该行的元组ID:

movies=# SELECT ctid, customer_id, customer_name FROM customers;

 ctid  | customer_id |    customer_name

-------+-------------+----------------------

 (0,1) |           1 | Jones, Henry

 (0,2) |           2 | Rubin, William

 (0,3) |           3 | Panky, Henry

 (0,4) |           4 | Wonderland, Alice N.

 (0,5) |           8 | Wink Wankel

“ctid”是一个特殊的列(类似于oid),自动称为每一行的一部分。元组ID由块号、块内的元组号组成。上一个示例种所有行都存储在块0(表文件的第一个块)种。customers种“panky,Henry”行存储在块0的元组3种。知道一行的元组ID后,可以使用其ID再次请求该行:

movies=# SELECT customer_id, customer_name FROM customers

movies-#   WHERE ctid = '(0,3)';

 customer_id | customer_name

-------------+---------------

           3 | Panky, Henry

元组ID就像书签一样工作。但是仅在事务种有效。事务完成后,不应使用元组ID。每当计划器/优化器遇到ctid=expression或expression=ctid形式的约束时,都会使用Tid Scan算子。检索行最快的方式是通过其元组ID。当按元组ID选择时,Tid Scan算子读取元组ID指向的块并返回请求的元组。

物化

Materialize算子用于某些子选择操作。计划器/优化器可能觉得物化一个子选择一次比重复子选择工作代价要低。也可以用于一些merge join连接操作。特别是,如果merge join算子内部输入集不是由Seq Scan、Index Scan、Sort或Materialize算子生成,则计划器/优化器将在计划种插入物化算子。这个规则背后的原因并不明显--它更多与其他算子的能力有关而不是数据相关结构或者性能有关。Merge Join算子很复杂,要求输入集必须按连接列排序。另一个要求内部输入集必须可重新定位。也就是说merge join需要在输入集种前后移动。并非所有有序算子都可以前后启动。如果内部输入集由不可重新定位的算子生成,则计划器/优化器将插入Materialize算子。

Setop (Intersect, Intersect All, Except, Except All)

有4个Setop算子:Setop Intersect、Setop Intersect All、Setop except和Setop except All。仅当规划器/优化器分别遇到INTERSECT、INTERSECT ALL、EXCEPT或EXCEPT ALL子句时,才会生成这些算子。

所有Setop算子都需要两个输入集。Setop算子首先将输入集组合成一个排序列表,然后识别相同行的组。对于每个组,Setop算子计算每个输入集贡献的行数。最后,每个Setop算子使用计数来确定要添加到结果集中的行数。

我认为通过看一个例子会更容易理解。这是两个查询;第一个选择所有1960 年代出生的客户:

movies=# SELECT * FROM customers

movies-#   WHERE EXTRACT( DECADE FROM birth_date ) = 196;

 customer_id |    customer_name     |  phone   | birth_date | balance

-------------+----------------------+----------+------------+---------

           3 | Panky, Henry         | 555-1221 | 1968-01-21 |    0.00

           4 | Wonderland, Alice N. | 555-1122 | 1969-03-05 |    3.00

第二个选择balance大于0的所有行:

movies=# SELECT * FROM customers WHERE balance > 0;

 customer_id |    customer_name     |  phone   | birth_date | balance

-------------+----------------------+----------+------------+---------

           2 | Rubin, William       | 555-2211 | 1972-07-10 |   15.00

           4 | Wonderland, Alice N. | 555-1122 | 1969-03-05 |    3.00

现在使用INTERSECT语句组合这2个查询:

movies=# EXPLAIN

movies-#  SELECT * FROM customers

movies-#    WHERE EXTRACT( DECADE FROM birth_date ) = 196

movies-#  INTERSECT

movies-#    SELECT * FROM customers WHERE balance > 0;

SetOp Intersect

  -> Sort

     -> Append

        -> Subquery Scan *SELECT* 1

           -> Seq Scan on customers

         -> Subquery Scan *SELECT* 2

            -> Seq Scan on customers

查询执行器首先执行两个子查询,然后将结果组合成一个排序列表。添加了一个额外的列,指示哪个输入集贡献了每一行:

 customer_id |    customer_name     | birth_date | balance | input set

-------------+----------------------+------------+---------+----------

           2 | Rubin, William       | 1972-07-10 |   15.00 | inner

           3 | Panky, Henry         | 1968-01-21 |    0.00 | outer

           4 | Wonderland, Alice N. | 1969-03-05 |    3.00 | outer

           4 | Wonderland, Alice N. | 1969-03-05 |    3.00 | inner

SetOp运算符查找重复行组(忽略输入集伪列)。对于每个组,SetOp计算每个输入集贡献的行数。外部集贡献的行数称为count(outer)。内部结果集贡献的行数称为count(inner)。

以下是对每组进行计数后样本的外观:

 customer_id |    customer_name     | birth_date | balance | input set

-------------+----------------------+------------+---------+----------

           2 | Rubin, William       | 1972-07-10 |   15.00 | inner

                               count(outer) = 0

                               count(inner) = 1

           3 | Panky, Henry         | 1968-01-21 |    0.00 | outer

                               count(outer) = 1

                               count(inner) = 0

           4 | Wonderland, Alice N. | 1969-03-05 |    3.00 | outer

           4 | Wonderland, Alice N. | 1969-03-05 |    3.00 | inner

                               count(outer) = 1

                               count(inner) = 1

第一组包含单行,由内部输入集提供。第二组包含单行,由外部输入集提供。最后一组包含两行,每一行由每个输入集贡献。

当SetOp到达一组重复行的末尾时,它根据以下规则确定要写入结果集中的副本数:

1)INTERSECT:如果count(outer) > 0且count(inner) > 0,则将该行的一份副本写入结果集;否则,该行不包含在结果集中。

2)INTERSECT ALL:如果count(outer) > 0且count(inner) > 0 ,则将该行的n 个副本写入结果集;其中n是较大的count(outer)和count(inner)。

3)EXCEPT:如果count(outer) > 0并且count(inner) = 0,则将该行的一份副本写入结果集。

4)EXCEPT ALL:如果count(inner) >= count(outer),则将该行的n个副本写入结果集;其中n是count(outer) - count(inner)。

原文

https://etutorials.org/SQL/Postgresql/Part+I+General+PostgreSQL+Use/Chapter+4.+Performance/Understanding+How+PostgreSQL+Executes+a+Query/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值