PostgreSQL的学习心得和知识总结(一百六十三)|深入理解PostgreSQL数据库之 GUC参数compute_query_id 的使用和实现

目录结构

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

1、参考书籍:++《PostgreSQL数据库内核分析》++

2、参考书籍:++《数据库事务处理的艺术:事务管理与并发控制》++

3、++PostgreSQL数据库仓库链接,点击前往++

4、++日本著名PostgreSQL数据库专家 铃木启修 网站主页,点击前往++

5、参考书籍:++《PostgreSQL中文手册》++

6、++参考书籍:《PostgreSQL指南:内幕探索》,点击前往++

7、++pg百科 compute_query_id,点击前往++


1、本文内容全部来源于开源社区 GitHub和以上博主的贡献,本文也免费开源(可能会存在问题,评论区等待大佬们的指正)

2、本文目的:开源共享 抛砖引玉 一起学习

3、本文不提供任何资源 不存在任何交易 与任何组织和机构无关

4、大家可以根据需要自行 复制粘贴以及作为其他个人用途,但是不允许转载 不允许商用 (写作不易,还请见谅 💖)

5、本文内容基于PostgreSQL 17.0源码开发而成


深入理解PostgreSQL数据库之 GUC参数compute_query_id 的使用和实现



文章快速说明索引

学习目标:

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


学习内容:(详见目录)

1、深入理解PostgreSQL数据库之 GUC参数compute_query_id 的使用和实现


学习时间:

2024年12月11日 20:49:11


学习产出:

1、PostgreSQL数据库基础知识回顾 1个
2、CSDN 技术博客 1篇
3、PostgreSQL数据库内核深入学习


注:下面我们所有的学习环境是Centos8+PostgreSQL master+Oracle19C+MySQL8.0

sql 复制代码
postgres=# select version();
                                   version                                    
------------------------------------------------------------------------------
 PostgreSQL 17.0 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>

功能使用背景说明

最近在学习pg_hint_plan源码的时候,看到了如下GUC的要求:

c 复制代码
// contrib/pg_hint_plan/pg_hint_plan.c

...
	DefineCustomBoolVariable("pg_hint_plan.enable_hint_table",
							 "Let pg_hint_plan look up the hint table.",
							 NULL,
							 &pg_hint_plan_enable_hint_table,
							 false,
							 PGC_USERSET,
							 0,
							 enable_hint_table_check,
							 assign_enable_hint_table,
							 NULL);
...

该GUC参数的check函数,如下:

c 复制代码
static bool
enable_hint_table_check(bool *newval, void **extra, GucSource source)
{
	if (*newval)
	{
		EnableQueryId();

		if (!IsQueryIdEnabled())
		{
			GUC_check_errmsg("table hint is not activated because queryid is not available");
			GUC_check_errhint("Set compute_query_id to on or auto to use hint table.");
			return false;
		}
	}

	return true;
}

这里的要求就是:

由于 queryid 不可用,因此未激活表提示


compute_query_id 设置为 on 或 auto 以使用提示表。


其中compute_query_id是在PostgreSQL14的时候引入的。接下来我们先把这块内容分析完成之后,再回头去学习pg_hint_plan源码。下面是由大管家引入时候的相关提交记录:

c 复制代码
Make use of in-core query id added by commit 5fd9dfa5f5

Use the in-core query id computation for pg_stat_activity,
log_line_prefix, and EXPLAIN VERBOSE.

Similar to other fields in pg_stat_activity, only the queryid from the
top level statements are exposed, and if the backends status isn't
active then the queryid from the last executed statements is displayed.

Add a %Q placeholder to include the queryid in log_line_prefix, which
will also only expose top level statements.

For EXPLAIN VERBOSE, if a query identifier has been computed, either by
enabling compute_query_id or using a third-party module, display it.

Bump catalog version.

Discussion: https://postgr.es/m/20210407125726.tkvjdbw76hxnpwfi@nol

Author: Julien Rouhaud

Reviewed-by: Alvaro Herrera, Nitin Jadhav, Zhihong Yu

翻译一下,其目的就是:

  • 利用提交 5fd9dfa5f5 添加的内核查询 ID
  • 使用 pg_stat_activity、log_line_prefix 和 EXPLAIN VERBOSE 的核心内查询 ID 计算
  • 与 pg_stat_activity 中的其他字段类似,仅显示来自顶级语句的 queryid,如果后端状态不活动,则显示来自最后执行的语句的 queryid
  • 添加 %Q 占位符以将 queryid 包含在 log_line_prefix 中,这也将仅显示顶级语句
  • 对于 EXPLAIN VERBOSE,如果已计算查询标识符(通过启用 compute_query_id 或使用第三方模块),则显示它

如上,在PostgreSQL14将原pg_stat_statements插件的Query Identifier计算模块剥离到内核中, 使得内部可以直接使用query id功能。那么query id是什么呢?

  • 例如多条sql支持某些输入的条件不一样,其他部分都一样,可以认为是同类sql,那么通过query id来表达会比较方便
  • 注意指的不是绑定变量的sql
  • 同样需要注意,queryid和sqlid是两个概念。关于sqlid的解释 我们后面再找机会详解

c 复制代码
SHA-1: 5fd9dfa5f50e4906c35133a414ebec5b6d518493

* Move pg_stat_statements query jumbling to core.

Add compute_query_id GUC to control whether a query identifier should be
computed by the core (off by default).  It's thefore now possible to
disable core queryid computation and use pg_stat_statements with a
different algorithm to compute the query identifier by using a
third-party module.

To ensure that a single source of query identifier can be used and is
well defined, modules that calculate a query identifier should throw an
error if compute_query_id specified to compute a query id and if a query
idenfitier was already calculated.

Discussion: https://postgr.es/m/20210407125726.tkvjdbw76hxnpwfi@nol

Author: Julien Rouhaud

Reviewed-by: Alvaro Herrera, Nitin Jadhav, Zhihong Yu

翻译一下:

  • 添加 compute_query_id GUC 以控制查询标识符是否应由核心计算(默认情况下关闭)。因此,现在可以禁用核心 queryid 计算,并使用 pg_stat_statements 和不同的算法通过第三方模块来计算查询标识符。
  • 为了确保可以使用单一的查询标识符源并且定义良好,如果指定 compute_query_id 来计算查询 ID 并且已经计算了查询 ID,则计算查询标识符的模块应该抛出错误。

下面看一下使用示例1,如下:

sql 复制代码
postgres=# select version();
                                   version                                    
------------------------------------------------------------------------------
 PostgreSQL 17.0 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.1.0, 64-bit
(1 row)

postgres=# create table t1 (id int primary key);
CREATE TABLE
postgres=# insert into t1 select generate_series(1,1000);
INSERT 0 1000
postgres=# create table t2 as select * from t1;
SELECT 1000
postgres=#
sql 复制代码
postgres=# explain (verbose, costs off) select * from t1, t2 where t1.id = t2.id;
            QUERY PLAN             
-----------------------------------
 Hash Join
   Output: t1.id, t2.id
   Inner Unique: true
   Hash Cond: (t2.id = t1.id)
   ->  Seq Scan on public.t2
         Output: t2.id
   ->  Hash
         Output: t1.id
         ->  Seq Scan on public.t1
               Output: t1.id
(10 rows)

postgres=# show compute_query_id ;
 compute_query_id 
------------------
 auto
(1 row)

postgres=# set compute_query_id = on;
SET
postgres=# explain (verbose, costs off) select * from t1, t2 where t1.id = t2.id;
               QUERY PLAN               
----------------------------------------
 Hash Join
   Output: t1.id, t2.id
   Inner Unique: true
   Hash Cond: (t2.id = t1.id)
   ->  Seq Scan on public.t2
         Output: t2.id
   ->  Hash
         Output: t1.id
         ->  Seq Scan on public.t1
               Output: t1.id
 Query Identifier: -1771263242468121122
(11 rows)

postgres=#

使用示例2,如下:


使用示例3,如下:

这里有一点需要注意(这点后面还会再次解释一下):

对于log_statement输出的行,%Q 总是报告零标识符, 因为log_statement在标识符能被计算之前生成输出,包括无效标识符不能计算的无效语句。

详细可以参见中文手册:


功能实现源码解析

启用查询标识符的内核计算:

  • 查询标识符可以在pg_stat_activity视图中显示,或使用EXPLAIN命令,或者在通过log_line_prefix参数进行配置的情况下在日志中发出
  • pg_stat_statements扩展也需要计算查询标识符
  • 请注意,如果内核查询标识符计算方法不可接受,也可以使用外部模块。在这种情况下,必须始终禁用内核计算
  • 有效值为 off(始终禁用)、on(始终启用)、auto(允许 pgstatstatements 等模块自动启用它)和 regress(与 auto 具有相同的效果,但查询标识符不会显示在 EXPLAIN 输出中,以便于自动回归测试)
  • 默认值为 auto

为确保只计算和显示一个查询标识符,计算查询标识符的扩展应该在已经计算查询标识符时抛出错误。


接下来我们直接看一下PostgreSQL 17.0的相关源码,如下:

c 复制代码
// src/backend/utils/misc/guc_tables.c

	{
		{"compute_query_id", PGC_SUSET, STATS_MONITORING,
			gettext_noop("Enables in-core computation of query identifiers."),
			NULL
		},
		&compute_query_id,
		COMPUTE_QUERY_ID_AUTO, compute_query_id_options,
		NULL, NULL, NULL
	},

下面我们选择执行一个SQL,调试一下query id计算的过程。如下:

此时的函数堆栈,如下:

c 复制代码
IsQueryIdEnabled()
parse_analyze_fixedparams(RawStmt * parseTree, const char * sourceText, const Oid * paramTypes, int numParams, QueryEnvironment * queryEnv)
pg_analyze_and_rewrite_fixedparams(RawStmt * parsetree, const char * query_string, const Oid * paramTypes, int numParams, QueryEnvironment * queryEnv)
exec_simple_query(const char * query_string) 
...

如上,在语义分析阶段,才去计算id值。而log_statement的打印如下:

c 复制代码
// 

...
	/*
	 * Do basic parsing of the query or queries (this should be safe even if
	 * we are in aborted transaction state!)
	 */
	parsetree_list = pg_parse_query(query_string); // 词法语法解析

	/* Log immediately if dictated by log_statement */ // here
	if (check_log_statement(parsetree_list))
	{
		ereport(LOG,
				(errmsg("statement: %s", query_string),
				 errhidestmt(true),
				 errdetail_execute(parsetree_list)));
		was_logged = true;
	}
...
	/*
	 * Run through the raw parsetree(s) and process each one.
	 */
	foreach(parsetree_item, parsetree_list)
	{
		...
		// 计算过程在语义分析阶段
		querytree_list = pg_analyze_and_rewrite_fixedparams(parsetree, query_string,
															NULL, 0, NULL);
		...
	}
...

计算逻辑,如下:


计算结果如下:

此时的函数堆栈,如下:

c 复制代码
hash_bytes_extended(const unsigned char * k, int keylen, uint64 seed)
hash_any_extended(const unsigned char * k, int keylen, uint64 seed)
JumbleQuery(Query * query)
parse_analyze_fixedparams(RawStmt * parseTree, const char * sourceText, const Oid * paramTypes, int numParams, QueryEnvironment * queryEnv)
pg_analyze_and_rewrite_fixedparams(RawStmt * parsetree, const char * query_string, const Oid * paramTypes, int numParams, QueryEnvironment * queryEnv)
exec_simple_query(const char * query_string) 
...
c 复制代码
// src/common/hashfn.c

/*
 * hash_bytes_extended() -- hash into a 64-bit value, using an optional seed
 *		k		: the key (the unaligned variable-length array of bytes) // 键(未对齐的可变长度字节数组)
 *		len		: the length of the key, counting by bytes // 密钥的长度,以字节为单位
 *		seed	: a 64-bit seed (0 means no seed)
 *
 * Returns a uint64 value.  Otherwise similar to hash_bytes.
 */
uint64
hash_bytes_extended(const unsigned char *k, int keylen, uint64 seed);

接下来,我们再看一下(仅更换了常量的)另一个SQL计算,如下:

原因比较简单,计算id的hash函数传参都是一样的:

c 复制代码
	/* Compute query ID and mark the Query node with it */
	_jumbleNode(jstate, (Node *) query);
	query->queryId = DatumGetUInt64(hash_any_extended(jstate->jumble,
													  jstate->jumble_len,
													  0));

JumbleState *jstate的来源是上面函数_jumbleNode进行处理的:

sql 复制代码
			case T_Query:
				_jumbleQuery(jstate, expr);
				break;
c 复制代码
// src/backend/nodes/queryjumblefuncs.funcs.c

static void
_jumbleQuery(JumbleState *jstate, Node *node)
{
	Query *expr = (Query *) node;

	JUMBLE_FIELD(commandType);
	JUMBLE_NODE(utilityStmt);
	JUMBLE_NODE(cteList);
	JUMBLE_NODE(rtable);
	JUMBLE_NODE(jointree);
	JUMBLE_NODE(mergeActionList);
	JUMBLE_NODE(mergeJoinCondition);
	JUMBLE_NODE(targetList);
	JUMBLE_NODE(onConflict);
	JUMBLE_NODE(returningList);
	JUMBLE_NODE(groupClause);
	JUMBLE_FIELD(groupDistinct);
	JUMBLE_NODE(groupingSets);
	JUMBLE_NODE(havingQual);
	JUMBLE_NODE(windowClause);
	JUMBLE_NODE(distinctClause);
	JUMBLE_NODE(sortClause);
	JUMBLE_NODE(limitOffset);
	JUMBLE_NODE(limitCount);
	JUMBLE_FIELD(limitOption);
	JUMBLE_NODE(rowMarks);
	JUMBLE_NODE(setOperations);
}
sql 复制代码
-- 下面前两个SQL的query id一致

postgres=# select * from pg_sleep(20);
 pg_sleep 
----------
 
(1 row)

postgres=# select * from pg_sleep(2);
 pg_sleep 
----------
 
(1 row)

postgres=# select pg_sleep(11);
 pg_sleep 
----------
 
(1 row)

postgres=#

他们的query结构,按照上面顺序依次如下:

query_20,如下:


query_2,如下:


query2_11,如下:


如上,前两个SQL仅常量值的不一样,可是他们的Query id一样。就是因为在计算过程中,Const仅计算了consttypelocation

相关推荐
C++忠实粉丝2 分钟前
MySQL 表的内外连接
数据库·mysql
GreedySnaker15 分钟前
Qt-chart 画折线图(文字x轴)
java·数据库·qt
m0_7493175240 分钟前
分页查询和事务管理
数据库·sql·mysql·oracle·mybatis·状态模式
菜菜江江41 分钟前
在 MacOS 上安装 MongoDB Community Edition(Homebrew 版本)
数据库·mongodb·macos
Happy_Traveller2 小时前
Level DB --- coding
数据结构·数据库
云和恩墨2 小时前
私有云dbPaaS为何被Gartner技术成熟度曲线标记为“废弃”?
数据库
Python大数据分析@2 小时前
SQL和Python 哪个更容易自学?
数据库·python·sql
尘浮生2 小时前
Java项目实战II基于Java+Spring Boot+MySQL的社区帮扶对象管理系统的设计与实现(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·mysql·微信小程序·小程序
master-dragon4 小时前
实际项目中的一个OOM和事务的问题
数据库