目录结构
注:提前言明 本文借鉴了以下博主、书籍或网站的内容,其列表如下:
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
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的要求:
// 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函数,如下:
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
源码。下面是由大管家引入时候的相关提交记录:
Make use of in-core query id added by commit 5fd9dfa5f5Use 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@nolAuthor: Julien RouhaudReviewed-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的解释 我们后面再找机会详解
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@nolAuthor: Julien RouhaudReviewed-by: Alvaro Herrera, Nitin Jadhav, Zhihong Yu
翻译一下:
- 添加 compute_query_id GUC 以控制查询标识符是否应由核心计算(默认情况下关闭)。因此,现在可以禁用核心 queryid 计算,并使用 pg_stat_statements 和不同的算法通过第三方模块来计算查询标识符。
- 为了确保可以使用单一的查询标识符源并且定义良好,如果指定 compute_query_id 来计算查询 ID 并且已经计算了查询 ID,则计算查询标识符的模块应该抛出错误。
下面看一下使用示例1,如下:
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=#
postgres=# explain (verbose, costs off) select * from t1, t2 where t1.id = t2.id;QUERY PLAN
-----------------------------------Hash JoinOutput: t1.id, t2.idInner Unique: trueHash Cond: (t2.id = t1.id)-> Seq Scan on public.t2Output: t2.id-> HashOutput: t1.id-> Seq Scan on public.t1Output: 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 JoinOutput: t1.id, t2.idInner Unique: trueHash Cond: (t2.id = t1.id)-> Seq Scan on public.t2Output: t2.id-> HashOutput: t1.id-> Seq Scan on public.t1Output: t1.idQuery Identifier: -1771263242468121122
(11 rows)postgres=#
使用示例2,如下:
使用示例3,如下:
这里有一点需要注意(这点后面还会再次解释一下):
对于log_statement输出的行,%Q 总是报告零标识符, 因为log_statement在标识符能被计算之前生成输出,包括无效标识符不能计算的无效语句。
详细可以参见中文手册:
- log_line_prefix 点击前往
功能实现源码解析
- PostgreSQL Documentation: compute_query_id parameter
启用查询标识符的内核计算:
- 查询标识符可以在pg_stat_activity视图中显示,或使用EXPLAIN命令,或者在通过log_line_prefix参数进行配置的情况下在日志中发出
- pg_stat_statements扩展也需要计算查询标识符
- 请注意,如果内核查询标识符计算方法不可接受,也可以使用外部模块。在这种情况下,必须始终禁用内核计算
- 有效值为 off(始终禁用)、on(始终启用)、auto(允许 pgstatstatements 等模块自动启用它)和 regress(与 auto 具有相同的效果,但查询标识符不会显示在 EXPLAIN 输出中,以便于自动回归测试)
- 默认值为 auto
为确保只计算和显示一个查询标识符,计算查询标识符的扩展应该在已经计算查询标识符时抛出错误。
接下来我们直接看一下PostgreSQL 17.0
的相关源码,如下:
// 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计算的过程。如下:
此时的函数堆栈,如下:
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的打印如下:
// .../** 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 */ // hereif (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);...}
...
计算逻辑,如下:
计算结果如下:
此时的函数堆栈,如下:
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)
...
// 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函数传参都是一样的:
/* 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
进行处理的:
case T_Query:_jumbleQuery(jstate, expr);break;
// src/backend/nodes/queryjumblefuncs.funcs.cstatic 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的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
。