目录结构
注:提前言明 本文借鉴了以下博主、书籍或网站的内容,其列表如下:
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
仅计算了consttype
和location
。