分区剪枝

PolarDB PostgreSQL版(兼容Oracle)支持分区剪枝功能,可以显著提高对分区表的查询速度。

概述

PolarDB PostgreSQL版(兼容Oracle)提供了分区剪枝(Partition Pruning)功能,如果启用了分区剪枝,规划器将会检查每个分区的定义并且检验该分区是否因为不包含符合查询WHERE子句的行而无需扫描。若不包含,则会把分区从查询计划中排除(剪枝)。分区剪枝极大地减少了从磁盘检索的数据量并缩短了处理时间,从而提高了查询性能并优化了资源利用率。

根据实际的SQL语句,PolarDB PostgreSQL版(兼容Oracle)数据库支持使用静态或动态剪枝。

  • 静态剪枝发生在编译时,并预先访问有关分区的信息。静态剪枝的一个示例场景是包含WHERE条件的SQL语句,该条件在分区键列上带有常量文字。

  • 动态剪枝发生在运行时,事先不知道语句要访问的确切分区。动态剪枝的一个示例是在WHERE条件中使用运算符或函数。

分区剪枝会影响发生剪枝的对象的统计信息,也会影响语句的执行计划。

分区剪枝技术将数据搜索限制为仅搜索您要搜索的值可能所在的分区。 这两种剪枝技术都会从查询的执行计划中删除分区,从而提高性能。

分区剪枝和约束排除的区别

分区剪枝和约束排除之间的区别在于:

  • 分区剪枝了解分区表中分区之间的关系。 约束排除则不然。

    例如,当查询在列表分区表中搜索特定值时,分区剪枝可能会导致只有特定分区可以保存该值。 约束排除必须检查为每个分区定义的约束。

  • 分区剪枝发生在优化器的早期,以减少规划者必须考虑的分区数量。 约束排除发生在优化器的后期。

不同阶段的分区剪枝

PolarDB PostgreSQL版(兼容Oracle)中将条件表达式分为三个级别,即不变的(Immutable)、稳定的(Stable)、易变的(Volatile)。这三个级别依次对应了三种剪枝:

  • 如果条件表达式值是不变的(比如常量静态值),则它会被用于最早的优化器剪枝;

  • 如果条件表达式值是稳定的(比如now()),则会发生执行器初始阶段的剪枝;

  • 如果条件表达式是易变的(比如random()),则会发生执行器运行时剪枝。

化期剪枝

可以通过如下示例来了解优化期阶段的剪枝。

PolarDB PostgreSQL版(兼容Oracle)中创建一个measurement表,logdate作为分区键,然后为其分别创建4个分区,即measurement_y2023q1,measurement_y2023q2, measurement_y2023q3, measurement_y2023q4四个分区,分别对应了2023年的四个季度。

CREATE TABLE measurement(
    city_id         int not null,
    logdate         date not null,
    peaktemp        int,
    unitsales       int
) PARTITION BY RANGE (logdate);
CREATE TABLE measurement_y2023q1 PARTITION OF measurement
    FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');
CREATE TABLE measurement_y2023q2 PARTITION OF measurement
    FOR VALUES FROM ('2023-04-01') TO ('2023-07-01');
CREATE TABLE measurement_y2023q3 PARTITION OF measurement
    FOR VALUES FROM ('2023-07-01') TO ('2023-10-01');
CREATE TABLE measurement_y2023q4 PARTITION OF measurement
    FOR VALUES FROM ('2023-10-01') TO ('2024-04-01');

此时查询logdate大于2023-10-01的所有数据。使用EXPLAIN可以看到,第一、二、三季度的数据默认都被剪枝掉了,不会执行查询,因为前三个分区的范围明显不满足logdate >= DATE '2023-10-01'。这就是优化器阶段的分区剪枝,可以看到SQL中的限定条件是分区键logdate,且条件表达式的值DATE '2023-10-01'是静态的,或者是不可变的,它可以在优化阶段计算出来。

EXPLAIN SELECT * FROM measurement WHERE logdate >= DATE '2023-10-01';
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Append  (cost=0.00..34.09 rows=567 width=20)
   ->  Seq Scan on measurement_y2023q4  (cost=0.00..31.25 rows=567 width=20)
         Filter: (logdate >= '01-OCT-23 00:00:00'::date)
(3 rows)

行期初始剪枝

同样通过上文measurement表来了解执行期初始剪枝。

如下可以看到,同样的measurement表,同样的SQL查询,但是查询条件的表达式值从静态值变成了now(),这是一个稳定的表达式,它不能在优化阶段计算,但是可以在执行器初始阶段计算。假设今天是2023年7月,因此可以看到前两个季度分区被移除了Subplans Removed: 2, 只剩下了第三季度和第四季度。这就是执行期初始阶段的分区剪枝,可以看到SQL中的限定条件是分区键logdate,且条件表达式的值now()是稳定的,它可以在执行期初始阶段计算出来。

EXPLAIN SELECT * FROM measurement WHERE logdate >= now();
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Append  (cost=0.00..153.34 rows=2268 width=20)
   Subplans Removed: 2
   ->  Seq Scan on measurement_y2023q3  (cost=0.00..35.50 rows=567 width=20)
         Filter: (logdate >= now())
   ->  Seq Scan on measurement_y2023q4  (cost=0.00..35.50 rows=567 width=20)
         Filter: (logdate >= now())
(6 rows)

执行期运行时剪枝

同样通过上文measurement表来了解执行期运行时剪枝。

如下可以看到,同样的measurement表,同样的SQL查询,但是查询条件的表达式值从静态值变成了(select to_date('2023-10-1')),这是一个易变的子连接,它不能在优化阶段计算,也不能在执行期初始阶段计算,只能在执行期运行时计算。

使用EXPLAIN ANALYZE可以看出前三个季度的分区都是标记了(never executed), 这就是执行期运行时剪枝。 它适用于易变的表达式值、子查询、子连接,以及join条件表达式。可以看到SQL中的限定条件是分区键logdate,且条件表达式的值(select to_date('2023-10-1'))是易变的子连接,它可以在执行期运行阶段计算出来。

EXPLAIN ANALYZE SELECT * FROM measurement WHERE logdate >= (select  to_date('2023-10-1'));
                                                      QUERY PLAN                                                       
-----------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.01..136.35 rows=2268 width=20) (actual time=0.067..0.068 rows=0 loops=1)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.01 rows=1 width=8) (actual time=0.051..0.053 rows=1 loops=1)
   ->  Seq Scan on measurement_y2023q1  (cost=0.00..31.25 rows=567 width=20) (never executed)
         Filter: (logdate >= $0)
   ->  Seq Scan on measurement_y2023q2  (cost=0.00..31.25 rows=567 width=20) (never executed)
         Filter: (logdate >= $0)
   ->  Seq Scan on measurement_y2023q3  (cost=0.00..31.25 rows=567 width=20) (never executed)
         Filter: (logdate >= $0)
   ->  Seq Scan on measurement_y2023q4  (cost=0.00..31.25 rows=567 width=20) (actual time=0.004..0.004 rows=0 loops=1)
         Filter: (logdate >= $0)