探囊取物——理解,诊断,修复Greenplum中的Opclass和Opfamily相关问题

我已经不止一次有这样的感觉了: 当遇到一个特性类型的bug后,短时间内会频繁遇到本质一样的bug。这次是Greenplum关于OpclassOpfamily的话题。这种模式不是偶然,我有一个猜想来解释:遇到第一个问题的时候,如果能够深入仔细的研究思考,由点及面,然后这部分储备的知识和思维的训练,让你更容易从代码和issue里看出同类问题。

本文介绍Greenplum Database里的OpclassOpfamily相关概念,然后列出Greenplum开源社区仓库 greenplum-db/gpdb 里一些关于这个话题的issue并逐一诊断分析。各小节独立自成一个话题,小节之间可能会显得跳跃。不同的参考资料也将散落在各个小节内部。Issue修复的过程是动态的,我会把在开源社区出现过的东西online session里讨论清楚并公布录屏。

Dynamic Scoping & Static Scoping

上来就是一个看似偏离主旨的话题,这个话题是程序语言里很经典的问题。本质是如何确定某个表达式里自由变量的值。一个函数体里的自由变量的值,要去一个环境里找,到底应该用函数定义时候的那个环境,还是函数调用时候的环境呢?如果选择后者,则实现的是Dynamic Scope;如果选择前者,则实现的是Static Scope。历史上,人们曾经自然而然的选择了Dynamic Scope,诞生了这个著名的”bug” (参考: SICP Video: 7B: Metacircular Evaluator, Part 2):

A famous “bug”

因此Static Scope要求函数定义产生的结构必须包含定义时的environment,整个函数定义加上envrionment一起,称之为closure

Greenplum Database创建的索引、分区、表的分布式信息等,在使用的时候(索引: index scan,分区: partition pruning,分布式信息: direct dispatch, join motion)会涉及到需要相关的算子(operator),这些算子不能在使用的时候根据名称去解析搜索,必须溯源数据库对象创建时保存的信息。这些从某种意义上看,挺像Static ScopingDynamic Scoping问题,后面我会介绍具体的Github Issue。

推荐阅读:

用Ocaml实现一个Binary Search Tree

open Base

module type Comparable = sig
  type t
  val compare : t -> t -> int
end

module type Bst = sig
  type element
  type bst

  val create : bst
  val insert : bst -> element -> bst
  val search : bst -> element -> element option
  val min : bst -> element option
end

module Make_Bst(Element : Comparable)
  : (Bst with type element = Element.t) = struct

  type element = Element.t
  type bst = | Node of bst * element * bst
             | Empty

  let create = Empty

  let rec insert tree v =
    match tree with
    | Empty -> Node (Empty, v, Empty)
    | Node (left, v1, right) ->
      if Element.compare v v1 < 0 then
        Node (insert left v, v1, right)
      else
        Node (left, v1, insert right v)

  let rec search tree v =
    match tree with
    | Empty -> None
    | Node (left, v1, right) ->
      let r = Element.compare v v1 in
      if r = 0 then Some v
      else if r < 0 then search left v
      else search right v

  let rec min tree =
    match tree with
    | Empty -> None
    | Node (Empty, v, _) -> Some v
    | Node (left, v, _) -> min left

end

这里Make_Bst这个Functor接收一个提供比较大小的module生成特定类型的Bst模块,这个模块只保证构建的Binary Search Tree满足Comparable模块规定的全序。看下面的测试程序:

module AbsCmp = struct
  type t = int
  let compare a b = Int.compare (Int.abs a) (Int.abs b)
end

module Bst = Make_Bst (AbsCmp)

open Bst

let tr = insert (insert create (-5)) 2 in
min tr

用户使用了绝对值后的大小比较,那么Bst模块能够提供的顺序也是基于此,用户自己在开发程序的时候需要意识到这点。

这里做一个类比:数据库用户创建分区表时候,指定了分区键的opclass,假设也是需要绝对值后的整数比较,那么用户后续再编写SQL查询这个分区表的时候,要铭记这个分区(数据结构)用的opclass (类比comparable module),这样才能利用分区表 (数据结构) 提供的功能: 剪枝 (Bst.min)。后面我们再用更多的Greenplum Database开源社区的issue来理解。

推荐阅读:

opclass和opfamily简介

最典型的应用场景是索引。索引就是某种数据结构,如同上面的Ocaml实现的BST例子。索引需要支持多种数据类型,且具有扩展性。扩展性在这里的含义是,用户可以定义索引数据结构里的关键算子,如btree index需要对元素大小进行比较,hash index则需要对数据计算hash valueGreenplum (Postgres) 扩展了SQL语法,在创建索引时,用户可以指定opclass,从而指定相关数据结构的方法。下面是PostgresCreate Index文档里对opclass的描述:

An operator class with optional parameters can be specified for each column of an index. The operator class identifies the operators to be used by the index for that column. For example, a B-tree index on four-byte integers would use the int4_ops class; this operator class includes comparison functions for four-byte integers. In practice the default operator class for the column’s data type is usually sufficient. The main point of having operator classes is that for some data types, there could be more than one meaningful ordering. For example, we might want to sort a complex-number data type either by absolute value or by real part. We could do this by defining two operator classes for the data type and then selecting the proper class when creating an index. More information about operator classes is in Section 11.10 and in Section 38.16.

Postgres Document

因此,对于不同的数据结构 (Access Method),创建一个opclass就需要对某一个类型指定对应的算子(operator)和方法 (support function)。opclass可以看成是一些基础元信息的合集:

  • pg_type: 类型的元信息
  • pg_operator: 算子的元信息, 其中有一列oprcode关联到pg_proc
  • pg_proc: 函数的元信息

Greenplum (Postgres)create operator class语法创建一个新的class自定义hash运算。

create function int4_abs_eq(a int4, b int4) returns boolean as
$$
return a == b
$$
language plpython3u;

create operator |=|
(
  leftarg = int4,
  rightarg = int4,
  procedure = int4_abs_eq,
  hashes, merges
);

create function int4_abs_hash(a int4) returns int4 as
$$
return hash(a)
$$
language plpython3u;

create operator class abs_int4_ops for type int4
  using hash as
  operator 1 |=|,
  function 1 int4_abs_hash(int4);

Hash AM下要求一个opclass提供对等值的理解以及一个计算hash value的函数。并且有一个至关重要的推论:

\text{Given an opclass under hash AM } opc(=, f_{hash}, type), \forall a, b : type, a = b \implies f_{hash}(a) = f_{hash}(b)

上面的推论是优化器能够作一些优化的基础。其逆否命题也成立。从opclass扩充到opfamily的过程较为自然,即opclass只限定在一种类型,如果某一类类型都有同样的某种属性的时候,需要类似的处理交叉类型,方便优化器处理查询的时候,就需要opfamily。简而言之,opclass处理单类型,opfamily可以包含交叉类型。可以参考 Issue 14982 里的例子如果构建包含int4, int8的绝对值后比较的btree AM的一个opfamily

自定义opclass或者opfamily就如同是前面章节案例中编写OcamlModule一样。

推荐阅读:

opclassopfamily在分区表中应用的理论基础

Greenplum 7的分区表支持三种模式: list partition, range partition, hash partition。其中前两个也是Greenplum 6支持的。Hash Partition和分布式有些类似,理论基础一致,我们可以在下一个小节讨论。本小节我们重点讨论 list partition, range partition这两个opclass必须是btree AM下的分区裁剪问题。

分区表的裁剪是数据库里性能至关重要的一环。分为动态裁剪和静态裁剪。静态裁剪是指在查询计划生成阶段,根据谓词条件 (一般是partkey < = > const 这样的表达式) 裁剪掉不需要扫描的分区。动态裁剪在Greenplum里,指的是两个表的join操作,外表涉及分区表的扫描,内表查询计划需要率先全部扫描结束的情况下,可以在开始扫描外表的时候,就知道哪些分区会涉及。下面的例子展示Greenplum里的静态分区裁剪和动态分区裁剪:

create table t (a int, b int) partition by list (b) distributed by (a);
create table t1 partition of t for values in (1);
create table t2 partition of t for values in (2);
create table t3 partition of t for values in (3);

-- 静态裁剪分区示例
explain (costs off) select * from t where b < 3;
                QUERY PLAN
------------------------------------------
 Gather Motion 2:1  (slice1; segments: 2)
   ->  Append
         ->  Seq Scan on t1
               Filter: (b < 3)
         ->  Seq Scan on t2
               Filter: (b < 3)
 Optimizer: Postgres query optimizer

-- 动态裁剪分区示例
create table test (a int, b int);
insert into test values (1,1), (2,2);
explain (costs off, analyze) select * from t , test where t.b = test.b;
                                                   QUERY PLAN
-----------------------------------------------------------------------------------------------------------------
 Gather Motion 2:1  (slice1; segments: 2) (actual time=5.397..5.401 rows=0 loops=1)
   ->  Hash Join (actual time=0.000..4.723 rows=0 loops=1)
         Hash Cond: (t1.b = test.b)
         ->  Append (actual time=0.000..0.076 rows=0 loops=1)
               Partition Selectors: $0
               ->  Seq Scan on t1 (actual time=0.000..0.036 rows=0 loops=1)
               ->  Seq Scan on t2 (actual time=0.000..0.036 rows=0 loops=1)
               ->  Seq Scan on t3 (never executed)
         ->  Hash (actual time=0.032..0.034 rows=2 loops=1)
               Buckets: 524288  Batches: 1  Memory Usage: 4097kB
               ->  Partition Selector (selector id: $0) (actual time=0.021..0.026 rows=2 loops=1)
                     ->  Broadcast Motion 2:2  (slice2; segments: 2) (actual time=0.010..0.013 rows=2 loops=1)
                           ->  Seq Scan on test (actual time=0.018..0.019 rows=1 loops=1)
 Optimizer: Postgres query optimizer

着重提一下上面动态分区裁剪的计划,上面的计划用explain analyze展示可以看出执行过程中,t3分区被运行时裁剪了。这是一个Hash Joinplan,内表查询计划会提前全量扫描生成hash table,再根据join条件产生的等价关系,故而可以动态裁剪。下面我们开始讨论动态裁剪的等价类和分区表需要满足什么关系。问题被抽象成:给定分区表的定义和等价类,(假设其他动态裁剪的条件都满足),判断是否可以生成动态剪裁分区的查询计划。为了讨论准确,我们先规定一些符号,同时为了讨论简单,我们只考虑只有一个分区键:

  • =_{opf} 表示在某opfamily内的等号运算符 (具体例子里会有类型信息), 其它< >类似
  • =_{opc} 表示在某opclass内的等号运算符,其它< >类似
  • list partition: \{k_p, opc, [k_p =_{opc} c_1, k_p =_{opc} c_2 \dots]\}
  • range partition: \{k_p, opc, [c_1 <_{opc} k_p <_{opc} c_2, c_3 < k_p <_{opc} c_4, \dots ]\}
  • 等价类: \{opf, [e_1, e_2, e_3, \dots]\}
\forall a : t1, b:t2, a =_{opf} b \implies a >_{opf} c \implies b >_{opf} c

对其它的运算符同样也有上面的逻辑命题成立。因此想要传递等价关系用来推理分区表的裁剪,必须等价类的opfamily和分区表分区的opclass对应的opfamily一致。

opclassopfamily在分布式表中应用的理论基础

有了上面的介绍,这部分的核心内容也就比较简单了。简而言之,想要对分布式表的分布信息做推理:direct dispatch或者判断是否co-locate,也必须满足:

  • direct dispatch推理: 等价关系的opfamilydistky的必须一致
  • co-locate推理: distkeysopfamily必须一致

Issue 14941: GPDB failed to retrieve operator when inserting data into partitioned table

Issue link: https://github.com/greenplum-db/gpdb/issues/14941

这个bug只存在Greenplum 6和历史版本中,GreenplumPostgres更早实现分区表,自从Greenplum 7开始,分区表融合了上游Postgres的实现。这里的问题发生在插入执行器的代码里,需要决定数据的最终目的地时候。Debug的时候,我们可以用rr录制完整的执行过程,也可以根据报错日志的代码位置,直接阅读代码就可以诊断。

这个issue是用户自己创建了自定义类型,这个自定义类型是可以创建在任何schema下的。而Greenplum 6range_test部分的代码,使用了hard-codepg_catalog这个schema下去搜索需要的运算符。当然就找不到了。修复的核心是如何找到需要的运算符,这需要根据分区表定义时候存下来的opclass

看见一角的时候,就要想象全貌。顺着这块代码的所有可能的调用路径,思考修复的方案是否完善,以及能否挖出更多的问题。思考后发现:

  • 如果是insert产生的调用路径,因为最终要把数据插入到磁盘里,因此最终的tuple会进行一次类型转换和目标表的定义一致。这时候不存在跨类型的比较。而且这里直接了当,修了就没问题了。
  • 如果是动态裁剪产生的调用路径,首先得保证查询计划是正确的。也就是产生这类计划的其中一个前提是等价类的family要和分区表的一致。快速去检查一下优化器的代码,没有发现,然后构造一个测试,找出来很多问题,orca,以及Greenplum 6planner都有问题,后面的issue再详细提。

Issue 14887: Wrong result when direct dispatch is enabled

Issue link: https://github.com/greenplum-db/gpdb/issues/14887

我在issue本身的页面有诊断类比和讨论,这里不赘述。简单的从planner的角度探讨下direct dispatch的流程和issue的修复。针对谓词进行direct dispatch的优化planner会判断是否为var=const这类表达式并且var是分布键。这里能进行direct dispatch (等价于hash partition pruning)是需要计算出分布键的hash值,给定等价关系要从常数表达式的hash值推理出分布键的,必须满足此处的=必须属于分布键的family。而PlannerORCA之前都忽略了这一点。

Issue 14982: Wrong result: dynamic partition pruning does not consider opclass of partition key

Issue link: https://github.com/greenplum-db/gpdb/issues/14982

这个问题就是在分析issue 14941的时候思考发现的。这里还是着重讨论下,Greenplum 6planner里面是如何生成这样的查询计划的。在需要展开分区表的时候,就考虑到未来可能会进行动态分区裁剪,故而在函数expand_inherited_rtentry执行过程中 (函数调用链是: expand_inherited_rtentry <-- expand_inherited_tables <-- subquery_planner ) 会构建相应的数据结构,并存在优化器的大上下文root->dynamicScans里:

	if (parent_is_partitioned)
	{
		DynamicScanInfo *dsinfo;

		dsinfo = palloc(sizeof(DynamicScanInfo));
		dsinfo->parentOid = parentOID;
		dsinfo->rtindex = rti;
		dsinfo->hasSelector = false;

		dsinfo->children = child_relids;

		dsinfo->partKeyAttnos = rel_partition_key_attrs(parentOID);

		root->dynamicScans = lappend(root->dynamicScans, dsinfo);
		dsinfo->dynamicScanId = list_length(root->dynamicScans);
	}

在优化器处理过程中,所有的等价类信息都在上下文里存储root->eq_classes。基于root->eq_classesroot->dynamicScansGreenplum 6planner会在构建join plan的时候尝试是否可以加入动态分区裁剪: inject_partition_selectors_for_join <-- create_join_plan。很遗憾的是,在这里忽略的一个重要的大前提,即等价类的family必须和分区表的一致,才可以用这个技术。

PR 14986 Fix incorrect result from hash join on char column

PR link: https://github.com/greenplum-db/gpdb/pull/14986

这个PR是修复ORCA错误的判断了join的左右子plan是co-locate,从而导致没有加motion得到了错误结果。因此这个问题的核心在于给定了两个分布键和一个等价类,判断是否co-locate。

两组分布键能够co-locate的大前提就是,分布键所属于的opfamily必须一致。只有这样才可以基于等价关系进行hash value等值的推理。

Issue 8918: ERROR: could not find hash function for type 1184 in operator family 2040 (cdbhash.c:438)

Issue link: https://github.com/greenplum-db/gpdb/issues/8918

这部分有一个之前写的blog解释一些原理,可以参考Understand distkey in Greenplum。这里我们直接就issue里的问题,进行复盘诊断。

这里有一个谓词的表达式是b=timestamptz '2016/11/09',注意到b的类型是timestamp,因此这里的等号运算符和属于的opfamily如下:

gpadmin=# select oid, typname from pg_type where typname like 'timestamp%';
 oid  |   typname
------+-------------
 1114 | timestamp
 1184 | timestamptz
(2 rows)

gpadmin=# select oid, oprname from pg_operator where oprleft = 1114 and oprright = 1184 and oprname = '=';
 oid  | oprname
------+---------
 2536 | =
(1 row)

gpadmin=# select * from pg_amop where amopmethod = (select oid from pg_am where amname = 'btree') and amopopr = 2536;
  oid  | amopfamily | amoplefttype | amoprighttype | amopstrategy | amoppurpose | amopopr | amopmethod | amopsortfamily
-------+------------+--------------+---------------+--------------+-------------+---------+------------+----------------
 10368 |        434 |         1114 |          1184 |            3 | s           |    2536 |        403 |              0
(1 row)

gpadmin=# select * from pg_opfamily where oid = 434;
 oid | opfmethod |   opfname    | opfnamespace | opfowner
-----+-----------+--------------+--------------+----------
 434 |       403 | datetime_ops |           11 |       10
(1 row)

因此这里的=是属于datetime_ops这个btree family的等号运算符。在进行两边的join path是否需要加motion的判断时候,等价类和分布键信息如下:

-- outer path locus
CdbPathLocus [locustype=CdbLocusType_Hashed numsegments=2]
        [distkey]
                DistributionKey [dk_opfamily=1977]
                        [dk_eclasses]
                                EquivalenceClass [ec_collation=0 ec_relids=0x00000006 ec_has_const=false ec_has_volatile=false ec_below_outer_join=false ec_broken=false ec_sortref=0 ec_min_security=0
                                                  ec_max_security=0]
                                        [ec_opfamilies] OidList: [1976]
                                        [ec_members]
                                                EquivalenceMember [em_relids=0x00000002 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=23]
                                                        [em_expr] Var [varno=1 varattno=1 vartype=23 varnoold=1 varoattno=1]
                                                EquivalenceMember [em_relids=0x00000004 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=23]
                                                        [em_expr] Var [varno=2 varattno=1 vartype=23 varnoold=2 varoattno=1]
                                        [ec_sources] (List *)0x5605e9897d90
                                        [ec_derives] (List *)0x5605e98998b0
                DistributionKey [dk_opfamily=2040]
                        [dk_eclasses]
                                EquivalenceClass [ec_collation=0 ec_relids=0x00000002 ec_has_const=true ec_has_volatile=false ec_below_outer_join=false ec_broken=false ec_sortref=0 ec_min_security=0
                                                  ec_max_security=0]
                                        [ec_opfamilies] OidList: [434]
                                        [ec_members]
                                                EquivalenceMember [em_relids=0x00000002 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=1114]
                                                        [em_expr] Var [varno=1 varattno=2 vartype=1114 varnoold=1 varoattno=2]
                                                EquivalenceMember [em_relids=0x0 em_nullable_relids=0x0 em_is_const=true em_is_child=false em_datatype=1184]
                                                        [em_expr] Const [consttype=1184 constlen=8 constvalue=531964800000000 constisnull=false constbyval=true]
                                        [ec_sources] (List *)0x5605e9898068

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

-- inner path locus
CdbPathLocus [locustype=CdbLocusType_Hashed numsegments=2]
        [distkey]
                DistributionKey [dk_opfamily=1977]
                        [dk_eclasses]
                                EquivalenceClass [ec_collation=0 ec_relids=0x00000006 ec_has_const=false ec_has_volatile=false ec_below_outer_join=false ec_broken=false ec_sortref=0 ec_min_security=0
                                                  ec_max_security=0]
                                        [ec_opfamilies] OidList: [1976]
                                        [ec_members]
                                                EquivalenceMember [em_relids=0x00000002 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=23]
                                                        [em_expr] Var [varno=1 varattno=1 vartype=23 varnoold=1 varoattno=1]
                                                EquivalenceMember [em_relids=0x00000004 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=23]
                                                        [em_expr] Var [varno=2 varattno=1 vartype=23 varnoold=2 varoattno=1]
                                        [ec_sources] (List *)0x5605e9897d90
                                        [ec_derives] (List *)0x5605e98998b0
                DistributionKey [dk_opfamily=2040]
                        [dk_eclasses]
                                EquivalenceClass [ec_collation=0 ec_relids=0x00000004 ec_has_const=false ec_has_volatile=false ec_below_outer_join=false ec_broken=false ec_sortref=0 ec_min_security=4294967295
                                                  ec_max_security=0]
                                        [ec_opfamilies] OidList: [434]
                                        [ec_members]
                                                EquivalenceMember [em_relids=0x00000004 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=1114]
                                                        [em_expr] Var [varno=2 varattno=2 vartype=1114 varnoold=2 varoattno=2]

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

-- eqclass information

(gdb) pgprint root->eq_classes
EquivalenceClass [ec_collation=0 ec_relids=0x00000006 ec_has_const=false ec_has_volatile=false ec_below_outer_join=false ec_broken=false ec_sortref=0 ec_min_security=0
                  ec_max_security=0]
        [ec_opfamilies] OidList: [1976]
        [ec_members]
                EquivalenceMember [em_relids=0x00000002 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=23]
                        [em_expr] Var [varno=1 varattno=1 vartype=23 varnoold=1 varoattno=1]
                EquivalenceMember [em_relids=0x00000004 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=23]
                        [em_expr] Var [varno=2 varattno=1 vartype=23 varnoold=2 varoattno=1]
        [ec_sources] (List *)0x5605e9897d90
        [ec_derives] (List *)0x5605e98998b0
EquivalenceClass [ec_collation=0 ec_relids=0x00000002 ec_has_const=true ec_has_volatile=false ec_below_outer_join=false ec_broken=false ec_sortref=0 ec_min_security=0
                  ec_max_security=0]
        [ec_opfamilies] OidList: [434]
        [ec_members]
                EquivalenceMember [em_relids=0x00000002 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=1114]
                        [em_expr] Var [varno=1 varattno=2 vartype=1114 varnoold=1 varoattno=2]
                EquivalenceMember [em_relids=0x0 em_nullable_relids=0x0 em_is_const=true em_is_child=false em_datatype=1184]
                        [em_expr] Const [consttype=1184 constlen=8 constvalue=531964800000000 constisnull=false constbyval=true]
        [ec_sources] (List *)0x5605e9898068
EquivalenceClass [ec_collation=0 ec_relids=0x00000004 ec_has_const=false ec_has_volatile=false ec_below_outer_join=false ec_broken=false ec_sortref=0 ec_min_security=4294967295
                  ec_max_security=0]
        [ec_opfamilies] OidList: [434]
        [ec_members]
                EquivalenceMember [em_relids=0x00000004 em_nullable_relids=0x0 em_is_const=false em_is_child=false em_datatype=1114]
                        [em_expr] Var [varno=2 varattno=2 vartype=1114 varnoold=2 varoattno=2]

我们在试图通过motion调节bar的locus去匹配foo的时候,必须牢记判断coloate的一个大前提,即等价类的family关于等号运算符必须也得在分布键的hash family里:

gpadmin=# select * from pg_amop where amopfamily = 434 and amopstrategy = 3 and amoplefttype = 1114 and amoprighttype = 1184;
  oid  | amopfamily | amoplefttype | amoprighttype | amopstrategy | amoppurpose | amopopr | amopmethod | amopsortfamily
-------+------------+--------------+---------------+--------------+-------------+---------+------------+----------------
 10368 |        434 |         1114 |          1184 |            3 | s           |    2536 |        403 |              0
(1 row)

gpadmin=# select * from pg_amop where amopopr = 2536 and amopmethod = 405;
 oid | amopfamily | amoplefttype | amoprighttype | amopstrategy | amoppurpose | amopopr | amopmethod | amopsortfamily
-----+------------+--------------+---------------+--------------+-------------+---------+------------+----------------
(0 rows)

上面的结果告诉我们这个条件不满足。因此不能使用只加一个redistributed motion的策略。改下数据类型,就可以实现这个策略:

gpadmin=# EXPLAIN
SELECT a, b
FROM foo JOIN bar ON a = c AND b = timestamptz '2016/11/09';
                                         QUERY PLAN
--------------------------------------------------------------------------------------------
 Gather Motion 2:1  (slice1; segments: 2)  (cost=1.22..2.74 rows=12 width=12)
   ->  Hash Join  (cost=1.22..2.56 rows=6 width=12)
         Hash Cond: (bar.c = foo.a)
         ->  Broadcast Motion 2:2  (slice2; segments: 2)  (cost=0.00..1.24 rows=12 width=4)
               ->  Seq Scan on bar  (cost=0.00..1.06 rows=6 width=4)
         ->  Hash  (cost=1.14..1.14 rows=6 width=12)
               ->  Seq Scan on foo  (cost=0.00..1.14 rows=6 width=12)
                     Filter: (b = '2016-11-09 00:00:00+00'::timestamp with time zone)
 Optimizer: Postgres query optimizer
(9 rows)

gpadmin=# EXPLAIN
SELECT a, b
FROM foo JOIN bar ON a = c AND b = timestamp '2016/11/09';
                                          QUERY PLAN
----------------------------------------------------------------------------------------------
 Gather Motion 2:1  (slice1; segments: 2)  (cost=1.22..2.66 rows=12 width=12)
   ->  Hash Join  (cost=1.22..2.48 rows=6 width=12)
         Hash Cond: (bar.c = foo.a)
         ->  Redistribute Motion 2:2  (slice2; segments: 2)  (cost=0.00..1.18 rows=6 width=4)
               Hash Key: bar.c, '2016-11-09 00:00:00'::timestamp without time zone
               ->  Seq Scan on bar  (cost=0.00..1.06 rows=6 width=4)
         ->  Hash  (cost=1.14..1.14 rows=6 width=12)
               ->  Seq Scan on foo  (cost=0.00..1.14 rows=6 width=12)
                     Filter: (b = '2016-11-09 00:00:00'::timestamp without time zone)
 Optimizer: Postgres query optimizer
(10 rows)

Issue 6971: Unique index can be violated, if it uses a non-default opclass.

Issue link: https://github.com/greenplum-db/gpdb/issues/6971

Greenplum里的唯一索引是在每个segment上给表构建索引,当前的MPP架构,是在每个segment局部保证唯一性,然后唯一索引的唯一是全局的约束,因此,要构建唯一索引,必须满足:当开始检测唯一性的时候,已经具备了全部决策数据在哪一个segment的信息。这就意味着在Greenplum里对一个表构建唯一索引:

  • replicated table
  • hash distributed table, 并且分区键是分布键的超集

唯一索引是btree的access method,而分布键用的是hash access method,判断唯一行的等号运算符,必须和hash分布的等号运算符是一模一样的(大家对等号的理解一致)才行。

顺藤摸瓜,联想到Postgres有Hash Partition,这和分布非常类似,估计Postgres也没有考虑到,果然。让朋友(彼时还是刚刚开始看Postgres)去给pg社区刷了一个commit: https://www.postgresql.org/message-id/flat/4731.1585155636%40sss.pgh.pa.us#7dc100f9e568f5662614451d032d9904

Issue 6796: Wrong results with user defined opclass

Issue link: https://github.com/greenplum-db/gpdb/issues/6796

这里不赘述,留给有兴趣的读者自行探索。

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.