PostgreSQL的学习心得和知识总结(一百六十九)|深入理解PostgreSQL数据库之 Group By 键值消除 的使用和实现

news2025/2/21 21:45:13

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

1、参考书籍:《PostgreSQL数据库内核分析》
2、参考书籍:《数据库事务处理的艺术:事务管理与并发控制》
3、PostgreSQL数据库仓库链接,点击前往
4、日本著名PostgreSQL数据库专家 铃木启修 网站主页,点击前往
5、参考书籍:《PostgreSQL中文手册》
6、参考书籍:《PostgreSQL指南:内幕探索》,点击前往


1、本文内容全部来源于开源社区 GitHub和以上博主的贡献,本文也免费开源(可能会存在问题,评论区等待大佬们的指正)
2、本文目的:开源共享 抛砖引玉 一起学习
3、本文不提供任何资源 不存在任何交易 与任何组织和机构无关
4、大家可以根据需要自行 复制粘贴以及作为其他个人用途,但是不允许转载 不允许商用 (写作不易,还请见谅 💖)
5、本文内容基于PostgreSQL master源码开发而成


深入理解PostgreSQL数据库之 Group By 键值消除 的使用和实现

  • 文章快速说明索引
  • 功能使用背景说明
  • 功能使用源码解析
  • 功能实现案例调试
    • case 1
    • case 2
    • case 3
    • case 4
    • case 5
    • case 6
    • case 7
    • case 8
    • case 9
    • case 10
    • case 11
  • 核心函数流程图示



文章快速说明索引

学习目标:

做数据库内核开发久了就会有一种 少年得志,年少轻狂 的错觉,然鹅细细一品觉得自己其实不算特别优秀 远远没有达到自己想要的。也许光鲜的表面掩盖了空洞的内在,每每想到于此,皆有夜半临渊如履薄冰之感。为了睡上几个踏实觉,即日起 暂缓其他基于PostgreSQL数据库的兼容功能开发,近段时间 将着重于学习分享Postgres的基础知识和实践内幕。


学习内容:(详见目录)

1、Group By 键值消除 的使用和实现


学习时间:

2025年02月09日 22:36:35


学习产出:

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) 13.1.0, 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>

功能使用背景说明

最近PostgreSQL合入了一个提交(Detect redundant GROUP BY columns using UNIQUE indexes):现在可以使用唯一索引检测冗余的 GROUP BY 列,类似于 PostgreSQL 9.6 中添加的功能(如下图所示),它使用主键执行相同的操作。

在这里插入图片描述
在这里插入图片描述

使用下表定义的基本示例:

postgres=# CREATE TABLE t1 (a INT NOT NULL, b INT, UNIQUE(a));
CREATE TABLE

在 PostgreSQL 17 及更早版本中,对列 a 和 b 进行分组的查询将同时包含在计划的组键中:

postgres=# EXPLAIN (costs off) SELECT a, b FROM t1 GROUP BY a, b;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: a, b
   ->  Seq Scan on t1
(3 rows)

从 PostgreSQL 18 开始,计划的组键将仅包含列 a:

postgres=# EXPLAIN (costs off) SELECT a, b FROM t1 GROUP BY a, b;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: a
   ->  Seq Scan on t1
(3 rows)

而此次的提交,如下:

在这里插入图片描述


功能使用源码解析

注:关于group by键值消除,在张树杰书的第3.7章节做了介绍 内容上言简意赅,我们今天借助于此次的提交把 tom lane 和 David Rowley的这几次提交合并学习一下。建议在看之前可以先阅读一下张树杰相关章节!

几次提交的commit信息,如下:

删除在功能上依赖于其他列的 GROUP BY 列。Tom Lane 9年前 (2 11th, 2016 2:34 下午)

  • 如果 GROUP BY 子句包含非延迟主键的所有列以及同一关系的其他列,则这些其他列是多余的,可以从分组中删除;pkey 足以确保表的每一行对应于一个单独的组
  • 删除多余的列将减少实现 GROUP BY 所需的排序或散列的成本,并且实际上可以完全消除排序步骤的需要。
  • 这似乎值得测试,因为许多查询作者都没有意识到规则中的 GROUP-BY-primary-key 例外,即查询不允许在其目标列表或 HAVING 子句中引用非分组列。因此,冗余的 GROUP BY 项并不少见。此外,我们可以在大多数查询中使测试变得非常便宜,因为在我们发现至少两个列在 GROUP BY 中之前不查找关系的主键,这样测试就不会有帮助。

不要从继承父级的 GROUP BY 中删除多余的列 David Rowley 6年前 (7 3rd, 2019 4:44 凌晨)

  • d4c3a156c 添加了代码,用于在分组依据中存在所有主键列时,从 GROUP BY 子句中删除不属于表的 PRIMARY KEY 约束的列。这样做没有问题,因为我们知道每个组只有一行来自此关系。但是,该逻辑未能考虑继承父级关系。这些关系可以有不带主键的子关系,但即使有,它们也可能重复父级的一行或另一个子关系的一行。在这种情况下,需要这些额外的 GROUP BY 列。
  • 通过禁用继承父级表的优化来解决这个问题。在 v11 及更高版本中,分区表没有问题,因为分区不能重叠,并且在 v11 之前的分区表不能有主键。

删除冗余分组和 DISTINCT 列。Tom Lane 2年前 (1 18th, 2023 9:37 上午)

  • 避免明确按我们知道对排序而言冗余的列进行分组,例如,我们只需要在 SELECT ... WHERE x = y GROUP BY x, y 中按 x 和 y 中的一个进行分组。这种情况出现的频率比您想象的要高,正如回归测试中的变化所示。
  • 检测也几乎是免费的,因为我们只是搭载了检测冗余路径键的现有逻辑。(在一些现有的计划发生变化时,可以看到,分组步骤之前的排序步骤已经没有按冗余列进行排序,这使得旧计划看起来有点傻。)为此,请构建忽略任何可证明冗余的排序项的treated_groupClause和processed_distinctClause列表,并在相关时查阅那些非原始列表。
  • 这意味着在规划器中,如果想知道要对哪些列进行分组,通常应查阅 root->processed_groupClause 或 root->processed_distinctClause;但要检查是否正在进行分组或区分,请检查 parse->groupClause 或 parse->distinctClause 是否为非零。这与处理 HAVING 子句的长期规则相当,因此我认为这不会是一个巨大的维护问题。
  • nodeAgg.c 还需要进行一些小修改,因为现在可以生成具有零个分组列的 AGG_PLAIN 和 AGG_SORTED Agg 节点。

将 remove_useless_groupby_columns() 工作推迟到 query_planner() David Rowley 2个月前 (12 11th, 2024 5:22 下午)

  • 传统上,remove_useless_groupby_columns() 是在 grouping_planner() 期间调用的,紧接着调用 preprocess_groupclause()。虽然在很多方面,同时填充字段并从 processing_groupClause 中删除功能依赖列是有意义的,但这样做的缺点是,remove_useless_groupby_columns() 是在为查询中提到的关系填充 RelOptInfos 之前被调用的。没有可用的 RelOptInfos 意味着我们需要手动查询目录表以获取有关表主键约束的所需详细信息。
  • 在这里,我们将 remove_useless_groupby_columns() 调用移至 query_planner(),并将其直接放在填充 RelOptInfos 之后。这样做没有问题,因为此时processed_groupClause仍未最终确定,因为它仍可通过make_pathkeys_for_sortclauses_extended()在standard_qp_callback()中进行修改。
  • 此提交只是重构,只是将remove_useless_groupby_columns()移入initsplan.c。计划中的后续提交将调整该函数,使其使用RelOptInfo而不是进行目录查找,并教它如何使用唯一索引作为证明,以扩展我们可以从GROUP BY中删除功能相关列的情况。

使用 UNIQUE 索引检测冗余的 GROUP BY 列 David Rowley 2个月前 (12 11th, 2024 6:28 下午)

  • d4c3a156c 添加了支持,当 GROUP BY 包含属于关系 PRIMARY KEY 的所有列时,属于该关系的所有其他列都将从 GROUP BY 子句中删除。这是可能的,因为所有其他列在功能上都依赖于 PRIMARY KEY,并且仅凭这些列就可以确保组是不同的。
  • 在这里,我们扩展了该优化,并允许它适用于表上的任何唯一索引,而不仅仅是 PRIMARY KEY 索引。这通常要求索引中的所有列都定义为 NOT NULL,但是,当索引定义为 NULLS NOT DISTINCT 时,我们可以放宽该要求。
  • 当有多个合适的索引允许删除列时,我们更喜欢列数最少的索引,因为这允许我们删除最多的 GROUP BY 列。有一天,我们可能想要重新考虑这个决定,因为就数据类型和存储/查询数据的宽度而言,使用较窄的列集可能更有意义。
  • 这还会调整代码以使用 RelOptInfo.indexlist,而不是查找目录表。
  • 顺便说一句,添加另一条短路路径,以便在无法删除冗余的 GROUP BY
    列的情况下更早地退出。现在,这种提前退出比最初编写此代码时更便宜,因为 00b41463c 使检查空位图集的成本更低。
postgres=# select version();
                                     version                                     
---------------------------------------------------------------------------------
 PostgreSQL 18devel on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.1.0, 64-bit
(1 row)

postgres=# 

patch中最重要的函数remove_useless_groupby_columns,如下:

在这里插入图片描述

如上函数是我们接下来学习的重中之重,其源码解析稍后详解!


在这里插入图片描述

// src/backend/catalog/pg_constraint.c

/*
 * get_primary_key_attnos
 *		Identify the columns in a relation's primary key, if any.
 *		识别表主键中的列(如果有)。
 *
 * Returns a Bitmapset of the column attnos of the primary key's columns,
 * with attnos being offset by FirstLowInvalidHeapAttributeNumber so that
 * system columns can be represented.
 * 返回主键列的列 attnos 的 Bitmapset,
 * 其中 attnos 由 FirstLowInvalidHeapAttributeNumber 偏移,以便可以表示系统列。
 *
 * If there is no primary key, return NULL.  We also return NULL if the pkey
 * constraint is deferrable and deferrableOk is false.
 * 如果没有主键,则返回 NULL。
 * 如果 pkey 约束可延迟且 deferrableOk 为 false,我们也将返回 NULL。
 *
 * *constraintOid is set to the OID of the pkey constraint, or InvalidOid
 * on failure.
 * *constraintOid 设置为 pkey 约束的 OID,或者在失败时设置为 InvalidOid。
 */
Bitmapset *
get_primary_key_attnos(Oid relid, bool deferrableOk, Oid *constraintOid);

如上函数开表pg_constraint,跳过非CONSTRAINT_PRIMARY的约束;若是发现,则构造出主键列的位图!


// src/backend/optimizer/plan/initsplan.c

/*
 * remove_useless_groupby_columns
 *		Remove any columns in the GROUP BY clause that are redundant due to
 *		being functionally dependent on other GROUP BY columns.
 *		删除 GROUP BY 子句中由于功能上依赖于其他 GROUP BY 列而多余的列。
 *
 * Since some other DBMSes do not allow references to ungrouped columns, it's
 * not unusual to find all columns listed in GROUP BY even though listing the
 * primary-key columns, or columns of a unique constraint would be sufficient.
 * Deleting such excess columns avoids redundant sorting or hashing work, so
 * it's worth doing.
 * 由于其他一些 DBMS 不允许引用未分组的列,因此即使列出主键列或唯一约束的列就足够了,也经常会发现 GROUP BY 中列出了所有列。
 * 删除这些多余的列可以避免多余的排序或散列工作,因此值得一试。
 *
 * Relcache invalidations will ensure that cached plans become invalidated
 * when the underlying supporting indexes are dropped or if a column's NOT
 * NULL attribute is removed.
 * Relcache 失效将确保当底层支持索引被删除或者列的 NOT NULL 属性被删除时,缓存的计划变为无效。
 */
void
remove_useless_groupby_columns(PlannerInfo *root)
{
	Query	   *parse = root->parse;
	Bitmapset **groupbyattnos;
	Bitmapset **surplusvars;
	bool		tryremove = false;
	ListCell   *lc;
	int			relid;

	/* No chance to do anything if there are less than two GROUP BY items */
	// 如果 GROUP BY 项少于两个,则没有机会执行任何操作
	if (list_length(root->processed_groupClause) < 2)
		return;

	/* Don't fiddle with the GROUP BY clause if the query has grouping sets */
	// 如果查询有分组集,就不要摆弄 GROUP BY 子句
	if (parse->groupingSets)
		return;

	/*
	 * Scan the GROUP BY clause to find GROUP BY items that are simple Vars.
	 * Fill groupbyattnos[k] with a bitmapset of the column attnos of RTE k
	 * that are GROUP BY items.
	 * 扫描 GROUP BY 子句以查找简单变量的 GROUP BY 项。
	 * 使用 RTE k 中属于 GROUP BY 项的列 attnos 的位图集填充 groupbyattnos[k]。
	 */
	groupbyattnos = (Bitmapset **) palloc0(sizeof(Bitmapset *) *
										   (list_length(parse->rtable) + 1));
	foreach(lc, root->processed_groupClause)
	{
		SortGroupClause *sgc = lfirst_node(SortGroupClause, lc);
		TargetEntry *tle = get_sortgroupclause_tle(sgc, parse->targetList);
		Var		   *var = (Var *) tle->expr;

		/*
		 * Ignore non-Vars and Vars from other query levels.
		 * 忽略非变量和来自其他查询级别的变量。
		 *
		 * XXX in principle, stable expressions containing Vars could also be
		 * removed, if all the Vars are functionally dependent on other GROUP
		 * BY items.  But it's not clear that such cases occur often enough to
		 * be worth troubling over.
		 * XXX 原则上,如果所有 Var 在功能上都依赖于其他 GROUP BY 项,则包含 Var 的稳定表达式也可以被删除。
		 * 但目前尚不清楚这种情况是否经常发生,是否值得为此烦恼。
		 */
		if (!IsA(var, Var) ||
			var->varlevelsup > 0)
			continue;

		/* OK, remember we have this Var */
		relid = var->varno;
		Assert(relid <= list_length(parse->rtable));

		/*
		 * If this isn't the first column for this relation then we now have
		 * multiple columns.  That means there might be some that can be
		 * removed.
		 * 如果这不是此关系的第一列,那么我们现在有多个列。
		 * 这意味着可能有一些可以删除。
		 */
		tryremove |= !bms_is_empty(groupbyattnos[relid]);
		groupbyattnos[relid] = bms_add_member(groupbyattnos[relid],
											  var->varattno - FirstLowInvalidHeapAttributeNumber);
	}

	/*
	 * No Vars or didn't find multiple Vars for any relation in the GROUP BY?
	 * If so, nothing can be removed, so don't waste more effort trying.
	 * 没有 Vars 或未在 GROUP BY 中找到任何关系的多个 Vars?
	 * 如果是这样,则无法删除任何内容,因此不要再浪费精力尝试。
	 */
	if (!tryremove)
		return;

	/*
	 * Consider each relation and see if it is possible to remove some of its
	 * Vars from GROUP BY.  For simplicity and speed, we do the actual removal
	 * in a separate pass.  Here, we just fill surplusvars[k] with a bitmapset
	 * of the column attnos of RTE k that are removable GROUP BY items.
	 * 考虑每个关系,看看是否有可能从 GROUP BY 中删除部分 Var。
	 * 为了简单起见并加快速度,我们在单独的过程中进行实际删除。
	 * 在这里,我们只需用 RTE k 的可删除 GROUP BY 项的列 attnos 的位图集填充 residualvars[k]。
	 */
	surplusvars = NULL;			/* don't allocate array unless required */ // 除非需要,否则不分配数组
	relid = 0;
	foreach(lc, parse->rtable)
	{
		RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);
		RelOptInfo *rel;
		Bitmapset  *relattnos;
		Bitmapset  *best_keycolumns = NULL;
		int32		best_nkeycolumns = PG_INT32_MAX;

		relid++;

		/* Only plain relations could have primary-key constraints */
		// 只有普通关系才可以有主键约束
		if (rte->rtekind != RTE_RELATION)
			continue;

		/*
		 * We must skip inheritance parent tables as some of the child rels
		 * may cause duplicate rows.  This cannot happen with partitioned
		 * tables, however.
		 * 我们必须跳过继承父表,因为某些子关系可能会导致重复行。
		 * 然而,分区表不会发生这种情况。
		 */
		if (rte->inh && rte->relkind != RELKIND_PARTITIONED_TABLE)
			continue;

		/* Nothing to do unless this rel has multiple Vars in GROUP BY */
		// 除非此关系在 GROUP BY 中有多个变量,否则无需执行任何操作
		relattnos = groupbyattnos[relid];
		if (bms_membership(relattnos) != BMS_MULTIPLE)
			continue;

		rel = root->simple_rel_array[relid];

		/*
		 * Now check each index for this relation to see if there are any with
		 * columns which are a proper subset of the grouping columns for this
		 * relation.
		 * 现在检查此关系的每个索引,看是否有任何列是此关系的分组列的适当子集。
		 */
		foreach_node(IndexOptInfo, index, rel->indexlist)
		{
			Bitmapset  *ind_attnos;
			bool		nulls_check_ok;

			/*
			 * Skip any non-unique and deferrable indexes.  Predicate indexes
			 * have not been checked yet, so we must skip those too as the
			 * predOK check that's done later might fail.
			 * 跳过任何非唯一和可延迟索引。
			 * 谓词索引尚未检查,因此我们也必须跳过它们,因为稍后进行的 predOK 检查可能会失败。
			 */
			if (!index->unique || !index->immediate || index->indpred != NIL)
				continue;

			/* For simplicity, we currently don't support expression indexes */
			// 为了简单起见,我们目前不支持表达式索引
			if (index->indexprs != NIL)
				continue;

			ind_attnos = NULL;
			nulls_check_ok = true;
			for (int i = 0; i < index->nkeycolumns; i++)
			{
				/*
				 * We must insist that the index columns are all defined NOT
				 * NULL otherwise duplicate NULLs could exist.  However, we
				 * can relax this check when the index is defined with NULLS
				 * NOT DISTINCT as there can only be 1 NULL row, therefore
				 * functional dependency on the unique columns is maintained,
				 * despite the NULL.
				 * 我们必须坚持索引列全部定义为 NOT NULL,否则可能存在重复的 NULL。
				 * 但是,当索引定义为 NULLS NOT DISTINCT 时,我们可以放宽此检查,因为只能有 1 个 NULL 行,因此尽管存在 NULL,仍可保持对唯一列的函数依赖性。
				 */
				if (!index->nullsnotdistinct &&
					!bms_is_member(index->indexkeys[i],
								   rel->notnullattnums))
				{
					nulls_check_ok = false;
					break;
				}

				ind_attnos =
					bms_add_member(ind_attnos,
								   index->indexkeys[i] -
								   FirstLowInvalidHeapAttributeNumber);
			}

			if (!nulls_check_ok)
				continue;

			/*
			 * Skip any indexes where the indexed columns aren't a proper
			 * subset of the GROUP BY.
			 * 跳过索引列不是 GROUP BY 的适当子集的任何索引。
			 */
			if (bms_subset_compare(ind_attnos, relattnos) != BMS_SUBSET1)
				continue;

			/*
			 * Record the attribute numbers from the index with the fewest
			 * columns.  This allows the largest number of columns to be
			 * removed from the GROUP BY clause.  In the future, we may wish
			 * to consider using the narrowest set of columns and looking at
			 * pg_statistic.stawidth as it might be better to use an index
			 * with, say two INT4s, rather than, say, one long varlena column.
			 * 记录具有最少列的索引中的属性编号。
			 * 这样可以从 GROUP BY 子句中删除最大数量的列。
			 * 将来,我们可能希望考虑使用最窄的列集并查看 pg_statistic.stawidth,因为使用具有两个 INT4 的索引可能比使用一个长 varlena 列更好。
			 */
			if (index->nkeycolumns < best_nkeycolumns)
			{
				best_keycolumns = ind_attnos;
				best_nkeycolumns = index->nkeycolumns;
			}
		}

		/* Did we find a suitable index? */
		if (!bms_is_empty(best_keycolumns))
		{
			/*
			 * To easily remember whether we've found anything to do, we don't
			 * allocate the surplusvars[] array until we find something.
			 * 为了轻松记住我们是否找到了任何要做的事情,我们在找到某些事情之前不会分配 residualvars[] 数组。
			 */
			if (surplusvars == NULL)
				surplusvars = (Bitmapset **) palloc0(sizeof(Bitmapset *) *
													 (list_length(parse->rtable) + 1));

			/* Remember the attnos of the removable columns */
			surplusvars[relid] = bms_difference(relattnos, best_keycolumns);
		}
	}

	/*
	 * If we found any surplus Vars, build a new GROUP BY clause without them.
	 * (Note: this may leave some TLEs with unreferenced ressortgroupref
	 * markings, but that's harmless.)
	 * 如果我们发现任何多余的 Var,则构建一个不包含它们的新 GROUP BY 子句。
	 * (注意:这可能会使一些 TLE 留下未引用的 ressortgroupref 标记,但这是无害的。)
	 */
	if (surplusvars != NULL)
	{
		List	   *new_groupby = NIL;

		foreach(lc, root->processed_groupClause)
		{
			SortGroupClause *sgc = lfirst_node(SortGroupClause, lc);
			TargetEntry *tle = get_sortgroupclause_tle(sgc, parse->targetList);
			Var		   *var = (Var *) tle->expr;

			/*
			 * New list must include non-Vars, outer Vars, and anything not
			 * marked as surplus.
			 * 新列表必须包括非变量、外部变量以及未标记为多余的任何内容。
			 */
			if (!IsA(var, Var) ||
				var->varlevelsup > 0 ||
				!bms_is_member(var->varattno - FirstLowInvalidHeapAttributeNumber,
							   surplusvars[var->varno]))
				new_groupby = lappend(new_groupby, sgc);
		}

		root->processed_groupClause = new_groupby;
	}
}

功能实现案例调试

postgres=# create temp table t1 (a int, b int, c int, d int, primary key (a, b));
CREATE TABLE
postgres=# create temp table t2 (x int, y int, z int, primary key (x, y));
CREATE TABLE
postgres=# create temp table t3 (a int, b int, c int, primary key(a, b) deferrable);
CREATE TABLE
postgres=#

case 1

第一种:可以从 GROUP BY 中删除非主键列

postgres=# explain (costs off) select * from t1 group by a,b,c,d;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: a, b
   ->  Seq Scan on t1
(3 rows)

postgres=#

在这里插入图片描述

此时的函数堆栈,如下:

remove_useless_groupby_columns(PlannerInfo * root)
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, ParseState * pstate, ParamListInfo params)
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, DestReceiver * dest, DestReceiver * altdest, QueryCompletion * qc)
exec_simple_query(const char * query_string)
...

如上root->processed_groupClause就是这四个group by项;此时的parse->rtable含有两项,如下:

在这里插入图片描述

这两项分别是:RTE_RELATIONRTE_GROUP,关于新的RTE(这个我们后面再详述) 如下:

Introduce an RTE for the grouping step

If there are subqueries in the grouping expressions, each of these
subqueries in the targetlist and HAVING clause is expanded into
distinct SubPlan nodes.  As a result, only one of these SubPlan nodes
would be converted to reference to the grouping key column output by
the Agg node; others would have to get evaluated afresh.  This is not
efficient, and with grouping sets this can cause wrong results issues
in cases where they should go to NULL because they are from the wrong
grouping set.  Furthermore, during re-evaluation, these SubPlan nodes
might use nulled column values from grouping sets, which is not
correct.

This issue is not limited to subqueries.  For other types of
expressions that are part of grouping items, if they are transformed
into another form during preprocessing, they may fail to match lower
target items.  This can also lead to wrong results with grouping sets.

To fix this issue, we introduce a new kind of RTE representing the
output of the grouping step, with columns that are the Vars or
expressions being grouped on.  In the parser, we replace the grouping
expressions in the targetlist and HAVING clause with Vars referencing
this new RTE, so that the output of the parser directly expresses the
semantic requirement that the grouping expressions be gotten from the
grouping output rather than computed some other way.  In the planner,
we first preprocess all the columns of this new RTE and then replace
any Vars in the targetlist and HAVING clause that reference this new
RTE with the underlying grouping expressions, so that we will have
only one instance of a SubPlan node for each subquery contained in the
grouping expressions.

Bump catversion because this changes the querytree produced by the
parser.

Thanks to Tom Lane for the idea to invent a new kind of RTE.

Per reports from Geoff Winkless, Tobias Wendorff, Richard Guo from
various threads.

接下来,foreach(lc, root->processed_groupClause)循环扫描 GROUP BY 子句以查找简单变量的 GROUP BY 项。使用 RTE k 中属于 GROUP BY 项的列 attnos 的位图集填充 groupbyattnos[k]。换言之:这里就是将a b c d这四项(因为属于同一个表(所以范围表中此变量关系的索引都是1),且都是普通Var) 都添加到groupbyattnos[1]这个位图集中进行记录!

这里的tryremove存在的意义就是:如果这不是此关系的第一列(指的就是该RTE的groupby项不止一个 存在可以删除的可能),那么我们现在有多个列。这意味着可能有一些可以删除。

	/*
	 * No Vars or didn't find multiple Vars for any relation in the GROUP BY?
	 * If so, nothing can be removed, so don't waste more effort trying.
	 * 没有 Vars 或未在 GROUP BY 中找到任何关系的多个 Vars?
	 * 如果是这样,则无法删除任何内容,因此不要再浪费精力尝试。
	 */
	if (!tryremove)
		return;

接下来这里遍历parse->rtable,目的就是:检查每个关系,看看是否有可能从 GROUP BY 中删除部分 Var。上面groupbyattnos[1]这个位图集的内容,如下(注:因为本文并非是一次调试完成的,所以存在变量地址不一样的情况 大家注意忽略即可):

在这里插入图片描述


然后接下来遍历rel->indexlist,其目的是:现在检查此关系的每个索引,看是否有任何列是此关系的分组列的适当子集。对于这个t1的索引情况,如下:

在这里插入图片描述

该表就一个主键,ab是其主键列 如下:

postgres=# \d+ t1
                                           Table "public.t1"
 Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 a      | integer |           | not null |         | plain   |             |              | 
 b      | integer |           | not null |         | plain   |             |              | 
 c      | integer |           |          |         | plain   |             |              | 
 d      | integer |           |          |         | plain   |             |              | 
Indexes:
    "t1_pkey" PRIMARY KEY, btree (a, b)
Not-null constraints:
    "t1_a_not_null" NOT NULL "a"
    "t1_b_not_null" NOT NULL "b"
Access method: heap

postgres=#

因为它们符合条件,这里就不进行忽略:跳过任何非唯一和可延迟索引。谓词索引尚未检查,因此我们也必须跳过它们,因为稍后进行的 predOK 检查可能会失败。且也不是表达式索引

在这里插入图片描述

如上所示:这里索引列和非空列的检查就是匹配的,于是这两列检查完成之后 nulls_check_ok = true,且都被add到ind_attnos位图集中了,如下:

在这里插入图片描述

接下来这里很有意思,如下:

			/*
			 * 跳过索引列不是 GROUP BY 的适当子集的任何索引
			 */
			if (bms_subset_compare(ind_attnos, relattnos) != BMS_SUBSET1)
				continue;

在这里插入图片描述

如上的目的:跳过索引列不是 GROUP BY 的适当子集的任何索引。 这里主键索引ab列正好是4列的子集!

继续:

			/*
			 * 记录具有最少列的索引中的属性编号。
			 * 这样可以从 GROUP BY 子句中删除最大数量的列。
			 * 将来,我们可能希望考虑使用最窄的列集并查看 pg_statistic.stawidth,因为使用具有两个 INT4 的索引可能比使用一个长 varlena 列更好。
			 */
			if (index->nkeycolumns < best_nkeycolumns)
			{
				best_keycolumns = ind_attnos;
				best_nkeycolumns = index->nkeycolumns;
			}

这里解释一下:现如今(2025年02月15日 16:47:52)的做法 就是 (选择最少列的索引)为了简单的删除更多的分组列,开发者的备注大家也看到了 为了说不定会更换方案,大家有兴趣的可以自行研究 给pghacker提交patch!


遍历rel->indexlist结束之后,best_keycolumns位图集不为空 说明:找到了一个合适的索引,接下来 将记下可以删除的列的信息到surplusvars[relid]中:

			/* Remember the attnos of the removable columns */
			surplusvars[relid] = bms_difference(relattnos, best_keycolumns);

于是surplusvars[1]现在存放的就是cd,于是处理的逻辑 如下:

	/*
	 * 如果我们发现任何多余的 Var,则构建一个不包含它们的新 GROUP BY 子句。
	 * (注意:这可能会使一些 TLE 留下未引用的 ressortgroupref 标记,但这是无害的。)
	 */
	if (surplusvars != NULL)
	{
		List	   *new_groupby = NIL;

		foreach(lc, root->processed_groupClause)
		{
			SortGroupClause *sgc = lfirst_node(SortGroupClause, lc);
			TargetEntry *tle = get_sortgroupclause_tle(sgc, parse->targetList);
			Var		   *var = (Var *) tle->expr;

			/*
			 * 新列表必须包括非变量、外部变量以及未标记为多余的任何内容。
			 */
			if (!IsA(var, Var) ||
				var->varlevelsup > 0 ||
				!bms_is_member(var->varattno - FirstLowInvalidHeapAttributeNumber,
							   surplusvars[var->varno])) // a 和 b 不是surplusvars[1]的成员
				new_groupby = lappend(new_groupby, sgc);// 于是它俩将会出现在 new_groupby 中
		}

		root->processed_groupClause = new_groupby;
	}

在这里插入图片描述


case 2

第二种:如果GROUP BY中不存在完整的PK,则无法进行删除

postgres=# explain (costs off) select a,c from t1 group by a,c,d;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: a, c, d
   ->  Seq Scan on t1
(3 rows)

postgres=#

在这里插入图片描述

如上,relattnos位图集就是a c d三列,继续:

在这里插入图片描述

如上,该主键索引列a b不是分组列a c d的合适子集,因此无法实现删除!


case 3

第三种:跨多个表删除测试

postgres=# explain (costs off) select *
from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.y,t2.z;
                      QUERY PLAN                      
------------------------------------------------------
 HashAggregate
   Group Key: t1.a, t1.b
   ->  Hash Join
         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
         ->  Seq Scan on t2
         ->  Hash
               ->  Seq Scan on t1
(7 rows)

postgres=#

在这里插入图片描述

如上,root->processed_groupClause就是它们7个列。

在这里插入图片描述

此时的parse->rtable此时就是4个元素,依次是:RTE_RELATIONRTE_RELATIONRTE_JOINRTE_GROUP

经过对root->processed_groupClause的遍历处理之后,如下:

groupbyattnos[1] = {a b c d}
groupbyattnos[2] = {x y z} 

接下来在对parse->rtable的遍历中,当然了 只遍历RTE_RELATION,首先是t1,如下:

	best_keycolumns = ind_attnos; // a b
	best_nkeycolumns = index->nkeycolumns; // 2

	surplusvars[1] = c d

接下来是t2,如下:

	best_keycolumns = ind_attnos; // x y
	best_nkeycolumns = index->nkeycolumns; // 2

	surplusvars[2] = z

然后在构造new_groupby阶段,去掉可删除列 内容如下:

在这里插入图片描述

大家也许会有疑问,这时请大家翻到上面看一下tom lane的提交,继续:

在这里插入图片描述

此时的函数堆栈,如下:

make_pathkeys_for_sortclauses_extended(PlannerInfo * root, List ** sortclauses, List * tlist, _Bool remove_redundant, _Bool remove_group_rtindex, _Bool * sortable, _Bool set_ec_sortref)
standard_qp_callback(PlannerInfo * root, void * extra)
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, ParseState * pstate, ParamListInfo params)
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, DestReceiver * dest, DestReceiver * altdest, QueryCompletion * qc)
exec_simple_query(const char * query_string)

在这里插入图片描述

// src/backend/optimizer/path/pathkeys.c

/*
 * pathkey_is_redundant
 *	   Is a pathkey redundant with one already in the given list?
 *	   给定列表中是否已存在与该路径键重复的键?
 *
 * We detect two cases:
 * 我们检测两种情况:
 *
 * 1. If the new pathkey's equivalence class contains a constant, and isn't
 * below an outer join, then we can disregard it as a sort key.  An example:
 * 如果新路径键的等价类包含常量,并且不在外连接之下,那么我们可以将其忽略,不作为排序键。例如:
 *			SELECT ... WHERE x = 42 ORDER BY x, y;
 * We may as well just sort by y.  Note that because of opfamily matching,
 * this is semantically correct: we know that the equality constraint is one
 * that actually binds the variable to a single value in the terms of any
 * ordering operator that might go with the eclass.  This rule not only lets
 * us simplify (or even skip) explicit sorts, but also allows matching index
 * sort orders to a query when there are don't-care index columns.
 * 我们也可以按y排序。
 * 请注意,由于opfamily匹配,这在语义上是正确的:我们知道等式约束实际上是一个将变量绑定到单个值的约束,这是根据任何可能与eclass一起使用的排序运算符来实现的。
 * 此规则不仅让我们简化(甚至跳过)显式排序,而且允许在有无索引列时将索引排序顺序与查询相匹配。
 *
 * 2. If the new pathkey's equivalence class is the same as that of any
 * existing member of the pathkey list, then it is redundant.  Some examples:
 * 如果新路径键的等价类与路径键列表中任何现有成员的等价类相同,则它是多余的。以下是一些示例:
 *			SELECT ... ORDER BY x, x;
 *			SELECT ... ORDER BY x, x DESC;
 *			SELECT ... WHERE x = y ORDER BY x, y;
 * In all these cases the second sort key cannot distinguish values that are
 * considered equal by the first, and so there's no point in using it.
 * Note in particular that we need not compare opfamily (all the opfamilies
 * of the EC have the same notion of equality) nor sort direction.
 * 在所有这些情况下,第二个排序键无法区分第一个排序键认为相等的值,因此使用它毫无意义。
 * 特别注意,我们不需要比较 opfamily(EC 的所有 opfamily 都具有相同的相等概念)或排序方向。
 *
 * Both the given pathkey and the list members must be canonical for this
 * to work properly, but that's okay since we no longer ever construct any
 * non-canonical pathkeys.  (Note: the notion of a pathkey *list* being
 * canonical includes the additional requirement of no redundant entries,
 * which is exactly what we are checking for here.)
 * 为了正常工作,给定的路径键和列表成员都必须是规范的,但这没关系,因为我们不再构造任何非规范的路径键。
 * (注意:路径键*列表*规范的概念包括没有冗余条目的额外要求,这正是我们在这里检查的。)
 *
 * Because the equivclass.c machinery forms only one copy of any EC per query,
 * pointer comparison is enough to decide whether canonical ECs are the same.
 * 因为 equivclass.c 机制每个查询只形成任何 EC 的一个副本,所以指针比较足以决定规范 EC 是否相同。
 */
static bool
pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys)
{
	EquivalenceClass *new_ec = new_pathkey->pk_eclass;
	ListCell   *lc;

	/* Check for EC containing a constant --- unconditionally redundant */
	// 检查 EC 是否包含常量 --- 无条件冗余
	if (EC_MUST_BE_REDUNDANT(new_ec))
		return true;

	/* If same EC already used in list, then redundant */
	// 如果列表中已经使用了相同的 EC,则为冗余
	foreach(lc, pathkeys)
	{
		PathKey    *old_pathkey = (PathKey *) lfirst(lc);

		if (new_ec == old_pathkey->pk_eclass)
			return true;
	}

	return false;
}

之后pathkeys 也就是 root->group_pathkeys*sortclauses都将只有a b,如下:

在这里插入图片描述


case 4

第四种:t1可以优化但t2不能优化的测试用例

postgres=# explain (costs off) select t1.*,t2.x,t2.z
from t1 inner join t2 on t1.a = t2.x and t1.b = t2.y
group by t1.a,t1.b,t1.c,t1.d,t2.x,t2.z;
                      QUERY PLAN                      
------------------------------------------------------
 HashAggregate
   Group Key: t1.a, t1.b, t2.z
   ->  Hash Join
         Hash Cond: ((t2.x = t1.a) AND (t2.y = t1.b))
         ->  Seq Scan on t2
         ->  Hash
               ->  Seq Scan on t1
(7 rows)

postgres=#

这里关键数据,如下:

groupbyattnos[1] = {a b c d}
groupbyattnos[2] = {x z}

surplusvars[1] = {c d}

t2的列无法删除(不符合索引),因此new_groupby如下:

在这里插入图片描述

然后在make_pathkeys_for_sortclauses_extended中将x删掉,如下:

在这里插入图片描述


case 5

第五种:PK可延迟时deferrable无法优化

postgres=# explain (costs off) select * from t3 group by a,b,c;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: a, b, c
   ->  Seq Scan on t3
(3 rows)

postgres=#

在这里插入图片描述

该索引是可延迟索引,不可使用!


case 6

第六种:

postgres=# create temp table t1c () inherits (t1);
CREATE TABLE
-- 确保当 t1 有子表时我们不会删除任何列
postgres=# explain (costs off) select * from t1 group by a,b,c,d;
             QUERY PLAN              
-------------------------------------
 HashAggregate
   Group Key: t1.a, t1.b, t1.c, t1.d
   ->  Append
         ->  Seq Scan on t1 t1_1
         ->  Seq Scan on t1c t1_2
(5 rows)

在这里插入图片描述

原因如下:我们必须跳过继承父表,因为某些子关系可能会导致重复行。然而,分区表不会发生这种情况。 因为这里Seq Scan子表,因此上面不会删除列!

下面是用deepseek查询出来的结果,大家可以参考着看:

/*
在 PostgreSQL 中,继承表和分区表的行为有所不同,尤其是在数据行重复和主键约束方面。

### 继承表
1. **数据行重复**:
   - 继承表不会自动防止数据行重复。如果向父表和子表分别插入相同的数据,父表和子表中可能会出现重复行。
   - 例如,如果父表和子表都有相同的行,查询父表时会返回这些重复行。

2. **主键约束**:
   - 父表的主键不会自动应用到子表。子表需要单独定义主键。
   - 如果父表和子表都有主键约束,且插入的数据在主键列上相同,会导致冲突。

### 分区表
1. **数据行重复**:
   - 分区表通过分区键确保数据行不会重复。数据行根据分区键被分配到特定分区,同一行不会出现在多个分区中。
   - 因此,分区表不会出现数据行重复。

2. **主键约束**:
   - 分区表的主键约束会应用到所有分区。主键约束在整个分区表中是全局的,确保数据行在分区表中唯一。
   - 如果尝试插入重复的主键值,无论插入到哪个分区,都会触发主键冲突。

### 总结
- **继承表**:可能出现数据行重复,父表和子表的主键约束独立。
- **分区表**:不会出现数据行重复,主键约束在整个分区表中全局有效。

在设计数据库时,应根据需求选择继承表或分区表,并注意它们的不同行为。
*/

-- 如果我们只查询父级,那么可以删除列。
postgres=# explain (costs off) select * from only t1 group by a,b,c,d;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: a, b
   ->  Seq Scan on t1
(3 rows)

postgres=#

相关语法如下:

extended_relation_expr:
			qualified_name '*'
				{
					/* inheritance query, explicitly */
					$$ = $1;
					$$->inh = true;
					$$->alias = NULL;
				}
			| ONLY qualified_name
				{
					/* no inheritance */
					$$ = $2;
					$$->inh = false;
					$$->alias = NULL;
				}
			| ONLY '(' qualified_name ')'
				{
					/* no inheritance, SQL99-style syntax */
					$$ = $3;
					$$->inh = false;
					$$->alias = NULL;
				}
		;

在这里插入图片描述


postgres=# create temp table p_t1 (
postgres(#   a int,
postgres(#   b int,
postgres(#   c int,
postgres(#   d int,
postgres(#   primary key(a,b)
postgres(# ) partition by list(a);
CREATE TABLE
postgres=# create temp table p_t1_1 partition of p_t1 for values in(1);
CREATE TABLE
postgres=# create temp table p_t1_2 partition of p_t1 for values in(2);
CREATE TABLE
-- 确保我们可以删除分区表的非 PK 列。
postgres=# explain (costs off) select * from p_t1 group by a,b,c,d;
           QUERY PLAN           
--------------------------------
 HashAggregate
   Group Key: p_t1.a, p_t1.b
   ->  Append
         ->  Seq Scan on p_t1_1
         ->  Seq Scan on p_t1_2
(5 rows)

postgres=#

在这里插入图片描述

如上是分区表,不再赘述!


case 7

第七种:确保我们不会从 GROUP BY 中删除任何可空列上的唯一索引的列。

postgres=# create unique index t3_c_uidx on t3(c);
CREATE INDEX
postgres=# explain (costs off) select b,c from t3 group by b,c;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: b, c
   ->  Seq Scan on t3
(3 rows)
postgres=# \d+ t3
                                           Table "public.t3"
 Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 a      | integer |           | not null |         | plain   |             |              | 
 b      | integer |           | not null |         | plain   |             |              | 
 c      | integer |           |          |         | plain   |             |              | 
Indexes:
    "t3_pkey" PRIMARY KEY, btree (a, b) DEFERRABLE
    "t3_c_uidx" UNIQUE, btree (c)
Not-null constraints:
    "t3_a_not_null" NOT NULL "a"
    "t3_b_not_null" NOT NULL "b"
Access method: heap

postgres=#

在这里插入图片描述

如上,现在t3有两个index,如下:

postgres=# select * from pg_index where indrelid = 't3'::regclass;
 indexrelid | indrelid | indnatts | indnkeyatts | indisunique | indnullsnotdistinct | indisprimary | indisexclusion | indimmediate | indisclustered | indisvalid | indcheckxmin | indisready | indislive | indisreplident | indkey | indcollation | indclass  | indoption | indexprs | indpred 
------------+----------+----------+-------------+-------------+---------------------+--------------+----------------+--------------+----------------+------------+--------------+------------+-----------+----------------+--------+--------------+-----------+-----------+----------+---------
      16403 |    16398 |        2 |           2 | t           | f                   | t            | f              | f            | f              | t          | f            | t          | t         | f              | 1 2    | 0 0          | 1978 1978 | 0 0       |          | 
      16438 |    16398 |        1 |           1 | t           | f                   | f            | f              | t            | f              | t          | f            | t          | t         | f              | 3      | 0            | 1978      | 0         |          | 
(2 rows)

postgres=#

在这里插入图片描述

该索引是唯一索引,但是这并不够。我们必须坚持索引列全部定义为 NOT NULL,否则可能存在重复的 NULL。但是,当索引定义为 NULLS NOT DISTINCT 时,我们可以放宽此检查,因为只能有 1 个 NULL 行,因此尽管存在 NULL,仍可保持对唯一列的函数依赖性。 因此该索引也是忽略的,至于那个主键索引 请参考上面 case 5,不再赘述:

在这里插入图片描述


case 8

第八种:使列为 NOT NULL,并确保我们删除了冗余列

postgres=# alter table t3 alter column c set not null;
ALTER TABLE
postgres=# explain (costs off) select b,c from t3 group by b,c;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: c
   ->  Seq Scan on t3
(3 rows)

上面这种情况下,关键数据如下:

groupbyattnos[relid] = {9 10} // b c  // 2 - (-7) = 9

在这里插入图片描述

此时的t3_c_uidx的非空检查就是OK的:

在这里插入图片描述

此时索引列c是分组列b c的一个合适子集,于是就可以删除冗余了!


case 9

第九种:当有多个支持唯一索引且 GROUP BY 包含涵盖所有这些索引的列时,请确保我们选择列数最少的索引,以便我们可以从 GROUP BY 中删除更多列。

postgres=# explain (costs off) select a,b,c from t3 group by a,b,c;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: c
   ->  Seq Scan on t3
(3 rows)

与上述相同,但尝试以不同的方式排列列以确保获得相同的结果。

postgres=# explain (costs off) select a,b,c from t3 group by c,a,b;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: c
   ->  Seq Scan on t3
(3 rows)

注:该case 上面的例子是错的,我给PostgreSQL已提交patch修正了,如下:

  • 邮件列表:Modify an incorrect regression test case in the group by key value elimination function,点击前往

  • git提交记录:Fix poorly written regression test,点击前往


下面我们换正确的测试用例,如下:

postgres=# \d+ t2
                                           Table "public.t2"
 Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 x      | integer |           | not null |         | plain   |             |              | 
 y      | integer |           | not null |         | plain   |             |              | 
 z      | integer |           |          |         | plain   |             |              | 
Indexes:
    "t2_pkey" PRIMARY KEY, btree (x, y)
Not-null constraints:
    "t2_x_not_null" NOT NULL "x"
    "t2_y_not_null" NOT NULL "y"
Access method: heap

postgres=# create unique index t2_z_uidx on t2(z);
CREATE INDEX
postgres=# alter table t2 alter column z set not null;
ALTER TABLE
postgres=# 
postgres=# \d+ t2
                                           Table "public.t2"
 Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 x      | integer |           | not null |         | plain   |             |              | 
 y      | integer |           | not null |         | plain   |             |              | 
 z      | integer |           | not null |         | plain   |             |              | 
Indexes:
    "t2_pkey" PRIMARY KEY, btree (x, y)
    "t2_z_uidx" UNIQUE, btree (z)
Not-null constraints:
    "t2_x_not_null" NOT NULL "x"
    "t2_y_not_null" NOT NULL "y"
    "t2_z_not_null" NOT NULL "z"
Access method: heap

postgres=#
postgres=# explain (costs off) select x,y,z from t2 group by x,y,z;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: z
   ->  Seq Scan on t2
(3 rows)

postgres=#

索引t2_z_uidx的效果,如下:

在这里插入图片描述

索引t2_pkey的效果,如下:

在这里插入图片描述

于是有best_nkeycolumns的存在,自然选择索引t2_z_uidx 可以删除更多的列!


postgres=# explain (costs off) select x,y,z from t2 group by z,x,y;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: z
   ->  Seq Scan on t2
(3 rows)

postgres=#

上面虽然调整了分组列的顺序,但是不影响两个位图集的内容 自然对结果影响不大:

ind_attnos
 和
relattnos = groupbyattnos[relid];

case 10

第十种:确保我们不使用部分索引作为函数依赖性的证明

postgres=# drop index t2_z_uidx;
DROP INDEX
postgres=# create index t2_z_uidx on t2 (z) where z > 0;
CREATE INDEX
postgres=# \d+ t2
                                           Table "public.t2"
 Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 x      | integer |           | not null |         | plain   |             |              | 
 y      | integer |           | not null |         | plain   |             |              | 
 z      | integer |           | not null |         | plain   |             |              | 
Indexes:
    "t2_pkey" PRIMARY KEY, btree (x, y)
    "t2_z_uidx" btree (z) WHERE z > 0
Not-null constraints:
    "t2_x_not_null" NOT NULL "x"
    "t2_y_not_null" NOT NULL "y"
    "t2_z_not_null" NOT NULL "z"
Access method: heap

postgres=# 
postgres=# \x
Expanded display is on.
postgres=# 
postgres=# select indnatts,indnkeyatts,indnullsnotdistinct,indisprimary,indisunique,indimmediate,indpred,indkey from pg_index where indrelid = 't2'::regclass;
-[ RECORD 1 ]-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
indnatts            | 2
indnkeyatts         | 2
indnullsnotdistinct | f
indisprimary        | t
indisunique         | t
indimmediate        | t
indpred             | 
indkey              | 1 2
-[ RECORD 2 ]-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
indnatts            | 1
indnkeyatts         | 1
indnullsnotdistinct | f
indisprimary        | f
indisunique         | f
indimmediate        | t
indpred             | {OPEXPR :opno 521 :opfuncid 147 :opresulttype 16 :opretset false :opcollid 0 :inputcollid 0 :args ({VAR :varno 1 :varattno 3 :vartype 23 :vartypmod -1 :varcollid 0 :varnullingrels (b) :varlevelsup 0 :varreturningtype 0 :varnosyn 1 :varattnosyn 3 :location -1} {CONST :consttype 23 :consttypmod -1 :constcollid 0 :constlen 4 :constbyval true :constisnull false :location -1 :constvalue 4 [ 0 0 0 0 0 0 0 0 ]}) :location -1}
indkey              | 3

postgres=#
postgres=# explain (costs off) select y,z from t2 group by y,z;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: y, z
   ->  Seq Scan on t2
(3 rows)

postgres=#

如上,两个索引均未能实现键值消除,原因如下:

在这里插入图片描述

在这里插入图片描述


case 11

第十一种:定义为 NULLS NOT DISTINCT 的唯一索引不需要对索引列进行支持的 NOT NULL 约束。确保从此类表的 GROUP BY 中删除冗余列。

postgres=# drop index t2_z_uidx;
DROP INDEX
postgres=# alter table t2 alter column z drop not null;
ALTER TABLE
postgres=# create unique index t2_z_uidx on t2(z) nulls not distinct;
CREATE INDEX
postgres=# \d+ t2
                                           Table "public.t2"
 Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 x      | integer |           | not null |         | plain   |             |              | 
 y      | integer |           | not null |         | plain   |             |              | 
 z      | integer |           |          |         | plain   |             |              | 
Indexes:
    "t2_pkey" PRIMARY KEY, btree (x, y)
    "t2_z_uidx" UNIQUE, btree (z) NULLS NOT DISTINCT
Not-null constraints:
    "t2_x_not_null" NOT NULL "x"
    "t2_y_not_null" NOT NULL "y"
Access method: heap

postgres=# explain (costs off) select y,z from t2 group by y,z;
      QUERY PLAN      
----------------------
 HashAggregate
   Group Key: z
   ->  Seq Scan on t2
(3 rows)

postgres=#
postgres=# select indnatts,indnkeyatts,indnullsnotdistinct,indisprimary,indisunique,indimmediate,indpred,indkey from pg_index where indrelid = 't2'::regclass;
 indnatts | indnkeyatts | indnullsnotdistinct | indisprimary | indisunique | indimmediate | indpred | indkey 
----------+-------------+---------------------+--------------+-------------+--------------+---------+--------
        2 |           2 | f                   | t            | t           | t            |         | 1 2
        1 |           1 | t                   | f            | t           | t            |         | 3
(2 rows)

postgres=#

如上t2_z_uidx索引就可以直接使用了,原因如下:

在这里插入图片描述

/*
在 PostgreSQL 中,创建索引时可以使用 `NULLS NOT DISTINCT` 选项来控制索引对 `NULL` 值的处理方式。
这个选项从 PostgreSQL 15 开始引入,用于确保索引将 `NULL` 值视为非唯一值。

### 背景
在 PostgreSQL 中,默认情况下,唯一索引(`UNIQUE` 索引)会将 `NULL` 值视为不同的值。
这意味着,即使有多个 `NULL` 值存在,唯一索引也不会将它们视为冲突。例如:

CREATE TABLE example (
    id SERIAL PRIMARY KEY,
    value INT UNIQUE
);

INSERT INTO example (value) VALUES (NULL);
INSERT INTO example (value) VALUES (NULL); -- 允许插入,因为 NULL 被视为不同

### `NULLS NOT DISTINCT` 的作用
`NULLS NOT DISTINCT` 选项改变了这种行为,使得唯一索引将 `NULL` 值视为相同的值。
这意味着,如果尝试插入多个 `NULL` 值,唯一索引会将其视为冲突,从而阻止插入。

CREATE TABLE example (
    id SERIAL PRIMARY KEY,
    value INT UNIQUE NULLS NOT DISTINCT
);

INSERT INTO example (value) VALUES (NULL);
INSERT INTO example (value) VALUES (NULL); -- 报错,因为 NULL 被视为相同

### 使用场景
`NULLS NOT DISTINCT` 适用于那些希望确保列中最多只能有一个 `NULL` 值的场景。
这在某些业务逻辑中可能非常有用,例如当 `NULL` 值代表某种特殊状态时,你可能希望确保这种状态只能出现一次。

### 示例
假设你有一个表 `users`,其中有一个 `email` 列,你希望确保每个用户的 `email` 是唯一的,但同时你也希望 `email` 可以为 `NULL`,并且最多只能有一个 `NULL` 值。你可以这样创建索引:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email TEXT UNIQUE NULLS NOT DISTINCT
);

INSERT INTO users (email) VALUES ('user1@example.com');
INSERT INTO users (email) VALUES (NULL);
INSERT INTO users (email) VALUES (NULL); -- 报错,因为 NULL 被视为相同

### 总结
`NULLS NOT DISTINCT` 是 PostgreSQL 15 引入的一个新特性,用于控制唯一索引对 `NULL` 值的处理方式。
默认情况下,唯一索引将 `NULL` 值视为不同的值,而使用 `NULLS NOT DISTINCT` 后,唯一索引会将 `NULL` 值视为相同的值,从而确保列中最多只能有一个 `NULL` 值。
*/

核心函数流程图示

下面是我根据张树杰书的图片做的函数remove_useless_groupby_columns的图示,如下:

在这里插入图片描述

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

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

相关文章

Python基于循环神经网络的情感分类系统(附源码,文档说明)

博主介绍&#xff1a;✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3…

Zookeeper应用案例-分布式锁-实现思路

以下是具体实现代码 第一步&#xff1a;注册锁节点 第二步&#xff1a;获取锁节点&#xff0c;如果自己是最小的节点&#xff0c;就获取权限 第三步&#xff1a;拿到锁就开始自己的业务逻辑 第四步&#xff1a;业务逻辑好了就要释放这把锁 第五步&#xff1a;重新注册监听&…

java练习(32)

ps&#xff1a;题目来自力扣 环形链表 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表…

伯克利 CS61A 课堂笔记 10 —— Trees

本系列为加州伯克利大学著名 Python 基础课程 CS61A 的课堂笔记整理&#xff0c;全英文内容&#xff0c;文末附词汇解释。 目录 01 Trees 树 Ⅰ Tree Abstraction Ⅱ Implementing the Tree Abstraction 02 Tree Processing 建树过程 Ⅰ Fibonacci tree Ⅱ Tree Process…

让编程变成一种享受-明基RD320U显示器

引言 作为一名有着多年JAVA开发经验的从业者&#xff0c;在工作过程中&#xff0c;显示器的重要性不言而喻。它不仅是我们与代码交互的窗口&#xff0c;更是影响工作效率和体验的关键因素。在多年的编程生涯中&#xff0c;我遇到过各种各样的问题。比如&#xff0c;在进行代码…

10分钟上手DeepSeek开发:SpringBoot + Vue2快速构建AI对话系统

作者&#xff1a;后端小肥肠 目录 1. 前言 为什么选择DeepSeek&#xff1f; 本文技术栈 2. 环境准备 2.1. 后端项目初始化 2.2. 前端项目初始化 3. 后端服务开发 3.1. 配置文件 3.2. 核心服务实现 4. 前端服务开发 4.1. 聊天组件ChatWindow.vue开发 5. 效果展示及源…

Linux环境开发工具

Linux软件包管理器yum Linux下安装软件方式&#xff1a; 源代码安装rpm安装——Linux安装包yum安装——解决安装源、安装版本、安装依赖的问题 yum对应于Windows系统下的应用商店 使用Linux系统的人&#xff1a;大部分是职业程序员 客户端怎么知道去哪里下载软件&#xff1…

JupyterNotebook高级使用:常用魔法命令

%%writefile test.py def Test(name):print("Test",name,"success")运行结果&#xff1a;就是在我们的文件目录下面创建了这个test.py文件&#xff0c;主要是认识一下这个里面的%%writefile表示创建新的文件&#xff0c;这个文件里面的内容就是上面我们定义…

C++ Primer 类的作用域

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

50页PDF|数字化转型成熟度模型与评估(附下载)

一、前言 这份报告依据GBT 43439-2023标准&#xff0c;详细介绍了数字化转型的成熟度模型和评估方法。报告将成熟度分为五个等级&#xff0c;从一级的基础转型意识&#xff0c;到五级的基于数据的生态价值构建与创新&#xff0c;涵盖了组织、技术、数据、资源、数字化运营等多…

机器学习实战(8):降维技术——主成分分析(PCA)

第8集&#xff1a;降维技术——主成分分析&#xff08;PCA&#xff09; 在机器学习中&#xff0c;降维&#xff08;Dimensionality Reduction&#xff09; 是一种重要的数据处理技术&#xff0c;用于减少特征维度、去除噪声并提高模型效率。主成分分析&#xff08;Principal C…

前端插件使用xlsx-populate,花样配置excel内容,根据坐添加标替换excel内容,修改颜色,合并单元格...。

需求要求&#xff1a;业务人员有个非常复杂得excel表格&#xff0c;各种表头等&#xff0c;但是模板是固定得。当然也可以实现在excel上搞出各种表格&#xff0c;但是不如直接用已有模板替换其中要动态得内容方便&#xff0c;这里我们用到CSDN得 xlsx-populate 插件。 实列中我…

分布式大语言模型服务引擎vLLM论文解读

论文地址&#xff1a;Efficient Memory Management for Large Language Model Serving with PagedAttention 摘要 大语言模型&#xff08;LLMs&#xff09;的高吞吐量服务需要一次对足够多的请求进行批处理。然而&#xff0c;现有系统面临困境&#xff0c;因为每个请求的键值…

如何开发一个大模型应用?

1. 背景 AIGC技术的突破性进展彻底改变了技术开发的范式&#xff0c;尤其是以GPT为代表的LLM&#xff0c;凭借其强大的自然语言理解与生成能力&#xff0c;迅速成为全球科技领域的焦点。2023年末&#xff0c;随着ChatGPT的爆火&#xff0c;AIGC技术从实验室走向规模化应用&…

[数据结构]二叉搜索树详解

目录 一、二叉搜索树的概念 二、二叉搜索树的性能分析 三、二叉搜索树的中序遍历用于排序去重 四、二叉搜索树的查找 1、查找的非递归写法 2、查找的递归写法 五、二叉搜索树的插入 1、插入的非递归写法 2、插入的递归写法 六、二叉搜索树的删除 1、删除的非递归写法…

撕碎QT面具(2):groupBox内容居中显示

问题描述&#xff1a; 当笔者在GroupBox中使用Form Layout构建图中内容时&#xff0c;不能居中显示。 解决方案&#xff1a; 1、首先在form layout左右添加横向弹簧&#xff0c;并ctrl进行选中这三个控件。点击水平布局&#xff0c;让中间的控件不变形。 2、选中groupBox&#…

SpringBoot速成(14)文件上传P23-P26

1. 什么是 multipart/form-data&#xff1f; 想象一下&#xff0c;你有一个包裹要寄给朋友&#xff0c;但包裹里有不同类型的东西&#xff1a;比如一封信&#xff08;文字&#xff09;、一张照片&#xff08;图片&#xff09;和一个小礼物&#xff08;文件&#xff09;。为了确…

图论入门算法:拓扑排序(C++)

上文中我们了解了图的遍历(DFS/BFS), 本节我们来学习拓扑排序. 在图论中, 拓扑排序(Topological Sorting)是对一个有向无环图(Directed Acyclic Graph, DAG)的所有顶点进行排序的一种算法, 使得如果存在一条从顶点 u 到顶点 v 的有向边 (u, v) , 那么在排序后的序列中, u 一定…

【iOS】SwiftUI状态管理

State ObservedObject StateObject 的使用 import SwiftUIclass CountModel: ObservableObject {Published var count: Int 0 // 通过 Published 标记的变量会触发视图更新init() {print("TimerModel initialized at \(count)")} }struct ContentView: View {State…

自制简单的图片查看器(python)

图片格式&#xff1a;支持常见的图片格式&#xff08;JPG、PNG、BMP、GIF&#xff09;。 import os import tkinter as tk from tkinter import filedialog, messagebox from PIL import Image, ImageTkclass ImageViewer:def __init__(self, root):self.root rootself.root.…