分区剪枝

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

概述

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

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

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

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

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

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

  • 分区剪枝无法优化对子分区表的查询,也无法优化分区于多个列上的RANGE分区表的查询。

约束排除类似,分区剪枝只能对包含WHERE子句的查询进行优化,且只有当WHERE子句中的限定符满足某种格式时,才可进行优化。

参数

参数名称

描述

polar_comp_enable_pruning

用于控制分区剪枝,取值范围如下:

  • on(默认),表示启用分区剪枝,快速剪去某些特定的查询。

  • off,表示不启用分区剪枝。

使用说明

按分区类型

LIST分区表查询

  • 对于LIST分区表的查询,分区剪枝能快速剪去包含WHERE子句的查询,运算符为等于(=)或IS NULL/IS NOT NULL

    示例:

    ------指定一个LIST分区表
    CREATE TABLE sales_hist(..., country text, ...) 
        PARTITION BY LIST(country) (
        PARTITION americas VALUES('US', 'CA', 'MX'), 
        PARTITION europe VALUES('BE', 'NL', 'FR'), 
        PARTITION asia VALUES('JP', 'PK', 'CN'), 
        PARTITION others VALUES(DEFAULT)
    ) 
    ------在该LIST分区表的基础上,提取出包含如下WHERE子句的信息
    WHERE country = 'US' WHERE country IS NULL;          

    通过第一个WHERE子句, 分区剪枝将排除分区europe、asia和others,因为这些分区不满足WHERE country = 'US'

    通过第二个WHERE子句, 分区剪枝将排除分区americas、europe和asia。因为这些分区不满足WHERE country IS NULL

RANGE分区表查询

  • 对于RANGE分区表的查询,分区剪枝能快速剪去包含WHERE子句的查询。运算符可以是等于(=)、大于(>)、大于等于(>=)、小于(<)、小于等于(<=),或者是IS NULL/IS NOT NULL,也可以是更为复杂的包含运算符ANDBETWEEN的表达式,例如:

    WHERE size > 100 AND size <= 200 WHERE size BETWEEN 100 AND 200
    说明

    分区剪枝不支持包含ORIN运算符的表达式。

    示例:

    ------指定一个RANGE分区表
    CREATE TABLE boxes(id int, size int, color text) 
      PARTITION BY RANGE(size)
    (
        PARTITION small VALUES LESS THAN(100),
        PARTITION medium VALUES LESS THAN(200),
        PARTITION large VALUES LESS THAN(300)
    )
    ------在该RANGE分区表的基础上,提取出包含如下WHERE子句的信息。
    WHERE size > 100     -- 扫描medium和large分区
    WHERE size >= 100    -- 扫描medium和large分区
    WHERE size = 100     -- 扫描medium分区
    WHERE size <= 100    -- 扫描small和medium分区
    WHERE size < 100     -- 扫描small分区
    WHERE size > 100 AND size < 199     -- 扫描medium分区
    WHERE size BETWEEN 100 AND 199      -- 扫描medium分区
    WHERE color = 'red' AND size = 100  -- 扫描medium分区
    WHERE color = 'red' AND (size > 100 AND size < 199) -- 扫描medium分区

按不同阶段

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)

示例

SELECT

CREATE TABLE sales
(
  dept_no     number,   
  part_no     varchar2,
  country     varchar2(20),
  date        date,
  amount      number
)
PARTITION BY LIST(country)
(
  PARTITION europe VALUES('FRANCE', 'ITALY'),
  PARTITION asia VALUES('INDIA', 'PAKISTAN'),
  PARTITION americas VALUES('US', 'CANADA')
);

------执行包含EXPLAIN语句的约束查询,服务器将只扫描表sales_asia,country值为INDIA的记录将会存储到该表中
EXPLAIN (COSTS OFF) SELECT * FROM sales WHERE country = 'INDIA';
                    QUERY PLAN                     
---------------------------------------------------
 Append
   ->  Seq Scan on sales
         Filter: ((country)::text = 'INDIA'::text)
   ->  Seq Scan on sales_asia
         Filter: ((country)::text = 'INDIA'::text)
(5 rows)

------执行如下查询,搜索未包括在分区键中的值匹配的记录,服务器将只扫描表sales_asia,country值为INDIA的记录将会存储到该表中
EXPLAIN (COSTS OFF) SELECT * FROM sales WHERE dept_no = '30'; 
               QUERY PLAN               
-----------------------------------------
 Append
   ->  Seq Scan on sales
         Filter: (dept_no = 30::numeric)
   ->  Seq Scan on sales_europe
         Filter: (dept_no = 30::numeric)
   ->  Seq Scan on sales_asia
         Filter: (dept_no = 30::numeric)
   ->  Seq Scan on sales_americas
         Filter: (dept_no = 30::numeric)
(9 rows)

约束排除在查询子分区表时同样适用:

CREATE TABLE sales
(
  dept_no     number,
  part_no     varchar2,
  country     varchar2(20),
  date        date,
  amount      number
)
PARTITION BY RANGE(date) SUBPARTITION BY LIST (country)
(
  PARTITION "2011" VALUES LESS THAN('01-JAN-2012')
  (
    SUBPARTITION europe_2011 VALUES ('ITALY', 'FRANCE'),
    SUBPARTITION asia_2011 VALUES ('PAKISTAN', 'INDIA'),
    SUBPARTITION americas_2011 VALUES ('US', 'CANADA')
  ),
  PARTITION "2012" VALUES LESS THAN('01-JAN-2013')
  (
    SUBPARTITION europe_2012 VALUES ('ITALY', 'FRANCE'),
    SUBPARTITION asia_2012 VALUES ('PAKISTAN', 'INDIA'),
    SUBPARTITION americas_2012 VALUES ('US', 'CANADA')
  ),
  PARTITION "2013" VALUES LESS THAN('01-JAN-2014')
  (
    SUBPARTITION europe_2013 VALUES ('ITALY', 'FRANCE'),
    SUBPARTITION asia_2013 VALUES ('PAKISTAN', 'INDIA'),
    SUBPARTITION americas_2013 VALUES ('US', 'CANADA')
  )
);

------当您查询这张表时,查询计划器会从搜索路径中剪去任何可能不包含您想要的结果集的分区或子分区
EXPLAIN (COSTS OFF) SELECT * FROM sales WHERE country = 'US' AND date = 'Dec 12, 2012';
                                 QUERY PLAN                                 
-----------------------------------------------------------------------------
 Append
   ->  Seq Scan on sales
         Filter: (((country)::text = 'US'::text) AND (date = '12-DEC-12 00:00:00'::timestamp without time zone))
   ->  Seq Scan on sales_2012
         Filter: (((country)::text = 'US'::text) AND (date = '12-DEC-12 00:00:00'::timestamp without time zone))
   ->  Seq Scan on sales_americas_2012
         Filter: (((country)::text = 'US'::text) AND (date = '12-DEC-12 00:00:00'::timestamp without time zone))
(7 rows)

UPDATE、DELETE

CREATE TABLE t1_hash (id int , value int) PARTITION BY hash(id) partitions 4;

------update操作
EXPLAIN UPDATE t1_hash SET value = value+1 WHERE id = least(1,2);
                               QUERY PLAN                                
-------------------------------------------------------------------------
 Update on t1_hash  (cost=0.00..92.18 rows=24 width=14)
   Update on t1_hash_p1
   Update on t1_hash_p2 t1_hash
   ->  Append  (cost=0.00..92.18 rows=24 width=14)
         Subplans Removed: 1
         ->  Seq Scan on t1_hash_p1  (cost=0.00..46.03 rows=12 width=14)
               Filter: (id = LEAST(1, 2))
(7 rows)

------delete操作
EXPLAIN DELETE FROM t1_hash WHERE id = least(1,2);
                               QUERY PLAN                                
-------------------------------------------------------------------------
 Delete on t1_hash  (cost=0.00..92.12 rows=24 width=10)
   Delete on t1_hash_p1
   Delete on t1_hash_p2 t1_hash
   ->  Append  (cost=0.00..92.12 rows=24 width=10)
         Subplans Removed: 1
         ->  Seq Scan on t1_hash_p1  (cost=0.00..46.00 rows=12 width=10)
               Filter: (id = LEAST(1, 2))
(7 rows)

相关参考

约束排除

参数

参数名称

描述

constraint_exclusion

用于控制约束排除,取值范围如下:

  • partition(默认),表示启用约束排除。

  • on,表示启用约束排除。

  • off,表示不启用约束排除。

当您启用了约束排除,服务器就会检查定义给每个分区的约束来决定分区是否能满足查询。

  • 当执行不包含WHERE子句的SELECT语句时,查询计划会推荐用于搜索整个表的执行计划。

  • 当执行包含WHERE子句的SELECT语句时,查询计划会决定要存储记录的分区,并发送查询碎片给这个分区,然后从执行计划中剪去不能包含记录的分区。

如果您不使用分区表功能,则建议禁用约束排除,从而提高性能。

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

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

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

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

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