pg_stat_statements代码分析
pg_Stat_statements概述
pg_stat_statements 是 PostgreSQL 数据库的一个扩展模块,用于跟踪和记录 SQL 查询语句的性能统计信息
- 功能:
pg_stat_statements 扩展模块跟踪数据库中执行的 SQL 查询语句,以及这些查询的性能统计信息。它捕获的信息包括查询的文本、执行次数、总运行时间、平均运行时间、最小和最大运行时间等。这些统计信息可以帮助开发人员和管理员确定哪些查询是最耗时的,从而更有针对性地进行性能优化。
2. 安装和启用:
要使用 pg_stat_statements,首先需要确保已将其作为扩展编译到 PostgreSQL 中,或者使用 CREATE EXTENSION 命令在数据库中安装它。然后在数据库配置文件 postgresql.conf 中启用该扩展,将 shared_preload_libraries 设置为 ‘pg_stat_statements’。
- 配置选项:
pg_stat_statements 具有一些配置选项,可以根据需求进行调整,例如:
- pg_stat_statements.max:限制要跟踪的最大查询数量。
- pg_stat_statements.track:指定要跟踪的查询类型,如 none、top、all 等。
- pg_stat_statements.track_utility:是否跟踪数据库工具命令。
- pg_stat_statements.save:控制是否保存查询文本,以及保存的最大长度。
4.查看统计信息:
使用 SQL 查询可以从 pg_stat_statements 视图中检索查询的性能统计信息。其中一些重要的列包括: - query:查询文本。
- calls:查询被调用的次数。
- total_time:查询总运行时间。
- mean_time:平均运行时间。
- min_time 和 max_time:最小和最大运行时间。
- rows:查询返回的总行数。
pg_stat_statements初始化
PostgreSQL对于拓展库的初始化是在函数process_shared_preload_libraries完成的,该函数路径为:src/backend/utils/init/miscinit.c
void process_shared_preload_libraries(void)
{
process_shared_preload_libraries_in_progress = true;
load_libraries(shared_preload_libraries_string,
"shared_preload_libraries",
false);
process_shared_preload_libraries_in_progress = false;
process_shared_preload_libraries_done = true;
}
这个函数用于预加载所有的拓展库
在这个函数中,加载拓展库是调用load_libraries函数,第二个参数是字符串"shared_preload_libraries",它在上一节中提到的数据库配置文件postgresql.conf中提到。此时已将shared_preload_libraries设置为pg_stat_statements。
Hook
hook实际上就是static 的函数指针.
工作原理:
- 每一个hook是由一个全局性的函数指针构成的。服务端进行运行初始化其为NULL,当数据库必须调用的时候,首先会检测是否为NULL,不是则优先调用函数,否则执行标准函数。
设置函数指针:
- 当数据库载入共享库时,首先会将其载入到内存中,然后执行一个函数调用_PG_init。这个函数存在大多数共享库中是有效的。所以我们可以通过这个函数来加载我们自己的hook。
_PG_init(void)
{
/*
* Install hooks.
*/
prev_shmem_request_hook = shmem_request_hook;
shmem_request_hook = pgss_shmem_request;
prev_shmem_startup_hook = shmem_startup_hook;
shmem_startup_hook = pgss_shmem_startup;
prev_post_parse_analyze_hook = post_parse_analyze_hook;
post_parse_analyze_hook = pgss_post_parse_analyze;
prev_planner_hook = planner_hook;
planner_hook = pgss_planner;
prev_ExecutorStart = ExecutorStart_hook;
ExecutorStart_hook = pgss_ExecutorStart;
prev_ExecutorRun = ExecutorRun_hook;
ExecutorRun_hook = pgss_ExecutorRun;
prev_ExecutorFinish = ExecutorFinish_hook;
ExecutorFinish_hook = pgss_ExecutorFinish;
prev_ExecutorEnd = ExecutorEnd_hook;
ExecutorEnd_hook = pgss_ExecutorEnd;
prev_ProcessUtility = ProcessUtility_hook;
ProcessUtility_hook = pgss_ProcessUtility;
}
当数据库主进程启动只有,发现有shmem_startuo_hook时,就会去执行hook函数
执行查询语句与未用pg_stat_statements的不同
start
void
ExecutorStart(QueryDesc *queryDesc, int eflags)
{
pgstat_report_query_id(queryDesc->plannedstmt->queryId, false);
if (ExecutorStart_hook)
(*ExecutorStart_hook) (queryDesc, eflags);
else
standard_ExecutorStart(queryDesc, eflags);
}
安装前运行到该函数时,ExecutorStart_hook == NULL,会调用standard_ExecutorStart函数
安装了hook之后会调用pgss_ExecutorStart函数,该函数在pg_stat_statements.c文件中
/*
* ExecutorStart hook: start up tracking if needed
*/
static void
pgss_ExecutorStart(QueryDesc *queryDesc, int eflags)
{
if (prev_ExecutorStart)
prev_ExecutorStart(queryDesc, eflags);
else
standard_ExecutorStart(queryDesc, eflags);
/*
如果查询的queryId为零,则不跟踪它。
这可以防止对直接包含在实用程序语句中的可优化语句进行重复计数。
*/
if (pgss_enabled(exec_nested_level) && queryDesc->plannedstmt->queryId != UINT64CONST(0))
{
/*
设置为跟踪ExecutorRun中的总运行时间。
确保在每个查询上下文中分配了空间,这样它就会离开ExecutorEnd
*/
if (queryDesc->totaltime == NULL)
{
MemoryContext oldcxt;
oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt);
queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false);
MemoryContextSwitchTo(oldcxt);
}
}
}
在该函数中仍然会执行standard_ExecutorStart函数,执行完standard_ExecutorStart后根据数据库配置文件中的参数跟踪一些数据totaltime
Run
Run和start执行类似
void
ExecutorRun(QueryDesc *queryDesc,
ScanDirection direction, uint64 count,
bool execute_once)
{
if (ExecutorRun_hook)
(*ExecutorRun_hook) (queryDesc, direction, count, execute_once);
else
standard_ExecutorRun(queryDesc, direction, count, execute_once);
}
未安装pg_stat_statement时,ExecutorRun_hook == NULL运行到该函数会调用standard_ExecutorRun函数
安装之后会调用pgss_ExecutorRun函数
static void
pgss_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, uint64 count,
bool execute_once)
{
exec_nested_level++;
PG_TRY();
{
if (prev_ExecutorRun)
prev_ExecutorRun(queryDesc, direction, count, execute_once);
else
standard_ExecutorRun(queryDesc, direction, count, execute_once);
}
PG_FINALLY();
{
exec_nested_level--;
}
PG_END_TRY();
}
在该函数中,依然会调用standard_ExecutorRun函数,但是会额外跟踪嵌套深度,统计query的总消耗时间
在执行到ExecutorFinish和ExecutorEnd函数时,会类似于ExecutorRun,调用pg_stat_statement中的函数。
hook | 描述 |
---|---|
pgss_shmem_startup | 加载外部file文件,初始化pgss_hash和file文件 |
pgss_post_parse_analyze | 初始化query jumbl,根据query tree计算query id 在sql执行时首先调用 |
pgss_ExecutorStart | 初始化totaltime |
pgss_ExecutorRun | 处理查询执行时调用的hook,统计query的总消耗时间 |
pgss_ExecutorFinish | 处理查询结束时调用的hook |
pgss_ExecutorEnd | 处理查询完成后调用的hook |
Hash创建和初始化
数据库进程启动之后,当发现有shmem_startup_hook的时候,会去执行此hook函数,然后使用pgss_shmem_startup函数创建和初始化一个共享内存(Hash Table)用于存储数据
数据的存储
执行到pgss_ExecutorEnd的时候,调用了pgss_store来存储sql运行信息到共享内存的hash表里:
static void
pgss_shmem_startup(void)
{
……
pgss_hash = ShmemInitHash("pg_stat_statements hash",
pgss_max, pgss_max,
&info,
HASH_ELEM | HASH_BLOBS);
……
}
/*
* ExecutorEnd hook: store results if needed
*/
static void
pgss_ExecutorEnd(QueryDesc *queryDesc)
{
if (queryDesc->totaltime && pgss_enabled())
{
InstrEndLoop(queryDesc->totaltime);
pgss_store(queryDesc->sourceText,queryDesc->totaltime->total,
queryDesc->estate->es_processed, &queryDesc->totaltime->bufusage);
}
if (prev_ExecutorEnd)
prev_ExecutorEnd(queryDesc);
else
standard_ExecutorEnd(queryDesc);
}
static void pgss_store(const char *query, uint64 queryId,
int query_location, int query_len,
pgssStoreKind kind,
double total_time, uint64 rows,
const BufferUsage *bufusage,
const WalUsage *walusage,
const struct JitInstrumentation *jitusage,
JumbleState *jstate)
{
/*
该函数用于将查询执行的统计信息存储在共享内存中,以便后续分析和查询性能优化。
参数:
- `query`: 查询文本
- `queryId`: 查询标识符
- `query_location`: 查询文本在原始查询中的位置
- `query_len`: 查询文本的长度
- `kind`: 统计类型(执行计划还是执行阶段)
- `total_time`: 查询执行的总时间
- `rows`: 影响的行数
- `bufusage`: 缓冲区使用情况统计
- `walusage`: Write-Ahead Logging(WAL)使用情况统计
- `jitusage`: JIT(Just-In-Time)编译使用情况统计
- `jstate`: 查询状态信息
执行流程:
- 对输入参数进行检查,确保pg_stat_statements扩展被启用,并且共享内存已分配。
- 如果`queryId`为0,则没有其他模块计算查询标识符,因此不进行处理。
- 对查询文本进行规范化,处理查询中的多语句情况。
- 生成一个`pgssHashKey`作为在哈希表中搜索的键。
- 使用共享锁从哈希表中查找现有的统计信息条目。
- 如果没有找到现有条目,则需要创建一个新的条目。这可能涉及生成规范化的查询文本,将查询文本写入文件,并创建新的哈希表条目。
- 对统计信息条目进行更新,包括查询执行次数、总时间、行数、缓冲区使用情况、WAL使用情况等等,根据执行类型不同进行不同的统计更新。
- 最终释放共享锁,并进行必要的内存释放。
该函数在数据库查询执行过程中被调用,用于捕获和存储关于查询性能的重要统计信息,这些信息可以帮助数据库管理员和开发人员进行性能优化和故障排除。
*/
}
统计信息位存储在结构体Counter中,看一下该数据结构
/*
* pgssEntry 中实际保存的统计计数器。
*/
typedef struct Counters
{
int64 calls[PGSS_NUMKIND]; /* 计划/执行的次数 */
double total_time[PGSS_NUMKIND]; /* 计划/执行总时间,以毫秒为单位 */
double min_time[PGSS_NUMKIND]; /* 计划/执行最小时间,以毫秒为单位 */
double max_time[PGSS_NUMKIND]; /* 计划/执行最大时间,以毫秒为单位 */
double mean_time[PGSS_NUMKIND]; /* 计划/执行平均时间,以毫秒为单位 */
double sum_var_time[PGSS_NUMKIND]; /* 计划/执行时间方差之和,以毫秒为单位 */
int64 rows; /* 检索或影响的总行数 */
int64 shared_blks_hit; /* 共享缓冲区命中次数 */
int64 shared_blks_read; /* 从共享磁盘块读取次数 */
int64 shared_blks_dirtied; /* 共享磁盘块脏化次数 */
int64 shared_blks_written; /* 向共享磁盘块写入次数 */
int64 local_blks_hit; /* 本地缓冲区命中次数 */
int64 local_blks_read; /* 从本地磁盘块读取次数 */
int64 local_blks_dirtied; /* 本地磁盘块脏化次数 */
int64 local_blks_written; /* 向本地磁盘块写入次数 */
int64 temp_blks_read; /* 临时块读取次数 */
int64 temp_blks_written; /* 临时块写入次数 */
double blk_read_time; /* 读取块所花时间,以毫秒为单位 */
double blk_write_time; /* 写入块所花时间,以毫秒为单位 */
double temp_blk_read_time; /* 读取临时块所花时间,以毫秒为单位 */
double temp_blk_write_time; /* 写入临时块所花时间,以毫秒为单位 */
double usage; /* 使用因子 */
int64 wal_records; /* 生成的 WAL 记录数 */
int64 wal_fpi; /* 生成的 WAL 完整页映像数 */
uint64 wal_bytes; /* 生成的总 WAL 字节数 */
int64 jit_functions; /* 发出的 JIT 函数总数 */
double jit_generation_time; /* 生成 JIT 代码的总时间 */
int64 jit_inlining_count; /* 内联时间大于 0 的次数 */
double jit_inlining_time; /* 内联 JIT 代码的总时间 */
int64 jit_optimization_count; /* 优化时间大于 0 的次数 */
double jit_optimization_time; /* 优化 JIT 代码的总时间 */
int64 jit_emission_count; /* 发出时间大于 0 的次数 */
double jit_emission_time; /* 发出 JIT 代码的总时间 */
} Counters;
Counters在pgssEntry中
typedef struct pgssEntry
{
pgssHashKey key; /* entry的哈希键 - 必须在最前面 */
Counters counters; /* 此查询的统计信息 */
Size query_offset; /* 查询文本在外部文件中的偏移量 */
int query_len; /* 查询字符串中有效字节数,或者为-1 */
int encoding; /* 查询文本编码 */
slock_t mutex; /* 仅保护计数器 */
} pgssEntry;
typedef struct pgssHashKey
{
Oid userid; /* 用户 OID */
Oid dbid; /* 数据库 OID */
uint64 queryid; /* 查询标识符 */
bool toplevel; /* 是否在顶层执行的查询 */
} pgssHashKey;
取出数据
在pg_stat_statements_internal函数中,从hash表中取出了所有数据
static void
pg_stat_statements_internal(FunctionCallInfo fcinfo,
pgssVersion api_version,
bool showtext)
{
/*
从哈希表中获取统计信息的计数器,并根据需要计算标准差。
将统计信息和相关数据构造为一行数据,将其插入到结果集中
*/
}