PostgreSQL的学习心得和知识总结(一百五十三)|[performance]将 OR 子句转换为 ANY 表达式


注:提前言明 本文借鉴了以下博主、书籍或网站的内容,其列表如下:

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的内容,如下:

  1. 在优化的初步阶段,当我们仍在处理表达式树时,将 (expr op C1) OR (expr op C2) ... 替换为 expr op ANY(ARRAY[C1,C2, ...])
  2. 这里 Cn 是第 n 个常量表达式,“expr”是非常量表达式,“op”是一个返回布尔结果并具有通勤器的运算符commuter(用于表达式的常量和非常量部分的反向顺序的情况,如 Cn op expr
  3. 有时它会导致计划不理想。这就是为什么有 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 | mysql4 | 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.t1Output: id, nameRecheck Cond: ((t1.id = 2) OR (t1.id = 4))->  BitmapOr->  Bitmap Index Scan on t1_pkeyIndex Cond: (t1.id = 2)->  Bitmap Index Scan on t1_pkeyIndex 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.t1Output: id, nameRecheck Cond: (t1.id = ANY ('{2,4}'::integer[]))->  Bitmap Index Scan on t1_pkeyIndex 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.t1Output: id, nameRecheck Cond: (t1.id = ANY ('{2,4}'::integer[]))->  Bitmap Index Scan on t1_pkeyIndex 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.t1Output: id, nameRecheck Cond: (t1.id = ANY ('{2,4}'::integer[]))->  Bitmap Index Scan on t1_pkeyIndex 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.t1Output: id, nameRecheck Cond: ((t1.id = 2) OR (t1.id = 4))->  BitmapOr->  Bitmap Index Scan on t1_pkeyIndex Cond: (t1.id = 2)->  Bitmap Index Scan on t1_pkeyIndex 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=#

函数流程解释 简要如下:

  1. 处理空 OR 列表

    • 如果传入的 orlist 为空(没有子句),返回一个恒为 FALSE 的布尔常量,表示 OR 为空的情况。
  2. 处理单一子句

    • 如果 orlist 只有一个子句,直接返回该子句,因为一个单独的 OR 子句不需要进一步简化。
  3. 选择最短的 AND 子句作为参考

    • 遍历 orlist,找到最短的 AND 子句作为参考。如果我们发现一个不是 AND 的子句,我们可以将其视为一个单元素 AND 子句,该子句必然是最短的。
  4. 消除参考子句中的重复

    • 对参考子句进行去重,以确保参考列表中没有重复。
  5. 查找所有 OR 子句的共同条件

    • 遍历参考子句,检查每个参考子句是否存在于所有 OR 子句中。如果参考子句出现在所有 OR 子句中,则将其标记为“胜出者” (winners)。
  6. 处理没有胜出者的情况

    • 如果没有共同的胜出子句,检查 OR 子句列表的长度。如果 OR 子句足够长,则尝试将其转换为 SAOP(ScalarArrayOp: Scalar Array Op,即标量数组操作)。转换后返回优化后的 OR 子句列表。
  7. 有胜出者的情况,生成由剩余子句组成的新 OR 列表

    • 生成一个新的 OR 列表,去除已在所有子句中胜出的条件。如果任何子句退化为空,则会出现类似 (A AND B) OR (A) 的情况,该情况可以简化为 A — 也就是说,OR 的其他分支中的附加条件无关紧要。请注意,由于我们使用 list_difference,因此 AND 子句中多次出现的获胜子句将被自动删除。
  8. 再次尝试进行 SAOP 转换

    • 再次检查新生成的 OR 列表,如果其长度达到转换限制,则尝试将其转换为 SAOP。
  9. 处理特殊情况并构建最终的表达式

    • 如果新的 OR 列表不为空,则将其与胜出的子句合并。如果简化的 OR 不是退化的,则将其附加到获胜者列表中,正确处理一个元素的特殊情况(这真的会发生吗?)。此外,还要小心维护 AND/OR 平坦度,以防我们拉出子子 OR 子句。
  10. 最后 若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

原理讲解

在开始之前 先看一下重要的数据结构 keyentry(因为该函数内部使用了一个哈希表 这个也是其他人不太喜欢的一个点),如下:

/** 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列表:

  1. 检查每个OR条件是否是一个OpExpr
  2. 如果条件涉及布尔运算(通过检查返回类型是否为BOOL),则继续处理,否则将条件 保留 (维持原状)
  3. 识别等式的左侧和右侧操作数,判断其中一侧是否是常量(如果两侧都是常量则跳过该条件)
  4. 对常量的类型和表达式进行检查,确保它们是简单的变量和常量,不涉及复杂类型或易变函数
  5. 开始分组 条件:将每个符合条件的表达式按常量和变量分组,存储在哈希表中。如果已经存在相同变量的组,则将常量添加到该组;或者 新开一组

第三步 构建ANY表达式:

  1. 如果某个变量的分组包含多个常量,则将这些常量转换为数组表达式,并创建一个新的ScalarArrayOpExpr,即ANY表达式
  2. 如果无法进行转换(如常量没有数组类型),则将原始表达式保留在OR列表中

第四步 返回结果:返回新的OR列表,其中包含优化后的ANY表达式。


调试情况1

postgres=# SET or_to_any_transform_limit = 0;
SET
postgres=# table t1;id |    name    
----+------------1 | oracle2 | mysql3 | Sql Server4 | 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.t1Output: id, nameFilter: ((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; // 就是 idhashkey.opno = opno; // 96 int = hashkey.consttype = consttype; // 23hashkey.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; // 就是 namehashkey.opno = opno; // 1209 text ~~ hashkey.consttype = consttype; // 25hashkey.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; // 23newa->array_typeid = array_type; // 1007newa->multidims = false;newa->elements = aexprs; // 2、3newa->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.t1Output: id, nameFilter: ((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.t1Output: id, nameFilter: (t1.id < ANY ('{2,3}'::integer[]))
(3 rows)postgres=#

快速调试,如下:

// 相同操作符{ oid => '97', oid_symbol => 'Int4LessOperator', descr => 'less than',oprname => '<', oprleft => 'int4', oprright => 'int4', oprresult => 'bool',// hereoprcom => '>(int4,int4)', oprnegate => '>=(int4,int4)', oprcode => 'int4lt',oprrest => 'scalarltsel', oprjoin => 'scalarltjoinsel' },

两次对应的哈希key值,如下:

		hashkey.type = T_Invalid; // 0 忽略hashkey.expr = (Expr *) nconst_expr; // 就是 idhashkey.opno = opno; // 97 int <hashkey.consttype = consttype; // 23hashkey.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.t1Output: id, nameFilter: ((t1.id < 2) OR (t1.id > 3))
(3 rows)postgres=#

如果是这种情况,两个操作符不同 那么就会在哈希表中插入两个不同的entry,但是每个entry->consts = 1个,无法转换,最后就维持了原样。

在这里插入图片描述


社区争议回退原因

SHA-1: ff9f72c68f678ded340b431c3e280fe56644a3e7* revert: Transform OR clauses to ANY expressionThis commit reverts 72bd38cc99 due to implementation and design issues.Reported-by: Tom Lane

有兴趣的小伙伴可以自行查看邮件列表:

  • https://postgr.es/m/3604469.1712628736%40sss.pgh.pa.us

本文不再赘述,相信不久的将来 该规则还会回来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1559158.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

树控件QTreeWidget

树控件跟表格控件类似&#xff0c;也可以有多列&#xff0c;也可以只有1列&#xff0c;可以有多行&#xff0c;只不过每一行都是一个QTreeWidgetItem&#xff0c;每一行都是一个可以展开的树 常用属性和方法 显示和隐藏标题栏 树控件只有水平标题栏 //获取和设置标题栏的显…

PPT在线画SWOT分析图!这2个在线软件堪称办公必备!

swot分析ppt怎么做&#xff1f; swot分析是一个非常常用的战略分析框架&#xff0c;经常会在ppt中使用。想在ppt中绘制swot分析图&#xff0c;使用自带的形状工具可以制作出来&#xff0c;但绘制效率不够高&#xff0c;在需要大批量制作的场景下&#xff0c;会让人非常心累………

DepthB2R靶机打靶记录

一、靶机介绍 下载地址&#xff1a;https://download.vulnhub.com/depth/DepthB2R.ova 二、信息收集 根据靶机主页显示&#xff0c;确认靶机ip为192.168.242.132 端口扫描 nmap -p- -A 192.168.242.132 发现只开放了8080端口 用dirsearch扫个目录 apt-get update apt-get …

基于LORA的一主多从监测系统_0.96OLED

关联&#xff1a;0.96OLED hal硬件I2C LORA 在本项目中每个节点都使用oled来显示采集到的数据以及节点状态&#xff0c;OLED使用I2C接口与STM32连接&#xff0c;这个屏幕内部驱动IC为SSD1306&#xff0c;SSD1306作为从机地址为0x78 发送数据&#xff1a;起始…

【Linux】基本认知全套入门

目录 Linux简介 Linux发行版本 发行版选择建议 Centos-社区企业操作系统 Centos版本选择 Linux系统目录 Linux常用命令 SSH客户端 Linux文件操作命令 vim重要快捷键 应用下载与安装 netstat&#xff0c;ps与kill命令使用 Linux应用服务化 Linux用户与权限 Linu…

Telephony CarrierConfig配置

1、CarrierConfig配置介绍 CarrierConfig&#xff08;运营商配置&#xff09;&#xff0c;是Android为了针对不同运营商配置不同功能的配置文件&#xff0c;类似Modem的MBN配置&#xff0c;可以实现插入不同运营商卡&#xff0c;不同的功能实现或菜单显示等。 2、CarrierConfig…

力扣之1355.活动参与者

题目&#xff1a; Sql 测试用例&#xff1a; Create table If Not Exists Friends (id int, name varchar(30), activity varchar(30)); Create table If Not Exists Activities (id int, name varchar(30)); Truncate table Friends; insert into Friends (id, name, acti…

【数据结构与算法-高阶】并查集

【数据结构与算法-高阶】并查集 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;数据结构与算法&#x1f345; &#x1f33c;文章目录&#x1f33c; 1. 并查集原理 2. 并查集实现 3. 并查集应用 1. 并查集原理 在一些应用问题中&…

charAt,chartCodeAt,codePointAt,fromCodePoint,fromCharCode

生僻字的length算2,有些空格是特殊空格,比如\u3000 u3000不是全角空格&#xff0c;u3000是表意字空格&#xff08;Ideographic Space&#xff09;&#xff0c;宽度和一个表意字&#xff08;汉字&#xff09;相同。它应当被当做汉字来处理。比如&#xff0c;在一些排版中&#x…

OpenSource - License 开源项目 TrueLicense

文章目录 官网集成Demo 官网 https://truelicense.namespace.global/ https://github.com/christian-schlichtherle/truelicense 集成Demo https://github.com/christian-schlichtherle/truelicense-maven-archetype https://github.com/zifangsky/LicenseDemo https://git…

map和set(c++)

前言 在前面我们在介绍二叉搜索树时我们分别实现了一个key结构和key-val结构&#xff0c;如果我们再进一步完善这棵树&#xff0c;将二叉搜索树升级为红黑树去存储key和key-val那么我们就可以得到我们今天要介绍的主角map和set。当然了标准库的实现还是有很多需要注意的地方&a…

植物大战僵尸修改器-MFC

创建项目 创建mfc应用 基于对话框 打开资源视图下的 IDD_MFCAPPLICTION2_DIALOG 限制对话框大小 将属性中Border的值改为对话框外框 删除对话框中原有的控件 属性-外观-Caption 设置对话框标题 工具箱中拖放一个按钮 修改按钮名称 将按钮ID改为IDC_COURSE 在MFCApplication2…

k8s微服务

一 、什么是微服务 用控制器来完成集群的工作负载&#xff0c;那么应用如何暴漏出去&#xff1f;需要通过微服务暴漏出去后才能被访问 Service是一组提供相同服务的Pod对外开放的接口。 借助Service&#xff0c;应用可以实现服务发现和负载均衡。 service默认只支持4层负载均…

QT安装成功后-在创建项目时,发现仅有项目名文件

&#xff08;1&#xff09;QT安装成功后&#xff0c;发现仅有项目名文件其他可编辑文件缺失 &#xff08;2&#xff09;点击文件名左上角的感叹号显示【No kits are enabled for this project. Enable】 小编在尝试多次后发现&#xff0c;可以通过以下方式解决&#xff1a;QT软…

接着上一篇stp 实验继续

理论看上一篇&#xff0c;我们直接实验 首先找出&#xff52;&#xff4f;&#xff4f;&#xff54; 桥 很明显 &#xff53;&#xff57;&#xff11; 为&#xff52;&#xff4f;&#xff4f;&#xff54; 桥&#xff0c;所谓&#xff53;&#xff57;&#xff11;  &a…

从Hinton获得今年的诺贝尔物理学奖说起

“深度人工智能”是成都深度智谷科技旗下的人工智能教育机构订阅号&#xff0c;主要分享人工智能的基础知识、技术发展、学习经验等。此外&#xff0c;订阅号还为大家提供了人工智能的培训学习服务和人工智能证书的报考服务&#xff0c;欢迎大家前来咨询&#xff0c;实现自己的…

JavaSE——集合1:Collection接口(Iterator和增强for遍历集合)

目录 一、集合框架体系(重要) 二、集合引入 (一)集合的理解与好处 三、Collection接口 (一)Collection接口实现类的特点 (二)Collection接口常用方法 (三)Collection接口遍历元素的方式(Iterator和增强for) 1.使用Iterator(迭代器) 1.1Iterator(迭代器)介绍 1.2Itera…

OmniH2O——通用灵巧且可全身远程操作并学习的人形机器人(其前身H2O是HumanPlus的重要参考)

前言 由于我司一直在针对各个工厂、公司、客户特定的业务场景&#xff0c;做解决方案或定制开发&#xff0c;所以针对每一个场景&#xff0c;我们都会反复考虑用什么样的机器人做定制开发 于此&#xff0c;便不可避免的追踪国内外最前沿的机器人技术进展&#xff0c;本来准备…

指针函数C++

指针函数概念 指针函数在C中是一种特殊类型的函数。从本质上讲&#xff0c;它是一个函数&#xff0c;不过其返回值是一个指针类型的数据。例如&#xff0c;像int* plusfunction(int a, int b);这样的函数声明&#xff0c;plusfunction就是一个指针函数&#xff0c;它接受两个i…

【学习笔记】Linux系统基础知识4 —— date命令详解

提示&#xff1a;学习Linux系统基础命令 date 命令详解 一、前期准备 1.已经正确安装并成功进入Linux系统 说明&#xff1a;本实验采用的 Redhat 系统&#xff08;因系统不一致&#xff0c;可能部分显示存在差异&#xff09; 二、学习内容 1、date命令 1. 功能说明 date …