【 OpenGauss源码学习 —— 列存储(analyze)(一)】

声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》一书以及OpenGauss社区学习文档

概述

  在OpenGauss中,“ANALYZE” 是一个用于数据库优化的关键操作,它用于收集表中统计信息,以便优化查询性能。对于列存储表ANALYZE 功能的作用是分析表中的数据分布值的密度其他统计信息以便查询优化器能够更好地决定执行计划,从而提高查询的执行效率

ANALYZE 功能在列存储表中的工作过程通常包括以下步骤

  1. 数据采样收集: 首先,ANALYZE 会从列存储表中随机抽样一部分数据,以避免对整个表进行完整扫描,从而减少分析的时间和资源消耗。
  2. 统计信息计算: 对于选定的采样数据,ANALYZE 会计算各列中不同值的分布、空值的比例、数据密度等统计信息。这些统计信息可以帮助优化器更好地预测查询的执行计划。
  3. 直方图生成: 在列存储中,直方图是一种用于表示列中值分布的数据结构。ANALYZE 会基于采样数据生成直方图,以便优化器可以更精确地了解数据分布情况。
  4. 数据密度估计: 通过分析采样数据,ANALYZE 还可以估计不同列中数据值的密度,这有助于优化器在执行计划中进行成本估算。
  5. 统计信息存储: 分析完成后,生成的统计信息会被存储在数据库系统的系统表中,以供查询优化器使用。

  当涉及 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 函数

  我们首先先来学习 vacuum 函数,该函数在 OpenGauss 数据库中执行 VACUUMANALYZE 命令的主要入口函数VACUUM 用于回收已删除行的空间,以及处理未使用的数据页,从而减少表的碎片和存储空间ANALYZE 则用于计算表中列的统计信息,以便优化查询计划。以下是这个函数的大致执行步骤:

  1. 首先,函数输出一个日志消息,指示是否在协调节点上运行以及当前 PGXC 节点的编号。
  2. 声明了一些变量,如 stmttype 用于记录操作类型(VACUUM 还是 ANALYZE)、in_outer_xact 用于判断是否在外部事务中,以及 use_own_xacts 用于判断是否需要使用自己的事务。
  3. 检查是否启用了特殊的 enable_show_any_tuples 模式,如果启用了则抛出错误。
  4. 设置当前 IO 状态为 VACUUM
  5. 根据操作选项判断当前是否需要执行 VACUUM,如果是的话,阻止事务链并设置 in_outer_xact 为假。
  6. 检查是否正在执行 VACUUMANALYZE 操作,如果是则抛出错误。
  7. 如果当前是 VACUUM 操作且不是自动 VACUUM 工作进程,则向统计收集器发送死对象信息。
  8. 为跨事务内存存储创建一个专用的上下文。
  9. 如果未传入缓冲策略对象 bstrategy,则在跨事务内存上下文中创建一个新的策略对象。
  10. 构建需要处理的关系列表,如果传入的 relidInvalidOid,则从 vacstmt 中获取。
  11. 根据操作选项判断是否需要自己的事务来处理。
  12. 如果需要自己的事务,将当前事务提交,以便释放锁定。
  13. 设置 VACUUM 成本统计,用于统计执行 VACUUMANALYZE 的开销。
  14. 使用一个循环来处理每一个选定的关系。
  15. 在循环中,处理选定的关系,可能执行 VACUUMANALYZE 操作。
  16. 在关系处理结束后,恢复之前的等待状态,释放内存,报告当前状态。
  17. 如果在循环处理过程中出现异常,设置相关状态并抛出异常。
  18. 最后,恢复各种状态和内存,以及必要的清理工作。

  这个函数主要用于执行 VACUUMANALYZE 操作,它根据传入的参数操作选项来判断需要执行哪种操作,并根据不同情况进行事务管理统计信息收集等。

/*
 * 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 结构体

  VacuumStmt 结构体是在 PostgreSQL 数据库中用于表示 VACUUMANALYZE 命令的数据结构。它在数据库源代码中定义,用于存储 VACUUMANALYZE 命令的各种参数和选项。以下是 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 结构体

  BufferAccessStrategy 是一个在 PostgreSQL 数据库中用于定义缓冲区访问策略的数据结构。它允许控制在数据库中读取和写入缓冲区时的一些策略,以优化内存使用和 I/O 操作。该结构体的目的是提供一种机制,使用户可以在不同的操作(如 VACUUMANALYZE、查询等)中自定义如何访问数据库缓冲区
  具体而言,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_rel 函数用于执行分析操作ANALYZE)或者清理操作VACUUM)的入口函数,它接收一个关系(表)的对象标识符(OID)、分析或清理的语句信息、以及缓冲区访问策略作为参数,然后根据这些参数执行相应的操作
  在给定的 relidvacstmt 参数的情况下,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_get_relation 函数是用于在进行分析操作analyzevacuum)之前,打开要分析的关系(表),并获取适当的锁确保分析的正确执行。函数源码如下:(路径: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 函数

  analyze_rel_internal 函数用于分析一个关系(表)它接收一个待分析的关系、分析或清理命令的语句信息、缓冲区访问策略、分析类型以及用于 DFS 表、增量表或复杂表的样本行信息作为参数。它执行以下操作:

  1. 设置消息级别和错误级别(用于不同的日志输出)。
  2. 检查用户权限,判断当前用户是否有分析权限。
  3. 忽略其他后端的临时表或全局临时表。
  4. 检查表的类型,包括是否是特殊表(如 pg_statistic)、普通表或外部表。
  5. 根据表的类型执行相应的分析操作:
  • 对于普通表,获取表的大小和分区列表,执行常规分析。
  • 对于外部表,调用外部表的 FDW(外部数据包装器)钩子函数,检查是否支持分析操作。
  • 对于其他类型的表,输出警告信息。
  1. 告诉其他后端正在进行 ANALYZE 操作。
  2. 执行常规的非递归 ANALYZE 操作。
  3. 如果表是分区表并且使用 PAX 存储格式,则执行递归的 ANALYZE 操作。
  4. 关闭源关系,保留锁,以防在提交之前删除关系。
  5. 重置 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);
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值