目录结构
注:提前言明 本文借鉴了以下博主、书籍或网站的内容,其列表如下:
1、参考书籍:《PostgreSQL数据库内核分析》
2、参考书籍:《数据库事务处理的艺术:事务管理与并发控制》
3、PostgreSQL数据库仓库链接,点击前往
4、日本著名PostgreSQL数据库专家 铃木启修 网站主页,点击前往
5、参考书籍:《PostgreSQL中文手册》
6、参考书籍:《PostgreSQL指南:内幕探索》,点击前往
7、参考书籍:《事务处理 概念与技术》
1、本文内容全部来源于开源社区 GitHub和以上博主的贡献,本文也免费开源(可能会存在问题,评论区等待大佬们的指正)
2、本文目的:开源共享 抛砖引玉 一起学习
3、本文不提供任何资源 不存在任何交易 与任何组织和机构无关
4、大家可以根据需要自行 复制粘贴以及作为其他个人用途,但是不允许转载 不允许商用 (写作不易,还请见谅 💖)
5、本文内容基于PostgreSQL master源码开发而成
将 OR 子句转换为 ANY 表达式
- 文章快速说明索引
- 功能实现背景说明
- 功能实现源码解析
- or_to_any_transform_limit
- process_duplicate_ors
- transform_or_to_any
- 原理讲解
- 调试情况1
- 调试情况2
- 调试情况3
- 调试情况4
- 社区争议回退原因
文章快速说明索引
学习目标:
做数据库内核开发久了就会有一种 少年得志,年少轻狂 的错觉,然鹅细细一品觉得自己其实不算特别优秀 远远没有达到自己想要的。也许光鲜的表面掩盖了空洞的内在,每每想到于此,皆有夜半临渊如履薄冰之感。为了睡上几个踏实觉,即日起 暂缓其他基于PostgreSQL数据库的兼容功能开发,近段时间 将着重于学习分享Postgres的基础知识和实践内幕。
学习内容:(详见目录)
1、将 OR 子句转换为 ANY 表达式
学习时间:
2024年10月05日 12:16:59
学习产出:
1、PostgreSQL数据库基础知识回顾 1个
2、CSDN 技术博客 1篇
3、PostgreSQL数据库内核深入学习
注:下面我们所有的学习环境是Centos8+PostgreSQL master +Oracle19C+MySQL8.0
postgres=# select version();
version
------------------------------------------------------------------------------------------------------------
PostgreSQL 18devel on x86_64-pc-linux-gnu, compiled by gcc (GCC) 8.5.0 20210514 (Red Hat 8.5.0-21), 64-bit
(1 row)
postgres=#
#-----------------------------------------------------------------------------#
SQL> select * from v$version;
BANNER Oracle Database 19c EE Extreme Perf Release 19.0.0.0.0 - Production
BANNER_FULL Oracle Database 19c EE Extreme Perf Release 19.0.0.0.0 - Production Version 19.17.0.0.0
BANNER_LEGACY Oracle Database 19c EE Extreme Perf Release 19.0.0.0.0 - Production
CON_ID 0
#-----------------------------------------------------------------------------#
mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.27 |
+-----------+
1 row in set (0.06 sec)
mysql>
功能实现背景说明
大概描述一下此次的patch的内容,如下:
- 在优化的初步阶段,当我们仍在处理表达式树时,将
(expr op C1) OR (expr op C2) ...
替换为expr op ANY(ARRAY[C1,C2, ...])
- 这里 Cn 是第 n 个常量表达式,“
expr
”是非常量表达式,“op
”是一个返回布尔结果并具有通勤器的运算符commuter
(用于表达式的常量和非常量部分的反向顺序的情况,如Cn op expr
)- 有时它会导致计划不理想。这就是为什么有
or_to_any_transform_limit
GUC。它指定触发 OR-to-ANY 转换的 OR 表达式中参数长度的阈值。通常,更多可分组的 OR 参数意味着转换获胜的可能性大于失败的可能性
postgres=# create table t1 (id int primary key, name text);
CREATE TABLE
postgres=#
postgres=# insert into t1 values (1, 'oracle');
INSERT 0 1
postgres=# insert into t1 values (2, 'mysql');
INSERT 0 1
postgres=# insert into t1 values (3, 'Sql Server');
INSERT 0 1
postgres=# insert into t1 values (4, 'postgresql');
INSERT 0 1
postgres=# select * from t1 where id = 2 or id = 4;
id | name
----+------------
2 | mysql
4 | postgresql
(2 rows)
postgres=#
postgres=# explain(costs off, verbose) select * from t1 where id = 2 or id = 4;
QUERY PLAN
----------------------------------------------
Bitmap Heap Scan on public.t1
Output: id, name
Recheck Cond: ((t1.id = 2) OR (t1.id = 4))
-> BitmapOr
-> Bitmap Index Scan on t1_pkey
Index Cond: (t1.id = 2)
-> Bitmap Index Scan on t1_pkey
Index Cond: (t1.id = 4)
(8 rows)
postgres=#
postgres=# explain(costs off, verbose) select * from t1 where id = ANY ('{2, 4}'::integer[]);
QUERY PLAN
--------------------------------------------------------
Bitmap Heap Scan on public.t1
Output: id, name
Recheck Cond: (t1.id = ANY ('{2,4}'::integer[]))
-> Bitmap Index Scan on t1_pkey
Index Cond: (t1.id = ANY ('{2,4}'::integer[]))
(5 rows)
postgres=#
然后看一下该patch的作用,如下:
postgres=# SET or_to_any_transform_limit = 1;
SET
postgres=# explain(costs off, verbose) select * from t1 where id = 2 or id = 4;
QUERY PLAN
--------------------------------------------------------
Bitmap Heap Scan on public.t1
Output: id, name
Recheck Cond: (t1.id = ANY ('{2,4}'::integer[]))
-> Bitmap Index Scan on t1_pkey
Index Cond: (t1.id = ANY ('{2,4}'::integer[]))
(5 rows)
postgres=#
postgres=# SET or_to_any_transform_limit = 2;
SET
postgres=# explain(costs off, verbose) select * from t1 where id = 2 or id = 4;
QUERY PLAN
--------------------------------------------------------
Bitmap Heap Scan on public.t1
Output: id, name
Recheck Cond: (t1.id = ANY ('{2,4}'::integer[]))
-> Bitmap Index Scan on t1_pkey
Index Cond: (t1.id = ANY ('{2,4}'::integer[]))
(5 rows)
postgres=#
postgres=# SET or_to_any_transform_limit = 3;
SET
postgres=# explain(costs off, verbose) select * from t1 where id = 2 or id = 4;
QUERY PLAN
----------------------------------------------
Bitmap Heap Scan on public.t1
Output: id, name
Recheck Cond: ((t1.id = 2) OR (t1.id = 4))
-> BitmapOr
-> Bitmap Index Scan on t1_pkey
Index Cond: (t1.id = 2)
-> Bitmap Index Scan on t1_pkey
Index Cond: (t1.id = 4)
(8 rows)
postgres=#
关于上面GUC参数的解释,如下:
设置 OR 表达式中参数的最小长度,超过该长度,规划器将尝试查找并将多个相似的 OR 表达式分组为 ANY 表达式。此转换的分组技术基于变量侧的等价性。这种表达式的一侧必须是常量子句,另一侧必须包含变量子句。默认值为 5。值 -1 完全禁用转换。
此 OR-to-ANY 转换的优点是查询规划和执行速度更快。在某些情况下,此转换还会导致更有效的计划,其中包含单个索引扫描而不是多个位图扫描。但是,当不同的 OR 参数更适合匹配不同的索引时,它也可能导致规划回归。当它们具有不同的匹配部分索引或查询中使用的其他列的分布不同时,可能会发生这种情况。通常,更多可分组的 OR 参数意味着转换获胜的可能性大于失败的可能性。
但是因为设计和实现上存在争议,目前该patch已回滚 如下:
不过本着内核开发人员学习的态度,我们还是看一下其内部的实现。(从社区的态度来看 后面说不定还会归来)
功能实现源码解析
or_to_any_transform_limit
首先看一下该GUC的定义,如下:
// src/backend/utils/misc/guc_tables.c
{
{"or_to_any_transform_limit", PGC_USERSET, QUERY_TUNING_OTHER,
gettext_noop("Set the minimum length of the list of OR clauses to attempt the OR-to-ANY transformation."),
gettext_noop("Once the limit is reached, the planner will try to replace expression like "
"'x=c1 OR x=c2 ..' to the expression 'x = ANY(ARRAY[c1,c2,..])'"),
GUC_EXPLAIN
},
&or_to_any_transform_limit,
5, -1, INT_MAX,
NULL, NULL, NULL
},
言简意赅:正如上面演示那样,设置 OR 子句列表的最小长度以尝试进行 OR 到 ANY 的转换。
process_duplicate_ors
接下来看一下今天学习的重点,如下:
// src/backend/optimizer/prep/prepqual.c
/*
* process_duplicate_ors
* Given a list of exprs which are ORed together, try to apply
* the inverse OR distributive law.
* 给定一个进行 “或” 运算的表达式列表,尝试应用逆 “或” 分配律。
*
* Returns the resulting expression (could be an AND clause, an OR
* clause, or maybe even a single subexpression).
* 返回结果表达式(可以是 AND 子句、OR 子句、甚至可能是单个子表达式)。
*/
static Expr *
process_duplicate_ors(List *orlist);
前置条件:关于这个函数的内部,请看 张树杰 博客:
- PostgreSQL代码分析,查询优化部分,process_duplicate_ors,点击前往
这里详细分析了每一行,我这里不再赘述。其中的示例,如下:
postgres=# create table ta(a int primary key);
CREATE TABLE
postgres=# create table tb(b int primary key);
CREATE TABLE
postgres=#
postgres=# create table tc(c int primary key);
CREATE TABLE
postgres=# create table td(d int primary key);
CREATE TABLE
postgres=#
postgres=# insert into ta values (1);
INSERT 0 1
postgres=# insert into tb values (1);
INSERT 0 1
postgres=# insert into tc values (1);
INSERT 0 1
postgres=# insert into td values (1);
INSERT 0 1
postgres=# insert into tb values (2);
INSERT 0 1
postgres=# insert into tc values (3);
INSERT 0 1
postgres=# insert into td values (4);
INSERT 0 1
postgres=# explain select * from ta, tb, tc, td WHERE (A=1 AND B=1) OR (A=1 AND C=1) OR (A=1 AND D=1);
QUERY PLAN
-----------------------------------------------------------------------------------------
Nested Loop (cost=0.15..331708934.17 rows=19499851 width=16)
Join Filter: ((tb.b = 1) OR (tc.c = 1) OR (td.d = 1))
-> Nested Loop (cost=0.15..81392.30 rows=6502500 width=12)
-> Nested Loop (cost=0.15..69.17 rows=2550 width=8)
-> Index Only Scan using ta_pkey on ta (cost=0.15..8.17 rows=1 width=4)
Index Cond: (a = 1)
-> Seq Scan on tb (cost=0.00..35.50 rows=2550 width=4)
-> Materialize (cost=0.00..48.25 rows=2550 width=4)
-> Seq Scan on tc (cost=0.00..35.50 rows=2550 width=4)
-> Materialize (cost=0.00..48.25 rows=2550 width=4)
-> Seq Scan on td (cost=0.00..35.50 rows=2550 width=4)
(11 rows)
postgres=#
函数流程解释 简要如下:
-
处理空 OR 列表:
- 如果传入的
orlist
为空(没有子句),返回一个恒为FALSE
的布尔常量,表示 OR 为空的情况。
- 如果传入的
-
处理单一子句:
- 如果
orlist
只有一个子句,直接返回该子句,因为一个单独的OR
子句不需要进一步简化。
- 如果
-
选择最短的 AND 子句作为参考:
- 遍历
orlist
,找到最短的 AND 子句作为参考。如果我们发现一个不是 AND 的子句,我们可以将其视为一个单元素 AND 子句,该子句必然是最短的。
- 遍历
-
消除参考子句中的重复:
- 对参考子句进行去重,以确保参考列表中没有重复。
-
查找所有 OR 子句的共同条件:
- 遍历参考子句,检查每个参考子句是否存在于所有 OR 子句中。如果参考子句出现在所有 OR 子句中,则将其标记为“胜出者” (
winners
)。
- 遍历参考子句,检查每个参考子句是否存在于所有 OR 子句中。如果参考子句出现在所有 OR 子句中,则将其标记为“胜出者” (
-
处理没有胜出者的情况:
- 如果没有共同的胜出子句,检查 OR 子句列表的长度。如果 OR 子句足够长,则尝试将其转换为 SAOP(
ScalarArrayOp: Scalar Array Op
,即标量数组操作)。转换后返回优化后的 OR 子句列表。
- 如果没有共同的胜出子句,检查 OR 子句列表的长度。如果 OR 子句足够长,则尝试将其转换为 SAOP(
-
有胜出者的情况,生成由剩余子句组成的新 OR 列表:
- 生成一个新的 OR 列表,去除已在所有子句中胜出的条件。如果任何子句退化为空,则会出现类似 (A AND B) OR (A) 的情况,该情况可以简化为 A — 也就是说,OR 的其他分支中的附加条件无关紧要。请注意,由于我们使用
list_difference
,因此 AND 子句中多次出现的获胜子句将被自动删除。
- 生成一个新的 OR 列表,去除已在所有子句中胜出的条件。如果任何子句退化为空,则会出现类似 (A AND B) OR (A) 的情况,该情况可以简化为 A — 也就是说,OR 的其他分支中的附加条件无关紧要。请注意,由于我们使用
-
再次尝试进行 SAOP 转换:
- 再次检查新生成的 OR 列表,如果其长度达到转换限制,则尝试将其转换为 SAOP。
-
处理特殊情况并构建最终的表达式:
- 如果新的 OR 列表不为空,则将其与胜出的子句合并。如果简化的 OR 不是退化的,则将其附加到获胜者列表中,正确处理一个元素的特殊情况(这真的会发生吗?)。此外,还要小心维护 AND/OR 平坦度,以防我们拉出子子 OR 子句。
-
最后 若winners列表长度为1返回该表达式;否则返回构造的 AND 子句,再次警惕单个元素和 AND/OR 平坦度。
接下来 不急着分析transform_or_to_any
函数,我们对函数process_duplicate_ors
调试一下:
第一个SQL,如下:
explain select * from ta, tb, tc, td WHERE (A=1 AND B=1) OR (A=1 AND C=1) OR (A=1 AND D=1);
如上,参考list这里选择的就是A=1 AND B=1
,接下来就是winners list
的获得:
该过程就是reference list
中的每一个,都在orlist
的元素中检查是否出现 是则进入winners list
。否 则不再继续向下检查,直接跳过 如下:
因为这三个都是and子句,然后又因为winners list: A=1
不为空。于是这里的list_difference
将自动进行裁剪,如下:
裁剪拼接之后的neworlist
,实际上就是:B=1、C=1和D=1
。
最后先是以递归方式将嵌套的 OR 子句展平为单个 or 子句列表 B=1 or C=1 or D=1
,然后以递归方式将嵌套的 AND 子句展平为单个 and 子句列表 (A=1) AND (B=1 or C=1 or D=1)
。此刻的函数堆栈,如下:
process_duplicate_ors(List * orlist)
find_duplicate_ors(Expr * qual, _Bool is_check)
canonicalize_qual(Expr * qual, _Bool is_check)
preprocess_expression(PlannerInfo * root, Node * expr, int kind)
preprocess_qual_conditions(PlannerInfo * root, Node * jtnode)
subquery_planner(PlannerGlobal * glob, Query * parse, PlannerInfo * parent_root, _Bool hasRecursion, double tuple_fraction, SetOperationStmt * setops)
standard_planner(Query * parse, const char * query_string, int cursorOptions, ParamListInfo boundParams)
planner(Query * parse, const char * query_string, int cursorOptions, ParamListInfo boundParams)
pg_plan_query(Query * querytree, const char * query_string, int cursorOptions, ParamListInfo boundParams)
standard_ExplainOneQuery(Query * query, int cursorOptions, IntoClause * into, ExplainState * es, const char * queryString, ParamListInfo params, QueryEnvironment * queryEnv)
ExplainOneQuery(Query * query, int cursorOptions, IntoClause * into, ExplainState * es, const char * queryString, ParamListInfo params, QueryEnvironment * queryEnv)
ExplainQuery(ParseState * pstate, ExplainStmt * stmt, ParamListInfo params, DestReceiver * dest)
standard_ProcessUtility(PlannedStmt * pstmt, const char * queryString, _Bool readOnlyTree, ProcessUtilityContext context, ParamListInfo params, QueryEnvironment * queryEnv, DestReceiver * dest, QueryCompletion * qc)
ProcessUtility(PlannedStmt * pstmt, const char * queryString, _Bool readOnlyTree, ProcessUtilityContext context, ParamListInfo params, QueryEnvironment * queryEnv, DestReceiver * dest, QueryCompletion * qc)
PortalRunUtility(Portal portal, PlannedStmt * pstmt, _Bool isTopLevel, _Bool setHoldSnapshot, DestReceiver * dest, QueryCompletion * qc)
FillPortalStore(Portal portal, _Bool isTopLevel)
PortalRun(Portal portal, long count, _Bool isTopLevel, _Bool run_once, DestReceiver * dest, DestReceiver * altdest, QueryCompletion * qc)
exec_simple_query(const char * query_string)
...
第二个SQL,如下:
explain select * from ta, tb, tc WHERE (A=1 AND B=1) OR (A=1 AND C=1) OR (A=1);
如上,在选择reference list
的时候,首先and子句A=1 AND B=1
进入;第二段长度也是2 忽略;第三段更短 因此成为reference list: A=1
。
然后在随后的winners
获取的二级循环中 成功匹配:
接下来在剪枝中,发生了退化degenerate case
如下:
这种情况下 实际上原条件(A=1 AND B=1) OR (A=1 AND C=1) OR (A=1)
与A=1
效果一样,于是后期处理如下:
postgres=# explain select * from ta, tb, tc WHERE (A=1 AND B=1) OR (A=1 AND C=1) OR (A=1);
QUERY PLAN
-----------------------------------------------------------------------------------
Nested Loop (cost=0.15..81392.30 rows=6502500 width=12)
-> Nested Loop (cost=0.15..69.17 rows=2550 width=8)
-> Index Only Scan using ta_pkey on ta (cost=0.15..8.17 rows=1 width=4)
Index Cond: (a = 1)
-> Seq Scan on tb (cost=0.00..35.50 rows=2550 width=4)
-> Materialize (cost=0.00..48.25 rows=2550 width=4)
-> Seq Scan on tc (cost=0.00..35.50 rows=2550 width=4)
(7 rows)
postgres=#
上面两个都可以抽取出公共项,即:winners list
不为空。接下来 看第三个SQL:
explain select * from ta, tb, tc, td WHERE (A=1 AND B=1) OR (A=1 AND C=1) OR (D=1);
如上,reference
此时就只有一个,然后检查是否是winners list
。因为第一个检查就未匹配,此时winners
将是空,那么将进入如下逻辑:
// src/backend/optimizer/prep/prepqual.c
/*
* If no winners, we can't do OR-to-ANY transformation.
*/
if (winners == NIL)
{
/*
* Make an attempt to group similar OR clauses into SAOP if the list
* is lengthy enough.
*/
if (or_to_any_transform_limit >= 0 &&
list_length(orlist) >= or_to_any_transform_limit)
orlist = transform_or_to_any(orlist);
/* Transformation could group all OR clauses to a single SAOP */
return (list_length(orlist) == 1) ?
(Expr *) linitial(orlist) : make_orclause(orlist);
}
因为我们这里没有开or_to_any的转换,那么最终返回的表达式还是make_orclause(orlist)
,保持了原样:
postgres=# explain select * from ta, tb, tc, td WHERE (A=1 AND B=1) OR (A=1 AND C=1) OR (D=1);
QUERY PLAN
-------------------------------------------------------------------------------------------
Nested Loop (cost=0.00..1057270004879.88 rows=16594374899 width=16)
Join Filter: (((ta.a = 1) AND (tb.b = 1)) OR ((ta.a = 1) AND (tc.c = 1)) OR (td.d = 1))
-> Nested Loop (cost=0.00..207348588.00 rows=16581375000 width=12)
-> Nested Loop (cost=0.00..81358.62 rows=6502500 width=8)
-> Seq Scan on ta (cost=0.00..35.50 rows=2550 width=4)
-> Materialize (cost=0.00..48.25 rows=2550 width=4)
-> Seq Scan on tb (cost=0.00..35.50 rows=2550 width=4)
-> Materialize (cost=0.00..48.25 rows=2550 width=4)
-> Seq Scan on tc (cost=0.00..35.50 rows=2550 width=4)
-> Materialize (cost=0.00..48.25 rows=2550 width=4)
-> Seq Scan on td (cost=0.00..35.50 rows=2550 width=4)
(11 rows)
postgres=#
有了上面的铺垫,接下来再看transform_or_to_any
就会清晰很多,如下:
// src/backend/optimizer/prep/prepqual.c
/*
* transform_or_to_any -
* Discover the args of an OR expression and try to group similar OR
* expressions to SAOP expressions.
* 发现 OR 表达式的参数,并尝试将相似的 OR 表达式分组为 SAOP 表达式。
*
* This transformation groups two-sided equality expression. One side of
* such an expression must be a plain constant or constant expression. The
* other side must be a variable expression without volatile functions.
* To group quals, opno, inputcollid of variable expression, and type of
* constant expression must be equal too.
* 此转换将两边相等表达式分组。
* 此类表达式的一侧必须是普通常量或常量表达式。
* 另一侧必须是没有易失性函数的变量表达式。
* 要分组,变量表达式的 quals、opno、inputcollid 和常量表达式的类型也必须相等。
*
* The grouping technique is based on the equivalence of variable sides of
* the expression: using exprId and equal() routine, it groups constant sides
* of similar clauses into an array. After the grouping procedure, each
* couple ('variable expression' and 'constant array') forms a new SAOP
* operation, which is added to the args list of the returning expression.
* 分组技术基于表达式变量侧的等价性:使用 exprId 和 equal() 例程,它将相似子句的常量侧分组为一个数组。
* 分组过程之后,每对(“变量表达式”和“常量数组”)形成一个新的 SAOP 操作,该操作将添加到返回表达式的参数列表中。
*/
static List *
transform_or_to_any(List *orlist);
上面函数transform_or_to_any
的目的:发现有合适的or表达式,就处理成ANY表达式。而函数process_duplicate_ors
根据winners list
是否为空 也选择了不同的道路。
- 为空,最后
make_orclause
例如上面的第三个SQL - 不为空,对于非
degenerate
的情况(neworlist != NIL
),例如第一个SQL
函数transform_or_to_any
的调用,如下:
transform_or_to_any
原理讲解
在开始之前 先看一下重要的数据结构 key
和 entry
(因为该函数内部使用了一个哈希表 这个也是其他人不太喜欢的一个点),如下:
/*
* The key for grouping similar operator expressions in transform_or_to_any().
*
* 在transform_or_to_any()中对相似的运算符表达式进行分组的 key
*/
typedef struct OrClauseGroupKey
{
/* We need this to put this structure into list together with other nodes */
/* 我们需要将它与其他节点一起放入列表中。 当然这个也不太好 这是个什么 Node? */
NodeTag type;
/* The expression of the variable side of operator */
/* 运算符变量侧的表达式 */
Expr *expr;
/* The operator of the operator expression */
/* 运算符表达式的运算符 */
Oid opno;
/* The collation of the operator expression */
/* 运算符表达式的排序规则 */
Oid inputcollid;
/* The type of constant side of operator */
/* 运算符常量端的类型 */
Oid consttype;
} OrClauseGroupKey;
/*
* The group of similar operator expressions in transform_or_to_any().
*
* transform_or_to_any() 中的一组类似的运算符表达式
*/
typedef struct OrClauseGroupEntry
{
OrClauseGroupKey key;
/* The list of constant sides of operators */
/* 运算符常量侧的列表 */
List *consts;
/*
* List of source expressions. We need this for convenience in case we
* will give up on transformation.
* 源表达式列表。如果我们放弃转换,我们需要这个列表以方便使用。
*/
List *exprs;
} OrClauseGroupEntry;
这里面还自定义了一些哈希回调,有兴趣的小伙伴可以了解一下:
...
info.hash = orclause_hash;
info.keycopy = orclause_keycopy;
info.match = orclause_match;
...
static List *
transform_or_to_any(List *orlist)
{
List *neworlist = NIL;
List *entries = NIL;
ListCell *lc;
HASHCTL info;
HTAB *or_group_htab = NULL;
int len_ors = list_length(orlist);
OrClauseGroupEntry *entry = NULL;
Assert(or_to_any_transform_limit >= 0 &&
len_ors >= or_to_any_transform_limit);
MemSet(&info, 0, sizeof(info));
info.keysize = sizeof(OrClauseGroupKey);
info.entrysize = sizeof(OrClauseGroupEntry);
info.hash = orclause_hash;
info.keycopy = orclause_keycopy;
info.match = orclause_match;
or_group_htab = hash_create("OR Groups",
len_ors,
&info,
HASH_ELEM | HASH_FUNCTION | HASH_COMPARE | HASH_KEYCOPY);
foreach(lc, orlist)
{
Node *orqual = lfirst(lc);
Node *const_expr;
Node *nconst_expr;
OrClauseGroupKey hashkey;
bool found;
Oid opno;
Oid consttype;
Node *leftop,
*rightop;
if (!IsA(orqual, OpExpr))
{
entries = lappend(entries, orqual);
continue;
}
opno = ((OpExpr *) orqual)->opno;
if (get_op_rettype(opno) != BOOLOID)
{
/* Only operator returning boolean suits OR -> ANY transformation */
/* 仅返回布尔值的运算符适合 OR -> ANY 转换 */
entries = lappend(entries, orqual);
continue;
}
/*
* Detect the constant side of the clause. Recall non-constant
* expression can be made not only with Vars, but also with Params,
* which is not bonded with any relation. Thus, we detect the const
* side - if another side is constant too, the orqual couldn't be an
* OpExpr. Get pointers to constant and expression sides of the qual.
*
* 检测子句的常量侧。
* 回想一下,非常量表达式不仅可以用 Var 来创建,还可以用 Params 来创建,后者与任何relation都没有关系。
* 因此,我们检测 const 侧 - 如果另一侧也是常量,则 orqual 不能是 OpExpr。
* 获取指向 qual 的常量侧和表达式侧的指针。
*/
leftop = get_leftop(orqual);
if (IsA(leftop, RelabelType))
leftop = (Node *) ((RelabelType *) leftop)->arg;
rightop = get_rightop(orqual);
if (IsA(rightop, RelabelType))
rightop = (Node *) ((RelabelType *) rightop)->arg;
if (IsA(leftop, Const))
{
opno = get_commutator(opno);
if (!OidIsValid(opno))
{
/* commutator doesn't exist, we can't reverse the order */
/* commutator不存在,我们无法反转顺序 */
entries = lappend(entries, orqual);
continue;
}
nconst_expr = get_rightop(orqual);
const_expr = get_leftop(orqual);
}
else if (IsA(rightop, Const))
{
const_expr = get_rightop(orqual);
nconst_expr = get_leftop(orqual);
}
else
{
entries = lappend(entries, orqual);
continue;
}
/*
* Forbid transformation for composite types, records, and volatile
* expressions.
* 禁止对复合类型、记录和易失性表达式进行转换。
*/
consttype = exprType(const_expr);
if (type_is_rowtype(exprType(const_expr)) ||
type_is_rowtype(consttype) ||
contain_volatile_functions((Node *) nconst_expr))
{
entries = lappend(entries, orqual);
continue;
}
/*
* At this point we definitely have a transformable clause. Classify
* it and add into specific group of clauses, or create new group.
* 此时我们肯定有一个可转换子句。将其分类并添加到特定的子句组中,或创建新的组。
*/
hashkey.type = T_Invalid;
hashkey.expr = (Expr *) nconst_expr;
hashkey.opno = opno;
hashkey.consttype = consttype;
hashkey.inputcollid = exprCollation(const_expr);
entry = hash_search(or_group_htab, &hashkey, HASH_ENTER, &found);
if (unlikely(found))
{
entry->consts = lappend(entry->consts, const_expr);
entry->exprs = lappend(entry->exprs, orqual);
}
else
{
entry->consts = list_make1(const_expr);
entry->exprs = list_make1(orqual);
/*
* Add the entry to the list. It is needed exclusively to manage
* the problem with the order of transformed clauses in explain.
* Hash value can depend on the platform and version. Hence,
* sequental scan of the hash table would prone to change the
* order of clauses in lists and, as a result, break regression
* tests accidentially.
* 将条目添加到列表中。
* 它专门用于管理 explain 中转换子句的顺序问题。
* 哈希值可能取决于平台和版本。
* 因此,哈希表的顺序扫描很容易改变列表中子句的顺序,从而意外破坏回归测试。
*/
entries = lappend(entries, entry);
}
}
/* Let's convert each group of clauses to an ANY expression. */
/* 让我们将每组子句转换为 ANY 表达式。 */
/*
* Go through the list of groups and convert each, where number of consts
* more than 1. trivial groups move to OR-list again
* 遍历组列表并转换每个组,其中 const 的数量大于 1。trivial 组再次移至 OR 列表
*/
foreach(lc, entries)
{
Oid scalar_type;
Oid array_type;
if (!IsA(lfirst(lc), Invalid))
{
neworlist = lappend(neworlist, lfirst(lc));
continue;
}
entry = (OrClauseGroupEntry *) lfirst(lc);
Assert(list_length(entry->consts) > 0);
Assert(list_length(entry->exprs) == list_length(entry->consts));
if (list_length(entry->consts) == 1)
{
/*
* Only one element returns origin expression into the BoolExpr
* args list unchanged.
* 只有一个元素将原始表达式不加改变地返回到 BoolExpr 参数列表中。
*/
list_free(entry->consts);
neworlist = list_concat(neworlist, entry->exprs);
continue;
}
/*
* Do the transformation.
*/
scalar_type = entry->key.consttype;
array_type = OidIsValid(scalar_type) ? get_array_type(scalar_type) :
InvalidOid;
if (OidIsValid(array_type))
{
/*
* OK: coerce all the right-hand non-Var inputs to the common type
* and build an ArrayExpr for them.
* 确定:将所有右侧非 Var 输入强制转换为通用类型并为它们构建一个 ArrayExpr。
*/
List *aexprs = NIL;
ArrayExpr *newa = NULL;
ScalarArrayOpExpr *saopexpr = NULL;
HeapTuple opertup;
Form_pg_operator operform;
List *namelist = NIL;
ListCell *lc2;
foreach(lc2, entry->consts)
{
Node *node = (Node *) lfirst(lc2);
node = coerce_to_common_type(NULL, node, scalar_type,
"OR ANY Transformation");
aexprs = lappend(aexprs, node);
}
newa = makeNode(ArrayExpr);
/* array_collid will be set by parse_collate.c */
newa->element_typeid = scalar_type;
newa->array_typeid = array_type;
newa->multidims = false;
newa->elements = aexprs;
newa->location = -1;
/*
* Try to cast this expression to Const. Due to current strict
* transformation rules it should be done [almost] every time.
* 尝试将此表达式转换为 Const。由于当前严格的转换规则,[几乎] 每次都应该这样做。
*/
newa = (ArrayExpr *) eval_const_expressions(NULL, (Node *) newa);
opertup = SearchSysCache1(OPEROID,
ObjectIdGetDatum(entry->key.opno));
if (!HeapTupleIsValid(opertup))
elog(ERROR, "cache lookup failed for operator %u",
entry->key.opno);
operform = (Form_pg_operator) GETSTRUCT(opertup);
if (!OperatorIsVisible(entry->key.opno))
namelist = lappend(namelist, makeString(get_namespace_name(operform->oprnamespace)));
namelist = lappend(namelist, makeString(pstrdup(NameStr(operform->oprname))));
ReleaseSysCache(opertup);
saopexpr =
(ScalarArrayOpExpr *)
make_scalar_array_op(NULL,
namelist,
true,
(Node *) entry->key.expr,
(Node *) newa,
-1);
saopexpr->inputcollid = entry->key.inputcollid;
neworlist = lappend(neworlist, (void *) saopexpr);
}
else
{
/*
* If the const node's (right side of operator expression) type
* don't have “true” array type, then we cannnot do the
* transformation. We simply concatenate the expression node.
*
* 如果 const 节点(运算符表达式的右侧)的类型没有“真”数组类型,则无法进行转换。
* 我们只需连接表达式节点即可。
*/
list_free(entry->consts);
neworlist = list_concat(neworlist, entry->exprs);
}
}
hash_destroy(or_group_htab);
list_free(entries);
/* One more trick: assemble correct clause */
/* 还有一个技巧:组装正确的子句 */
return neworlist;
}
大概梳理一下该函数,如下:
第一步 初始化:创建一个局部使用的哈希表(HTAB)来 存储 OR条件的分组,按变量和常量来分类。每个OR条件要么是一个简单的布尔操作符表达式(OpExpr),要么被直接保留。
第二步 遍历OR列表:
- 检查每个OR条件是否是一个OpExpr
- 如果条件涉及布尔运算(通过检查返回类型是否为BOOL),则继续处理,否则将条件 保留 (维持原状)
- 识别等式的左侧和右侧操作数,判断其中一侧是否是常量(如果两侧都是常量则跳过该条件)
- 对常量的类型和表达式进行检查,确保它们是简单的变量和常量,不涉及复杂类型或易变函数
- 开始分组 条件:将每个符合条件的表达式按常量和变量分组,存储在哈希表中。如果已经存在相同变量的组,则将常量添加到该组;或者 新开一组
第三步 构建ANY表达式:
- 如果某个变量的分组包含多个常量,则将这些常量转换为数组表达式,并创建一个新的ScalarArrayOpExpr,即ANY表达式
- 如果无法进行转换(如常量没有数组类型),则将原始表达式保留在OR列表中
第四步 返回结果:返回新的OR列表,其中包含优化后的ANY表达式。
调试情况1
postgres=# SET or_to_any_transform_limit = 0;
SET
postgres=# table t1;
id | name
----+------------
1 | oracle
2 | mysql
3 | Sql Server
4 | postgresql
(4 rows)
postgres=#
下面开始调试,SQL 如下:
postgres=# explain(costs off, verbose) select * from t1 where id = 2 or id = 3 or name like '%sql%';
QUERY PLAN
------------------------------------------------------------------------------
Seq Scan on public.t1
Output: id, name
Filter: ((t1.id = ANY ('{2,3}'::integer[])) OR (t1.name ~~ '%sql%'::text))
(3 rows)
postgres=#
首先对于第一个条件,符合要求(两边相等表达式:此类表达式的一侧必须是普通常量或常量表达式 = 另一侧必须是没有易失性函数的变量表达式),开启一个分组,如下(存入到哈希表中):
该条件的opno
为96,自然返回类型就是bool,如下:
// src/include/catalog/pg_operator.dat
{ oid => '96', oid_symbol => 'Int4EqualOperator', descr => 'equal',
oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'int4',
oprright => 'int4', oprresult => 'bool', oprcom => '=(int4,int4)',
oprnegate => '<>(int4,int4)', oprcode => 'int4eq', oprrest => 'eqsel',
oprjoin => 'eqjoinsel' },
HASH_ENTER
之后,返回的entry
就是这样一个代表如下表达式的分组:
hashkey.type = T_Invalid; // 0 忽略
hashkey.expr = (Expr *) nconst_expr; // 就是 id
hashkey.opno = opno; // 96 int =
hashkey.consttype = consttype; // 23
hashkey.inputcollid = exprCollation(const_expr); // 0 忽略
然后该条件进入列表,如下:
entries = lappend(entries, entry);
对于第二个条件,自然也是OK的 对于哈希表来说,同样的key 自然是可以找到上面的分组 如下:
于是条件二 就附加到上一个的entry
中(参考上面的数据结构),如下:
注意:总的entries
还是1
接下来是条件三,它的opno = 1209
,如下:
{ oid => '1209', oid_symbol => 'OID_TEXT_LIKE_OP',
descr => 'matches LIKE expression',
oprname => '~~', oprleft => 'text', oprright => 'text', oprresult => 'bool',
oprnegate => '!~~(text,text)', oprcode => 'textlike', oprrest => 'likesel',
oprjoin => 'likejoinsel' },
它的返回值类型依然是bool,于是继续 如下:
但是它对应的哈希key值,如下:
hashkey.type = T_Invalid; // 0 忽略
hashkey.expr = (Expr *) nconst_expr; // 就是 name
hashkey.opno = opno; // 1209 text ~~
hashkey.consttype = consttype; // 25
hashkey.inputcollid = exprCollation(const_expr); // 100
于是它自己新建一个entry
,并附加到entries
中(这里面现在有2个)。
接下来看一下相关的转换(也就是遍历entries
里面的,检查能否转换),以及构造新的neworlist
,如下:
对于第一个entry
里面就是有两个常量2、3
所以就可以顺利通过如下逻辑(没有被原样保留):
if (list_length(entry->consts) == 1)
{
/*
* Only one element returns origin expression into the BoolExpr
* args list unchanged.
* 只有一个元素将原始表达式不加改变地返回到 BoolExpr 参数列表中
*/
list_free(entry->consts);
neworlist = list_concat(neworlist, entry->exprs);
continue;
}
因为基本类型是int 23
,自然其array类型为1007(可以转换):
// src/include/catalog/pg_type.dat
{ oid => '23', array_type_oid => '1007',
descr => '-2 billion to 2 billion integer, 4-byte storage',
typname => 'int4', typlen => '4', typbyval => 't', typcategory => 'N',
typinput => 'int4in', typoutput => 'int4out', typreceive => 'int4recv',
typsend => 'int4send', typalign => 'i' },
注:若是这里 const 节点(运算符表达式的右侧)的类型没有 “真” 数组类型,则无法进行转换。我们只需连接表达式节点即可。
转换的过程,如下:
然后先创建一个ArrayExpr结点,如下:
newa = makeNode(ArrayExpr);
/* array_collid will be set by parse_collate.c */
newa->element_typeid = scalar_type; // 23
newa->array_typeid = array_type; // 1007
newa->multidims = false;
newa->elements = aexprs; // 2、3
newa->location = -1;
然后根据这个entry->key.opno = 96
,可以得到oprname => '='
;此外array的变量 就是entry->key.expr = id列
,如下:
至此,条件id = 2 or id = 3
,就转换成了t1.id = ANY ('{2,3}'::integer[])
。
至于第二个entry->consts = 1个
,无法转换 自然就和上面说明那样直接拼接到neworlist
,如下:
完成这一切之后哈希表就可以销毁了,hash_destroy(or_group_htab);
而这个也是其他开发人员所质疑的一点。
调试情况2
postgres=# explain(costs off, verbose) select * from t1 where id = 2 or id = 3 or name like '%sql%' or name like '%server%';
QUERY PLAN
-------------------------------------------------------------------------------------------------
Seq Scan on public.t1
Output: id, name
Filter: ((t1.id = ANY ('{2,3}'::integer[])) OR (t1.name ~~ ANY ('{%sql%,%server%}'::text[])))
(3 rows)
postgres=#
如上SQL 补充了情况1下的 第二个entry->consts = 1个
,无法转换。具体调试不再展开。
调试情况3
上面说的 两边相等表达式 分组,并非特指 =
表达式。如下SQL:
postgres=# explain(costs off, verbose) select * from t1 where id < 2 or id < 3 ;
QUERY PLAN
----------------------------------------------
Seq Scan on public.t1
Output: id, name
Filter: (t1.id < ANY ('{2,3}'::integer[]))
(3 rows)
postgres=#
快速调试,如下:
// 相同操作符
{ oid => '97', oid_symbol => 'Int4LessOperator', descr => 'less than',
oprname => '<', oprleft => 'int4', oprright => 'int4', oprresult => 'bool',// here
oprcom => '>(int4,int4)', oprnegate => '>=(int4,int4)', oprcode => 'int4lt',
oprrest => 'scalarltsel', oprjoin => 'scalarltjoinsel' },
两次对应的哈希key值,如下:
hashkey.type = T_Invalid; // 0 忽略
hashkey.expr = (Expr *) nconst_expr; // 就是 id
hashkey.opno = opno; // 97 int <
hashkey.consttype = consttype; // 23
hashkey.inputcollid = exprCollation(const_expr); // 0
因为基本类型是int 23
,自然其array类型为1007(可以转换)。这次有所不同的就是操作符的变化:
调试情况4
postgres=# explain(costs off, verbose) select * from t1 where id < 2 or id > 3 ;
QUERY PLAN
----------------------------------------
Seq Scan on public.t1
Output: id, name
Filter: ((t1.id < 2) OR (t1.id > 3))
(3 rows)
postgres=#
如果是这种情况,两个操作符不同 那么就会在哈希表中插入两个不同的entry
,但是每个entry->consts = 1个
,无法转换,最后就维持了原样。
社区争议回退原因
SHA-1: ff9f72c68f678ded340b431c3e280fe56644a3e7
* revert: Transform OR clauses to ANY expression
This commit reverts 72bd38cc99 due to implementation and design issues.
Reported-by: Tom Lane
有兴趣的小伙伴可以自行查看邮件列表:
- https://postgr.es/m/3604469.1712628736%40sss.pgh.pa.us
本文不再赘述,相信不久的将来 该规则还会回来。