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

因此Static Scope
要求函数定义产生的结构必须包含定义时的environment
,整个函数定义加上envrionment
一起,称之为closure
。
Greenplum Database创建的索引、分区、表的分布式信息等,在使用的时候(索引: index scan
,分区: partition pruning
,分布式信息: direct dispatch, join motion
)会涉及到需要相关的算子(operator
),这些算子不能在使用的时候根据名称去解析搜索,必须溯源数据库对象创建时保存的信息。这些从某种意义上看,挺像Static Scoping
和Dynamic 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
来理解。
推荐阅读:
- Real World OCaml FUNCTIONAL PROGRAMMING FOR THE MASSES 2nd Edition (Oct 2022)
- Essentials of Programming Languages: 3rd edition
opclass和opfamily简介
最典型的应用场景是索引。索引就是某种数据结构,如同上面的Ocaml
实现的BST
例子。索引需要支持多种数据类型,且具有扩展性。扩展性在这里的含义是,用户可以定义索引数据结构里的关键算子,如btree index
需要对元素大小进行比较,hash index
则需要对数据计算hash value
。Greenplum
(Postgres
) 扩展了SQL
语法,在创建索引时,用户可以指定opclass
,从而指定相关数据结构的方法。下面是Postgres
的Create 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
Postgres Documentint4_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.
因此,对于不同的数据结构 (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
的函数。并且有一个至关重要的推论:
上面的推论是优化器能够作一些优化的基础。其逆否命题也成立。从opclass
扩充到opfamily
的过程较为自然,即opclass
只限定在一种类型,如果某一类类型都有同样的某种属性的时候,需要类似的处理交叉类型,方便优化器处理查询的时候,就需要opfamily
。简而言之,opclass
处理单类型,opfamily
可以包含交叉类型。可以参考 Issue 14982 里的例子如果构建包含int4, int8
的绝对值后比较的btree AM
的一个opfamily
。
自定义opclass
或者opfamily
就如同是前面章节案例中编写Ocaml
的Module
一样。
推荐阅读:
opclass
和opfamily
在分区表中应用的理论基础
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 Join
的plan
,内表查询计划会提前全量扫描生成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]\}
对其它的运算符同样也有上面的逻辑命题成立。因此想要传递等价关系用来推理分区表的裁剪,必须等价类的opfamily
和分区表分区的opclass
对应的opfamily
一致。
opclass
和opfamily
在分布式表中应用的理论基础
有了上面的介绍,这部分的核心内容也就比较简单了。简而言之,想要对分布式表的分布信息做推理:direct dispatch
或者判断是否co-locate
,也必须满足:
direct dispatch
推理: 等价关系的opfamily
和distky
的必须一致co-locate
推理:distkeys
的opfamily
必须一致
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
和历史版本中,Greenplum
比Postgres
更早实现分区表,自从Greenplum 7
开始,分区表融合了上游Postgres
的实现。这里的问题发生在插入执行器的代码里,需要决定数据的最终目的地时候。Debug
的时候,我们可以用rr
录制完整的执行过程,也可以根据报错日志的代码位置,直接阅读代码就可以诊断。
这个issue
是用户自己创建了自定义类型,这个自定义类型是可以创建在任何schema
下的。而Greenplum 6
在range_test
部分的代码,使用了hard-code
在pg_catalog
这个schema
下去搜索需要的运算符。当然就找不到了。修复的核心是如何找到需要的运算符,这需要根据分区表定义时候存下来的opclass
。
看见一角的时候,就要想象全貌。顺着这块代码的所有可能的调用路径,思考修复的方案是否完善,以及能否挖出更多的问题。思考后发现:
- 如果是
insert
产生的调用路径,因为最终要把数据插入到磁盘里,因此最终的tuple会进行一次类型转换和目标表的定义一致。这时候不存在跨类型的比较。而且这里直接了当,修了就没问题了。 - 如果是动态裁剪产生的调用路径,首先得保证查询计划是正确的。也就是产生这类计划的其中一个前提是等价类的
family
要和分区表的一致。快速去检查一下优化器的代码,没有发现,然后构造一个测试,找出来很多问题,orca
,以及Greenplum 6
的planner
都有问题,后面的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
。而Planner
和ORCA
之前都忽略了这一点。
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 6
的planner
里面是如何生成这样的查询计划的。在需要展开分区表的时候,就考虑到未来可能会进行动态分区裁剪,故而在函数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_classes
和root->dynamicScans
,Greenplum 6
的planner
会在构建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
这里不赘述,留给有兴趣的读者自行探索。