postgresql 10 分区探密

Thingsboard从配置到运行成功小白级别的步骤以及踩过的坑 最近在研究开源的ThingsBoard相关源码的内容,作为一个移动端APP的开发者,第一次接触这种后端项目,真的是无从下手。只能根据网上的步骤一点点的进行操作,项目好不容易编译成功了、数据库也安装成功了,但是就是项目运行不起来。把自己遇到的问题在一些QQ群中咨询,可能是自己的问题比较低级,也没有人回复,在网上也没有找到立即对症下药的帖子,自己一遍又一遍的换关键字搜索,功夫不负有心人,终于可以让项目运行起来,顺便把我遇到的问题记录下,相比较与网上有一些比较好的博客,更适合像我这种从前端看后端项目的人,里面的有 阅读详情

postgresql官方终于要出分区了,开发线上已经看到提交分区代码了,下一个版本10带有分区功能应该没问题了,那么这个分区功能如何呢?且和我深入源码一探究竟。

原有分区介绍

分区用法介绍

首先介绍下原有的“分区”功能,这个很早就有了,以继承表的方式创建子表方式曲线实现的分区,如下例子:

 create table tbl(
 a int,
 b varchar(10)
 );
 create table tbl_1 (
check ( a <= 1000 )       
) INHERITS (tbl);
create table tbl_2 (
check ( a <= 10000 and a >1000 )       
) INHERITS (tbl);
create table tbl_3 (
check ( a <= 100000 and a >10000 )       
) INHERITS (tbl);

再通过创建触发器或者规则,实现数据分发,只需要向主表插入数据,可以自动分发到子分区表中,下面以触发器为例如下:

CREATE OR REPLACE FUNCTION tbl_part_tg()                      
RETURNS TRIGGER AS $$  
BEGIN  
    IF ( NEW. a <= 1000 ) THEN  
        INSERT INTO tbl_1 VALUES (NEW.*);  
    ELSIF ( NEW. a > 1000 and NEW.a <= 10000 ) THEN  
        INSERT INTO tbl_2 VALUES (NEW.*); 
    ELSIF ( NEW. a > 10000 and NEW.a <= 100000 ) THEN  
        INSERT INTO tbl_3 VALUES (NEW.*); 
    ELSIF ( NEW. a > 100000 and NEW.a <= 1000000 ) THEN  
        INSERT INTO tbl_4 VALUES (NEW.*); 
    ELSE  
        RAISE EXCEPTION 'data out of range!';  
    END IF;  
    RETURN NULL;  
END;  
$$  
LANGUAGE plpgsql;

CREATE TRIGGER insert_tbl_part_tg
   BEFORE INSERT ON tbl
   FOR EACH ROW EXECUTE PROCEDURE tbl_part_tg();

这样一个postgres分区表就创建完毕,对应用来说透明的,插入查询都对主表操作,非常方便。

如何实现分区过滤

对于分区表来说,最大的好处在于分区剪枝功能,如果有50个分区表,对于某个条件值如果能确定,那么很可能就直接过滤掉了49个分区,大大提高扫描速度,当然也能将不同子分区表放在不同物理盘上,提高IO速度。那么对于查询是怎么实现子分区表过滤的呢?约束排除,是否能使用约束排除由constraint_exclusion 参数控制,它三个可设值,on,off,partition, on代表无条件打开,所有情况都会检束约束,off代表关闭,所有约束都不生效,partition代表对分区表(或者说继承表)会进行约束排查,其它表则不会,因为检查约束在生成计划时会有额外开销,为了精准定位才有了这三个参数,默认值是partition,即对分区表约束生效。
如:select *from tbl where a = 12345; 首先找到主表tbl,然后通过tbl找到它的子表,找到后再对再拿着谓词条件a = 12345对一个个子表约束进行检查,不符合条件表就去掉不扫描,实现分区表过滤,下面简单介绍下约束排除源码逻辑。

过滤代码分析

//从set_rel_size 基表大小估计函数开始介绍
static void
set_rel_size(PlannerInfo *root, RelOptInfo *rel,
             Index rti, RangeTblEntry *rte)
{
    //检查是否需要扫描
    if (rel->reloptkind == RELOPT_BASEREL &&
        relation_excluded_by_constraints(root, rel, rte)) //检查约束是否能排除掉该表
    {
        set_dummy_rel_pathlist(rel); //可以排除,不需要扫描该表
    }
    else if (rte->inh)//检查是否有子表
    {
        set_append_rel_size(root, rel, rti, rte);//有子表则开始检查所有子表并把不需要的去掉
    }


//设置需要扫描的表
static void
set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
                    Index rti, RangeTblEntry *rte)
{
    int         parentRTindex = rti;
        ... 为减少篇幅,忽略不重要代码
        ...

    foreach(l, root->append_rel_list)
    {   //遍历所有,root->append_rel_list 是含父表、子表所有relation的list,在前面已经准备好
        AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(l);
        int         childRTindex;
        ...
        //拿到真实条件表达式,如这个用例就是拿到 a = 12345 这个条件
        childquals = get_all_actual_clauses(rel->baserestrictinfo);
       //调整append的relation属性,可能需要对一些特殊的表达式或查询结构复制一份并转换,本用例中不涉及
        childquals = (List *) adjust_appendrel_attrs(root,
                                                     (Node *) childquals,

        //常量表达式处理,对一些常量表达式会将值直接算出来
        //显然本例中a=12345是一个列的OpExpr表达式    ,因此这里不会发生改变                                          
        childqual = eval_const_expressions(root, (Node *)
                                           make_ands_explicit(childquals));

        //下面条件成立直接判断不需要扫描该表,本例中均不会成立                                   
        if (childqual && IsA(childqual, Const) &&
            (((Const *) childqual)->constisnull ||
             !DatumGetBool(((Const *) childqual)->constvalue)))
        {
            set_dummy_rel_pathlist(childrel);
            continue;
        }

        //可以简单认为make_ands_implicit与上面make_ands_explicit互逆
        childquals = make_ands_implicit((Expr *) childqual);
        //根据clause生成一个RestrictInfo结构
        childquals = make_restrictinfos_from_actual_clauses(root,
                                                            childquals);        
        childrel->baserestrictinfo = childquals;

        //检查约束是否能排除掉该表,即判断某个分区是要要扫描
        if (relation_excluded_by_constraints(root, childrel, childRTE))
        {
            set_dummy_rel_pathlist(childrel);//进来即是排除掉,继续下一个表检查
            continue;
        }
        ...
        ...
        //若约束无法排除掉某个分区,后续代码继续正常执行分区表相关计算                               

    //表约束排查函数relation_excluded_by_constraints 简介
    bool
    relation_excluded_by_constraints(PlannerInfo *root,
                                     RelOptInfo *rel, RangeTblEntry *rte)
    {
        List       *safe_restrictions;
        ...

        //初步判断是否需要进行约束排除,return false则是不能,如constraint_exclusion 是off状态时
        //这时根本没开约束排除功能,约束自然不能生效
        if (constraint_exclusion == CONSTRAINT_EXCLUSION_OFF ||
        (constraint_exclusion == CONSTRAINT_EXCLUSION_PARTITION &&
         !(rel->reloptkind == RELOPT_OTHER_MEMBER_REL ||
           (root->hasInheritedTarget &&
            rel->reloptkind == RELOPT_BASEREL &&
            rel->relid == root->parse->resultRelation))))
        return false;

    //检查 谓词条件(a=12345)调用的函数是否结果稳定,若稳定结果则将条件挂到safe_restrictions上
    safe_restrictions = NIL;
    foreach(lc, rel->baserestrictinfo)
    {
        RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
        if (!contain_mutable_functions((Node *) rinfo->clause))
            safe_restrictions = lappend(safe_restrictions, rinfo->clause);
    }
    //检查safe_restrictions条件本身是不是冲突,自身冲突则排除掉
    if (predicate_refuted_by(safe_restrictions, safe_restrictions))
        return true;

    /* Only plain relations have constraints */
    if (rte->rtekind != RTE_RELATION || rte->inh)
        return false;

    //把这个表的约束取出来
    constraint_pred = get_relation_constraints(root, rte->relid, rel, true);

    //检查这些约束的条件所调用的函数结果是否稳定,不稳定的不能作为排查条件
    safe_constraints = NIL;
    foreach(lc, constraint_pred)
    {
        Node       *pred = (Node *) lfirst(lc);
        if (!contain_mutable_functions(pred))
            safe_constraints = lappend(safe_constraints, pred);
    }   

    //约束条件和谓词条件进行排查,如果冲突得返回true去掉该分区表
    //如:约束条件为 a>1000 and a<=10000,谓词条件为a=12345,它们冲突则返回true
    if (predicate_refuted_by(safe_constraints, rel->baserestrictinfo))
        return true;

上面的例子如下,有四个分区的表,直接定位到了子表tbl_3,注意父表不能过滤,默认都要扫描,但如果分区表设计合理,父表不应该有数据,扫描代价为0,对性能几乎无影响

postgres=# explain select  *from tbl where a =11111;
                         QUERY PLAN
-------------------------------------------------------------
 Append  (cost=0.00..24.50 rows=7 width=42)
   ->  Seq Scan on tbl  (cost=0.00..0.00 rows=1 width=42)
         Filter: (a = 11111)
   ->  Seq Scan on tbl_3  (cost=0.00..24.50 rows=6 width=42)
         Filter: (a = 11111)
(5 rows)

简单来说,postgresql原来分区是通过谓词条件和表上约束条件之间关系实现过滤的,当然这只能实现静态剪枝。

如何实现数据分发

基于规则的话,会在查询重写阶段按时替换规则生成新的插入语句,基于触发器会在insert主表前触发另外一个insert操作,这两个逻辑都比较简单,相关代码不再介绍。

postgresql10分区介绍

那么postgres10分区具有什么样的功能呢?先看用例

LIST分区语法

postgres=# CREATE TABLE list_parted (
postgres(# a int
postgres(# ) PARTITION BY LIST (a);
CREATE TABLE
postgres=# CREATE TABLE part_1 PARTITION OF list_parted FOR VALUES IN (1);
CREATE TABLE
postgres=# CREATE TABLE part_2 PARTITION OF list_parted FOR VALUES IN (2);
CREATE TABLE
postgres=# CREATE TABLE part_3 PARTITION OF list_parted FOR VALUES IN (3);
CREATE TABLE
postgres=# CREATE TABLE part_4 PARTITION OF list_parted FOR VALUES IN (4);
CREATE TABLE
postgres=# CREATE TABLE part_5 PARTITION OF list_parted FOR VALUES IN (5);
CREATE TABLE
postgres=#
postgres=# insert into list_parted values(32); --faled
ERROR:  no partition of relation "list_parted" found for row
DETAIL:  Failing row contains (32).
postgres=# insert into part_1 values(1);
INSERT 0 1
postgres=# insert into part_1 values(2);--faled
ERROR:  new row for relation "part_1" violates partition constraint
DETAIL:  Failing row contains (2).
postgres=# explain select *from list_parted where a =1;
                           QUERY PLAN
-----------------------------------------------------------------
 Append  (cost=0.00..41.88 rows=14 width=4)
   ->  Seq Scan on list_parted  (cost=0.00..0.00 rows=1 width=4)
         Filter: (a = 1)
   ->  Seq Scan on part_1  (cost=0.00..41.88 rows=13 width=4)
         Filter: (a = 1)
(5 rows)

上面是LIST表,建表是先建主表,再建子表,子表以 PARTITION OF 方式说明和主表关系,约束条件应该就是后面的in里面,再来个范围表的例子。

RANGE分区语法

postgres=# CREATE TABLE range_parted (
postgres(# a int
postgres(# ) PARTITION BY RANGE (a);
CREATE TABLE
postgres=# CREATE TABLE range_parted1 PARTITION OF range_parted FOR VALUES from (1) TO (1000);
CREATE TABLE
postgres=# CREATE TABLE range_parted2 PARTITION OF range_parted FOR VALUES FROM (1000) TO (10000);
CREATE TABLE
postgres=# CREATE TABLE range_parted3 PARTITION OF range_parted FOR VALUES FROM (10000) TO (100000);
CREATE TABLE
postgres=#
postgres=# insert into range_parted1 values(343);
INSERT 0 1
postgres=#
postgres=# explain select *from range_parted where a=32425;
                             QUERY PLAN
---------------------------------------------------------------------
 Append  (cost=0.00..41.88 rows=14 width=4)
   ->  Seq Scan on range_parted  (cost=0.00..0.00 rows=1 width=4)
         Filter: (a = 32425)
   ->  Seq Scan on range_parted3  (cost=0.00..41.88 rows=13 width=4)
         Filter: (a = 32425)
(5 rows)
postgres=# set constraint_exclusion = off;
SET
postgres=# explain select *from range_parted where a=32425;
                             QUERY PLAN
---------------------------------------------------------------------
 Append  (cost=0.00..125.63 rows=40 width=4)
   ->  Seq Scan on range_parted  (cost=0.00..0.00 rows=1 width=4)
         Filter: (a = 32425)
   ->  Seq Scan on range_parted1  (cost=0.00..41.88 rows=13 width=4)
         Filter: (a = 32425)
   ->  Seq Scan on range_parted2  (cost=0.00..41.88 rows=13 width=4)
         Filter: (a = 32425)
   ->  Seq Scan on range_parted3  (cost=0.00..41.88 rows=13 width=4)
         Filter: (a = 32425)
(9 rows)

和LIST差不多,就是语法略有不同,范围表值是一个连续的范围,LIST表是单点或多点的集合。从上面例子可以看到,显然还是走的约束排除过滤子表的方式。

HASH分区语法

postgres=# CREATE TABLE hash_parted (
postgres(# a int
postgres(# ) PARTITION BY HASH (a);
ERROR:  unrecognized partitioning strategy "hash"
postgres=#
postgres=#
postgres=# CREATE TABLE cccc_parted (
postgres(# a int
postgres(# ) PARTITION BY cccc (a);
ERROR:  unrecognized partitioning strategy "cccc"
postgres=#

HASH分区语法还不支持,以后或许会支持。

postgresql10语法小结

与原来老的建分区方式比,简单了不少,不用建约束了,应该是内部创建了。插入能自动计算出子表插入位置,应该是在执行器增加根据给定值直接计算出目标分区,提高性能。分区过滤的原来一样,通过约束排除,目前没看到增强迹象,但毕竟只是开始。总的来说语法功能初步完善,但这仅是一个开始,以后肯定会越来越丰富强大的。

postgresql10性能简单测试

这里对分区测试主要关注两个点,1是分区剪枝,2是导入数据性能,参考德哥测试方法结果如下:
测试环境:pg10 DEBUG版,全默认编译,win7 i74770s cpu,普通硬盘。
传统方式建分区1000个,时间33.65秒,建规则22.2秒,总时间55.85秒。
第一个分区插入100W行用时185.75秒,第996个分区488.9秒。
第一个分区select用时172ms,几乎全是生成计划时间
第996个分区select用时171 ms,几乎全是生成计划时间
pg10分区方式建分区1000个,时间40.2秒,总时间40.2秒,环境同上。
第一个分区插入100W行用时3.5秒,第996个分区4.72秒。
第一个分区select用时133.5ms,几乎全是生成计划时间
第996个分区select用时133.8 ms,几乎全是生成计划时间

总的来说,新老分区剪枝走的策略一样,提升有限,但还是略有一点,主要是pg10计划时间稍短点,但insert性能实实在在的实现了质的飞跃, 两个数量级差别!
测试实况如下:

传统方式
create table test1(id int8, info text, crt_time timestamp);
 do language plpgsql $$
declare
  i int;
begin
  for i in 1..1000 loop
    execute 'create table test1_'||i||'(like test1 including all) inherits(test1)'; 
    execute 'alter table test1_'||i||' add constraint ck_test1_'||i||' check(id>='||20000000::int8*(i-1)+1||' and id<'||20000000::int8*i+1||')';
  end loop; 
end;
$$;
--规则
do language plpgsql $$
declare
  i int;
begin
  for i in 1..1000 loop
    execute 'create or replace rule r'||i||' as on insert to test1 where id >= '||20000000::int8*(i-1)+1||' and id<'||20000000::int8*i+1||' do instead (insert into test1_'||i||' values (new.id,new.info,new.crt_time))';
  end loop; 
end;
$$;
postgres=# insert into test1 select generate_series (19990000,21000000);
INSERT 0 0
Time: 185755.382 ms (03:05.755)

postgres=# insert into test1 select generate_series (19919990000,19921000000);
INSERT 0 0
Time: 488937.560 ms (08:08.938)

postgres=# explain analyze select *from test1 where id=20000000;
                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..170.01 rows=2 width=48) (actual time=1.493..1.493 rows=1 loops=1)
   ->  Seq Scan on test1  (cost=0.00..0.00 rows=1 width=48) (actual time=0.163..0.163 rows=0 loops=1)
         Filter: (id = 20000000)
   ->  Seq Scan on test1_1  (cost=0.00..170.01 rows=1 width=48) (actual time=1.328..1.328 rows=1 loops=1)
         Filter: (id = 20000000)
         Rows Removed by Filter: 10000
 Planning time: 166.882 ms
 Execution time: 1.552 ms
(8 rows)
Time: 172.326 ms

postgres=# explain analyze select *from test1 where id=19919990000;
                                                 QUERY PLAN
------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..170.01 rows=2 width=48) (actual time=0.155..1.628 rows=1 loops=1)
   ->  Seq Scan on test1  (cost=0.00..0.00 rows=1 width=48) (actual time=0.135..0.135 rows=0 loops=1)
         Filter: (id = '19919990000'::bigint)
   ->  Seq Scan on test1_996  (cost=0.00..170.01 rows=1 width=48) (actual time=0.019..1.491 rows=1 loops=1)
         Filter: (id = '19919990000'::bigint)
         Rows Removed by Filter: 10000
 Planning time: 165.187 ms
 Execution time: 1.690 ms
(8 rows)
Time: 171.021 ms
pg10分区方式
postgres=# create table test(id int8, info text, crt_time timestamp)partition by range(id);
CREATE TABLE
Time: 41.593 ms
postgres=#
postgres=#  do language plpgsql $$
postgres$# declare
postgres$#   i int;
postgres$# begin
postgres$#   for i in 1..1000 loop
postgres$#     execute 'create table test_'||i||' PARTITION OF test FOR VALUES FROM ('||20000000::int8*(i-1)+1||') to ('||20000000::int8*i+1||')';
postgres$#   end loop;
postgres$# end;
postgres$# $$;
DO
Time: 40272.109 ms (00:40.272)

postgres=# insert into test select generate_series (19990000,21000000);
INSERT 0 1010001
Time: 3500.975 ms (00:03.501)
postgres=# insert into test select generate_series (19919990000,19921000000);
INSERT 0 1010001
Time: 4723.782 ms (00:04.724)

postgres=#  explain analyze select *from test where id=20000000;
                                               QUERY PLAN
---------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..170.01 rows=2 width=48) (actual time=1.449..1.450 rows=1 loops=1)
   ->  Seq Scan on test  (cost=0.00..0.00 rows=1 width=48) (actual time=0.121..0.121 rows=0 loops=1)
         Filter: (id = 20000000)
   ->  Seq Scan on test_1  (cost=0.00..170.01 rows=1 width=48) (actual time=1.328..1.328 rows=1 loops=1)
         Filter: (id = 20000000)
         Rows Removed by Filter: 10000
 Planning time: 128.244 ms
 Execution time: 1.491 ms
(8 rows)
Time: 133.588 ms

postgres=#  explain analyze select *from test where id=19919990000;
                                                QUERY PLAN
-----------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..170.01 rows=2 width=48) (actual time=0.153..1.566 rows=1 loops=1)
   ->  Seq Scan on test  (cost=0.00..0.00 rows=1 width=48) (actual time=0.136..0.136 rows=0 loops=1)
         Filter: (id = '19919990000'::bigint)
   ->  Seq Scan on test_996  (cost=0.00..170.01 rows=1 width=48) (actual time=0.015..1.429 rows=1 loops=1)
         Filter: (id = '19919990000'::bigint)
         Rows Removed by Filter: 10000
 Planning time: 128.298 ms
 Execution time: 1.623 ms
(8 rows)
Time: 133.836 ms

postgresql10分区相关代码简析

parser语法篇:

AlterTableStmt:
...
|   ALTER TABLE relation_expr partition_cmd  
|   ALTER TABLE IF_P EXISTS relation_expr 
partition_cmd:
            ATTACH PARTITION qualified_name ForValues
                ...
            | DETACH PARTITION qualified_name
                ...
        ;
...

alter table 增加了分区支持,如:
ALTER TABLE list_parted2 ATTACH PARTITION part_2 FOR VALUES IN (2);
注意这里的子表part_2表得先建好,然后才能alter table 挂到主表上。

CreateStmt: CREATE OptTemp TABLE qualified_name '(' OptTableElementList ')'
            OptInherit OptPartitionSpec OptWith OnCommitOption OptTableSpace
            ...
            | CREATE OptTemp TABLE qualified_name PARTITION OF qualified_name
            OptPartitionElementList ForValues OptPartitionSpec OptWith
            OnCommitOption OptTableSpace
            ...

OptPartitionSpec: PartitionSpec { $$ = $1; }
			| /*EMPTY*/			{ $$ = NULL; }
        ;

PartitionSpec: PARTITION BY part_strategy '(' part_params ')'
        ...
        ;

part_strategy:  IDENT                   { $$ = $1; }
				| unreserved_keyword	{ $$ = pstrdup($1); }
        ;

create table在OptInherit 后面OptPartitionSpec 说明的是主表是一个分区表,ForValues 后面的OptPartitionSpec 则说明子分区的下面还可以挂子分区,至少代码上表现是这样的,试了试效果如下:

postgres=#  CREATE TABLE range_list (
postgres(# a int
postgres(# ) PARTITION BY RANGE (a);
CREATE TABLE
postgres=# CREATE TABLE range_pa1 PARTITION OF range_list FOR VALUES from (1) TO (1000)  PARTITION BY LIST (a);;
CREATE TABLE
postgres=# CREATE TABLE range_pa2 PARTITION OF range_list FOR VALUES FROM (1000) TO (10000);
CREATE TABLE
postgres=# CREATE TABLE range_list1  PARTITION OF range_pa1 FOR VALUES IN (10);
CREATE TABLE
postgres=# CREATE TABLE range_list2  PARTITION OF range_pa1 FOR VALUES IN (20);
CREATE TABLE
postgres=# insert into  range_pa1 values(20);
INSERT 0 1
postgres=# insert into range_list2 values(20);
INSERT 0 1
postgres=# explain select *from range_list where a =20;
                            QUERY PLAN
-------------------------------------------------------------------
 Append  (cost=0.00..41.88 rows=15 width=4)
   ->  Seq Scan on range_list  (cost=0.00..0.00 rows=1 width=4)
         Filter: (a = 20)
   ->  Seq Scan on range_pa1  (cost=0.00..0.00 rows=1 width=4)
         Filter: (a = 20)
   ->  Seq Scan on range_list2  (cost=0.00..41.88 rows=13 width=4)
         Filter: (a = 20)
(7 rows)

果然能支持多级分区,且各级子分区都能起到过滤作用,但是子分区都能插入数据,即使它的下面还有下一级子分区。

CreateForeignTableStmt:
            ...
        | CREATE FOREIGN TABLE qualified_name
            PARTITION OF qualified_name OptPartitionElementList ForValues
            SERVER name create_generic_options
            ...
        | CREATE FOREIGN TABLE IF_P NOT EXISTS qualified_name
            PARTITION OF qualified_name OptPartitionElementList ForValues
            SERVER name create_generic_options

看起来似乎是支持直接把外部表作为某个表的子表,这个功能若好用会很实用,不过还没做测试。

DefineRelation分区建表篇:

这是建表DDL函数,建一个普通表都会进入该接口,里面增加了分区处理信息的逻辑.

ObjectAddress
DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
               ObjectAddress *typaddress, const char *queryString)
{           //增加了queryString存建表语句,主表不用,子表创建时用
    char        relname[NAMEDATALEN];
    Oid         namespaceId;
    List       *schema = stmt->tableElts;
    ...//中间省略部分代码
    if (stmt->partbound)//如果有分区键约束信息
    {
        Node       *bound;
        ParseState *pstate;
        Oid         parentId = linitial_oid(inheritOids);
        Relation    parent;

        parent = heap_open(parentId, NoLock);
        if (parent->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
            ereport(ERROR,
                    (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
                     errmsg("\"%s\" is not partitioned",
                            RelationGetRelationName(parent))));

        pstate = make_parsestate(NULL);
        pstate->p_sourcetext = queryString;
        //解析处理分区条件,如  from (1) to (10) 转成一个list
        bound = transformPartitionBound(pstate, parent, stmt->partbound);
        //分区范围检查,如果是否和已有分区冲突等
        check_new_partition_bound(relname, parent, bound);
        heap_close(parent, NoLock);

        //保存子分区范围信息,实际上子表已经在上面创建好了,这里把分区范围信息更新就行
        //pg_class中新增加了一列relpartbound存范围信息,这存的是一个就Node转出来的字符串结构
        StorePartitionBound(rel, bound);

        //更新本地立即可见
        CommandCounterIncrement();
    }
    if (stmt->partspec)
    {
        char            strategy;
        ...
        //解析处理分区键,如 partition by range(a),将这个转成一个PartitionSpec Node。
        //这里有一点实现比较奇怪,对于分区类型,list,range,在parser语法解析阶段并不精确确定,存的是一个
        //字符串,进入此函数中再做字符串比较,只允许list,range这两种情况,这为什么不放到gram.y中处理?
        //放gram.y中处理更高效,也更清晰
        stmt->partspec = transformPartitionSpec(rel, stmt->partspec,
                                                &strategy);
        //计算分区键属性值,拿着分区键去pg_attribute 表找全信息                                     
        ComputePartitionAttrs(rel, stmt->partspec->partParams,
                              partattrs, &partexprs, partopclass,
                              partcollation);
        //分区键个数
        partnatts = list_length(stmt->partspec->partParams);
        //把分区信息存到 pg_partitioned_table 表中
        StorePartitionKey(rel, strategy, partnatts, partattrs, partexprs,
                          partopclass, partcollation);

        //如果是分区键,必须设成非空
        if (strategy == PARTITION_STRATEGY_RANGE)
        {
            for (i = 0; i < partnatts; i++)
            {
                AttrNumber  partattno = partattrs[i];
                Form_pg_attribute attform = descriptor->attrs[partattno-1];

                if (partattno != 0 && !attform->attnotnull)
                {
                    /* Add a subcommand to make this one NOT NULL */
                    AlterTableCmd *cmd = makeNode(AlterTableCmd);

                    cmd->subtype = AT_SetNotNull;
                    cmd->name = pstrdup(NameStr(attform->attname));
                    cmds = lappend(cmds, cmd);
                }
            }
            /*
             * Although, there cannot be any partitions yet, we still need to
             * pass true for recurse; ATPrepSetNotNull() complains if we don't
             */
            if (cmds != NIL)
                AlterTableInternal(RelationGetRelid(rel), cmds, true);
        }
    }
    ...
    return address;
}

整体来说,建表逻辑主要增加了对主表的分区键处理逻辑(partition by range 和partition by list)和对子表的分区范围处理逻辑(for values from …to … 和for values in ..)。
分区键相关信息放在pg_partitioned_table系统表中,而分区范围值放在pg_class的relpartbound中,并且建主表时是不知道有多少子表的,也不需要知道,使用时可以动态取到,这对list、range分区表没问题,但是对于hash分区表就不行了,子表数不定就无法确定数据分发规则,因此基于这一套逻辑基本是无法实现hash分区表。

Select 查询篇

当对分区表执行查询时,如果constraint_exclusion 设置为on或者partition时,最终会在relation_excluded_by_constraints 函数中进行约束排除不需要扫描的表,排除逻辑和原来继续表逻辑是一样的,不再赘述。其中关键的区别约束或者分区表的范围来源途径不同,不管是普通表还是分区表,约束都在这里拿到:
constraint_pred = get_relation_constraints(root, rte->relid, rel, true);
在get_relation_constraints函数里面会区别约束和分区表范围,具体请看代码如下:

static List *
get_relation_constraints(PlannerInfo *root,
                         Oid relationObjectId, RelOptInfo *rel,
                         bool include_notnull)
{
    List       *result = NIL;
    Index       varno = rel->relid;
    Relation    relation;
    TupleConstr *constr;
    List        *pcqual;

    relation = heap_open(relationObjectId, NoLock);
    //constr取到约束结构,普通表的
    constr = relation->rd_att->constr;
    //当然上面的约束还不是可直接用的,还需要转换成约束排除接口可用的
    if (constr != NULL)
    {
        //如果这个表有约束则在这里面进行转换计算   
    }

    //如果是分区表,那么下面的pcqual将取到分区范围,实际上这个pcqual取到的就是
    //relation->rd_partcheck,如果这个值不空就直接拿,为空说明是第一次拿,还要从系统表中取一下,
    //从pg_class的relpartbound字段拿到,就是上面建表时存的。
    pcqual = RelationGetPartitionQual(relation, true);
    if (pcqual)
    {
    ...
    //拿到后进行一些简单计算处理
    }
}
//最终返回和约束完全相同的结构,分区范围也是表的一种约束,但与普通约束分开逻辑更加清晰,以后扩展功能也更方便

分区表Insert篇

前面语法篇测insert性能与继续表+触发器实现的分区比有了质的飞跃,性能提升2个数量级,初步猜测执行时增加了计算插入目标分区功能,实际情况如何呢?请看下面ExecInsert插入函数

static TupleTableSlot *
ExecInsert(ModifyTableState *mtstate,
           TupleTableSlot *slot,
           TupleTableSlot *planSlot,
           List *arbiterIndexes,
           OnConflictAction onconflict,
           EState *estate,
           bool canSetTag)
{
    HeapTuple   tuple;
    ResultRelInfo *resultRelInfo;
    ResultRelInfo *saved_resultRelInfo = NULL;
    Relation    resultRelationDesc;
    Oid         newId;
    ...
    //如果有分区进入下面逻辑
    if (mtstate->mt_partition_dispatch_info)
    {
        int     leaf_part_index;
        ...
        //直接用要插入的值slot去找目标分区
        leaf_part_index = ExecFindPartition(resultRelInfo,
                                        mtstate->mt_partition_dispatch_info,
                                            slot,
                                            estate);
        ...
        //mtstate->mt_partitions上存的是所有的分区ResultRelInfo结构,leaf_part_index是代表
        //目标表是第几个,直接跳转换到目标所在的内存取到目标分区表ResultRelInfo结构   
        resultRelInfo = mtstate->mt_partitions + leaf_part_index;
        ...
    }
    //其它基本同原来insert逻辑

int     //找分区
ExecFindPartition(ResultRelInfo *resultRelInfo, PartitionDispatch *pd,
                  TupleTableSlot *slot, EState *estate)
{
    int     result;
    ...
    //找到是第几个分区
    result = get_partition_for_tuple(pd, slot, estate, &failed_at);
    if (result < 0)
    {
        ...
        //负数即分区不存在,报错,要插的值有问题
    }
    //返回要插的分区数
    return result;
}
//get_partition_for_tuple函数中怎么找不再具体介绍,大意是拿着要插的分区键值去和所有的分区条件做比较,以二分法搜索,找到符合的就返回分区编号,如第三个符合则返回3

显然可以看到,在执行时增加了计算插入目标分区的函数,直接确定插入哪个分区,因此插入性能爆涨。

对于update表操作,实际就是select+insert操作,新分区表目前对这块并没做优化,这块不再介绍。

postgresql10分区改进思考

  1. 范围键限制性太强,不管是list分区还是range分区,范围都只能指定简单数值
  2. 分区键指定时带有函数,似乎不能剪枝(还需要仔细测试)
  3. hash分区支持,hash分区在有些场景下实用性很强。
  4. 分区表信息分开存在放,分区子表不一定要是一个完全功能的表,子表可不放pg_class,新建一个分区系统表,与pg_class分开,管理起来更方便。对于分区表使用多的应用场景,也不至于出现\d一下成千上万个表不停的在刷出来。
  5. 动态剪枝,目前来说是不支持分区动态剪枝的,但有些场景,是没办法在计划时确定条件值的,若不能动态调整计划就没办法进行裁剪。以后或者可以允许执行时,再回调某些计划函数,动态调整执行计划。
    以上是基于postgresql10分区当前已经提交代码的一些个人见解和想法,不一定正确,欢迎大家讨论。
二、时间序列的预处理 一般情况下,拿到一个观察值序列之后,首先要对它的平稳性和纯随机性进行检验,这两个重要的检验称为序列的预处理。根据检验的结果可以将序列分为不同的类型,对不同类型的序列我们会采用不同的分析方法。 一、平稳性检验 1、特征统计量 (1)概率分布           数理统计的基础知识告诉我们分布函数或者密度函数能够完整地描述一个随机变量的统计特征。同样,一个堆积变量族{Xt} 的统计特征也完全由... 阅读详情

相关推荐

Visual Studio调试时遇到ntdll.dll的PDB文件缺失?别慌,这3个方法帮你搞定(附详细步骤)

本文详细解析了Visual Studio调试时ntdll.dll的PDB文件缺失问题,提供了3种实用解决方案:调整调试设置、配置符号服务器与缓存目录、手动下载管理符号文件。通过分步指导和高级技巧,帮助开发者快速恢复完整的调试功能,特别适用于Windows平台下的C++项目开发。

weixin_42537919的博客 516

ORA-14501: object is not partitioned

ORA-14501: object is not partitioned alter table tap_so_station_spacing add partition tap_so_station_spacing0   values  less than (TO_DATE(' 2014-08-16 00:00:00', 'YYYY-MM-DD HH24:MI:SS', 'NLS_CALE

amberinheart的专栏 2万+

四旋翼滑模控制:从理论到Simulink实现

本文深入探讨了四旋翼滑模控制的理论与Simulink实现,对比了其相较于PID控制在抗干扰和鲁棒性方面的显著优势。文章详细介绍了从精确动力学建模、内外环滑模控制器设计到指令解算的完整流程,并分享了Simulink仿真搭建、参数调试及抗扰性能验证的实战经验,帮助工程师掌握在复杂环境下提升四旋翼稳定性的进阶技能。

weixin_29163581的博客 375

PostgreSQL 11 新特性之跨分区移动数据

PostgreSQL 11 新特性之跨分区移动数据

weixin_44519342的博客 1025

飞牛NAS1.1.19更新后SSH连不上?两步快速恢复连接

飞牛NAS1.1.19版本更新后SSH无法连接😟,实则是官方安全策略升级默认关闭了该服务,本文分享两步实操配置恢复SSH连接💻,还同步了官方的设备安全配置建议,帮助用户兼顾使用与设备防护🔒。

XiaoqiangClub的博客 2114

uniapp(微信小程序) - 实现uniapp小程序电子签名板,横屏手写姓名签名专用写字画板,客户专用签字板,手写签名签字画板组件,手写签名,(详细运行示例,一键复制开箱即用)

uniapp实现微信小程序的电子签名效果,uniapp微信小程序 电子签名 写字板,用uniapp实现微信小程序的电子签名,uniapp微信小程序canvans实现手写签名,记录一个uniapp写的小程序的手写板,横屏,用于签名,unniapp实现电子签名,uniapp怎么做横屏的手写签名,uniapp如何实现签字板,uniapp小程序平台下怎么做电子签名板组件,uniapp电子签名插件,uniapp最好用的电子写字板插件组件源码,实现uniapp 手写签名功能,手写板、签字板,uni-app插件市场最好的

✅ 精选推荐文章 1万+

FAILED:ValidationFailureSemanticException table is not partitioned but partition spec exists;{age=1}

2.重新新建一个数据表继续,原因是容错性较低,再来一次就可以...在为数据表usr1增加新的分区时出现以上报错(注意以上并不是报错。1.将分区进行改变 但还是存在问题。

weixin_60021669的博客 4088

【AI大模型应用开发】3.2 RAG实战 - RAG应用+UI实现加载本地文件并对话

前面我们实现了RAG基本流程,今天我们在此基础上给它加个UI界面,在浏览器打开,实现上传本地文件、解析,并用大模型实现与该文档的对话。也就是一个阉割版demo版的ChatPDF或知识库对话系统。

同学小张的博客 3056

Postgresql - Table Partitioning (二)

官档里对分区表的说明,继续划重点啦。#######################################################PostgreSQL offers a way to specify how to divide a table into pieces called partitions. The table that is divided is referred t...

chuckchen1222的博客 893

Altium Designer20 PCB板子绘制

版层 top layer 顶层 bottom layer 底层 设置板子大小 布局 过孔 top layer层连到bottom layer层。通过via连接。 设置线宽 在design rules里可以设置线宽。 右键new rule 可以新增线宽,电源设置成20mil 还可以设置很多其他的线宽 修改安全距离 安全距离太短容易出现毛刺,然后短路,一般设置成20mil 修改元器件旋转角度 修改rotation step,每次旋转45°。 修改光标大小 下面的cursor type是修改光标

及时总结 2582

25 - GRACE Mascon数据缺失月份数据插值

本篇基于文章的相关内容和文章公开的奇异谱分析(SSA)插值算法程序包,对grace Mascon数据种缺失的月份进行 插值 ,总结了使用程序包的相关过程,可以将该程序应用到其他数据处理插值计算中。

weixin_43339605的博客 1843

表分区简介1

目录 文档用途 详细信息 文档用途 了解Postgresql表分区 详细信息 Postgreslq内核中支持表分区(Table partitioning)包括:范围(range)、列表(list) PostgreSQL offers built-in support for the following forms of partitioning: Range Partitio...

HighGO 933

STM32实战开发(121):基于STM32的智能电导率检测系统设计与实现

STM32系列是由意法半导体(STMicroelectronics)推出的32位微控制器。STM32系列基于ARM Cortex-M处理器架构,具有高性能、低功耗、丰富的外设接口和强大的开发支持库。STM32的低功耗特性使其在嵌入式系统中非常受欢迎,特别是在便携式设备和传感器网络中。

2201_76125393的博客 1042

PostgreSQL 10 分区表探密

PostgreSQL 10 分区表探密 分区介绍 PostgreSQL的分区需要先建立主表,然后再建立子表,使用继承的特性,但不需要手动写触发器/规则了,目前支持range、list分区,10 正式版发布时不知道会不会有其他的,后面我会介绍我基于10 Beta2添加的has...

551

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

在类中用class时数据是共有还是私有_平安科技PostgreSQL案例分享|进程私有内存探密...

作者介绍石勇虎,平安科技资深数据库工程师,多年PostgreSQL数据库相关开发运维经验。2016年加入平安,一直致力于PostgreSQL数据库运维管理工作。对其他数据库产品也有一定的涉猎。背景介绍近期遇到一个案例,一个分析型应用的DB在早上的时候出现OOM,OS kill用户进程,数据库进入recovery,几十秒后数据库恢复。OS报错如下:1.#cat/var/log/...

weixin_39614704的博客 296

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

PostgreSQL分区表的创建和使用

pg支持两种分区表技术:表继承配合触发器实现分区、原生分区。 在pg9中只支持继承的方式实现分区表; pg10开始支持原生分区,但只支持range分区和list分区; pg11新增支持hash分区,支持默认分区等; pg12对于分区表的性能有有了极大的提升。 表继承的语法为: CREATE TABLE table_kid INHERITS(table_parents); 由于这种方法实现分区表效率很低,且现在也很少使用,所以就不过多介绍,这里主要介绍原生分区表技术。 目前内置的原生分区表支持:rang

Focus on PostgreSQL 4229

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

数据库报错没有分区Table has no partition for value,创建表分区失败

没有定义20231001以后的日期分区。

weixin_43751881的博客 3681

ORA-14501: object is not partitioned ORA-06512: at "KPI.P_KPI_M_2G_WASRATE_GRID", line 54

ORA-14501: object is not partitioned  ORA-06512: at "KPI.P_KPI_M_2G_WASRATE_GRID", line 54

雾里看花5566的专栏 4327

PostgreSQL 10 HASH分区实现

PostgreSQL 10 HASH分区 前面简单说明了基于pg10实现hash分区的使用语法,下面介绍参照range/list分区实现的hash分区。 注意:由于本人水平限制,难免会有遗漏及错误的地方,不保证正确性,并且是个人见解,发现问题欢迎留言指正。 思路 语法尽...

1053

PostgreSQL (PG) 高效分区表实现 - pg_pathman(1)

PG分区在数据应用中十分广泛。 背景 目前PostgreSQL社区版本的分区表功能比较弱,需要通过继承和触发器或RULE来实现分区表的功能,由于查询和更新涉及约束的检查、插入则涉及触发器或规则重写,导致分区功能性能较差。 商业版本EDB,以及数据仓库Greenplum都有比较好的分区支持。 去年GP开源后,阿里云RDS PostgreSQL的小伙伴将Greenplum的分区表功能por...

jiangbb8686的博客 3009

SQL NOT--PARTITION

USE DB3 GO --Create partition function CREATE PARTITION FUNCTION PF_Test(INT) AS RANGE LEFT FOR VALUES (1000,2000) GO --Create patition schema CREATE PARTITION SCHEME PS_Test AS PARTITION PF...

weixin_34365417的博客 163

PostgreSQL 分区表探索(pg_pathman)数据库优化

PostgreSQL 分区表探索(pg_pathman)使用场景分区概念说明安装RANGE分区结语 使用场景 许多系统在在使用几年之后数据量不断膨胀,这个时候单表数据量超过2000w+,数据库的查询也越来越慢,而随着时间的推移许多历史数据的重要性可能逐渐下降。这时候就可以考虑使用分区表来将冷热数据分区存储。 常用的使用场景比如sql分析的日志记录,常用的分区字段有按照创建时间、省份、以及业务类型,具体使用需要结合需求 Postgresql官方的建议是单表大小超过了服务器内存大小可以考虑分区(大概的了解了下按

秋̶᭄ꦿ攻城狮࿆ྂ 1405

mysql 分区功能介绍及实例

一,什么是数据库分区 前段时间写过一篇关于mysql分表的的文章,下面来说一下什么是数据库分区,以mysql为例。mysql数据库中的数据是以文件的形势存在磁盘上的,默认放在/mysql/data下面(可以通过my.cnf中的datadir来查看),一张表主要对应着三个文件,一个是frm存放表结构的,一个是myd存放表数据的,一个是myi存表索引的。如果一张表的数据量太大的话,那么myd,myi...

翻过这座山,就到菩提洞了 641
上一篇: postgresql 内核开发之系统参数增加(下)
下一篇: Postgresql 10 HASH分区实现
postgres20
博客等级 码龄10年 100粉丝 25原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值