当前位置: 首页 > news >正文

网站设计存在的问题建设监理协会网站

网站设计存在的问题,建设监理协会网站,嘉兴网页制作网站排名,佛山做网站业务工资目录结构 注#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容#xff0c;其列表如下#xff1a; 1、参考书籍#xff1a;《PostgreSQL数据库内核分析》 2、参考书籍#xff1a;《数据库事务处理的艺术#xff1a;事务管理与并发控制》 3、PostgreSQL数据库仓库… 目录结构 注提前言明 本文借鉴了以下博主、书籍或网站的内容其列表如下 1、参考书籍《PostgreSQL数据库内核分析》 2、参考书籍《数据库事务处理的艺术事务管理与并发控制》 3、PostgreSQL数据库仓库链接点击前往 4、日本著名PostgreSQL数据库专家 铃木启修 网站主页点击前往 5、参考书籍《PostgreSQL中文手册》 6、参考书籍《PostgreSQL指南内幕探索》点击前往 7、参考书籍《事务处理 概念与技术》 8、Magic Tricks for Postgres psql: Settings, Presets, Echo, and Saved Queries点击前往 1、本文内容全部来源于开源社区 GitHub和以上博主的贡献本文也免费开源可能会存在问题评论区等待大佬们的指正 2、本文目的开源共享 抛砖引玉 一起学习 3、本文不提供任何资源 不存在任何交易 与任何组织和机构无关 4、大家可以根据需要自行 复制粘贴以及作为其他个人用途但是不允许转载 不允许商用 写作不易还请见谅 5、本文内容基于PostgreSQL master源码开发而成 更好地处理冗余 IS [NOT] NULL 限定符 文章快速说明索引功能使用背景说明功能实现使用说明功能实现源码解析简单情况的处理永真永假的处理or语句的处理 连接情况的处理是否可忽略是否可简化or语句的处理 父子继承的处理 文章快速说明索引 学习目标 做数据库内核开发久了就会有一种 少年得志年少轻狂 的错觉然鹅细细一品觉得自己其实不算特别优秀 远远没有达到自己想要的。也许光鲜的表面掩盖了空洞的内在每每想到于此皆有夜半临渊如履薄冰之感。为了睡上几个踏实觉即日起 暂缓其他基于PostgreSQL数据库的兼容功能开发近段时间 将着重于学习分享Postgres的基础知识和实践内幕。 学习内容详见目录 1、更好地处理冗余 IS [NOT] NULL 限定符 学习时间 2024年08月01日 22:16:59 学习产出 1、PostgreSQL数据库基础知识回顾 1个 2、CSDN 技术博客 1篇 3、PostgreSQL数据库内核深入学习 注下面我们所有的学习环境是Centos8PostgreSQL master Oracle19CMySQL8.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功能使用背景说明 patch1如下 SHA-1: b262ad440edecda0b1aba81d967ab560a83acb8a* Add better handling of redundant IS [NOT] NULL qualsUntil now PostgreSQL has not been very smart about optimizing away IS NOT NULL base quals on columns defined as NOT NULL. The evaluation of these needless quals adds overhead. Ordinarily, anyone who came complaining about that would likely just have been told to not include the qual in their query if it’s not required. However, a recent bug report indicates this might not always be possible. Bug 17540 highlighted that when we optimize Min/Max aggregates the IS NOT NULL qual that the planner adds to make the rewritten plan ignore NULLs can cause issues with poor index choice. That particular case demonstrated that other quals, especially ones where no statistics are available to allow the planner a chance at estimating an approximate selectivity for can result in poor index choice due to cheap startup paths being prefered with LIMIT 1. Here we take generic approach to fixing this by having the planner check for NOT NULL columns and just have the planner remove these quals (when they’re not needed) for all queries, not just when optimizing Min/Max aggregates. Additionally, here we also detect IS NULL quals on a NOT NULL column and transform that into a gating qual so that we don’t have to perform the scan at all. This also works for join relations when the Var is not nullable by any outer join. This also helps with the self-join removal work as it must replace strict join quals with IS NOT NULL quals to ensure equivalence with the original query. 到目前为止PostgreSQL 在优化定义为 NOT NULL 的列上的 IS NOT NULL 基本限定符方面还不是很聪明。评估这些不必要的限定符会增加开销。通常任何抱怨这一点的人都可能被告知如果不需要不要在查询中包含限定符。然而最近的错误报告表明这可能并不总是可行的。 Bug 17540 强调当我们优化 Min/Max 聚合时规划器添加的 IS NOT NULL 限定符会使重写计划忽略 NULL这可能会导致索引选择不当的问题。该特定案例表明其他限定条件尤其是没有可用统计数据让规划器有机会估计近似选择性的限定条件可能会导致索引选择不佳因为 LIMIT 1 优先选择廉价的启动路径。 在这里我们采用通用方法解决这个问题让规划器检查 NOT NULL 列并让规划器在所有查询中删除这些限定条件当它们不需要时而不仅仅是在优化最小/最大聚合时。 此外这里我们还检测 NOT NULL 列上的 IS NULL 限定条件并将其转换为门控限定条件这样我们就不必执行扫描了。当 Var 不能被任何外连接为空时这也适用于连接关系。 这也有助于自连接删除工作因为它必须用 IS NOT NULL 限定条件替换严格连接限定条件以确保与原始查询等效。 功能实现使用说明 首先看一下使用案例案例一如下(测试 始终为真 的限制是否被忽略始终为假 的限制是否被常量 FALSE 替换) 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-15), 64-bit (1 row)-- 目前我们只检查 NullTest 限定词和包含 NullTest 限定词的 OR 子句。我们可能会在未来扩展它 postgres# CREATE TABLE pred_tab (a int NOT NULL, b int, c int NOT NULL); CREATE TABLE-- 全文用例1 -- 测试限制条款 -- 确保当列不可为空时忽略 IS_NOT_NULL 限定符 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t WHERE t.a IS NOT NULL;QUERY PLAN ------------------------Seq Scan on pred_tab t (1 row)-- 全文用例2 -- 确保可空列上的 IS_NOT_NULL 限定符不会被忽略 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t WHERE t.b IS NOT NULL;QUERY PLAN ---------------------------Seq Scan on pred_tab tFilter: (b IS NOT NULL) (2 rows)-- 全文用例3 -- 确保对于不可为空的列IS_NULL 限定符裁剪为常量 FALSE postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t WHERE t.a IS NULL;QUERY PLAN --------------------------ResultOne-Time Filter: false (2 rows)-- 全文用例4 -- 确保在可空列上 IS_NULL 限定符不会降低为常量 FALSE postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t WHERE t.b IS NULL;QUERY PLAN ------------------------Seq Scan on pred_tab tFilter: (b IS NULL) (2 rows)postgres#案例二如下(限制条款中的 OR 条款测试) -- 全文用例5 -- 确保当 OR 分支始终为真时OR 子句被忽略 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t WHERE t.a IS NOT NULL OR t.b 1;QUERY PLAN ------------------------Seq Scan on pred_tab t (1 row)-- 全文用例6 -- 确保对于无法证明始终正确的 NullTest不会忽略 OR 子句 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t WHERE t.b IS NOT NULL OR t.a 1;QUERY PLAN ----------------------------------------Seq Scan on pred_tab tFilter: ((b IS NOT NULL) OR (a 1)) (2 rows)-- 全文用例7 -- 确保当所有分支均可证明为假时OR 子句简化为常数 FALSE postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL;QUERY PLAN --------------------------ResultOne-Time Filter: false (2 rows)-- 全文用例8 -- 确保当并非所有分支都可证明为假时OR 子句不会简化为常量 FALSE postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t WHERE t.b IS NULL OR t.c IS NULL;QUERY PLAN ----------------------------------------Seq Scan on pred_tab tFilter: ((b IS NULL) OR (c IS NULL)) (2 rows)postgres#备注上面案例(最下面这个)是不是优化到 Filter: (b IS NULL)会更好因为c IS NULL永假如下 postgres# EXPLAIN (COSTS OFF) SELECT * FROM pred_tab t WHERE t.b IS NULL and t.c IS NULL;QUERY PLAN --------------------------ResultOne-Time Filter: false (2 rows)postgres#案例三如下(测试连接子句) -- 全文用例9 -- 确保忽略 IS_NOT_NULL 限定符因为 a) 它位于 NOT NULL 列上并且 b) 其 Var 不能通过任何外连接为空 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t1 postgres-# LEFT JOIN pred_tab t2 ON TRUE postgres-# LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;QUERY PLAN -------------------------------------------------Nested Loop Left Join- Seq Scan on pred_tab t1- Materialize- Nested Loop Left Join- Seq Scan on pred_tab t2- Materialize- Seq Scan on pred_tab t3 (7 rows)-- 全文用例10 -- 确保当外连接将列设为可空时不会忽略 IS_NOT_NULL 限定符 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t1 postgres-# LEFT JOIN pred_tab t2 ON t1.a 1 postgres-# LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;QUERY PLAN -------------------------------------------Nested Loop Left JoinJoin Filter: (t2.a IS NOT NULL)- Nested Loop Left JoinJoin Filter: (t1.a 1)- Seq Scan on pred_tab t1- Materialize- Seq Scan on pred_tab t2- Materialize- Seq Scan on pred_tab t3 (9 rows)-- 全文用例11 -- 确保 IS_NULL 限定符被简化为常量 FALSE因为 a) 它位于 NOT NULL 列上并且 b) 它的 Var 不能通过任何外连接为空 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t1 postgres-# LEFT JOIN pred_tab t2 ON TRUE postgres-# LEFT JOIN pred_tab t3 ON t2.a IS NULL AND t2.b 1;QUERY PLAN ---------------------------------------------------Nested Loop Left Join- Seq Scan on pred_tab t1- Materialize- Nested Loop Left JoinJoin Filter: (false AND (t2.b 1))- Seq Scan on pred_tab t2- ResultOne-Time Filter: false (8 rows)-- 全文用例12 -- 确保当列通过外连接可为空时IS_NULL 限定符不会简化为常量 FALSE postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t1 postgres-# LEFT JOIN pred_tab t2 ON t1.a 1 postgres-# LEFT JOIN pred_tab t3 ON t2.a IS NULL;QUERY PLAN -------------------------------------------Nested Loop Left JoinJoin Filter: (t2.a IS NULL)- Nested Loop Left JoinJoin Filter: (t1.a 1)- Seq Scan on pred_tab t1- Materialize- Seq Scan on pred_tab t2- Materialize- Seq Scan on pred_tab t3 (9 rows)postgres#案例四如下(测试连接子句中的 OR 子句) -- 全文用例13 -- 当可证明 OR 分支始终为真时确保忽略 OR 子句 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t1 postgres-# LEFT JOIN pred_tab t2 ON TRUE postgres-# LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b 1;QUERY PLAN -------------------------------------------------Nested Loop Left Join- Seq Scan on pred_tab t1- Materialize- Nested Loop Left Join- Seq Scan on pred_tab t2- Materialize- Seq Scan on pred_tab t3 (7 rows)postgres#-- 全文用例14 -- 确保当外连接中的列可为空时不会忽略 NullTest postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t1 postgres-# LEFT JOIN pred_tab t2 ON t1.a 1 postgres-# LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b 1;QUERY PLAN ---------------------------------------------------Nested Loop Left JoinJoin Filter: ((t2.a IS NOT NULL) OR (t2.b 1))- Nested Loop Left JoinJoin Filter: (t1.a 1)- Seq Scan on pred_tab t1- Materialize- Seq Scan on pred_tab t2- Materialize- Seq Scan on pred_tab t3 (9 rows)postgres#-- 全文用例15 -- 确保当所有 OR 分支均可证明为假时OR 子句简化为常数 FALSE postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t1 postgres-# LEFT JOIN pred_tab t2 ON TRUE postgres-# LEFT JOIN pred_tab t3 ON (t2.a IS NULL OR t2.c IS NULL) AND t2.b 1;QUERY PLAN ---------------------------------------------------Nested Loop Left Join- Seq Scan on pred_tab t1- Materialize- Nested Loop Left JoinJoin Filter: (false AND (t2.b 1))- Seq Scan on pred_tab t2- ResultOne-Time Filter: false (8 rows)postgres#-- 全文用例16 -- 确保当外连接中的列变为可空时OR 子句不会简化为常量 FALSE postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_tab t1 postgres-# LEFT JOIN pred_tab t2 ON t1.a 1 postgres-# LEFT JOIN pred_tab t3 ON t2.a IS NULL OR t2.c IS NULL;QUERY PLAN ---------------------------------------------------Nested Loop Left JoinJoin Filter: ((t2.a IS NULL) OR (t2.c IS NULL))- Nested Loop Left JoinJoin Filter: (t1.a 1)- Seq Scan on pred_tab t1- Materialize- Seq Scan on pred_tab t2- Materialize- Seq Scan on pred_tab t3 (9 rows)postgres#案例五如下(验证我们是否通过继承父级正确处理 IS NULL 和 IS NOT NULL 限定词) postgres# CREATE TABLE pred_parent (a int); CREATE TABLE postgres# CREATE TABLE pred_child () INHERITS (pred_parent); CREATE TABLE postgres# ALTER TABLE ONLY pred_parent ALTER a SET NOT NULL; ALTER TABLE postgres# \d pred_parentTable public.pred_parentColumn | Type | Collation | Nullable | Default -----------------------------------------------a | integer | | not null | Number of child tables: 1 (Use \d to list them.)postgres# \d pred_child Table public.pred_childColumn | Type | Collation | Nullable | Default -----------------------------------------------a | integer | | | Inherits: pred_parentpostgres# \d pred_parentTable public.pred_parentColumn | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ------------------------------------------------------------------------------------------------a | integer | | not null | | plain | | | Child tables: pred_child Access method: heappostgres# \d pred_child Table public.pred_childColumn | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ------------------------------------------------------------------------------------------------a | integer | | | | plain | | | Inherits: pred_parent Access method: heappostgres#-- 全文用例17 -- 确保对 pred_child 的扫描包含 IS NOT NULL 限定符 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_parent WHERE a IS NOT NULL;QUERY PLAN ---------------------------------------------Append- Seq Scan on pred_parent pred_parent_1- Seq Scan on pred_child pred_parent_2Filter: (a IS NOT NULL) (4 rows)postgres#-- 全文用例18 -- 确保我们只扫描 pred_child 而不扫描 pred_pa​​rent postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_parent WHERE a IS NULL;QUERY PLAN ------------------------------------Seq Scan on pred_child pred_parentFilter: (a IS NULL) (2 rows)postgres#postgres# ALTER TABLE pred_parent ALTER a DROP NOT NULL; ALTER TABLE postgres# ALTER TABLE pred_child ALTER a SET NOT NULL; ALTER TABLE postgres# \d pred_parentTable public.pred_parentColumn | Type | Collation | Nullable | Default -----------------------------------------------a | integer | | | Number of child tables: 1 (Use \d to list them.)postgres# \d pred_child Table public.pred_childColumn | Type | Collation | Nullable | Default -----------------------------------------------a | integer | | not null | Inherits: pred_parentpostgres#-- 全文用例19 -- 确保从 pred_child 扫描中删除 IS NOT NULL 限定符。 postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_parent WHERE a IS NOT NULL;QUERY PLAN ---------------------------------------------Append- Seq Scan on pred_parent pred_parent_1Filter: (a IS NOT NULL)- Seq Scan on pred_child pred_parent_2 (4 rows)postgres#-- 全文用例20 -- 确保我们只扫描 pred_pa​​rent 而不扫描 pred_child postgres# EXPLAIN (COSTS OFF) postgres-# SELECT * FROM pred_parent WHERE a IS NULL;QUERY PLAN -------------------------Seq Scan on pred_parentFilter: (a IS NULL) (2 rows)postgres#功能实现源码解析 在SQL的操作中几乎所有的操作比如查询最终都会落在实际的表上那么在执行计划中表的表示就比较重要。PostgreSQL用RelOptInfo结构体来表示它是执行计划路径生成的主要数据结构同样用于表述表、子查询、函数等。 在查询优化的过程中我们首先面对的是 FROM 子句中的表通常称之为范围表(RangeTable它可能是一个常规意义上的表 也可能是一个子查询或者还可能是一个查询结果的组织为表状的函数例如 TableFunction这些表处于查询执行计划树的叶子节点是产生最终查询结果的基础我们称之为基表这些基表可以用 RelOptlnfo 结构体表示它的RelOptlnfo-reloptkind 是 RELOPT_BASEREL 。 基表之间可以进行连接操作连接操作产生的“中间”结果也可以用 RelOptlnfo 结构体来表示它对应的 RelOptlnfo-reloptkind是LOPT_JOINREL 。另外还有RELOPT_OTHER_MEMBER_REL 类型的 RelOptlnfo 用来表示继承表的子表或者 UNION 操作的子查询等。由于 RelOptlnfo 结构体集多种功能于一身因此它的体积也比较庞大我们会随着学习的深入逐个学习和分析 注若是对该数据结构感兴趣可以自行查看 张树杰 《查询优化深度探索》4.1.1章节 RelOptInfo结构体是贯穿整个path生成过程的一个数据结构生成路径的最终结果始终存放其中生成和选择路径所需的许多数据也存放在其中。路径生成和选择涉及的所有操作几乎都是针对RelOptInfo结构体操作。 RelOptInfo结构体涉及baserel(基本关系)和joinrel(连接关系)baserel(基本关系)是一个普通表子查询或者范围表中出现的函数joinrel(连接关系)是两个或者两个以上的baserel(基本关系)在一定约束条件下的简单合并对任何一组baserel仅有一个joinrel即对于任何给定的baserel集合只有一个RelOptInfo结构体 /** RelOptInfo用于规划/优化的每个关系信息* * 出于规划目的base rel要么是普通关系表要么是出现在范围表中的子 SELECT 或函数的输出。* 无论哪种情况它都由 RT 索引唯一标识。* joinrel是两个或多个基础关系的连接。* 连接关系由其组件基础关系的 RT 索引集以及它已计算的任何外连接的 RT 索引标识。* 我们为每个基础关系和连接关系创建 RelOptInfo 节点并将它们分别存储在 PlannerInfo 的 simple_rel_array 和 join_rel_list 中。* * 请注意对于任何给定的组件基础关系集只有一个连接关系无论我们以何种顺序组装它们因此无序集是识别它的正确数据类型。* * 我们还有other rels它们与基础关系类似因为它们引用单个 RT 索引但它们不是连接树的一部分并被赋予不同的 RelOptKind 来标识它们。* 目前唯一类型的 otherrels 是为append relation的成员关系即继承集或 UNION ALL 子查询制作的。* * 附加关系具有父 RTE它是基关系代表整个附加关系。成员 RTE 是 otherrels。* 父级存在于查询连接树中但成员不存在。* 成员 RTE 和 otherrels 用于计划对附加集的各个表或子查询的扫描* 然后为父基关系提供 Append 和/或 MergeAppend 路径这些路径包含各个成员关系的最佳路径。* 有关更多信息请参阅 AppendRelInfo 的注释。* * 我们曾经还制作了 otherrels 来表示连接 RTE用于处理连接别名变量。* 目前不需要这样做因为所有连接别名变量在 preprocess_expression 期间都扩展为非别名形式。* * 我们还有表示不同分区表的子关系之间的连接的关系。* 这些关系不会添加到 join_rel_level 列表中因为它们不是由动态规划算法直接连接的。* * 还有一个用于upper关系的 RelOptKind它们是描述扫描/连接后处理步骤例如聚合的 RelOptInfos。* 这些 RelOptInfos 中的许多字段都没有意义但它们的 Path 字段始终包含显示执行该处理步骤的方法的 Paths。* * 此数据结构的部分内容特定于各种扫描和连接机制。似乎不值得为它们创建新的节点类型。*/关于RelOptInfo数据结构将是我们后面学习的重点 但是限于本篇的篇幅我们不在展开 今天学习的重点如下 // src/include/nodes/pathnodes.htypedef struct RelOptInfo {.../** Zero-based set containing attnums of NOT NULL columns. Not populated* for rels corresponding to non-partitioned inhtrue RTEs.* * 包含非空列的 attnums 的零基集。未填充与非分区 inhtrue RTE 对应的 rels。*/Bitmapset *notnullattnums;... }// src/backend/optimizer/util/plancat.c/** get_relation_info -* Retrieves catalog information for a given relation.* 检索给定关系的目录信息** Given the Oid of the relation, return the following info into fields* of the RelOptInfo struct:** min_attr lowest valid AttrNumber* max_attr highest valid AttrNumber* indexlist list of IndexOptInfos for relations indexes* statlist list of StatisticExtInfo for relations statistic objects* serverid if its a foreign table, the server OID* fdwroutine if its a foreign table, the FDW function pointers* pages number of pages* tuples number of tuples* rel_parallel_workers user-defined number of parallel workers** Also, add information about the relations foreign keys to root-fkey_list.* 另外将有关关系的外键的信息添加到 root-fkey_list。** Also, initialize the attr_needed[] and attr_widths[] arrays. In most* cases these are left as zeroes, but sometimes we need to compute attr* widths here, and we may as well cache the results for costsize.c.* 另外初始化 attr_needed[] 和 attr_widths[] 数组。* 在大多数情况下这些都保留为零但有时我们需要在这里计算 attr 宽度我们不妨将结果缓存在 costize.c 中。** If inhparent is true, all we need to do is set up the attr arrays:* the RelOptInfo actually represents the appendrel formed by an inheritance* tree, and so the parent rels physical size and index information isnt* important for it, however, for partitioned tables, we do populate the* indexlist as the planner uses unique indexes as unique proofs for certain* optimizations.* 如果 inhparent 为真我们需要做的就是设置 attr 数组* RelOptInfo 实际上代表由继承树形成的附加项因此父级 rel 的物理大小和索引信息对它来说并不重要* 但是对于分区表我们确实填充索引列表因为规划器使用唯一索引作为某些优化的唯一证明。*/ void get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,RelOptInfo *rel) {.../** Record which columns are defined as NOT NULL. We leave this* unpopulated for non-partitioned inheritance parent relations as its* ambiguous as to what it means. Some child tables may have a NOT NULL* constraint for a column while others may not. We could work harder and* build a unioned set of all child relations notnullattnums, but theres* currently no need. The RelOptInfo corresponding to the !inh* RangeTblEntry does get populated.* * 记录哪些列被定义为 NOT NULL。* 对于非分区继承父关系我们将其保留为未填充状态因为它的含义不明确。* 一些子表可能对某一列具有 NOT NULL 约束而其他子表可能没有。* 我们可以更加努力并构建所有子关系 notnullattnums 的联合集但目前没有必要。* 与 !inh RangeTblEntry 对应的 RelOptInfo 确实被填充。*/if (!inhparent || relation-rd_rel-relkind RELKIND_PARTITIONED_TABLE){for (int i 0; i relation-rd_att-natts; i){Form_pg_attribute attr TupleDescAttr(relation-rd_att, i);if (attr-attnotnull){rel-notnullattnums bms_add_member(rel-notnullattnums,attr-attnum);/** Per RemoveAttributeById(), dropped columns will have their* attnotnull unset, so we neednt check for dropped columns* in the above condition.* * 根据 RemoveAttributeById()删除的列将取消设置其 attnotnull因此我们不需要在上述条件下检查删除的列。*/Assert(!attr-attisdropped);}}}... }因为篇幅的限制我们这里在最小范围内介绍此次patch的核心功能实现。接下来我会依次调试相关用例剖析patch的每个功能点 该过程中其他相关的内容不过多介绍后面会由其他博客详细分析 简单情况的处理 永真永假的处理 表结构如下 postgres# \d pred_tab Table public.pred_tabColumn | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ------------------------------------------------------------------------------------------------a | integer | | not null | | plain | | | b | integer | | | | plain | | | c | integer | | not null | | plain | | | Access method: heappostgres#那么对于where条件中的IS [NOT] NULL来说自然有永真/假的情况存在。永真 直接忽略即可而永假则可以处理成 const false如下 此时的函数堆栈如下 build_simple_rel(PlannerInfo * root, int relid, RelOptInfo * parent) add_base_rels_to_query(PlannerInfo * root, Node * jtnode) add_base_rels_to_query(PlannerInfo * root, Node * jtnode) query_planner(PlannerInfo * root, query_pathkeys_callback qp_callback, void * qp_extra) grouping_planner(PlannerInfo * root, double tuple_fraction, SetOperationStmt * setops) 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) PostgresMain(const char * dbname, const char * username) BackendMain(char * startup_data, size_t startup_data_len) postmaster_child_launch(BackendType child_type, char * startup_data, size_t startup_data_len, ClientSocket * client_sock) BackendStartup(ClientSocket * client_sock) ServerLoop() PostmasterMain(int argc, char ** argv) main(int argc, char ** argv)相关函数介绍如下 // src/backend/optimizer/plan/initsplan.c/** add_base_rels_to_query** Scan the querys jointree and create baserel RelOptInfos for all* the base relations (e.g., table, subquery, and function RTEs)* appearing in the jointree.* 扫描查询的连接树并为连接树中出现的所有基本关系例如表、子查询和函数 RTE* 创建 baserel RelOptInfos** The initial invocation must pass root-parse-jointree as the value of* jtnode. Internally, the function recurses through the jointree.* 初始调用必须传递 root-parse-jointree 作为 jtnode 的值。* 在内部该函数通过 jointree 进行递归。** At the end of this process, there should be one baserel RelOptInfo for* every non-join RTE that is used in the query. Some of the baserels* may be appendrel parents, which will require additional otherrel* RelOptInfos for their member rels, but those are added later.* 在此过程结束时查询中使用的每个非连接 RTE 都应该有一个 baserel RelOptInfo。* 一些 baserel 可能是 appendrel 父级这将需要为其成员 rels 提供额外的“otherrel”RelOptInfo* 但这些是稍后添加的。*/ void add_base_rels_to_query(PlannerInfo *root, Node *jtnode);然后在接下来的逻辑里调用get_relation_info以得到的notnullattnums非空列位图信息(这个逻辑后面就不再赘述了)如下 get_relation_info(PlannerInfo * root, Oid relationObjectId, _Bool inhparent, RelOptInfo * rel) build_simple_rel(PlannerInfo * root, int relid, RelOptInfo * parent) add_base_rels_to_query(PlannerInfo * root, Node * jtnode) add_base_rels_to_query(PlannerInfo * root, Node * jtnode) query_planner(PlannerInfo * root, query_pathkeys_callback qp_callback, void * qp_extra) grouping_planner(PlannerInfo * root, double tuple_fraction, SetOperationStmt * setops) 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) ...接下来就到了构建 RestrictInfo 节点本身约束条件构建 如下 约束条件就是 WHERE/ON/HAYING 子句中的各个条件 在查询树中 它们以表达式(Expr)的方式存在主要存放在 FromExpr 的quals 链表中和 JoinExpr的 quals 链表中在本书中对于这类条件通常统称为约束条件但在分解连接树的过程中这部分还需要详细地分为过滤(Filter 条件和连接 Join 条件。通常而言出现 WHERE 子句中的条件是过滤条件出现在 ON 子句中的条件是连接条件但是随着约束条件的下推能下推的连接条件也会转化成过滤条件。 注若是对该条件生成过程感兴趣可以自行查看 张树杰 《查询优化深度探索》4.3.3.6章节 继续向下来到了distribute_restrictinfo_to_rels函数 // src/backend/optimizer/plan/initsplan.c /** distribute_restrictinfo_to_rels* Push a completed RestrictInfo into the proper restriction or join* clause list(s).* 将已完成的 RestrictInfo 推送到适当的限制或连接子句列表中。** This is the last step of distribute_qual_to_rels() for ordinary qual* clauses. Clauses that are interesting for equivalence-class processing* are diverted to the EC machinery, but may ultimately get fed back here.* 这是对普通 qual 子句执行 deliver_qual_to_rels() 的最后一步。* 对等价类处理感兴趣的子句被转移到 EC 机制但最终可能会在这里得到反馈。*/ void distribute_restrictinfo_to_rels(PlannerInfo *root,RestrictInfo *restrictinfo);然后就到了此次patch最核心的实现逻辑如下 // src/backend/optimizer/plan/initsplan.c/** add_base_clause_to_rel* Add restrictinfo as a baserestrictinfo to the base relation denoted* by relid. We offer some simple prechecks to try to determine if the* qual is always true, in which case we ignore it rather than add it.* If we detect the qual is always false, we replace it with* constant-FALSE.* * 将“restrictinfo”作为 baserestrictinfo 添加到“relid”表示的基本关系中。* 我们提供一些简单的预检查尝试确定该限定词是否始终为真在这种情况下我们会忽略它而不是添加它。* 如果我们检测到该限定词始终为假我们会将其替换为 constant-FALSE。*/ static void add_base_clause_to_rel(PlannerInfo *root, Index relid,RestrictInfo *restrictinfo) {RelOptInfo *rel find_base_rel(root, relid);RangeTblEntry *rte root-simple_rte_array[relid];Assert(bms_membership(restrictinfo-required_relids) BMS_SINGLETON);/** For inheritance parent tables, we must always record the RestrictInfo* in baserestrictinfo as is. If we were to transform or skip adding it,* then the original wouldnt be available in apply_child_basequals. Since* there are two RangeTblEntries for inheritance parents, one with* inhtrue and the other with inhfalse, were still able to apply this* optimization to the inhfalse one. The inhtrue one is what* apply_child_basequals() sees, whereas the inhfalse one is whats used* for the scan node in the final plan.* 对于继承父表我们必须始终按原样将 RestrictInfo 记录在 baserestrictinfo 中。* 如果我们要转换或跳过添加它那么原始内容将无法在 apply_child_basequals 中使用。* 由于有两个用于继承父表的 RangeTblEntries一个带有 inhtrue另一个带有 inhfalse* 因此我们仍然可以将此优化应用于 inhfalse。* inhtrue 是 apply_child_basequals() 看到的而 inhfalse 是最终计划中用于扫描节点的内容。** We make an exception to this for partitioned tables. For these, we* always apply the constant-TRUE and constant-FALSE transformations. A* qual which is either of these for a partitioned table must also be that* for all of its child partitions.* 对于分区表我们对此做了例外处理。* 对于这些表我们始终应用常量 TRUE 和常量 FALSE 转换。* 对于分区表符合上述任一条件的限定词也必须是其所有子分区的限定词。*/if (!rte-inh || rte-relkind RELKIND_PARTITIONED_TABLE){/* Dont add the clause if it is always true */// 如果该子句始终为真则不要添加该子句if (restriction_is_always_true(root, restrictinfo))return;/** Substitute the origin qual with constant-FALSE if it is provably* always false. Note that we keep the same rinfo_serial.* * 如果可以证明 origin qual 总是 false则用 constant-FALSE 替换 origin qual。* 请注意我们保留相同的 rinfo_serial。*/if (restriction_is_always_false(root, restrictinfo)){int save_rinfo_serial restrictinfo-rinfo_serial;restrictinfo make_restrictinfo(root,(Expr *) makeBoolConst(false, false),restrictinfo-is_pushed_down,restrictinfo-has_clone,restrictinfo-is_clone,restrictinfo-pseudoconstant,0, /* security_level */restrictinfo-required_relids,restrictinfo-incompatible_relids,restrictinfo-outer_relids);restrictinfo-rinfo_serial save_rinfo_serial;}}/* Add clause to rels restriction list */rel-baserestrictinfo lappend(rel-baserestrictinfo, restrictinfo);/* Update security level info */rel-baserestrict_min_security Min(rel-baserestrict_min_security,restrictinfo-security_level); }对于我们这个永真的用例1来说restriction_is_always_true返回真 那么就不会向rel-baserestrictinfo里面append条件于是这个就相当于做了一个忽略 /** expr_is_nonnullable* Check to see if the Expr cannot be NULL** If the Expr is a simple Var that is defined NOT NULL and meanwhile is not* nulled by any outer joins, then we can know that it cannot be NULL.*/ static bool expr_is_nonnullable(PlannerInfo *root, Expr *expr) {RelOptInfo *rel;Var *var;/* For now only check simple Vars */if (!IsA(expr, Var))return false;var (Var *) expr;/* could the Var be nulled by any outer joins? */if (!bms_is_empty(var-varnullingrels))return false;/* system columns cannot be NULL */if (var-varattno 0)return true;/* is the column defined NOT NULL? */rel find_base_rel(root, var-varno);if (var-varattno 0 bms_is_member(var-varattno, rel-notnullattnums))return true;return false; }/** restriction_is_always_true* Check to see if the RestrictInfo is always true.** Currently we only check for NullTest quals and OR clauses that include* NullTest quals. We may extend it in the future.*/ bool restriction_is_always_true(PlannerInfo *root,RestrictInfo *restrictinfo) {/* Check for NullTest qual */if (IsA(restrictinfo-clause, NullTest)){NullTest *nulltest (NullTest *) restrictinfo-clause;/* is this NullTest an IS_NOT_NULL qual? */if (nulltest-nulltesttype ! IS_NOT_NULL)return false;return expr_is_nonnullable(root, nulltest-arg);}/* If its an OR, check its sub-clauses */if (restriction_is_or_clause(restrictinfo)){ListCell *lc;Assert(is_orclause(restrictinfo-orclause));/** if any of the given OR branches is provably always true then the* entire condition is true.*/foreach(lc, ((BoolExpr *) restrictinfo-orclause)-args){Node *orarg (Node *) lfirst(lc);if (!IsA(orarg, RestrictInfo))continue;if (restriction_is_always_true(root, (RestrictInfo *) orarg))return true;}}return false; }/** restriction_is_always_false* Check to see if the RestrictInfo is always false.** Currently we only check for NullTest quals and OR clauses that include* NullTest quals. We may extend it in the future.*/ bool restriction_is_always_false(PlannerInfo *root,RestrictInfo *restrictinfo) {/* Check for NullTest qual */if (IsA(restrictinfo-clause, NullTest)){NullTest *nulltest (NullTest *) restrictinfo-clause;/* is this NullTest an IS_NULL qual? */if (nulltest-nulltesttype ! IS_NULL)return false;return expr_is_nonnullable(root, nulltest-arg);}/* If its an OR, check its sub-clauses */if (restriction_is_or_clause(restrictinfo)){ListCell *lc;Assert(is_orclause(restrictinfo-orclause));/** Currently, when processing OR expressions, we only return true when* all of the OR branches are always false. This could perhaps be* expanded to remove OR branches that are provably false. This may* be a useful thing to do as it could result in the OR being left* with a single arg. Thats useful as it would allow the OR* condition to be replaced with its single argument which may allow* use of an index for faster filtering on the remaining condition.*/foreach(lc, ((BoolExpr *) restrictinfo-orclause)-args){Node *orarg (Node *) lfirst(lc);if (!IsA(orarg, RestrictInfo) ||!restriction_is_always_false(root, (RestrictInfo *) orarg))return false;}return true;}return false; }对于我们这个永假的用例3来说restriction_is_always_false返回真 那么就会向rel-baserestrictinfo里面append一个常量假的条件于是这个就相当于做了一个转换 关于上面永真永假的实现函数restriction_is_always_true和restriction_is_always_false不再赘述 or语句的处理 对于这个含有永真的用例5来说restriction_is_always_true返回真 那么整个过滤条件都将被忽略如下 而对于or两边都为永假的用例7来说restriction_is_always_false返回真 那么整个过滤条件都将被裁剪为const false如下 而对于or两边并非所有分支都可证明为真假的用例8来说restriction_is_always_true和restriction_is_always_false都无法返回真 那么整个过滤条件得以保留如下 注在下以为 这里其实还是可以继续裁剪下去后面我将试着写一版patch去实现一下本文不再赘述 这里小结一下此次patch对于以下几种的or处理 1 or 1 — 11 or 0 — 11 or unknown — 10 or 0 — 00 or unknown — 不变 连接情况的处理 这块的新增逻辑如下 // src/backend/optimizer/util/joininfo.c/** add_join_clause_to_rels* Add restrictinfo to the joininfo list of each relation it requires.* 将“restrictinfo”添加到其所需的每个关系的 joininfo 列表中** Note that the same copy of the restrictinfo node is linked to by all the* lists it is in. This allows us to exploit caching of information about* the restriction clause (but we must be careful that the information does* not depend on context).* 请注意restrictinfo 节点的相同副本与其所在的所有列表链接。* 这使我们能够利用有关限制子句的信息缓存但我们必须注意信息不依赖于上下文。** restrictinfo describes the join clause* join_relids is the set of relations participating in the join clause* (some of these could be outer joins)* restrictinfo 描述连接子句 join_relids 是参与连接子句的关系集其中一些可能是外连接*/ void add_join_clause_to_rels(PlannerInfo *root,RestrictInfo *restrictinfo,Relids join_relids) {int cur_relid;/* Dont add the clause if it is always true */if (restriction_is_always_true(root, restrictinfo))return;/** Substitute constant-FALSE for the origin qual if it is always false.* Note that we keep the same rinfo_serial.* 如果 origin qual 始终为 false则用 constant-FALSE 代替。* 请注意我们保留相同的 rinfo_serial。*/if (restriction_is_always_false(root, restrictinfo)){int save_rinfo_serial restrictinfo-rinfo_serial;restrictinfo make_restrictinfo(root,(Expr *) makeBoolConst(false, false),restrictinfo-is_pushed_down,restrictinfo-has_clone,restrictinfo-is_clone,restrictinfo-pseudoconstant,0, /* security_level */restrictinfo-required_relids,restrictinfo-incompatible_relids,restrictinfo-outer_relids);restrictinfo-rinfo_serial save_rinfo_serial;}... }有了上面的铺垫接下来的join 就比较好理解了。不过我们这里给上述表插入一行数据将更好的理解上面join的执行计划 postgres# table pred_tab ;a | b | c ---------2 | | 2 (1 row)postgres# \d pred_tab Table public.pred_tabColumn | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ------------------------------------------------------------------------------------------------a | integer | | not null | | plain | | | b | integer | | | | plain | | | c | integer | | not null | | plain | | | Access method: heappostgres#是否可忽略 首先看一下用例9这里能够直接忽略的原因 如下 postgres# SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON TRUE;a | b | c | a | b | c ------------------2 | | 2 | 2 | | 2 (1 row)postgres#嵌套的left join的t2.a IS NOT NULL永真 非空 而用例10就不一定了如下 postgres# SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a 1;a | b | c | a | b | c ------------------2 | | 2 | | | (1 row)postgres#是否可简化 用例11被简化为常量 FALSE可以参考用例9用例12不能被简化为常量 FALSE可以参考用例10。 or语句的处理 用例13可以直接忽略可以参考用例9用例14不可以直接忽略可以参考用例10。 用例15被简化为常量 FALSE可以参考用例9用例16不能被简化为常量 FALSE可以参考用例10。 父子继承的处理 patch2如下 SHA-1: 3af7040985b6df504a72cd307aad5d69ac5f5384* Fix IS [NOT] NULL qual optimization for inheritance tablesb262ad440 添加了代码让规划器删除多余的 IS NOT NULL 限定符并消除在限定符的列具有 NOT NULL 约束的表上对 IS NULL 限定符进行不必要的扫描。 该提交未考虑到继承父表在父表和子表之间可能具有不同的 NOT NULL 约束。这会导致问题就好像我们在父表上消除了限定符当在 apply_child_basequals() 中将限定符应用于子表时该限定符可能未添加到父表的 baserestrictinfo 中。 在这里我们通过不应用优化来删除属于继承父表的 RelOptInfos 的冗余限定符并在 apply_child_basequals() 中再次应用优化来解决这个问题。实际上这意味着父表和子表被独立考虑因为父表具有 inhtrue 和 inhfalse RTE并且我们仍然将优化应用于与 inhfalse RTE 相对应的 RelOptInfo。 我们仍然可以对分区表应用 add_base_clause_to_rel() 中的优化因为分区的 NULL 性必须与其父分区的 NULL 性相匹配。而且如果我们扩展了 requirement_is_always_false() 和 requirement_is_always_true() 来处理分区约束那么我们可以应用相同的逻辑即使在多级分区表中当 qual 与分区表父分区的分区 qual 不匹配时也无法将值路由到分区。CHECK 约束也是如此因为它们也必须在未分区表和其分区之间匹配。 关于继承表的使用上可以参考这位老哥的博客 postgresql表继承详解点击前往 postgres# select relname, relkind, oid from pg_class where relname like pred_%;relname | relkind | oid -----------------------------pred_child | r | 16412pred_parent | r | 16409 (2 rows)postgres#接下来先看一下用例17父表的NOT NULL没有继承给子表那么对pred_parent这样的查询 就需要分开处理。 正如get_relation_info函数中注释的说明 对于非分区继承父关系我们将其保留为未填充状态因为它的含义不明确。一些子表可能对某一列具有 NOT NULL 约束而其他子表可能没有。我们可以更加努力并构建所有子关系 notnullattnums 的联合集但目前没有必要。 为查询中使用的所有基本关系构建 RelOptInfo 节点第一次处理 如下 top restrictinfo 其clause内容如下 当然在add_base_clause_to_rel也不会过滤掉/处理这个条件如下 继续如下 // src/backend/optimizer/plan/planmain.cRelOptInfo * query_planner(PlannerInfo *root,query_pathkeys_callback qp_callback, void *qp_extra) { .../** Now expand appendrels by adding otherrels for their children. We* delay this to the end so that we have as much information as possible* available for each baserel, including all restriction clauses. That* let us prune away partitions that dont satisfy a restriction clause.* Also note that some information such as lateral_relids is propagated* from baserels to otherrels here, so we must have computed it already.* * 现在通过为其子级添加“otherrels”来扩展附加节点。* 我们将此操作延迟到最后以便我们为每个基本节点提供尽可能多的信息包括所有限制子句。* 这让我们可以删除不满足限制子句的分区。* 还请注意此处某些信息例如 lateral_relids从基本节点传播到其他节点因此我们必须已经计算了它。*/add_other_rels_to_query(root); ... }因为这是继承表 (这里在查询父表数据的时候对应的继承表数据也被查询出来)如下 /** find_all_inheritors -* * 返回关系 OID 列表包括给定的 rel 以及所有直接或间接继承自该 rel 的关系。它还可以返回在以给定 rel 为根的继承树中为每个此类关系找到的父级数量可选* * 在所有子关系上获取指定的锁定类型但不在给定的 rel 上调用者应该已经锁定了它。* 如果 lockmode 为 NoLock则不会获取任何锁定但调用者必须注意可能出现的子关系 DROP 竞争条件。* * 注意 - 此例程的当前调用者对子级同时分离不感兴趣因此没有规定将它们包括在内。*/然后在这两次循环中也创建其他相关的 RelOptInfo。如下 此时的函数堆栈如下 apply_child_basequals(PlannerInfo * root, RelOptInfo * parentrel, RelOptInfo * childrel, RangeTblEntry * childRTE, AppendRelInfo * appinfo) build_simple_rel(PlannerInfo * root, int relid, RelOptInfo * parent) expand_inherited_rtentry(PlannerInfo * root, RelOptInfo * rel, RangeTblEntry * rte, Index rti) add_other_rels_to_query(PlannerInfo * root) query_planner(PlannerInfo * root, query_pathkeys_callback qp_callback, void * qp_extra) ...对于第一个子表(也就是父表本身)因为非空的存在 所以处理childrel-baserestrictinfo NULL如下(相当于忽略) 对于第二个子表(也就是子表本身)因为没有非空的存在 所以处理childrel-baserestrictinfo childquals如下(没有忽略) 有了上面的铺垫用例18 非常好理解第一个子表 非空的存在 条件永假所以处理 如下 if (!apply_child_basequals(root, parent, rel, rte, appinfo)) // false{/** Restriction clause reduced to constant FALSE or NULL. Mark as* dummy so we wont scan this relation.* 限制子句简化为常量 FALSE 或 NULL。标记为虚拟这样我们就不会扫描此关系。*/mark_dummy_rel(rel);}而经过如下alter之后父表和子表的非空限制 颠倒过来自然 用例19 20将有不同的呈现 postgres# ALTER TABLE pred_parent ALTER a DROP NOT NULL; ALTER TABLE postgres# ALTER TABLE pred_child ALTER a SET NOT NULL; ALTER TABLE postgres#
http://www.dnsts.com.cn/news/124727.html

相关文章:

  • 企业网站建设的方案ppt电脑优化系统的软件哪个好
  • 网站优化排名多少钱设计logo名字
  • 成都网站建设前50强程序员接私活平台
  • 专门做旅游攻略的网站可以自己做免费网站吗
  • 手机老是下载一些做任务的网站wordpress评论内容不显示
  • 推广引流网站兰州网络推广电话
  • 网站建设 山西利用赞赏码做网站收款
  • seo站长优化工具简单企业网站用什么
  • 酒店微信网站建设网站模板兼容手机
  • 网站设计与管理论文网站单页模板下载
  • 海口建站程序网站 蜘蛛
  • 做网站的目标磐安县住和城乡建设局网站
  • python做的网站哪些wordpress单页主题制作教程
  • 自己制作网站做外贸赚钱吗使用cn域名做网站的多吗
  • dede响应式网站模板广州自助建站模板
  • 做关于家乡的网站泉州做网站qzxiaolv
  • qq群引流推广网站专业的集团网站建设
  • 建站63年来第一次闭站?北京站辟谣中国中信建设有限责任公司
  • 哪些在线网站可以做系统进化树建设自己的二手房中介网站
  • 网站建设费能不能认定为广告费建站宝盒建网站
  • 互联网门户网站有哪些自己做的娱乐平台网站
  • 合肥网站建设技术重庆企业网站推广方法
  • 娱乐平台网站开发免费wordpress 控制台 慢
  • 购物网站开发 项目描述网站 软件
  • 贵港市建设局网站深圳网站制作运营
  • 五金制品网站源码广告牌设计效果图
  • 建材 网站 模板wordpress mylife
  • 西安做网站微信公司哪家好湛江企业网站
  • 网站设计协议云南企业展厅设计公司
  • 开发网站开票写什么中小学 网站建设 通知