声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》一书以及OpenGauss社区学习文档
在OpenGauss中,“ANALYZE” 是一个用于数据库优化的关键操作,它用于收集表中统计信息,以便优化查询性能。对于列存储表,ANALYZE 功能的作用是分析表中的数据分布、值的密度和其他统计信息,以便查询优化器能够更好地决定执行计划,从而提高查询的执行效率。
ANALYZE 功能在列存储表中的工作过程通常包括以下步骤:
- 数据采样收集: 首先,ANALYZE 会从列存储表中随机抽样一部分数据,以避免对整个表进行完整扫描,从而减少分析的时间和资源消耗。
- 统计信息计算: 对于选定的采样数据,ANALYZE 会计算各列中不同值的分布、空值的比例、数据密度等统计信息。这些统计信息可以帮助优化器更好地预测查询的执行计划。
- 直方图生成: 在列存储中,直方图是一种用于表示列中值分布的数据结构。ANALYZE 会基于采样数据生成直方图,以便优化器可以更精确地了解数据分布情况。
- 数据密度估计: 通过分析采样数据,ANALYZE 还可以估计不同列中数据值的密度,这有助于优化器在执行计划中进行成本估算。
- 统计信息存储: 分析完成后,生成的统计信息会被存储在数据库系统的系统表中,以供查询优化器使用。
当涉及 OpenGauss 数据库中的列存储和 ANALYZE 功能时,下面是一个基本的案例,展示了如何创建一个列存储表、插入数据、执行 ANALYZE 操作以及查看统计信息和优化计划。
1. 创建列存储表,执行以下 SQL 语句:
CREATE TABLE sales (
sale_id SERIAL PRIMARY KEY,
product_id INT,
sale_date DATE,
quantity INT,
amount DECIMAL
) WITH (ORIENTATION = COLUMN);
postgres=# select * from sales;
sale_id | product_id | sale_date | quantity | amount
---------+------------+-----------+----------+--------
(0 rows)
postgres=# \d+ sales
Table "public.sales"
Column | Type | Modifiers | Storage | Stats target | Description
------------+--------------------------------+---------------------------------------------------------+---------+--------------+-------------
sale_id | integer | not null default nextval('sales_sale_id_seq'::regclass) | plain | |
product_id | integer | | plain | |
sale_date | timestamp(0) without time zone | | plain | |
quantity | integer | | plain | |
amount | numeric | | main | |
Has OIDs: no
Options: orientation=column, compression=low
2. 插入一些示例数据到列存储表中:
INSERT INTO sales (product_id, sale_date, quantity, amount)
VALUES
(101, '2023-08-01', 10, 100.00),
(102, '2023-08-02', 5, 50.00),
(101, '2023-08-03', 8, 80.00);
postgres=# select * from sales;
sale_id | product_id | sale_date | quantity | amount
---------+------------+---------------------+----------+--------
1 | 101 | 2023-08-01 00:00:00 | 10 | 100.00
2 | 102 | 2023-08-02 00:00:00 | 5 | 50.00
3 | 101 | 2023-08-03 00:00:00 | 8 | 80.00
(3 rows)
3. 执行 ANALYZE 进行统计信息收集:
postgres=# ANALYZE sales;
ANALYZE
4. 查看统计信息和优化计划:
查看表的统计信息,如不同列的值分布等:
postgres=# SELECT * FROM pg_statistic WHERE starelid = 'sales'::regclass;
starelid | starelkind | staattnum | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 |
staop3 | staop4 | staop5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | stavalues1
| stavalues2 | stavalues3 | stavalues4 | stavalues5 | stadndistinct | staextinfo
----------+------------+-----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+
--------+--------+--------+-------------+-------------+-------------+-------------+-------------+----------------------------------------------------------------
-----+------------+------------+------------+------------+---------------+------------
40980 | c | 1 | f | 0 | 4 | -1 | 2 | 3 | 0 | 0 | 0 | 97 | 97 |
0 | 0 | 0 | | {1} | | | | {1,2,3}
| | | | | 0 |
40980 | c | 2 | f | 0 | 4 | -.666667 | 1 | 3 | 0 | 0 | 0 | 96 | 97 |
0 | 0 | 0 | {.666667} | {.5} | | | | {101}
| | | | | 0 |
40980 | c | 3 | f | 0 | 8 | -1 | 2 | 3 | 0 | 0 | 0 | 2062 | 2062 |
0 | 0 | 0 | | {1} | | | | {"2023-08-01 00:00:00","2023-08-02 00:00:00","2023-08-03 00:00:
00"} | | | | | 0 |
40980 | c | 4 | f | 0 | 4 | -1 | 2 | 3 | 0 | 0 | 0 | 97 | 97 |
0 | 0 | 0 | | {-.5} | | | | {5,8,10}
| | | | | 0 |
40980 | c | 5 | f | 0 | 11 | -1 | 2 | 3 | 0 | 0 | 0 | 1754 | 1754 |
0 | 0 | 0 | | {-.5} | | | | {50.00,80.00,100.00}
| | | | | 0 |
(5 rows)
查看查询的优化计划:
postgres=# EXPLAIN SELECT * FROM sales WHERE product_id = 101;
QUERY PLAN
---------------------------------------------------------------
Row Adapter (cost=3.01..3.01 rows=2 width=31)
-> CStore Scan on sales (cost=0.00..3.01 rows=2 width=31)
Filter: (product_id = 101)
(3 rows)
其中,analyze 的核心代码的实现在 vacuum.cpp (路径:src/gausskernel/optimizer/commands/vacuum.cpp
)和 analyze.cpp (路径:src/gausskernel/optimizer/commands/analyze.cpp
)中。
我们首先先来学习 vacuum 函数,该函数在 OpenGauss 数据库中执行 VACUUM 和 ANALYZE 命令的主要入口函数。VACUUM 用于回收已删除行的空间,以及处理未使用的数据页,从而减少表的碎片和存储空间。ANALYZE 则用于计算表中列的统计信息,以便优化查询计划。以下是这个函数的大致执行步骤:
- 首先,函数输出一个日志消息,指示是否在协调节点上运行以及当前 PGXC 节点的编号。
- 声明了一些变量,如 stmttype 用于记录操作类型(VACUUM 还是 ANALYZE)、in_outer_xact 用于判断是否在外部事务中,以及 use_own_xacts 用于判断是否需要使用自己的事务。
- 检查是否启用了特殊的 enable_show_any_tuples 模式,如果启用了则抛出错误。
- 设置当前 IO 状态为 VACUUM。
- 根据操作选项判断当前是否需要执行 VACUUM,如果是的话,阻止事务链并设置 in_outer_xact 为假。
- 检查是否正在执行 VACUUM 或 ANALYZE 操作,如果是则抛出错误。
- 如果当前是 VACUUM 操作且不是自动 VACUUM 工作进程,则向统计收集器发送死对象信息。
- 为跨事务内存存储创建一个专用的上下文。
- 如果未传入缓冲策略对象 bstrategy,则在跨事务内存上下文中创建一个新的策略对象。
- 构建需要处理的关系列表,如果传入的 relid 为 InvalidOid,则从 vacstmt 中获取。
- 根据操作选项判断是否需要自己的事务来处理。
- 如果需要自己的事务,将当前事务提交,以便释放锁定。
- 设置 VACUUM 成本统计,用于统计执行 VACUUM 和 ANALYZE 的开销。
- 使用一个循环来处理每一个选定的关系。
- 在循环中,处理选定的关系,可能执行 VACUUM 和 ANALYZE 操作。
- 在关系处理结束后,恢复之前的等待状态,释放内存,报告当前状态。
- 如果在循环处理过程中出现异常,设置相关状态并抛出异常。
- 最后,恢复各种状态和内存,以及必要的清理工作。
这个函数主要用于执行 VACUUM 和 ANALYZE 操作,它根据传入的参数和操作选项来判断需要执行哪种操作,并根据不同情况进行事务管理、统计信息收集等。
/*
* Primary entry point for VACUUM and ANALYZE commands.
*
* relid is normally InvalidOid; if it is not, then it provides the relation
* OID to be processed, and vacstmt->relation is ignored. (The non-invalid
* case is currently only used by autovacuum.)
*
* do_toast is passed as FALSE by autovacuum, because it processes TOAST
* tables separately.
*
* bstrategy is normally given as NULL, but in autovacuum it can be passed
* in to use the same buffer strategy object across multiple vacuum() calls.
*
* isTopLevel should be passed down from ProcessUtility.
*
* It is the caller's responsibility that vacstmt and bstrategy
* (if given) be allocated in a memory context that won't disappear
* at transaction commit.
*/
void vacuum(
VacuumStmt* vacstmt, Oid relid, bool do_toast, BufferAccessStrategy bstrategy, bool isTopLevel)
{
ereport(ES_LOGLEVEL, (errmsg("[Vacuum] > CN?[%d], [%d]", IS_PGXC_COORDINATOR, u_sess->pgxc_cxt.PGXCNodeId)));
const char* stmttype = NULL;
volatile bool in_outer_xact = false;
volatile bool use_own_xacts = false;
List* relations = NIL;
/* sanity checks on options */
Assert(vacstmt->options & (VACOPT_VACUUM | VACOPT_ANALYZE));
Assert((vacstmt->options & VACOPT_VACUUM) || !(vacstmt->options & (VACOPT_FULL | VACOPT_FREEZE)));
Assert((vacstmt->options & VACOPT_ANALYZE) || vacstmt->va_cols == NIL);
// 这一行代码的作用是根据 vacstmt 中的操作选项来确定当前执行的操作类型
stmttype = (vacstmt->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE";
// 检查是否启用了特殊模式 enable_show_any_tuples,如果启用了,则会在执行 VACUUM 或 ANALYZE 命令时抛出错误
if (u_sess->attr.attr_storage.enable_show_any_tuples)
ereport(ERROR,
(errcode(ERRCODE_QUERY_CANCELED),
errmsg(
"%s cannot be executed when u_sess->attr.attr_storage.enable_show_any_tuples is true.", stmttype)));
// 设置数据库统计信息中的 I/O 状态为 VACUUM
pgstat_set_io_state(IOSTATE_VACUUM);
/*
* We cannot run VACUUM inside a user transaction block; if we were inside
* a transaction, then our commit- and start-transaction-command calls
* would not have the intended effect! There are numerous other subtle
* dependencies on this, too.
*
* ANALYZE (without VACUUM) can run either way.
*/
// 是否需要在一个事务链上执行操作,并相应地设置事务状态
if (vacstmt->options & VACOPT_VACUUM) {
PreventTransactionChain(isTopLevel, stmttype);
in_outer_xact = false;
} else
in_outer_xact = IsInTransactionChain(isTopLevel);
/*
* Due to static variables vac_context, analyze_context and vac_strategy,
* vacuum() is not reentrant. This matters when VACUUM FULL or ANALYZE
* calls a hostile index expression that itself calls ANALYZE.
*/
// 这行代码判断当前是否正在执行 VACUUM 或 ANALYZE 操作
if (t_thrd.vacuum_cxt.in_vacuum)
ereport(
ERROR, (errcode(ERRCODE_QUERY_CANCELED), errmsg("%s cannot be executed from VACUUM or ANALYZE", stmttype)));
/*
* Send info about dead objects to the statistics collector, unless we are
* in autovacuum --- autovacuum.c does this for itself.
*/
// 用于在执行 VACUUM 操作时向统计收集器报告关于死对象的信息
if ((vacstmt->options & VACOPT_VACUUM) && !IsAutoVacuumWorkerProcess())
pgstat_vacuum_stat();
/*
* Create special memory context for cross-transaction storage.
*
* Since it is a child of t_thrd.mem_cxt.portal_mem_cxt, it will go away eventually even
* if we suffer an error; there's no need for special abort cleanup logic.
*/
t_thrd.vacuum_cxt.vac_context = AllocSetContextCreate(t_thrd.mem_cxt.portal_mem_cxt,
"Vacuum",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
/*
* If caller didn't give us a buffer strategy object, make one in the
* cross-transaction memory context.
*/
// 检查是否已经提供了一个缓冲策略对象(bstrategy),如果没有,则创建一个新的缓冲策略对象
if (bstrategy == NULL) {
MemoryContext old_context = MemoryContextSwitchTo(t_thrd.vacuum_cxt.vac_context);
bstrategy = GetAccessStrategy(BAS_VACUUM);
(void)MemoryContextSwitchTo(old_context);
}
vac_strategy = bstrategy;
/*
* Build list of relations to process, unless caller gave us one. (If we
* build one, we put it in vac_context for safekeeping.)
*/
// 用于获取需要处理的关系列表,以供后续的 VACUUM 和 ANALYZE 操作使用
relations = get_rel_oids(relid, vacstmt);
/*
* Decide whether we need to start/commit our own transactions.
*
* For VACUUM (with or without ANALYZE): always do so, so that we can
* release locks as soon as possible. (We could possibly use the outer
* transaction for a one-table VACUUM, but handling TOAST tables would be
* problematic.)
*
* For ANALYZE (no VACUUM): if inside a transaction block, we cannot
* start/commit our own transactions. Also, there's no need to do so if
* only processing one relation. For multiple relations when not within a
* transaction block, and also in an autovacuum worker, use own
* transactions so we can release locks sooner.
*/
// 用于决定是否需要在单独的事务中执行 VACUUM 或 ANALYZE 操作,以及如何设置相应的事务状态
if (vacstmt->options & VACOPT_VACUUM) {
use_own_xacts = true;
} else {
Assert(vacstmt->options & VACOPT_ANALYZE);
if (IsAutoVacuumWorkerProcess()) {
use_own_xacts = true;
} else if (in_outer_xact) { //是否在外部事务中
use_own_xacts = false;
} else if (list_length(relations) > 1) {
use_own_xacts = true;
} else {
use_own_xacts = false;
}
}
/*
* vacuum_rel expects to be entered with no transaction active; it will
* start and commit its own transaction. But we are called by an SQL
* command, and so we are executing inside a transaction already. We
* commit the transaction started in PostgresMain() here, and start
* another one before exiting to match the commit waiting for us back in
* PostgresMain().
*/
// 在需要的情况下启动和提交独立的事务
if (use_own_xacts) {
/* ActiveSnapshot is not set by autovacuum */
if (ActiveSnapshotSet())
PopActiveSnapshot();
/* matches the StartTransaction in PostgresMain() */
CommitTransactionCommand();
}
/* Turn vacuum cost accounting on or off */
PG_TRY();
{
ListCell* cur = NULL;
t_thrd.vacuum_cxt.in_vacuum = true;
t_thrd.vacuum_cxt.VacuumCostActive = (u_sess->attr.attr_storage.VacuumCostDelay > 0);
t_thrd.vacuum_cxt.VacuumCostBalance = 0;
t_thrd.vacuum_cxt.VacuumPageHit = 0;
t_thrd.vacuum_cxt.VacuumPageMiss = 0;
t_thrd.vacuum_cxt.VacuumPageDirty = 0;
/*
* Loop to process each selected relation.
*/
WaitState oldStatus = pgstat_report_waitstatus(STATE_VACUUM);
foreach (cur, relations) {
vacuum_object* vacObj = (vacuum_object*)lfirst(cur);
Oid relOid = vacObj->tab_oid;
vacstmt->flags = vacObj->flags;
vacstmt->onepartrel = NULL;
vacstmt->onepart = NULL;
vacstmt->partList = NIL;
/*
* do NOT vacuum partitioned table,
* as vacuum is an operation related with tuple and storage page reorganization
*/
if (vacstmt->options & VACOPT_VACUUM) {
if (vacuumPartition(vacstmt->flags) || vacuumRelation(vacstmt->flags) ||
vacuumMainPartition(vacstmt->flags)) {
if (!vacuum_rel(relOid, vacstmt, do_toast))
continue;
} else {
/* for partitioned table, just report collector that we just vacuumed. */
pgstat_report_vacuum(relOid, InvalidOid, false, 0);
}
}
vacstmt->flags = vacObj->flags;
vacstmt->onepartrel = NULL;
vacstmt->onepart = NULL;
if (vacstmt->options & VACOPT_ANALYZE) {
/*
* we have received user-defined table's stat info from remote coordinator
* in function FetchGlobalRelationStatistics, so we skip analyze
*/
if (udtRemoteAnalyze(relOid))
continue;
/*
* If using separate xacts, start one for analyze. Otherwise,
* we can use the outer transaction.
*/
if (use_own_xacts) {
StartTransactionCommand();
/* functions in indexes may want a snapshot set */
PushActiveSnapshot(GetTransactionSnapshot());
LockSharedObject(DatabaseRelationId, u_sess->proc_cxt.MyDatabaseId, 0, RowExclusiveLock);
}
/*
* do NOT analyze partition, as analyze is an operation related with
* data redistribution reflect and this is not meaningfull for one
* or more partitions, it must be done on basis of table level, either
* plain heap or partitioned heap.
*/
if (!vacuumPartition(vacstmt->flags)) {
pgstat_report_waitstatus_relname(STATE_ANALYZE, get_nsp_relname(relOid));
analyze_rel(relOid, vacstmt, vac_strategy);
}
if (use_own_xacts) {
PopActiveSnapshot();
CommitTransactionCommand();
}
}
}
(void)pgstat_report_waitstatus(oldStatus);
list_free_deep(relations);
}
PG_CATCH();
{
t_thrd.vacuum_cxt.in_vacuum = false;
/* Make sure cost accounting is turned off after error */
list_free_deep(relations);
t_thrd.vacuum_cxt.VacuumCostActive = false;
PG_RE_THROW();
}
PG_END_TRY();
t_thrd.vacuum_cxt.in_vacuum = false;
/*
* Reset query cancel signal here to prevent hange
* when multiple vacuum triggered (e.g. toast)
*/
if (t_thrd.int_cxt.QueryCancelPending) {
t_thrd.int_cxt.QueryCancelPending = false;
}
/* Turn off vacuum cost accounting */
t_thrd.vacuum_cxt.VacuumCostActive = false;
/*
* Finish up processing.
*/
if (use_own_xacts) {
/* here, we are not in a transaction
*
* This matches the CommitTransaction waiting for us in
* PostgresMain().
*/
StartTransactionCommand();
LockSharedObject(DatabaseRelationId, u_sess->proc_cxt.MyDatabaseId, 0, RowExclusiveLock);
}
if (((uint32)(vacstmt->options) & VACOPT_VACUUM) && !IsAutoVacuumWorkerProcess()) {
/*
* Update pg_database.datfrozenxid, and truncate pg_clog if possible.
* (autovacuum.c does this for itself.)
*/
vac_update_datfrozenxid();
}
/*
* Clean up working storage --- note we must do this after
* StartTransactionCommand, else we might be trying to delete the active
* context!
*/
if (t_thrd.vacuum_cxt.vac_context) {
MemoryContextDelete(t_thrd.vacuum_cxt.vac_context);
t_thrd.vacuum_cxt.vac_context = NULL;
}
}
VacuumStmt 结构体是在 PostgreSQL 数据库中用于表示 VACUUM 和 ANALYZE 命令的数据结构。它在数据库源代码中定义,用于存储 VACUUM 和 ANALYZE 命令的各种参数和选项。以下是 VacuumStmt 结构体的定义:(路径:src/include/nodes/parsenodes.h
)
typedef struct VacuumStmt {
NodeTag type; /* 结构体类型标签 */
int options; /* VacuumOption 标志的按位 OR */
int flags; /* 用于区分分区或 B 树的标志 */
/* 这些标志的值在 vacuum.h 中定义 */
Oid rely_oid; /* 对于 B 树,是堆 B 树的 OID,否则为 InvalidOid */
int freeze_min_age; /* 最小冻结年龄,-1 表示使用默认值 */
int freeze_table_age; /* 扫描整个表的年龄 */
RangeVar* relation; /* 要处理的单个表,或者为 NULL */
List* va_cols; /* 列名列表,为 NIL 表示所有列 */
Relation onepartrel; /* 用于跟踪已打开的关系 */
Partition onepart; /* 用于跟踪已打开的分区 */
List* partList; /* 分区列表 */
#ifdef PGXC
void* HDFSDnWorkFlow; /* @hdfs HDFSDnWorkFlow 存储分析操作相关信息 */
#endif
bool isForeignTables; /* @hdfs 当运行 "analyze [verbose] foreign table;" 命令时为 true */
bool isPgFdwForeignTables; /* 当外部表的 fdw 是 gc_fdw 时为 true */
#ifdef ENABLE_MOT
bool isMOTForeignTable; /* 当前是否是 MOT 外部表 */
#endif
/*
* @hdfs
* 参数 totalFileCnt 和 nodeNo 是由 CNSchedulingForAnalyze 设置的
* CNSchedulingForAnalyze(
* int *totalFilesCnt,
* int *nodeNo,
* Oid foreignTableId)
*/
unsigned int totalFileCnt; /* @hdfs 分析外部表操作中要采样的文件数 */
int nodeNo; /* @hdfs 哪个数据节点将执行分析操作,
@global 统计:其他协调器将从哪个协调器节点获取统计信息。 */
/*
* @hdfs 数据节点总数,我们使用这个数字来调整存储在 pg_class 中的 reltuples 数量
* 例如:我们执行操作 "analyze tablename",有 x 个数据节点,而 tablename 是一个 HDFS 外部表。
* 数据节点完成分析命令,协调器从数据节点获取元组数信息。这个数是总元组数的 1/x。在协调器中将这个数值调整为真实值。
*/
unsigned int DnCnt;
/*
* 添加全局统计的参数。
*/
DestReceiver* dest; /* 用于数据节点将样本行发送到协调器。 */
int num_samples; /* 从数据节点接收的样本行数。 */
HeapTuple* sampleRows; /* 从数据节点接收的样本行。 */
TupleDesc tupleDesc; /* 普通表的样本行的元组描述符。 */
int tableidx; /* 设置当前需要设置样本率或总行数的表索引 */
GlobalStatInfoEx pstGlobalStatEx[ANALYZE_MODE_MAX_NUM - 1]; /* 全局统计的辅助信息,扩展以识别 HDFS 表。 */
unsigned int orgCnNodeNo; /* 标识哪个协调器从客户端接收分析命令,其他协调器需要从它获取统计信息。 */
List* hdfsforeignMapDnList; /* 标识属于分片映射的一些数据节点,用于协调器从它们获取总 reltuples。 */
bool sampleTableRequired; /* 需要样本表以获取统计信息。 */
List* tmpSampleTblNameList; /* 在调试期间识别样本表名称。 */
bool isAnalyzeTmpTable; /* 如果分析的表是临时表,则为 true。 */
#ifdef PGXC
DistributionType disttype; /* 分析表的分布类型。 */
#endif
AdaptMem memUsage; /* 分配给语句的自适应内存 */
Oid curVerifyRel; /* 当前的关系用于数据库模式以发送远程查询 */
bool isCascade; /* 用于验证表 */
} VacuumStmt;
BufferAccessStrategy 是一个在 PostgreSQL 数据库中用于定义缓冲区访问策略的数据结构。它允许控制在数据库中读取和写入缓冲区时的一些策略,以优化内存使用和 I/O 操作。该结构体的目的是提供一种机制,使用户可以在不同的操作(如 VACUUM、ANALYZE、查询等)中自定义如何访问数据库缓冲区。
具体而言,BufferAccessStrategy 定义了一组函数指针,这些函数可以被 PostgreSQL 内核调用来执行实际的缓冲区访问操作。用户可以通过自定义这些函数来实现特定的缓冲区管理策略,例如决定何时读取数据块、何时写入数据块、何时释放缓冲区等。BufferAccessStrategy 结构体源码如下:
(BufferAccessStrategy 路径:src/include/storage/buf/buf.h
)
/*
* Buffer access strategy objects.
*/
typedef struct BufferAccessStrategyData* BufferAccessStrategy;
(BufferAccessStrategyData 路径:src/include/storage/buf/bufmgr.h
)
/*
* 用于管理一个共享缓冲区环的私有(非共享)状态。
* 目前这是唯一一种类型的 BufferAccessStrategy 对象,但将来可能会有更多种类。
*/
typedef struct BufferAccessStrategyData {
/* 整体策略类型 */
BufferAccessStrategyType btype;
/* buffers[] 数组中的元素数量 */
int ring_size;
/*
* 在环中的“当前”插槽的索引,即由 GetBufferFromRing 返回的最近的插槽。
*/
int current;
/* 在当前之后刷新的元素数量 */
int flush_rate;
/*
* 如果由 StrategyGetBuffer 返回的缓冲区已经在环中,则为 true。
*/
bool current_was_in_ring;
/*
* 缓冲区编号的数组。InvalidBuffer(即零)表示我们尚未选择该环插槽的缓冲区。
* 为了分配的简便性,这是与结构体的固定字段一起分配的。
*/
Buffer buffers[FLEXIBLE_ARRAY_MEMBER]; /* 可变大小的数组 */
} BufferAccessStrategyData;
analyze_rel 函数用于执行分析操作(ANALYZE)或者清理操作(VACUUM)的入口函数,它接收一个关系(表)的对象标识符(OID)、分析或清理的语句信息、以及缓冲区访问策略作为参数,然后根据这些参数执行相应的操作。
在给定的 relid 和 vacstmt 参数的情况下,analyze_rel 函数会根据 vacstmt 中的信息来确定要分析的表,然后构建一个包含这些表 OID 的列表。这个列表会在后续的代码中用于逐个分析这些表。函数源码如下:(路径:src/gausskernel/optimizer/commands/analyze.cpp
)
/*
* 描述:分析关系的入口。
*
* 参数:
* @in relid:关系的 OID
* @in vacstmt:进行分析或清理操作的语句
* @in bstrategy:缓冲区访问策略对象
*
* 返回值:void
*/
void analyze_rel(Oid relid, VacuumStmt* vacstmt, BufferAccessStrategy bstrategy) {
/*
* 尝试打开关系,如果打开失败,则跳过该关系。
* 关系将在 analyze_rel_internal() 中关闭。
*/
Relation onerel = analyze_get_relation(relid, vacstmt);
if (STMT_RETRY_ENABLED) {
// 如果启用了查询重试,暂时不做任何操作,只是跳过 validateTempRelation
} else if (onerel != NULL && onerel->rd_rel != NULL &&
onerel->rd_rel->relpersistence == RELPERSISTENCE_TEMP &&
!validateTempNamespace(onerel->rd_rel->relnamespace)) {
relation_close(onerel, NEED_EST_TOTAL_ROWS_DN(vacstmt) ? AccessShareLock : ShareUpdateExclusiveLock);
ereport(ERROR,
(errcode(ERRCODE_DATA_EXCEPTION),
errmsg("因为 datanode %s 重启,临时表的数据无效。请退出会话以清除无效的临时表。",
g_instance.attr.attr_common.PGXCNodeName)));
}
/*
* 如果有 onerel,就进行分析。否则,如果已经启动了事务,应该提交事务。
*/
if (onerel) {
/* 对普通表进行分析。 */
if (!RelationIsDfsStore(onerel)) {
analyze_rel_internal(onerel, vacstmt, bstrategy, ANALYZENORMAL);
} else {
/* 分析整个数据库期间 HDFS 表的 Delta 表。 */
GBLSTAT_HDFS_SAMPLE_ROWS stHdfsSampleRows = {0};
RangeVar* oldRelVar = vacstmt->relation;
Relation deltaRel;
stHdfsSampleRows.hdfs_sample_context = AllocSetContextCreate(t_thrd.vacuum_cxt.vac_context,
"AnalyzeHDFSSample",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
AssertEreport(OidIsValid(onerel->rd_rel->reldeltarelid), MOD_OPT, "Delta 表的 OID 必须有效。");
deltaRel = analyze_get_relation(onerel->rd_rel->reldeltarelid, vacstmt);
if (deltaRel != NULL) {
/* 对 Delta 表进行分析。 */
vacstmt->relation = NULL;
analyze_rel_internal(deltaRel, vacstmt, bstrategy, ANALYZEDELTA, &stHdfsSampleRows);
/* 对 DFS 表进行分析。 */
vacstmt->relation = oldRelVar;
analyze_rel_internal(onerel, vacstmt, bstrategy, ANALYZEMAIN, &stHdfsSampleRows);
} else {
/* 重置并关闭 DFS 表 */
LOCKMODE lockmode = NEED_EST_TOTAL_ROWS_DN(vacstmt) ? AccessShareLock : ShareUpdateExclusiveLock;
vacstmt->relation = oldRelVar;
relation_close(onerel, lockmode);
}
MemoryContextDelete(stHdfsSampleRows.hdfs_sample_context);
stHdfsSampleRows.hdfs_sample_context = NULL;
}
}
}
其中,Relation 结构体的解释在 CopyTo 一文中进行了介绍。
analyze_get_relation 函数是用于在进行分析操作(analyze 或 vacuum)之前,打开要分析的关系(表),并获取适当的锁来确保分析的正确执行。函数源码如下:(路径:src/gausskernel/optimizer/commands/analyze.cpp
)
/*
* analyze_get_relation() -- get one relation by relid before do analyze.
* 从 relid 获取一个关系(表),用于在执行分析之前。
* @in relid - the relation id for analyze or vacuum
* @in vacstmt - the statment for analyze or vacuum command
*/
Relation analyze_get_relation(Oid relid, VacuumStmt* vacstmt)
{
Relation onerel = NULL;
bool GetLock = false;
LOCKMODE lockmode = NEED_EST_TOTAL_ROWS_DN(vacstmt) ? AccessShareLock : ShareUpdateExclusiveLock;
/*
* Check for user-requested abort.
* 检查是否有用户请求的中断信号。
*/
CHECK_FOR_INTERRUPTS();
/*
* Open the relation, getting ShareUpdateExclusiveLock to ensure that two
* ANALYZEs don't run on it concurrently. (This also locks out a
* concurrent VACUUM, which doesn't matter much at the moment but might
* matter if we ever try to accumulate stats on dead tuples.)
* 打开关系(表),获取 ShareUpdateExclusiveLock 锁,以确保不会并发运行两个 ANALYZE 操作。
* 这也会锁定并发的 VACUUM 操作,虽然目前不是很重要,但在将来可能会在死元组上累积统计信息时变得重要。
* 如果关系已被删除,我们无需处理它。
*/
if ((vacuumRelation(vacstmt->flags) || vacuumMainPartition(vacstmt->flags)) &&
!(vacstmt->options & VACOPT_NOWAIT)) {
onerel = try_relation_open(relid, lockmode);
GetLock = true;
} else if ((vacuumRelation(vacstmt->flags) || vacuumMainPartition(vacstmt->flags)) &&
ConditionalLockRelationOid(relid, lockmode)) {
onerel = try_relation_open(relid, NoLock);
GetLock = true;
}
if (!GetLock) {
onerel = NULL;
if (IsAutoVacuumWorkerProcess() && u_sess->attr.attr_storage.Log_autovacuum_min_duration >= 0)
ereport(LOG,
(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
errmsg("skipping analyze of \"%s\" --- lock not available", vacstmt->relation->relname)));
}
return onerel;
}
analyze_rel_internal 函数用于分析一个关系(表),它接收一个待分析的关系、分析或清理命令的语句信息、缓冲区访问策略、分析类型以及用于 DFS 表、增量表或复杂表的样本行信息作为参数。它执行以下操作:
- 设置消息级别和错误级别(用于不同的日志输出)。
- 检查用户权限,判断当前用户是否有分析权限。
- 忽略其他后端的临时表或全局临时表。
- 检查表的类型,包括是否是特殊表(如 pg_statistic)、普通表或外部表。
- 根据表的类型执行相应的分析操作:
- 对于普通表,获取表的大小和分区列表,执行常规分析。
- 对于外部表,调用外部表的 FDW(外部数据包装器)钩子函数,检查是否支持分析操作。
- 对于其他类型的表,输出警告信息。
- 告诉其他后端正在进行 ANALYZE 操作。
- 执行常规的非递归 ANALYZE 操作。
- 如果表是分区表并且使用 PAX 存储格式,则执行递归的 ANALYZE 操作。
- 关闭源关系,保留锁,以防在提交之前删除关系。
- 重置 PGXACT 标志,表示不再进行 ANALYZE 操作。
函数源码如下:(路径:src/gausskernel/optimizer/commands/analyze.cpp
)
/*
* Description: 分析单个关系的入口函数。
*
* Parameters:
* @in onerel: 待分析的关系
* @in vacstmt: 分析或清理命令的语句信息
* @in bstrategy: 缓冲区访问策略对象
* @in analyzemode: 表的分析类型(普通表、DFS 表或增量表)
* @in pstHdfsSampleRows: 用于 DFS 表、增量表或复杂表的样本行信息
*/
static void analyze_rel_internal(Relation onerel, VacuumStmt* vacstmt, BufferAccessStrategy bstrategy,
AnalyzeMode analyzemode, GBLSTAT_HDFS_SAMPLE_ROWS* pstHdfsSampleRows)
{
AcquireSampleRowsFunc acquirefunc = NULL;
int elevel;
int messageLevel;
BlockNumber relpages = 0;
List* partList = NIL;
LOCKMODE lockmode = NEED_EST_TOTAL_ROWS_DN(vacstmt) ? AccessShareLock : ShareUpdateExclusiveLock;
AssertEreport(onerel, MOD_OPT, "在进行分析时 onerel 不应为 NULL");
/* 设置静态变量 */
u_sess->analyze_cxt.vac_strategy = bstrategy;
messageLevel = WARNING;
elevel = DEBUG2;
if (vacstmt->options & VACOPT_VERBOSE) {
messageLevel = VERBOSEMESSAGE;
elevel = VERBOSEMESSAGE;
}
/*
* 检查权限,这应与 VACUUM 的检查相匹配!
*/
AclResult aclresult = pg_class_aclcheck(RelationGetPgClassOid(onerel, false), GetUserId(), ACL_VACUUM);
if (aclresult != ACLCHECK_OK && !(pg_class_ownercheck(RelationGetPgClassOid(onerel, false), GetUserId()) ||
(pg_database_ownercheck(u_sess->proc_cxt.MyDatabaseId, GetUserId()) && !onerel->rd_rel->relisshared) ||
(isOperatoradmin(GetUserId()) && u_sess->attr.attr_security.operation_mode))) {
/* 如果在 VACUUM 过程中已经有相应的警告,无需再次输出 WARNING */
if (!(vacstmt->options & VACOPT_VACUUM)) {
if (onerel->rd_rel->relisshared)
ereport(messageLevel,
(errmsg("跳过 \"%s\" --- 只有系统管理员可以对其进行分析", RelationGetRelationName(onerel))));
else if (onerel->rd_rel->relnamespace == PG_CATALOG_NAMESPACE)
ereport(messageLevel,
(errmsg("跳过 \"%s\" --- 只有系统管理员或数据库所有者可以对其进行分析",
RelationGetRelationName(onerel))));
else
ereport(messageLevel,
(errmsg("跳过 \"%s\" --- 只有表或数据库所有者可以对其进行分析",
RelationGetRelationName(onerel))));
}
relation_close(onerel, lockmode);
return;
}
/*
* 静默地忽略其他后端的临时表 --- 对它们进行分析相当无意义,因为它们的内容可能在磁盘上不是最新的。
* (我们不在此处抛出警告;这只会在数据库范围的 ANALYZE 过程中引起冗余。)
*/
if (RELATION_IS_OTHER_TEMP(onerel)) {
relation_close(onerel, lockmode);
return;
}
if (RELATION_IS_GLOBAL_TEMP(onerel) && !gtt_storage_attached(RelationGetRelid(onerel))) {
relation_close(onerel, ShareUpdateExclusiveLock);
return;
}
/*
* 我们可以对任何表执行 ANALYZE,除了 pg_statistic。参见 update_attstats
*/
if (RelationGetRelid(onerel) == StatisticRelationId) {
AssertEreport(RelationIsNonpartitioned(onerel), MOD_OPT, "pg_statistic 不应为分区表。");
if (!IsInitdb && !IS_SINGLE_NODE) {
elog(WARNING, "系统目录 pg_statistic 不能进行分析,将跳过。");
}
relation_close(onerel, lockmode);
return;
}
/*
* 检查是否为普通表或外部表;我们以前在 get_rel_oids() 中进行了此检查,但在锁定关系之后检查似乎更安全。
*/
if (onerel->rd_rel->relkind == RELKIND_RELATION ||
onerel->rd_rel->relkind == RELKIND_MATVIEW) {
/* 普通表,所以我们将使用常规的行获取函数 */
/* 还会获取普通表的大小 */
if (RelationIsPartitioned(onerel)) {
Partition part = NULL;
ListCell* partCell = NULL;
partList = relationGetPartitionList(onerel, lockmode);
foreach (partCell, partList) {
part = (Partition)lfirst(partCell);
relpages += PartitionGetNumberOfBlocks(onerel, part);
}
} else {
relpages = RelationGetNumberOfBlocks(onerel);
}
} else if (onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE
|| onerel->rd_rel->relkind == RELKIND_STREAM) {
/*
* @hdfs
* 对于外部表,调用 FDW 的钩子函数来检查是否支持分析。
*/
bool retValue = false;
FdwRoutine* fdwroutine = GetFdwRoutineForRelation(onerel, false);
/* 是否支持分析操作 */
if (NULL != fdwroutine->AnalyzeForeignTable) {
/* 是否实现了 GetFdwType 接口,以及文件类型是否为 HDFS_ORC */
if (isObsOrHdfsTableFormTblOid(RelationGetRelid(onerel)) ||
(IS_OBS_CSV_TXT_FOREIGN_TABLE(RelationGetRelid(onerel)) && !isWriteOnlyFt(RelationGetRelid(onerel)))) {
/* 传递 AnalyzeForeignTable 所需的数据 */
retValue = fdwroutine->AnalyzeForeignTable(
onerel, &acquirefunc, &relpages, (void*)vacstmt->HDFSDnWorkFlow, false);
} else {
/* 其他类型的外部表 */
retValue = fdwroutine->AnalyzeForeignTable(onerel, &acquirefunc, &relpages, 0, false);
}
if (!retValue) {
/* 对于 mysql_fdw,抑制警告信息 */
messageLevel = isMysqlFDWFromTblOid(RelationGetRelid(onerel)) ? LOG : messageLevel;
ereport(messageLevel,
(errmsg(
"跳过 \"%s\" --- 无法对该外部表进行分析。", RelationGetRelationName(onerel))));
relation_close(onerel, lockmode);
return;
}
} else {
ereport(messageLevel,
(errmsg("表 %s 不支持分析操作。", RelationGetRelationName(onerel))));
relation_close(onerel, lockmode);
return;
}
} else {
/* 如果在 VACUUM 过程中已经有相应的警告,无需再次输出 WARNING */
if (!(vacstmt->options & VACOPT_VACUUM))
ereport(messageLevel,
(errmsg("跳过 \"%s\" --- 无法分析非表或特殊系统表",
RelationGetRelationName(onerel))));
if (RelationIsPartitioned(onerel)) {
releasePartitionList(onerel, &partList, lockmode);
}
relation_close(onerel, lockmode);
return;
}
/*
* 好了,开始分析。首先告诉其他后端我在进行 ANALYZE。
*/
LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
t_thrd.pgxact->vacuumFlags |= PROC_IN_ANALYZE;
LWLockRelease(ProcArrayLock);
/* 同时获取普通表的大小 */
if (RelationIsPartitioned(onerel)) {
vacstmt->partList = partList;
}
/*
* 执行常规的非递归 ANALYZE。
*/
do_analyze_rel(onerel, vacstmt, relpages, false, elevel, analyzemode, pstHdfsSampleRows);
/*
* 如果有子表,则执行递归的 ANALYZE。
*/
if (RelationIsPAXFormat(onerel))
do_analyze_rel(onerel, vacstmt, relpages, true, elevel, ANALYZECOMPLEX, pstHdfsSampleRows);
/*
* 现在关闭源关系,但保留锁,以便在提交之前没有人删除它。
* (如果有人这样做,他们将无法清除我们在 pg_statistic 中创建的条目。
* 此外,在提交之前释放锁会使我们暴露于 update_attstats 中的并发更新失败。)
*/
if (RelationIsPartitioned(onerel)) {
releasePartitionList(onerel, &partList, NoLock);
}
relation_close(onerel, NoLock);
/*
* 重置我的 PGXACT 标志。注意:我们需要在此处进行,而不是在 vacuum_rel 中,因为 end-of-xact 代码会清除 vacuum 标志。
*/
LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
t_thrd.pgxact->vacuumFlags &= ~PROC_IN_ANALYZE;
LWLockRelease(ProcArrayLock);
}