GPDB-疑难杂症-PlaceHolderVar
从GPDB5升级到GPDB6时,遇到以往可以执行的SQL不能执行了。报错:PlaceHolderVar found where not expected!语法不兼容了?
postgres=# CREATE TABLE t1( id1 int) WITH (appendonly=true, compresstype=none, blocksize=8192) DISTRIBUTED BY (id1);
postgres=# CREATE TABLE t4(id1 int, id2 text ) DISTRIBUTED BY (id1);
postgres=# set optimizer=off;
postgres=# explain analyze select count(t1.id1),count(t4id2) tid2 from t1 left join (select id1,format('%s',id2)t4id2 from t4) t31 on t1.id1=t31.id1 having count(t4id2) is not null ;
ERROR: PlaceHolderVar found where not expected (var.c:757)
向GPDB提交了bug:https://github.com/greenplum-db/gpdb/issues/16193
1、为什么GPDB5可以执行,GPDB6报错
找到GPDB6代码报错位置:pull_var_clause_walker函数中
可知,由入参context的phbehavior标签为PVC_REJECT_PLACEHOLDERS决定了报错。
那么就需要知道这个标签在什么地方设置的,接着通过gdb跟踪,向堆栈上层追溯,可以总结:在函数make_subplan_tlist中设置了该标签
接着,对比下GPDB5的代码,看下有何不同:
pull_var_clause函数仅2个入参,没有GPDB6中的第3个入参,并且pull_var_clause_walker中也没有对应报错的地方:
好了,到此,明白GPDB5和GPDB6代码的区别,了解到为什么仅GPDB6会报错了。至于为什么会有这个标签呢?
我们找到提交patch的commit:PG9.0.23内核修复bug时引入了该标签
Fix estimate_num_groups() to not fail on PlaceHolderVars, per report from Stefan Kaltenbrunner. The most reasonable behavior (at least for the near term) seems to be to ignore the PlaceHolderVar and examine its argument instead. In support of this, change the API of pull_var_clause() to allow callers to request recursion into PlaceHolderVars. Currently estimate_num_groups() is the only customer for that behavior, but where there's one there may be others.
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=1d97c19a0f748e94b9582dcbaec33ad74be062f9
2、GPDB6报错的机制
GPDB6中报错的机制是什么?为什么会有PlaceHolderVar,他是干什么的?
2.1以下面例子说明为什么使用PlaceHolderVar
yzs=#select sno,sname,ssex from student;
sno | sname | ssex
------|----------|------
1 | zhangsan | 1
2 | lisi | 1
(2 rows)
yzs=#select sno,cno,degree from student;
sno | cno | degree
------|-------|-------
1 | 1 | 36
(1 row)
正常的结果:
yzs=#select * from student st left join (select sno,coalesce(degree,60) from score) sc on st.sno=sc.sno;
sno | sname | ssex | sno | coalesce
------|----------|-------|-----|---------
2 | lisi | 1 | |
1 | zhangsan | 1 | 1 | 36
(2 rows)
若假设,上述子查询被强制提升,并不使用PlaceHolderVar替代,则会出现下面的结果:
yzs=#select * from student st left join (select sno,coalesce(degree,60) from score) sc on st.sno=sc.sno;
sno | sname | ssex | sno | coalesce
------|----------|-------|-----|---------
2 | lisi | 1 | | 60
1 | zhangsan | 1 | 1 | 36
(2 rows)
可以看到,进行子查询提升后,若不使用PlaceHolderVar替代,结果就不一样了。coalesce函数对NULL进行了处理,表示若为NULL,则使用60替代,可以看到,对left join结果中的NULL做了处理,被强制提升后,逻辑树就不一样了。为了使得逻辑树等价,就使用PlaceHolderVar来替代原来的节点,做下标记,其实该结构封装了原来的节点。
注:因为提升子查询后,PG会把子查询的关系并入FROM-LIST中,这样关系个数就会增加,从而增加join路径,以便提供更多join路径,有更多选择。
2.2提升子查询的条件
简单子查询会被提升,那么什么是简单子查询?简单子查询指简单的查询语句或者join语句组成,由函数is_simple_subquery判断:
bool
is_simple_subquery(PlannerInfo *root, Query *subquery, RangeTblEntry *rte,
JoinExpr *lowest_outer_join)
{
if (!IsA(subquery, Query) ||
subquery->commandType != CMD_SELECT ||
subquery->utilityStmt != NULL)
elog(ERROR, "subquery is bogus");
if (subquery->setOperations)
return false;
if (subquery->hasAggs ||
subquery->hasWindowFuncs ||
subquery->groupClause ||
subquery->havingQual ||
subquery->windowClause ||
subquery->sortClause ||
subquery->distinctClause ||
subquery->limitOffset ||
subquery->limitCount ||
subquery->hasForUpdate ||
subquery->cteList ||
root->parse->cteList)
return false;
if (rte && rte->security_barrier)
return false;
if (rte && rte->lateral){
bool restricted;
Relids safe_upper_varnos;
if (lowest_outer_join != NULL){
restricted = true;
safe_upper_varnos = get_relids_in_jointree((Node *) lowest_outer_join, true);
}else{
restricted = false;
safe_upper_varnos = NULL; /* doesn't matter */
}
if (jointree_contains_lateral_outer_refs((Node *) subquery->jointree, restricted, safe_upper_varnos))
return false;
if (lowest_outer_join != NULL){
Relids lvarnos = pull_varnos_of_level((Node *) subquery->targetList, 1);
if (!bms_is_subset(lvarnos, safe_upper_varnos))
return false;
}
}
if (expression_returns_set((Node *) subquery->targetList))
return false;
if (contain_volatile_functions((Node *) subquery->targetList))
return false;
if (subquery->jointree->fromlist == NIL)
return false;
return true;
}
总结起来有6点:
1)顶层操作(语法树的树顶)不是集合操作(UNION/INTERSECT/EXECEPT)
2)子查询中不含有SORT、LIMIT、CTE-LIST
3)子查询中不能有更新操作
4)子查询的目标列不能是聚合函数类型
5)子查询目标列不能含有易失函数
6)子查询存在连接条件
2.3什么时候使用PlaceHolderVar替代
我们看pullup_replace_vars_callback函数:
make_placeholder_expr函数创建PlaceHolderVar节点。创建该节点的条件为蓝框内条件。主要看下被提升的节点需要是一个非严格函数。
2.3什么地方导致拒绝PlaceHolderVar
havingQual中若有PlaceHolderVar,则拒绝。
2.3什么是非严格的函数
参数是NULL,则输出也是NULL则是严格的。在定义函数时可以指定strict。当然若函数是严格的,还需继续判断参数。
具体哪些是严格的,可以参见contain_nonstrict_functions_walker函数。
3、总结
1)子查询是一个join,是一个简单子查询。并且函数format是一个非严格函数。所以该子查询需要提升。为了保证逻辑等价,会使用PlaceHolderVar替代原format函数的表达式节点
2)havingQual条件即having count(t4id2) is not null,t4id2是子查询中的非严格函数。
3)GPDB6对havingQual有了限制,禁止该表达式中出现PlaceHolderVar节点
4)可以通过不使用havingQual以及修改函数strict属性以及参数类型来规避,或者通过改造使之不满足子查询提升的6个条件来临时避免该问题。
5)当然,感觉这是一个bug。官方也在提出bug后做出了修复:
https://github.com/greenplum-db/gpdb/pull/16240