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

news2024/11/15 23:35:26

列存储(analyze)

  • AcquireSampleCStoreRows 函数
    • es_get_attnums_to_analyze 函数
    • CStoreRelGetCUNumByNow 函数
    • CStore::GetLivedRowNumbers 函数
    • InitGetValFunc 函数
    • CStoreGetfstColIdx 函数
    • CStore::GetCUDesc 函数
    • CStore::IsTheWholeCuDeleted 函数
    • CStore::IsTheWholeCuDeleted 函数
    • CStore::CudescTupGetMinMaxDatum 函数

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

AcquireSampleCStoreRows 函数

  在前一章中(列存储(analyze)(三)),我们对函数 acquire_sample_rows 进行了学习,其中acquire_sample_rows 函数的作用是在 PostgreSQL 数据库中执行抽样操作,用于获取数据表的随机样本行。这个函数的主要目的是估计数据表的统计信息和数据分布,以帮助查询优化器生成更有效的执行计划
  本章,我们紧接着来看另一种数据库抽样操作 AcquireSampleCStoreRows ,该函数也是我们所需关注的重点,是本【OpenGauss源码学习 —— 列存储(analyze)】系列的重点和最终学习目标。AcquireSampleCStoreRows 是用于列式存储引擎(如列存储数据库表)的函数。它是为了从列存储表中获取样本数据而设计的,通常用于分析和统计查询中,以获取表的随机样本,以便估计表的统计信息和数据分布。
  其中,函数 AcquireSampleCStoreRows 的调用关系与 acquire_sample_rows 相同,如下所示:do_analyze_rel -> get_target_rows -> AcquireSampleCStoreRows

  函数的入参含义如下:

  1. Relation onerel: 被分析的关系(表)的 Relation 结构,表示要进行样本抽样的目标表。调试信息如下:
    在这里插入图片描述
  2. int elevel: 日志消息的错误级别。用于确定错误消息的日志级别。
  3. HeapTuple* rows: 一个用于存储样本数据的数组。函数将抽样的行数据存储在这个数组中。
  4. int64 targrows: 目标抽样行数,即希望从表中抽取的行数。
  5. double* totalrows: 用于存储表的估计总行数的指针。函数将估计的总行数存储在这个指针指向的变量中。
  6. double* totaldeadrows: 用于存储表的估计死行数的指针。函数将估计的死行数存储在这个指针指向的变量中。
  7. VacAttrStats** vacattrstats: 一个 VacAttrStats 结构的数组,表示用于分析的每个属性的统计信息。这些统计信息包括样本数据的属性分布等。
    在这里插入图片描述
  8. int analyzeAttrNum: 要分析的属性的数量。表示要分析的属性数量。

  AcquireSampleCStoreRows 函数的作用和目的是执行列存储表格的抽样操作,以获取列存储表格的随机样本行AcquireSampleCStoreRows 函数源码如下:(路径:src/gausskernel/optimizer/commands/analyze.cpp

*
 * AcquireSampleCStoreRows -- acquire a random sample of rows from the table
 *
 * Selected rows are returned in the caller-allocated array rows[], which
 * must have at least targrows entries.
 * The actual number of rows selected is returned as the function result.
 * We also estimate the total number of live and dead rows in the table,
 * and return them into *totalrows and *totaldeadrows, respectively.
 *
 * To improve the analyze efficiency in the condition of severl attrs are selected,
 * we add two parameters which record the information of seleced attrs.The perform
 * can only used for column storage table.
 *
 * The returned list of tuples is in order by physical position in the table.
 * (We will rely on this later to derive correlation estimates.)
 */
template <bool estimate_table_rownum>
static int64 AcquireSampleCStoreRows(Relation onerel, int elevel, HeapTuple* rows, int64 targrows, double* totalrows,
    double* totaldeadrows, VacAttrStats** vacattrstats, int analyzeAttrNum)
{
    int64 numrows = 0;             /* 当前在样本中的行数 */
    int64 delta_numrows = 0;       /* 当前在样本中的差异表行数 */
    int32 samplerows = 0;          /* 收集的总行数 */
    int64 liverows = 0;            /* 观察到的存活行数 */
    int64 deadrows = 0;            /* 观察到的已删除行数 */
    BlockNumber totalblocks = 0;   /* 总块数 */
    BlockNumber estBlkNum = 0;     /* 估算的块数 */
    BlockSamplerData bs;           /* 块采样数据结构 */
    double rstate;                 /* 随机状态 */
    AnlPrefetch anlprefetch;       /* 预取设置 */
    int cusize_threshold = maxAnalyzeUseCstoreBufferSize; /* CU大小阈值 */
    anlprefetch.blocklist = NULL;  /* 预取块列表初始化为空 */

    AssertEreport(targrows > 0, MOD_OPT, "采样时目标行数必须大于0");
    
    /*
     * 所有系统表都是行存储表,列存储表必须是用户定义的表,
     * 因此我们只返回而不采样。
     */
    if (IS_PGXC_COORDINATOR) {
        *totalrows = 0;
        *totaldeadrows = 0;
        return 0;
    }
    
    /* 为扩展的统计信息准备 */
    Bitmapset* bms_attnums = es_get_attnums_to_analyze(vacattrstats, analyzeAttrNum);
    int num_attnums = bms_num_members(bms_attnums);   /* 统计属性的数量 */
    int colTotalNum = onerel->rd_att->natts;          /* 表中总的列数 */
    int32* attrSeq = NULL;                           /* 属性顺序 */
    int16* colIdx = (int16*)palloc0(sizeof(int16) * num_attnums); /* 列索引数组 */
    int* slotIdList = NULL;                          /* CU槽位索引数组 */
    CStoreScanDesc cstoreScanDesc = NULL;            /* 列存储扫描描述符 */
    CU** cuPtr = NULL;                               /* CU指针数组 */
    Relation deltarel = NULL;                        /* 差异表 */
    double totalRowCnts = 0;                         /* 总行数 */
    double deltarows = 0;                            /* 差异表的行数 */
    double curows = 0;                               /* CU中的行数 */
    double cudeadrows = 0;                           /* CU中的已删除行数 */

    /* 首先考虑差异关系(delta relation) */
    Assert(OidIsValid(onerel->rd_rel->reldeltarelid));
    
    /* 打开差异表。我们只需要对其进行 AccessShareLock。 */
    deltarel = relation_open((onerel->rd_rel->reldeltarelid), AccessShareLock);
    
    /* 从差异关系中仅获取总行数的估计 */
    delta_numrows = acquire_sample_rows<true>(deltarel, elevel, rows, targrows, &deltarows, totaldeadrows);

    /* 在这里将 nulls 设置为 true,并且那些不需要分析的属性将始终为 null */
    for (int i = 0; i < num_attnums; ++i) {
        colIdx[i] = bms_first_member(bms_attnums);
    }

    /* 为当前分析初始化本地快照(local snapshot)。 */
    Snapshot tmpSnapshot = GetActiveSnapshot();
    Assert(tmpSnapshot != NULL);

    cstoreScanDesc = CStoreBeginScan(onerel, num_attnums, colIdx, tmpSnapshot, false);
    // 开始对列存储表进行扫描,准备抽样
    // onerel: 当前关系
    // num_attnums: 要分析的属性数量
    // colIdx: 要分析的属性列的索引
    // tmpSnapshot: 当前快照
    // false: 是否是 Delta 表的扫描

    totalblocks = CStoreRelGetCUNumByNow(cstoreScanDesc);
    // 获取当前 CU 的数量,用于统计

    curows = (double)cstoreScanDesc->m_CStore->GetLivedRowNumbers(&deadrows);
    // 从 CU 中获取存活行数,并更新 deadrows
    // curows: CU 中存活的行数
    // deadrows: 已删除的行数

    if (curows <= 0.0) {
        // 如果存活行数小于等于0,则没有需要分析的数据
        relation_close(deltarel, AccessShareLock); // 关闭 Delta 表
        CStoreEndScan(cstoreScanDesc); // 结束对列存储表的扫描
        pfree_ext(colIdx); // 释放 colIdx 的内存
        pfree_ext(bms_attnums); // 释放 bms_attnums 的内存
        return 0; // 返回0,表示没有进行抽样
    }

    *totalrows = totalRowCnts = curows + deltarows;
    // 设置总行数为存活行数加上 Delta 表的行数
    // totalrows: 总行数
    // totalRowCnts: 存活行数加上 Delta 表的行数

    cudeadrows = (double)deadrows;
    *totaldeadrows += cudeadrows;
    // 更新总的已删除行数
    deadrows = 0; // 在非 pretty 模式下重用 deadrows 来存储已删除元组的数量

    /* for pretty mode we just return actual lived rows */
    if (estimate_table_rownum && SUPPORT_PRETTY_ANALYZE) {
        // 如果是 "pretty" 模式并且需要估算表的行数
        // "pretty" 模式通常用于漂亮的输出,返回实际存活的行数,而不进行估算

        /* only get estimate rows, free delta rows and close relation */
        // 只获取估算的行数,释放 Delta 表的行并关闭关系
        for (int i = 0; i < delta_numrows; i++) {
            if (rows[i]) {
                pfree_ext(rows[i]); // 释放行的内存
                rows[i] = NULL;
            }
        }
        relation_close(deltarel, AccessShareLock); // 关闭 Delta 表
        CStoreEndScan(cstoreScanDesc); // 结束对列存储表的扫描
        pfree_ext(colIdx); // 释放 colIdx 的内存
        pfree_ext(bms_attnums); // 释放 bms_attnums 的内存
        return 0; // 返回0,表示没有进行抽样
    }

    attrSeq = (int*)palloc0(sizeof(int32) * num_attnums);
    // 分配内存以存储属性的顺序
    slotIdList = (int*)palloc0(sizeof(int) * num_attnums);
    // 分配内存以存储槽位的列表
    cuPtr = (CU**)palloc0(sizeof(CU*) * colTotalNum);
    // 分配内存以存储 CU 指针的数组

    if (!estimate_table_rownum) {
        // 如果不需要估算表的行数(非 pretty 模式)

        /* free rows */
        // 释放之前分配的行内存
        for (int i = 0; i < delta_numrows; i++) {
            if (rows[i]) {
                pfree_ext(rows[i]);
                rows[i] = NULL;
            }
        }
        int64 deltatargrows = 0;
        // 初始化 Delta 表的目标行数为0

        /* recalculate target num */
        // 重新计算目标行数,以根据 Delta 表的情况进行调整
        if (deltarows > 0) {
            // 如果 Delta 表中有存活的行

            deltatargrows = (int)rint(targrows * deltarows / totalRowCnts);
            // 根据存活行数比例重新计算目标行数
            /* Make sure we don't overrun due to roundoff error */
            // 确保不因四舍五入误差而超出目标行数
            deltatargrows = Min(deltatargrows, targrows);
            // 限制目标行数不超过原始目标行数
            if (deltatargrows > 0) {
                double trows = 0;
                double tdrows = 0;
                /* Fetch a random sample of the delta table's rows */
                // 从 Delta 表中获取随机样本行
                delta_numrows = acquire_sample_rows<false>(deltarel, elevel, rows, deltatargrows, &trows, &tdrows);
                // 重新抽样 Delta 表
                /* fix total rows */
                // 更新总行数和已删除行数
                *totalrows = trows + curows;
                *totaldeadrows = tdrows + cudeadrows;
            }
        }

        /* change cstore target num */
        // 调整列存储表的目标行数
        int64 temp_target = (int)rint(targrows * curows / totalRowCnts);
        /* Make sure we don't overrun due to roundoff error */
        // 确保不因四舍五入误差而超出目标行数
        temp_target = Min(temp_target, targrows - deltatargrows);
        targrows = temp_target;

        /*
         * calculate cu number by following formula
         * 1. get lived row number
         * 2. estimate row size by suppose the width of each column is 4
         * 3. estimate how many pages needed if it's row-stored
         * 4. evaluate how many CUs needed to sample
         */
        // 通过以下公式计算 CU 数:
        // 1. 获取存活行数
        // 2. 假定每列的宽度为4,估算行大小
        // 3. 估算如果是按行存储需要多少页
        // 4. 评估需要采样的 CU 数
        int totalwidth = 0;
        int relpages = 0;
        BlockNumber sampleCUs = 0;

        totalwidth = 4 * onerel->rd_att->natts;
        relpages = ceil(*totalrows * totalwidth / BLCKSZ);
        if (relpages == 0) {
            relpages = 1;
        }
        sampleCUs = ceil((double)targrows / relpages * totalblocks);
        sampleCUs = (sampleCUs > totalblocks) ? totalblocks : sampleCUs;

        elog(DEBUG2, "ANALYZE INFO : sample %u cu from totoal %u cu", sampleCUs, totalblocks);
        // 输出采样的 CU 数和总 CU 数
        BlockSampler_Init(&bs, totalblocks, sampleCUs);
    } else {
        AssertEreport(!SUPPORT_PRETTY_ANALYZE, MOD_OPT, "");
        // 如果需要估算表的行数,并且不支持 "pretty" 模式分析,则直接采样指定数量的行
        BlockSampler_Init(&bs, totalblocks, targrows);
        elog(DEBUG2, "ANALYZE INFO : sample %ld rows from totoal %u cu", targrows, totalblocks);
    }

    rstate = anl_init_selection_state(targrows);
    // 初始化行采样状态

    /* Prepare for sampling rows */
    // 为行采样做准备
    CStore* cstore = cstoreScanDesc->m_CStore;
    Form_pg_attribute* attrs = cstore->m_relation->rd_att->attrs;
    GetValFunc* getValFuncPtr = (GetValFunc*)palloc(sizeof(GetValFunc) * colTotalNum);

    /* change sample rows pointer */
    // 移动指针以指向下一个要采样的行
    rows += delta_numrows;

    for (int col = 0; col < num_attnums; ++col) {
        int colSeq = attrSeq[col] = colIdx[col] - 1;
        // 获取列的顺序,并将其保存在 attrSeq 数组中
        InitGetValFunc(attrs[colSeq]->attlen, getValFuncPtr, colSeq);
        // 初始化获取列值的函数指针
    }

    ADIO_RUN()
    {
        uint32 quantity = (uint32)CSTORE_ANALYZE_PREFETCH_QUANTITY;
        // 获取预取数量

        anlprefetch.fetchlist1.size = (uint32)((quantity > (totalblocks / 2 + 1)) ? (totalblocks / 2 + 1) : quantity);
        // 设置第一个预取列表的大小,限制在总块数的一半加一以内
        anlprefetch.fetchlist1.blocklist = (BlockNumber*)palloc(sizeof(BlockNumber) * anlprefetch.fetchlist1.size);
        // 为第一个预取列表分配内存
        anlprefetch.fetchlist1.anl_idx = 0;
        anlprefetch.fetchlist1.load_count = 0;

        anlprefetch.fetchlist2.size = anlprefetch.fetchlist1.size;
        // 第二个预取列表与第一个相同大小
        anlprefetch.fetchlist2.blocklist = (BlockNumber*)palloc(sizeof(BlockNumber) * anlprefetch.fetchlist2.size);
        // 为第二个预取列表分配内存
        anlprefetch.fetchlist2.anl_idx = 0;
        anlprefetch.fetchlist2.load_count = 0;
        anlprefetch.init = false;
        // 初始化预取状态为未完成

        ereport(DEBUG1,
            (errmodule(MOD_ADIO),
                errmsg("analyze prefetch for %s prefetch quantity(%u)",
                    RelationGetRelationName(onerel),
                    anlprefetch.fetchlist1.size)));
        // 发送调试信息,表示进行分析预取,显示表名和预取数量
    }
    ADIO_END();

    MemoryContext sample_context; // 用于存储抽样数据的内存上下文
    MemoryContext old_context;    // 保存当前内存上下文,以便稍后恢复
    BlockNumber targblock;        // 目标块号
    int fstColIdx = CStoreGetfstColIdx(onerel); // 获取列存储表的第一列索引
    Datum* constValues = (Datum*)palloc(sizeof(Datum) * colTotalNum); // 存储常量值的数组
    bool* nullValues = (bool*)palloc(sizeof(bool) * colTotalNum);    // 存储空值信息的数组
    Datum* values = (Datum*)palloc0(sizeof(Datum) * colTotalNum);    // 存储数据值的数组,初始化为零
    bool* nulls = (bool*)palloc0(sizeof(bool) * colTotalNum);        // 存储是否为 null 值的数组,初始化为 false
    int* funcIdx = (int*)palloc0(sizeof(int) * colTotalNum);         // 存储函数索引的数组,初始化为零
    List* sampleTupleInfo = NIL;    // 存储抽样元组信息的列表
    sample_tuple_cell* st_cell = NULL; // 抽样元组单元格
    ListCell* cell1 = NULL;       // 用于遍历列表的 ListCell 指针
    ListCell* cell2 = NULL;       // 用于遍历列表的 ListCell 指针
    ListCell* cell3 = NULL;       // 用于遍历列表的 ListCell 指针

    // 创建一个新的内存上下文,用于存储抽样数据,设置上下文名称和大小参数
    sample_context = AllocSetContextCreate(CurrentMemoryContext,
        "sample cstore rows for analyze",
        ALLOCSET_DEFAULT_MINSIZE,
        ALLOCSET_DEFAULT_INITSIZE,
        ALLOCSET_DEFAULT_MAXSIZE);

    while (
        InvalidBlockNumber != (targblock = BlockSampler_GetBlock<true>(
                                   cstoreScanDesc, &bs, &anlprefetch, num_attnums, attrSeq, estimate_table_rownum))) {
        List* valueLocation = NIL;     // 存储值的位置信息的列表
        List* targoffsetList = NIL;    // 存储目标偏移量的列表
        int location = 0;             // 当前位置
        CUDesc cuDesc;                // 列存储单元描述
        uint16 targoffset = 0;        // 目标偏移量
        uint16 maxoffset;             // 最大偏移量
        int64 total_cusize = 0;       // 总共的列存储单元大小
        int start_col = -1;           // 起始列索引
        int end_col = -1;             // 结束列索引
        bool first_batch = true;      // 是否是第一批次
        bool all_in_buffer = true;    // 是否所有数据都在缓冲区内

        targblock = targblock + FirstCUID + 1; // 计算目标块号

        // 如果获取列存储单元描述失败,则继续下一次循环
        if (cstoreScanDesc->m_CStore->GetCUDesc(fstColIdx, targblock, &cuDesc, tmpSnapshot) != true)
            continue;

        // 如果启用工作负载控制,进行 IO 调度和更新
        if (ENABLE_WORKLOAD_CONTROL)
            IOSchedulerAndUpdate(IO_TYPE_READ, 1, IO_TYPE_COLUMN);

        cstore->GetCUDeleteMaskIfNeed(targblock, tmpSnapshot); // 获取列存储单元删除掩码

        // 如果该列存储单元内的所有元组都已删除,则继续下一次循环
        if (cstore->IsTheWholeCuDeleted(cuDesc.row_count))
            continue;

        maxoffset = cuDesc.row_count;

        if (estimate_table_rownum) {
            liverows = maxoffset;
            estBlkNum++;

            if (liverows > 0)
                break;

            ereport(DEBUG1,
                (errmsg("ANALYZE INFO : estimate total rows of \"%s\" - no lived rows in cuid: %u",
                    RelationGetRelationName(onerel),
                    targblock)));

            totalblocks--;
            continue;
        }

        /* 重置 null 标志和常量值标志以供下一个块使用 */
        errno_t rc;

        // 将 nulls 数组的所有元素设置为 true
        rc = memset_s(nulls, sizeof(bool) * colTotalNum, true, sizeof(bool) * colTotalNum);
        securec_check(rc, "\0", "\0");

        // 将 nullValues 数组的所有元素设置为 false
        rc = memset_s(nullValues, sizeof(bool) * colTotalNum, false, sizeof(bool) * colTotalNum);
        securec_check(rc, "\0", "\0");

        // 将 constValues 数组的所有元素设置为 0
        rc = memset_s(constValues, sizeof(Datum) * colTotalNum, 0, sizeof(Datum) * colTotalNum);
        securec_check(rc, "\0", "\0");

/* copy data from cu buffer */
#define copy_sample_dataum(dest, col_num, offset)                                   \
    do {                                                                            \
        old_context = MemoryContextSwitchTo(sample_context);                        \
        Datum dm = getValFuncPtr[colNum][funcIdx[colNum]](cuPtr[colNum], (offset)); \
        int16 valueTyplen = attrs[(col_num)]->attlen;                               \
        bool valueTypbyval = attrs[(col_num)]->attlen == 0 ? false : true;          \
        if (valueTyplen < 0)                                                        \
            (dest) = PointerGetDatum(PG_DETOAST_DATUM_COPY(dm));                    \
        else                                                                        \
            (dest) = datumCopy(dm, valueTypbyval, valueTyplen);                     \
        (void)MemoryContextSwitchTo(old_context);                                   \
    } while (0)

/*
 * 检查是否选择了特定的行进行分析
 * 1. 跳过已删除的行(死行)
 * 2. 如果已获取的行数 < 目标行数,则获取所有行
 * 3. 随机选择一个位置替换一个元组
 */
#define check_match_tuple(copyTuple)                                              \
    do {                                                                          \
        (copyTuple) = false;                                                      \
        if (cstore->IsDeadRow((uint32)targblock, (uint32)targoffset)) {           \
            deadrows++;                                                           \
        } else if (numrows < targrows) {                                          \
            (copyTuple) = true;                                                   \
            location = numrows;                                                   \
            numrows++;                                                            \
            samplerows++;                                                         \
        } else {                                                                  \
            if (0 >= anl_get_next_S(samplerows, targrows, &rstate)) {             \
                location = (int64)(targrows * anl_random_fract());                \
                (copyTuple) = true;                                               \
                AssertEreport(location >= 0 && location < targrows, MOD_OPT, ""); \
            }                                                                     \
            samplerows++;                                                         \
        }                                                                         \
    } while (0)

#define load_cu_data(s_col, e_col, values, nulls, copy)                                                       \
    do {                                                                                                      \
        for (int col1 = (s_col); col1 <= (e_col); ++col1) {                                                   \
            int colNum = attrSeq[col1];                                                                       \
            if (cuPtr[colNum]) {                                                                              \
                if (cuPtr[colNum]->IsNull(targoffset)) {                                                      \
                    (nulls)[colNum] = true;                                                                   \
                    (values)[colNum] = 0;                                                                     \
                } else {                                                                                      \
                    (nulls)[colNum] = false;                                                                  \
                    if (copy)                                                                                 \
                        copy_sample_dataum((values)[colNum], colNum, targoffset);                             \
                    else                                                                                      \
                        (values)[colNum] = getValFuncPtr[colNum][funcIdx[colNum]](cuPtr[colNum], targoffset); \
                }                                                                                             \
            } else {                                                                                          \
                (nulls)[colNum] = nullValues[colNum];                                                         \
                (values)[colNum] = constValues[colNum];                                                       \
            }                                                                                                 \
        }                                                                                                     \
    } while (0)

		/*
		 * 试图加载所有的CU数据到cstore_buffers中:
		 * 1. 如果总CU大小小于cstore_buffers的1/8,将所有CU加载到CU缓冲区。
		 * 2. 如果总CU大小大于或等于cstore_buffers的1/8,将样本数据复制到临时内存以避免固定太多的CU。
		 */
		for (int col = 0; col < num_attnums; ++col) {
		    int colNum = attrSeq[col];
		
		    cuPtr[colNum] = NULL;
		    slotIdList[col] = CACHE_BLOCK_INVALID_IDX;
		
		    // 如果是当前处理的第一个列
		    if (-1 == start_col)
		        start_col = col;
		    end_col = col;
		
		    // 获取CU描述符
		    (void)cstore->GetCUDesc(colNum, targblock, &cuDesc, tmpSnapshot);
		
		    if (cuDesc.IsNullCU()) {
		        nullValues[colNum] = true;
		        constValues[colNum] = 0;
		        elog(DEBUG2,
		            "ANALYZE INFO - 表 \"%s\":attnum(%d),cuid(%u) 为空",
		            RelationGetRelationName(onerel),
		            colNum,
		            targblock);
		        continue;
		    } else if (cuDesc.IsSameValCU()) {
		        bool shoulFree = false;
		
		        nullValues[colNum] = false;
		        // 从CU描述符中获取常量值
		        old_context = MemoryContextSwitchTo(sample_context);
		        constValues[colNum] = CStore::CudescTupGetMinMaxDatum(&cuDesc, attrs[colNum], true, &shoulFree);
		        (void)MemoryContextSwitchTo(old_context);
		        elog(DEBUG2,
		            "ANALYZE INFO - 表 \"%s\":attnum(%d),cuid(%u) 包含常量值",
		            RelationGetRelationName(onerel),
		            colNum,
		            targblock);
		        continue;
		    } else {
		        nullValues[colNum] = false;
		        constValues[colNum] = 0;
		        // 获取CU数据并缓存
		        cuPtr[colNum] = cstore->GetCUData(&cuDesc, colNum, attrs[colNum]->attlen, slotIdList[col]);
		        funcIdx[colNum] = cuPtr[colNum]->HasNullValue() ? 1 : 0;
		        // 在每个CU的IO操作之前检查是否开启了Vacuum延迟。如果可以从CU缓存中获取CU数据,我们应该减少AMAP的调用次数。
		        vacuum_delay_point();
		
		        // 断言CU数据不是压缩的
		        AssertEreport(!cuPtr[colNum]->m_cache_compressed, MOD_OPT, "");
		        total_cusize += cuPtr[colNum]->GetUncompressBufSize();
		
		        /*
		         * 如果总CU大小小于cusize_threshold并且(或者是最后一列,以确保所有样本CU数据都已加载到临时内存中),
		         * 则继续下一列的处理。
		         */
		        if (total_cusize < cusize_threshold && (first_batch || (num_attnums - 1) != col))
		            continue;
		
		        if (NIL == sampleTupleInfo) {
		            ereport(LOG,
		                (errmsg(
		                     "ANALYZE INFO - 表 \"%s\":从CU缓冲区复制数据", RelationGetRelationName(onerel)),
		                    errdetail("增加cstore_buffers的大小以避免复制数据")));
		        }
		        elog(DEBUG2,
		            "ANALYZE INFO - 表 \"%s\":复制列数据 [%d, %d] 从CU缓冲区",
		            RelationGetRelationName(onerel),
		            start_col,
		            end_col);
		    }
		
		    all_in_buffer = false;
		    if (first_batch) {
		        /* 处理第一个批次的CU数据,我们应该首先初始化sample_tuple_cell */
		        bool copyTuple = false;
		        cell1 = list_head(sampleTupleInfo) ? list_head(sampleTupleInfo) : NULL;
		        for (targoffset = FirstOffsetNumber - 1; targoffset < maxoffset; targoffset++) {
		            check_match_tuple(copyTuple);
		            if (!copyTuple)
		                continue;
		
		            if (cell1 != NULL) {
		                errno_t rc;
		
		                st_cell = (sample_tuple_cell*)lfirst(cell1);
		                rc = memset_s(st_cell->nulls, sizeof(bool) * colTotalNum, true, sizeof(bool) * colTotalNum);
		                securec_check(rc, "\0", "\0");
		                rc = memset_s(st_cell->values, sizeof(Datum) * colTotalNum, 0, sizeof(Datum) * colTotalNum);
		                securec_check(rc, "\0", "\0");
		            } else {
		                errno_t rc;
		
		                st_cell = (sample_tuple_cell*)palloc0(sizeof(sample_tuple_cell));
		                sampleTupleInfo = lappend(sampleTupleInfo, st_cell);
		
		                st_cell->nulls = (bool*)palloc(sizeof(bool) * colTotalNum);
		                rc = memset_s(st_cell->nulls, sizeof(bool) * colTotalNum, true, sizeof(bool) * colTotalNum);
		                securec_check(rc, "\0", "\0");
		                st_cell->values = (Datum*)palloc0(sizeof(Datum) * colTotalNum);
		            }
		
		            valueLocation = lappend_int(valueLocation, location);
		            targoffsetList = lappend_int(targoffsetList, targoffset);
		            // 加载CU数据
		            load_cu_data(start_col, end_col, st_cell->values, st_cell->nulls, true);
		            cell1 = (cell1 && lnext(cell1)) ? lnext(cell1) : NULL;
		        }
		
		        for (int col1 = start_col; col1 <= end_col; ++col1) {
		            if (IsValidCacheSlotID(slotIdList[col1]))
		                CUCache->UnPinDataBlock(slotIdList[col1]);
		        }
		
		        first_batch = false;
		        AssertEreport(sampleTupleInfo, MOD_OPT, "");
		    } else {
		        /* 已经建立了sample_tuple_cell,只需将数据从缓冲区复制到临时内存中 */
		        AssertEreport(sampleTupleInfo, MOD_OPT, "");
		        forboth(cell1, sampleTupleInfo, cell3, targoffsetList)
		        {
		            st_cell = (sample_tuple_cell*)lfirst(cell1);
		            targoffset = lfirst_int(cell3);
		            // 加载CU数据
		            load_cu_data(start_col, end_col, st_cell->values, st_cell->nulls, true);
		        }
		
		        for (int col1 = start_col; col1 <= end_col; ++col1) {
		            if (IsValidCacheSlotID(slotIdList[col1]))
		                CUCache->UnPinDataBlock(slotIdList[col1]);
		        }
		    }
		
		    start_col = -1;
		}

	if (all_in_buffer) {
	    /* 已经将所有的CU加载到CU缓冲区,直接使用CU缓冲区数据构建元组 */
	    bool copyTuple = false;
	
	    // 断言已处理的列范围正确
	    AssertEreport(0 == start_col && end_col == num_attnums - 1, MOD_OPT, "");
	
	    for (targoffset = FirstOffsetNumber - 1; targoffset < maxoffset; targoffset++) {
	        check_match_tuple(copyTuple);
	        if (!copyTuple)
	            continue;
	
	        // 加载CU数据到元组
	        load_cu_data(0, num_attnums - 1, values, nulls, false);
	        tableam_tops_free_tuple(rows[location]);
	        // 使用CU数据构建HeapTuple
	        rows[location] = (HeapTuple)tableam_tops_form_tuple(onerel->rd_att, values, nulls, HEAP_TUPLE);
	        ItemPointerSet(&(rows[location])->t_self, targblock, targoffset + 1);
	    }
	
	    for (int col1 = 0; col1 < num_attnums; ++col1) {
	        if (IsValidCacheSlotID(slotIdList[col1]))
	            CUCache->UnPinDataBlock(slotIdList[col1]);
	    }
	} else {
	    /* 使用临时内存中的数据构建元组 */
	    AssertEreport(end_col == num_attnums - 1, MOD_OPT, "");
	    forthree(cell1, sampleTupleInfo, cell2, valueLocation, cell3, targoffsetList)
	    {
	        st_cell = (sample_tuple_cell*)lfirst(cell1);
	        location = lfirst_int(cell2);
	        targoffset = lfirst_int(cell3);
	        tableam_tops_free_tuple(rows[location]);
	        // 使用临时内存中的数据构建HeapTuple
	        rows[location] = (HeapTuple)tableam_tops_form_tuple(onerel->rd_att, st_cell->values, st_cell->nulls, HEAP_TUPLE);
	        ItemPointerSet(&(rows[location])->t_self, targblock, targoffset + 1);
	    }
	
	    // 释放临时列表
	    list_free_ext(valueLocation);
	    list_free_ext(targoffsetList);
	}
	
	// 重置内存上下文
	MemoryContextReset(sample_context);

	/* 步骤 4:释放内存 */
	pfree_ext(nullValues);
	pfree_ext(constValues);
	pfree_ext(nulls);
	pfree_ext(values);
	pfree_ext(attrSeq);
	pfree_ext(colIdx);
	pfree_ext(funcIdx);
	pfree_ext(cuPtr);
	pfree_ext(slotIdList);
	pfree_ext(getValFuncPtr);
	list_free_ext(sampleTupleInfo);
	pfree_ext(bms_attnums);
	
	/* 删除内存上下文 */
	MemoryContextDelete(sample_context);
	
	ADIO_RUN()
	{
	    pfree_ext(anlprefetch.fetchlist1.blocklist);
	    pfree_ext(anlprefetch.fetchlist2.blocklist);
	}
	ADIO_END();
	
	/* 步骤 5:关闭 delta 表并结束 cstore 扫描 */
	relation_close(deltarel, AccessShareLock);
	CStoreEndScan(cstoreScanDesc);
	
	if (estimate_table_rownum) {
	    /* 在非 pretty 模式下估算总行数 */
	    AssertEreport(!SUPPORT_PRETTY_ANALYZE, MOD_OPT, "");
	
	    if (totalblocks == 0 || (totalblocks > 0 && liverows > 0)) {
	        *totalrows = liverows * totalblocks + deltarows;
	    } else {
	        /* 目标 300 行的所有块都是死行。 */
	        *totalrows = INVALID_ESTTOTALROWS + targrows;
	    }
	
	    ereport(elevel,
	        (errmsg("ANALYZE INFO : \"%s\":扫描了 %u 个 CU,目标 cuid:%u,"
	                "包含 %ld 个活动行, %ld 个死亡行,估计总行数 %.0f",
	            RelationGetRelationName(onerel),
	            estBlkNum,
	            totalblocks,
	            targblock,
	            liverows,
	            deadrows,
	            *totalrows)));
	
	    return numrows + delta_numrows;
	}
	
	/*
	 * 步骤 6:对行进行排序并估算 reltuples
	 *
	 * 如果我们没有找到所需数量的元组,则无需排序,因为它们已经有序。
	 *
	 * 否则,我们需要根据位置(itempointer)对收集的元组进行排序。
	 * 不值得担心已经有序的边界情况。
	 */
	if (numrows == targrows)
	    qsort((void*)rows, numrows, sizeof(HeapTuple), compare_rows);
	
	/* 在数据节点中,输出一些有趣的关系信息 */
	if (IS_PGXC_DATANODE) {
	    ereport(elevel,
	        (errmsg("ANALYZE INFO : \"%s\":扫描了 %d 个 CU,样本 %ld 行,估算总 %.0f 行",
	            RelationGetRelationName(onerel),
	            bs.m,
	            totalblocks,
	            numrows,
	            *totalrows)));
	}
	
	return numrows + delta_numrows;

  该函数的关键部分和作用如下:

  1. 获取样本数据 函数通过对表格的列存储块进行抽样,获取一定数量的行数据,这些行数据将用于后续的分析和统计
  2. 估算总行数和死行数 函数会估算列存储表格的总行数和死行数。总行数是表格中的所有行数,而死行数是已标记为删除的行数。
  3. 处理 delta 表如果表格具有 delta 表(差异表),函数会首先尝试从 delta 表中获取样本数据,以提高估算的准确性。
  4. 数据处理 函数根据样本数据中的 null 值、常量值等信息进行数据处理,包括处理列的数据类型和 null 值。
  5. 样本数据排序如果成功获取了样本数据,并且样本数据数量达到了目标数量,函数会对样本数据进行排序,以确保它们按照物理位置在表格中的顺序排列
  6. 输出结果函数会返回实际获取的样本数据行数,以及估算的总行数和死行数。这些信息将用于查询优化和统计信息收集。
  7. 其他功能函数还包括一些额外的功能,例如处理样本数据的方式(复制到临时内存或直接在 CU 缓冲区中操作)、IO 调度(根据优化器的要求进行数据加载)等。

es_get_attnums_to_analyze 函数

  es_get_attnums_to_analyze 函数用于获取需要进行分析的所有列的 attnum属性号),包括单列统计多列统计

  1. 参数:
  • vacattrstatsVacAttrStats 数组,包含要分析的列的统计信息。
  • vacattrstats_sizeVacAttrStats 数组的大小。
  1. 返回值:
  • Bitmapset*:一个位图集合,包含所有需要分析的列的 attnum。调试信息如下:
    在这里插入图片描述
  1. 函数逻辑:
  • 创建一个名为 bms_attnum位图集合,用于存储需要分析的列的 attnum
  • 使用两个嵌套的循环遍历 vacattrstats 数组和其中的每个 VacAttrStats 元素。
    (1)外循环遍历 vacattrstats 数组。
    (2)内循环遍历每个 VacAttrStats 元素的属性列表,属性列表存储在 attrs 成员中。
  • 在内循环中,获取每个属性的 attnum(属性号)并将其添加到 bms_attnum 中,使用 bms_add_member 函数。
  • 最终,函数返回一个位图集合 bms_attnum,其中包含了需要进行分析的所有列attnum

  函数源码如下:(路径:src/common/backend/utils/adt/extended_statistics.cpp

/*
 * es_get_attnums_to_analyze
 *     get all attnums to be analyzed, including single column stats and multi-column stats
 *
 * @param (in) vacattrstats:
 *     the VacAttrStats array
 * @param (in) vacattrstats_size:
 *     the size of the VacAttrStats array
 *
 * @return:
 *     a Bitmapset including all columns' attnum
 */
Bitmapset* es_get_attnums_to_analyze(VacAttrStats** vacattrstats, int vacattrstats_size)
{
    Bitmapset* bms_attnum = NULL;
    for (int i = 0; i < vacattrstats_size; ++i) {
        for (unsigned int j = 0; j < vacattrstats[i]->num_attrs; ++j) {
            int2 attnum = vacattrstats[i]->attrs[j]->attnum;
            bms_attnum = bms_add_member(bms_attnum, attnum);
        }
    }
    return bms_attnum;
}

CStoreRelGetCUNumByNow 函数

  CStoreRelGetCUNumByNow 函数用于获取一个列存储Column Store在当前时刻的压缩单元CU)数量

  1. 参数:
  • cstoreScanState列存储扫描描述符,包含了对列存储表的扫描状态信息
  1. 返回值:
  • uint32:表示当前时刻列存储表的压缩单元数量
  1. 函数逻辑:
  • 获取与列存储表关联的元组描述符TupleDesc)和关联Relation)。
  • 打开与列存储表关联的 CUDesc压缩单元描述)关系和其索引
    (1)cudesc_relCUDesc 表的关系
    (2)cudesc_tupdescCUDesc 表的元组描述符
    (3)idx_relCUDesc 表的索引关系
  • 获取列存储表的第一个属性的属性号 attid
    (1)如果第一个属性已经被删除(attisdropped 为真),则获取列存储表中第一个未被删除属性的属性号(fstColIdx 指示第一个未被删除属性的索引)。
  • 初始化用于检索 CUDesc 表的索引扫描键(ScanKey),以便按照属性号(col_id)查找对应的压缩单元描述。
  • 开始有序系统表扫描(SysScanDesc),以获取与指定属性号匹配的压缩单元描述
  • 初始化一个变量 max_cuid,用于存储找到的最大压缩单元编号。将其初始化为 FirstCUID 的值。
  • 通过反向扫描(BackwardScanDirection)系统表,查找匹配的压缩单元描述。如果找到匹配的描述,则将其压缩单元编号存储在 max_cuid 中。
  • 结束有序系统表扫描
  • 关闭 CUDesc 表的索引和关系。
  • 返回压缩单元数量,计算方式为 max_cuid - FirstCUID,即找到的最大压缩单元编号减去起始压缩单元编号 FirstCUID

  函数源码如下:(路径:src/gausskernel/storage/cstore/cstore_am.cpp

// CStoreRelGetCUNumByNow
// 获取当前时刻关系的CU(压缩单元)数量

uint32 CStoreRelGetCUNumByNow(CStoreScanDesc cstoreScanState)
{
    ScanKeyData key;
    HeapTuple tup;
    bool isnull = false;
    Relation relation = cstoreScanState->m_CStore->m_relation;

    /*
     * 打开CUDesc关系和其索引。
     * CUDesc关系存储了列存储关系中每个列的CU(压缩单元)的描述信息,
     * 索引用于快速检索特定列的CU信息。
     */
    Relation cudesc_rel = heap_open(relation->rd_rel->relcudescrelid, AccessShareLock);
    TupleDesc cudesc_tupdesc = cudesc_rel->rd_att;
    Relation idx_rel = index_open(cudesc_rel->rd_rel->relcudescidx, AccessShareLock);

    int attid = relation->rd_att->attrs[0]->attnum;

    // 如果列已删除(attisdropped),则选择关系的第一个非删除列。
    if (relation->rd_att->attrs[0]->attisdropped) {
        int fstColIdx = CStoreGetfstColIdx(relation);
        attid = relation->rd_att->attrs[fstColIdx]->attnum;
    }

    /* 设置扫描键以通过列ID从索引中获取数据。 */
    ScanKeyInit(&key, (AttrNumber)CUDescColIDAttr, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum(attid));

    // 开始有序扫描CUDesc关系以获取与列相关的最后一个CU描述。
    SysScanDesc cudesc_scan = systable_beginscan_ordered(cudesc_rel, idx_rel, SnapshotNow, 1, &key);

    uint32 max_cuid = FirstCUID;

    /*
     * 优化以获取列的最后一个CU(压缩单元)描述。
     * 使用BackwardScanDirection扫描以获取最新的CU描述信息。
     */
    if ((tup = systable_getnext_ordered(cudesc_scan, BackwardScanDirection)) != NULL) {
        max_cuid = DatumGetUInt32(fastgetattr(tup, CUDescCUIDAttr, cudesc_tupdesc, &isnull));
    }
    systable_endscan_ordered(cudesc_scan);
    index_close(idx_rel, AccessShareLock);
    heap_close(cudesc_rel, AccessShareLock);

    // 返回最后一个CU的ID,并将其与FirstCUID相减以获得CU的数量。
    return (max_cuid - FirstCUID);
}

  该函数通常在列存储数据库的查询优化和统计信息收集过程中使用,以了解关系中当前 CU 的数量。

CStore::GetLivedRowNumbers 函数

  CStore::GetLivedRowNumbers 函数的作用是计算 CStore 存储引擎中指定关系的存活(未删除)行数,并提供了总删除行数。这个函数通过遍历列存储单元CU)的描述信息来实现这一目标。

代码的主要步骤如下:

  1. 初始化变量 rowNumbers0,该变量用于存储存活行数
  2. 初始化变量 * totaldeadrows0,该变量用于存储已删除行数
  3. 使用 LoadCUDescCtl 对象 loadInfo 初始化,该对象用于加载CU(压缩单元)的描述信息。
  4. 进入循环,通过 GetCURowCount 函数获取CU的行数信息,并存储在 loadInfo 中。
  5. 遍历每个CU的描述信息,获取其行数以及检查是否存在已删除的行。
  6. 如果存在已删除的行,通过计算位掩码中设置的位数来更新存活行数和已删除行数。
  7. 继续加载下一个CU的描述信息,直到所有CU都被处理。
  8. 销毁 loadInfo 对象,释放相关资源。
  9. 返回存活行数

  其函数源码如下:(路径:src/gausskernel/storage/cstore/cstore_am.cpp

/*
 * 获取CStore关系的存活行数。
 * 该函数计算CStore关系中存活(未删除)行的数量,
 * 并提供总删除行数。
 *
 * @param (out) totaldeadrows:
 *     指向存储总删除行数的变量的指针。
 *
 * @return:
 *     CStore关系中存活(未删除)行的数量。
 */
int64 CStore::GetLivedRowNumbers(int64* totaldeadrows)
{
    int64 rowNumbers = 0;  // 初始化变量以存储存活行数。
    LoadCUDescCtl loadInfo(m_startCUID);  // 创建用于加载CU描述符的控制对象。

    *totaldeadrows = 0;  // 将总删除行数初始化为零。

    // 遍历CU描述符。
    while (GetCURowCount(m_firstColIdx, &loadInfo, m_snapshot)) {
        CUDesc* cuDescArray = loadInfo.cuDescArray;

        // 遍历当前加载的CU描述符数组。
        for (uint32 i = 0; i < loadInfo.curLoadNum; ++i) {
            GetCUDeleteMaskIfNeed(cuDescArray[i].cu_id, m_snapshot);

            // 增加存活行数。
            rowNumbers += cuDescArray[i].row_count;

            if (m_hasDeadRow) {
                int nBytes = (cuDescArray[i].row_count + 7) / 8;

                // 遍历位图字节,计算总删除行数。
                for (int j = 0; j < nBytes; ++j) {
                    *totaldeadrows += NumberOfBit1Set[m_cuDelMask[j]];
                    rowNumbers -= NumberOfBit1Set[m_cuDelMask[j]];
                }
            }
        }
    }
    loadInfo.Destroy();

InitGetValFunc 函数

  InitGetValFunc 函数的主要作用是根据数据类型的字节长度 attlen 初始化一个函数指针数组 getValFuncPtr用于获取不同数据类型的值。这个函数在分析过程中非常有用,因为它允许针对不同的数据类型选择正确的函数来获取列数据,以确保数据的正确性高效性

具体来说,这个函数的作用如下:

  1. 根据传入的 attlen 参数,确定数据类型的字节长度,以便后续选择合适的函数来处理数据。
  2. 根据数据类型的字节长度,为该列初始化两个函数指针,一个用于处理带符号的数据,另一个用于处理无符号的数据。这些函数指针会存储在 getValFuncPtr 数组中的特定列中。
  3. 这些函数指针在分析期间用于获取列数据根据列的数据类型和是否带符号,选择适当的函数指针,以确保正确地获取数据值。
  4. 如果传入的 attlen 值不匹配任何已知的数据类型,函数会引发错误,报告不支持的数据类型。
    总结:InitGetValFunc 函数为分析过程提供了必要的工具,以根据不同的数据类型获取正确的列数据,确保了数据分析的准确性和效率

  其函数源码如下:(路径:src/gausskernel/optimizer/commands/analyze.cpp

void InitGetValFunc(int attlen, GetValFunc* getValFuncPtr, int col)
{
    switch (attlen) {
        case sizeof(uint8): {
            getValFuncPtr[col][0] = &GetValue<1, false>;
            getValFuncPtr[col][1] = &GetValue<1, true>;
            break;
        }
        case sizeof(uint16): {
            getValFuncPtr[col][0] = &GetValue<2, false>;
            getValFuncPtr[col][1] = &GetValue<2, true>;
            break;
        }
        case sizeof(uint32): {
            getValFuncPtr[col][0] = &GetValue<4, false>;
            getValFuncPtr[col][1] = &GetValue<4, true>;
            break;
        }
        case sizeof(uint64): {
            getValFuncPtr[col][0] = &GetValue<8, false>;
            getValFuncPtr[col][1] = &GetValue<8, true>;
            break;
        }
        case 12: {
            getValFuncPtr[col][0] = &GetValue<12, false>;
            getValFuncPtr[col][1] = &GetValue<12, true>;
            break;
        }
        case 16: {
            getValFuncPtr[col][0] = &GetValue<16, false>;
            getValFuncPtr[col][1] = &GetValue<16, true>;
            break;
        }
        case -1: {
            getValFuncPtr[col][0] = &GetValue<-1, false>;
            getValFuncPtr[col][1] = &GetValue<-1, true>;
            break;
        }
        case -2: {
            getValFuncPtr[col][0] = &GetValue<-2, false>;
            getValFuncPtr[col][1] = &GetValue<-2, true>;
            break;
        }
        default:
            ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("unsupported datatype")));
    }
}

CStoreGetfstColIdx 函数

  CStoreGetfstColIdx 函数的主要功能是遍历给定关系 rel 的所有列,并找到第一个未被删除(未被标记为 dropped)的列,然后返回该列的索引(列号)。这个函数通常用于在处理表或关系的列时,需要忽略已删除的列,从而准确地获取第一个有效的列。如果没有未被删除的列,它将返回 0,表示第一列未被删除。其函数源码如下:(路径:src/gausskernel/storage/cstore/cstore_am.cpp

/*
 * @Description: get the first colum index that is not dropped
 * @Param[IN] rel: the target relation
 * @Return: the first column index that is not dropped
 * @See also:
 */
int CStoreGetfstColIdx(Relation rel)
{
    for (int i = 0; i < rel->rd_att->natts; i++) {
        if (!rel->rd_att->attrs[i]->attisdropped)
            return i;
    }
    return 0;
}

CStore::GetCUDesc 函数

  CStore::GetCUDesc 函数的作用是根据给定的列号col)和 CU IDcuid)获取列的 CUDesc(列存储单元描述)。它主要完成以下任务:

  1. 初始化相关变量和上下文。
  2. 打开 CUDesc 表和其索引
  3. 设置用于索引扫描扫描键
  4. 执行索引扫描,找到匹配的 CUDesc 记录。
  5. 解析 CUDesc 记录,提取其中的信息并存储到 cuDescPtr 结构中。
  6. 关闭相关的数据库对象,释放资源
  7. 返回是否找到匹配的 CUDesc 记录。如果找到,返回 true;否则,返回 false

  其函数源码如下:(路径:src/gausskernel/storage/cstore/cstore_am.cpp

/*
 * Get CUDesc of column according to cuid.
 * 根据 cuid 获取列的 CUDesc(列存储单元描述)。
 */
bool CStore::GetCUDesc(_in_ int col, _in_ uint32 cuid, _out_ CUDesc* cuDescPtr, _in_ Snapshot snapShot)
{
    ScanKeyData key[2];
    HeapTuple tup;
    bool found = false;
    errno_t rc = EOK;
    Assert(col >= 0);

    // 当切换到下一批 cudesc 数据时,我们将重置 m_perScanMemCnxt。
    // 因此,仅在此批次中使用的空间应由 m_perScanMemCnxt 管理。
    AutoContextSwitch newMemCnxt(m_perScanMemCnxt);

    /*
     * 打开 CUDesc 关系和其索引
     */
    Relation cudesc_rel = heap_open(m_relation->rd_rel->relcudescrelid, AccessShareLock);
    TupleDesc cudesc_tupdesc = cudesc_rel->rd_att;
    Relation idx_rel = index_open(cudesc_rel->rd_rel->relcudescidx, AccessShareLock);
    bool isFixedLen = m_relation->rd_att->attrs[col]->attlen > 0 ? true : false;
    // 转换逻辑 ID 为属性的物理 ID
    int attid = m_relation->rd_att->attrs[col]->attnum;

    /*
     * 设置扫描键以通过 attid 从索引中获取。
     */
    ScanKeyInit(&key[0], (AttrNumber)CUDescColIDAttr, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum(attid));

    ScanKeyInit(&key[1], (AttrNumber)CUDescCUIDAttr, BTEqualStrategyNumber, F_OIDEQ, UInt32GetDatum(cuid));

    snapShot = (snapShot == NULL) ? GetActiveSnapshot() : snapShot;
    Assert(snapShot != NULL);

    SysScanDesc cudesc_scan = systable_beginscan_ordered(cudesc_rel, idx_rel, snapShot, 2, key);
    // 仅循环一次
    while ((tup = systable_getnext_ordered(cudesc_scan, ForwardScanDirection)) != NULL) {
        Datum values[CUDescCUExtraAttr] = {0};
        bool isnull[CUDescCUExtraAttr] = {0};
        char* valPtr = NULL;

        heap_deform_tuple(tup, cudesc_tupdesc, values, isnull);

        uint32 cu_id = DatumGetUInt32(values[CUDescCUIDAttr - 1]);
        Assert(!isnull[CUDescCUIDAttr - 1] && cu_id == cuid && found == false);

        cuDescPtr->xmin = HeapTupleGetRawXmin(tup);

        cuDescPtr->cu_id = cu_id;

        // 将最小值放入 cudesc->cu_min
        if (!isnull[CUDescMinAttr - 1]) {
            char* minPtr = cuDescPtr->cu_min;
            char len_1 = MIN_MAX_LEN;
            valPtr = DatumGetPointer(values[CUDescMinAttr - 1]);
            if (!isFixedLen) {
                *minPtr = (char)VARSIZE_ANY_EXHDR(valPtr);
                minPtr = minPtr + 1;
                len_1 -= 1;
            }
            rc = memcpy_s(minPtr, len_1, VARDATA_ANY(valPtr), VARSIZE_ANY_EXHDR(valPtr));
            securec_check(rc, "", "");
        }
        // 将最大值放入 cudesc->cu_max
        if (!isnull[CUDescMaxAttr - 1]) {
            char* maxPtr = cuDescPtr->cu_max;
            char len_2 = MIN_MAX_LEN;
            valPtr = DatumGetPointer(values[CUDescMaxAttr - 1]);
            if (!isFixedLen) {
                *maxPtr = VARSIZE_ANY_EXHDR(valPtr);
                maxPtr = maxPtr + 1;
                len_2 -= 1;
            }
            rc = memcpy_s(maxPtr, len_2, VARDATA_ANY(valPtr), VARSIZE_ANY_EXHDR(valPtr));
            securec_check(rc, "", "");
        }

        cuDescPtr->row_count = DatumGetInt32(values[CUDescRowCountAttr - 1]);
        Assert(!isnull[CUDescRowCountAttr - 1]);

        // 将 CUMode 放入 cudesc->cumode
        cuDescPtr->cu_mode = DatumGetInt32(values[CUDescCUModeAttr - 1]);
        Assert(!isnull[CUDescCUModeAttr - 1]);

        // 将 cusize 放入 cudesc->cu_size
        cuDescPtr->cu_size = DatumGetInt32(values[CUDescSizeAttr - 1]);
        Assert(!isnull[CUDescSizeAttr - 1]);

        // 将 CUPointer 放入 cudesc->cuPointer
        char* cu_ptr = DatumGetPointer(values[CUDescCUPointerAttr - 1]);
        Assert(!isnull[CUDescCUPointerAttr - 1] && cu_ptr);
        rc = memcpy_s(&cuDescPtr->cu_pointer, sizeof(CUPointer), VARDATA_ANY(cu_ptr), sizeof(CUPointer));
        securec_check(rc, "", "");
        Assert(VARSIZE_ANY_EXHDR(cu_ptr) == sizeof(CUPointer));

        cuDescPtr->magic = DatumGetUInt32(values[CUDescCUMagicAttr - 1]);
        Assert(!isnull[CUDescCUMagicAttr - 1]);
        found = true;
    }
    systable_endscan_ordered(cudesc_scan);
    index_close(idx_rel, AccessShareLock);
    heap_close(cudesc_rel, AccessShareLock);

    return found;
}

CStore::IsTheWholeCuDeleted 函数

  CStore::IsTheWholeCuDeleted 函数的作用是根据给定的 cuid(列存储单元 ID)加载相应的删除掩码,以标识是否存在已删除的行。它完成以下任务:

  1. 初始化相关变量。
  2. 如果已加载相同的删除掩码,直接返回,无需再次加载。
  3. 在进行扫描之前,切换到新的内存上下文以管理分配的内存。
  4. 打开 CUDesc 关系和其索引,以准备执行索引扫描。
  5. 设置用于索引扫描的扫描键。
  6. 如果未提供快照,则使用当前活动快照。
  7. 开始按顺序扫描 CUDesc 关系,查找匹配的记录。
  8. 如果找到匹配的记录,则提取 CUPointer(列存储单元指针)并相应地处理删除掩码。
  9. 结束索引扫描并关闭相关的数据库对象。
  10. 处理异常情况

  函数源码如下:(路径:src/gausskernel/storage/cstore/cstore_am.cpp

ull = false;
errno_t rc = EOK;
bool found = false;

// 如果删除掩码已加载,无需再次加载,直接返回
if (m_delMaskCUId == cuid)
    return;

// 当切换到下一批 cudesc 数据时,我们将重置 m_perScanMemCnxt。
// 因此,仅在此批次中使用的空间应由 m_perScanMemCnxt 管理。
AutoContextSwitch newMemCnxt(m_perScanMemCnxt);

// 打开 CUDesc 关系和其索引
Relation cudesc_rel = heap_open(m_relation->rd_rel->relcudescrelid, AccessShareLock);
TupleDesc cudesc_tupdesc = cudesc_rel->rd_att;
Relation idx_rel = index_open(cudesc_rel->rd_rel->relcudescidx, AccessShareLock);

// 设置扫描键以通过 attid 从索引中获取。
ScanKeyInit(&key[0], (AttrNumber)CUDescColIDAttr, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum(VitrualDelColID));

ScanKeyInit(&key[1], (AttrNumber)CUDescCUIDAttr, BTEqualStrategyNumber, F_OIDEQ, UInt32GetDatum(cuid));

// 如果未提供快照,则使用当前活动快照
snapShot = (snapShot == NULL) ? GetActiveSnapshot() : snapShot;
Assert(snapShot != NULL);

// 开始按顺序扫描 cudesc 关系
SysScanDesc cudesc_scan = systable_beginscan_ordered(cudesc_rel, idx_rel, snapShot, 2, key);

if ((tup = systable_getnext_ordered(cudesc_scan, ForwardScanDirection)) != NULL) {
    // 将 CUPointer 放入 cudesc->cuPointer
    Datum v = fastgetattr(tup, CUDescCUPointerAttr, cudesc_tupdesc, &isnull);
    if (isnull)
        m_hasDeadRow = false;
    else {
        m_hasDeadRow = true;
        int8* bitmap = (int8*)PG_DETOAST_DATUM(DatumGetPointer(v));
        rc = memcpy_s(m_cuDelMask, MaxDelBitmapSize, VARDATA_ANY(bitmap), VARSIZE_ANY_EXHDR(bitmap));
        securec_check(rc, "", "");

        // 因为可能创建了新的内存,所以我们必须检查并及时释放。
        if ((Pointer)bitmap != DatumGetPointer(v)) {
            pfree_ext(bitmap);
        }
    }

    found = true;
}

systable_endscan_ordered(cudesc_scan);
index_close(idx_rel, AccessShareLock);
heap_close(cudesc_rel, AccessShareLock);

// 如果未找到对应的删除掩码,则处理异常情况
if (!found) {
    TransactionId currGlobalXmin = pg_atomic_read_u64(&t_thrd.xact_cxt.ShmemVariableCache->recentGlobalXmin);
    Assert(snapShot->xmin > 0);
    if (TransactionIdPrecedes(snapShot->xmin, currGlobalXmin))
        ereport(ERROR,
                (errcode(ERRCODE_SNAPSHOT_INVALID),
                 (errmsg("Snapshot too old."),
                  errdetail("Could not get the old version of CUDeleteBitmap, RecentGlobalXmin: %lu, "
                            "snapShot->xmin: %lu, snapShot->xmax: %lu",
                            currGlobalXmin,
                            snapShot->xmin,
                            snapShot->xmax),
                  errhint("This is a safe error report, will not impact data consistency, retry your query if "
                          "needed."))));
    else {
        if (m_useBtreeIndex)
            m_delMaskCUId = InValidCUID;
        else {
            // 处理异常情况:CU 删除位图丢失
            ereport(PANIC,
                    (errmsg("CU Delete bitmap is missing."),
                     errdetail("There might be some issue about cu %u delete bitmap, Please contact HW engineers "
                               "for support.",
                               cuid)));
        }
    }
} else {
    // 记录已加载的删除掩码的 cuid
    m_delMaskCUId = cuid;
}

return;
}

CStore::IsTheWholeCuDeleted 函数

   CStore::IsTheWholeCuDeleted 函数的作用是 判断指定的列存储单元(CU)是否已经完全删除。函数接受一个参数 rowsInCu,表示列存储单元中的行数。

函数内部的逻辑如下:

  1. 首先,它检查成员变量 m_hasDeadRow 是否为 true,这个变量表示当前列存储中是否包含已删除的行。如果没有已删除的行,那么整个列存储单元就不可能被删除。
  2. 如果 m_hasDeadRowtrue,则调用 IsTheWholeCuDeleted 函数来进一步检查列存储单元的删除状态。它将 m_cuDelMask删除掩码)和 rowsInCu 作为参数传递给 IsTheWholeCuDeleted 函数,以确定列存储单元是否已完全删除

   其中,函数 IsTheWholeCuDeleted 被定义为两个版本,一个版本接受 int 类型的参数,另一个版本接受 char* 类型的参数。目的是优化性能,当没有已删除的行时,不需要执行更复杂的检查,直接返回结果。只有在存在已删除的行时,才需要进一步检查删除状态。
   函数源码如下:(路径:src/gausskernel/storage/cstore/cstore_am.cpp

bool CStore::IsTheWholeCuDeleted(int rowsInCu)
{
    return m_hasDeadRow && IsTheWholeCuDeleted((char*)m_cuDelMask, rowsInCu);
}
/**
 * @Description: 检查该CU内的所有元组是否已删除。
 *               如果是,则返回true;否则返回false。
 * @param rowsInCu: CU内包含的元组数量
 * @param delBitmapPtr: 删除位图
 * @return: 如果该位图内的所有元组都已删除,则返回true。
 *          如果有任何一个元组是活跃的,则返回false。
 * @See also:
 */
bool CStore::IsTheWholeCuDeleted(char* delBitmapPtr, int rowsInCu)
{
    // 预先计算用于快速比较的映射数组
    static const uint8 map[] = {0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF};
    unsigned int numUint64 = 0;
    unsigned int numUint8 = 0;
    unsigned int mapIdx = 0;

    /*
     * numUint64 表示当值的数量为 rowsInCu 时,使用多少个 Uint64 数据;
     * 这等于 (rowsInCu/64),因为 Uint64 可以保存 64 位。
     * numUint8 表示使用多少个 Uint8 数据,不包括 (numUint64 * 64) 个值;
     * 这等于 ((rowsInCu - numUint64 * 64) / 8)。同时我们排除了最后半字节。
     */
    compute_factors_of_n((unsigned int)rowsInCu, numUint64, numUint8, mapIdx);

    /* 通过将 *delBitmapPtr* 视为 uint64 数组来进行快速比较。*/
    uint64* uint64Item = (uint64*)delBitmapPtr;
    for (unsigned int i = 0; i < numUint64; ++i) {
        if (*uint64Item != 0xFFFFFFFFFFFFFFFF) {
            return false;
        }
        ++uint64Item;
    }

    /* 将剩余部分视为 char 数组来进行比较。*/
    uint8* uint8Item = (uint8*)uint64Item;
    for (unsigned int i = 0; i < numUint8; ++i) {
        if (*uint8Item != 0xFF) {
            return false;
        }
        ++uint8Item;
    }

    /*
     * 如果 (rowsInCu != 8*N),则必须特殊处理最后一个字节。
     * 我们将使用 *map[]* 直接进行快速比较。
     */
    if (mapIdx != 0) {
        return (*uint8Item == map[mapIdx]);
    }

    /* 没问题,整个 CU 已被删除。 */
    return true;
}

CStore::CudescTupGetMinMaxDatum 函数

  CStore::CudescTupGetMinMaxDatum 函数的作用是从 CUDesc 结构中提取并返回最小或最大值的 Datum 表示。该函数针对不同的列属性信息pColAttr)以及最小或最大值标志min),处理不同数据类型和大小的数据,并返回一个 Datum 表示。函数还通过 shouldFree 参数返回一个布尔值,指示是否应该释放提取的数据。函数源码如下:(路径:src/gausskernel/storage/cstore/cstore_am.cpp

/**
 * @Description: 从CUDesc结构中提取并返回最小或最大值的Datum表示。
 *               该函数根据提供的列属性信息,处理不同的数据类型和大小。
 * @param pCudesc: CUDesc结构指针,包含了最小和最大值
 * @param pColAttr: 列属性的指针,描述了列的数据类型和大小
 * @param min: 为true表示提取最小值,为false表示提取最大值
 * @param shouldFree: 返回一个布尔值,指示是否应该释放提取的数据
 * @return: 表示最小或最大值的Datum
 * @See also:
 */
Datum CStore::CudescTupGetMinMaxDatum(
    _in_ CUDesc* pCudesc, _in_ Form_pg_attribute pColAttr, _in_ bool min, _out_ bool* shouldFree)
{
    // 确保CUDesc结构表示了相同的值(即min和max相同)
    Assert(pCudesc->IsSameValCU());
    *shouldFree = false; // 默认情况下不需要释放数据

    char* value = NULL;
    char* dataPtr = min ? pCudesc->cu_min : pCudesc->cu_max; // 根据需要选择最小或最大值的指针
    errno_t rc = EOK;

    if (pColAttr->attbyval) {
        // 情况1:attlen > 0 && attlen <= sizeof(Datum)
        // 数据可以直接按值传递
        return (*(Datum*)dataPtr);
    }

    *shouldFree = true; // 数据需要释放

    if (pColAttr->attlen > (int)sizeof(Datum)) {
        // 情况2:attlen > sizeof(Datum) && attlen <= MIN_MAX_LEN
        Assert(pColAttr->attlen <= MIN_MAX_LEN);
        value = (char*)palloc(pColAttr->attlen); // 为数据分配内存
        rc = memcpy_s(value, pColAttr->attlen, dataPtr, pColAttr->attlen); // 复制数据
        securec_check(rc, "", "");
    } else if (pColAttr->attlen == -1) {
        // 情况3:attlen == -1,包括空字符串(非空字符串)
        Assert((int)dataPtr[0] >= 0 && (int)dataPtr[0] < MIN_MAX_LEN);
        value = (char*)palloc(dataPtr[0] + VARHDRSZ_SHORT); // 为字符串数据分配内存
        SET_VARSIZE_SHORT(value, dataPtr[0] + VARHDRSZ_SHORT); // 设置字符串头部大小
        if (dataPtr[0] > 0) {
            rc = memcpy_s(value + VARHDRSZ_SHORT, dataPtr[0], dataPtr + 1, dataPtr[0]); // 复制字符串数据
            securec_check(rc, "", "");
        }
    } else {
        // 情况4:attlen == -2,包括非空字符串
        Assert((int)dataPtr[0] > 0 && (int)dataPtr[0] < MIN_MAX_LEN);
        Assert(dataPtr[(int)dataPtr[0]] == '\0');
        value = (char*)palloc(dataPtr[0]); // 为字符串数据分配内存
        rc = memcpy_s(value, dataPtr[0], dataPtr + 1, dataPtr[0]); // 复制字符串数据
        securec_check(rc, "", "");
    }

    return PointerGetDatum(value); // 返回表示最小或最大值的Datum
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/987880.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习Bootstrap 5的第七天

目录 徽章 徽章 实例 上下文徽章 实例 胶囊徽章 实例 元素内的徽章 实例 进度条 基础进度条 实例 进度条高度 实例 彩色进度条 实例 条纹进度条 实例 动画进度条 实例 混合色彩进度条 实例 徽章 徽章 在 Bootstrap 中&#xff0c;徽章&#xff08;Badg…

【动手学深度学习】--文本预处理

文章目录 文本预处理1.读取数据集2.词元化3.词表4.整合所有功能 文本预处理 学习视频&#xff1a;文本预处理【动手学深度学习v2】 官方笔记&#xff1a;文本预处理 对于序列数据处理问题&#xff0c;在【序列模型】中评估了所需的统计工具和预测时面临的挑战&#xff0c;这…

2023年8月| 红帽RHCE考试战报-微思红帽官方授权培训中心

2023.8.15 新出炉一波红帽考试战报 恭喜微思10位学员顺利PASS红帽认证考试 通过RHCE认证 不仅从专业技术上证明了你的能力 在职场上更是一块进入Linux行业的“敲门砖” 让你在职场竞争中更具竞争力 微思红帽官方授权培训中心--全国直播&#xff0c;就近安排考试&#xff…

【技巧】安装 win11 必须联网?无法跳过?

安装 Win11 时自动检查更新或者让连接网络&#xff0c;没有提供取消按钮&#xff0c;之前有【我没有 Internet 连接】选项。 在这个界面按 ShiftF10 打开命令提示符 输入OOBE\BYPASSNRO 按回车。 回车之后之后系统会重新启动&#xff0c;此时发现下一步的左侧出现了熟悉的【我…

Matlab论文插图绘制模板第112期—带阴影标记的图

之前的文章中&#xff0c;分享了Matlab带线标记的图&#xff1a; 进一步&#xff0c;本期分享的是带阴影标记的图。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数据代码』已上传资源群中&#xff0c;加群的朋友请自行下载。有需要的朋友可以关注同名公号…

MCP2515调试心得

基于 STM32 芯片的 MCP2515 芯片调试心得 1. MCP2515 芯片解析1.1 外部时钟源1.2 可采用连续传输提高效率发送数据时&#xff0c;使用 TX0 为例&#xff1a; 1.3 关于 MASK 和 Filter 的注意事项1.3.1 Filter 的注意事项1.3.2 MASK 设置的一些问题 2. STM32 硬件 SPI 问题 1. M…

外滩大会发布银行数字化5大趋势:随身银行、AI风控、数字员工、边缘物联与云原生

通用人工智能风起云涌&#xff0c;金融行业将如何应对&#xff1f; 9月8日&#xff0c;由中国银行业协会指导&#xff0c;网商银行承办的外滩大会银行业数字化论坛上&#xff0c;IDC中国副总裁兼首席分析师武连峰发布了《银行数字科技五大趋势》&#xff1a;随身银行、AI风控、…

ULN2003 芯片

芯片介绍&#xff1a; ULN2003 是高耐压、大电流达林顿陈列&#xff0c;由七个硅 NPN 达林顿管组成。 达林顿管并联可以承受更大的电流。 此电路主要应用于继电器驱动器&#xff0c;字锤驱动器&#xff0c;灯驱动器&#xff0c;显示驱动器&#xff08;LED 气 体放电&#…

Apache Tomcat 漏洞复现

文章目录 Apache Tomcat 漏洞复现1. Tomcat7 弱密码和后端 Getshell 漏洞1.1 漏洞描述1.2 漏洞复现1.3 漏洞利用1.3.1 jsp小马1.3.2 jsp大马 2. Aapache Tomcat AJP任意文件读取/包含漏洞2.1 漏洞描述2.1 漏洞复现2.2 漏洞利用工具 3. 通过 PUT 方法的 Tomcat 任意写入文件漏洞…

10元/月?中国电信推出手机直连卫星功能,华为联合开启卫星之旅

2021年9月8日&#xff0c;华为Mate 60 Pro 系列手机首次推出“卫星语音通话”功能。此功能需与运营商合作&#xff0c;而中国电信率先推出了“手机直连卫星”服务。 中国电信的用户可以在自己的普通手机卡套餐基础上&#xff0c;加装直连卫星服务。此项服务的价格如下&#xff…

时序数据库 TimescaleDB 基础概念

时序数据在许多领域中具有广泛的应用&#xff0c;例如金融市场分析、气象预测、交通流量监测、生产过程监控等&#xff0c;时序数据通常是大规模的、高维度的、需要实时计算和分析&#xff0c;针对时序数据的特点与其所带来的挑战&#xff0c;针对时序数据处理所面临的挑战&…

1000元订金?华为折叠屏手机MateX5今日开始预订,售价尚未公布

华为最新款折叠屏手机Mate X5今日在华为商城开始预订&#xff0c;吸引了众多消费者的关注。预订时需交纳1000元的订金&#xff0c;而具体售价尚未公布。据华为商城配置表显示&#xff0c;Mate X5预计将搭载Mate 60系列同款麒麟9000S处理器&#xff0c;或可能搭载麒麟9100处理器…

vue3:4、组合式API-setup选项

setup每次都要return&#xff0c;好麻烦。怎么解决&#xff1f; 使用 <script setup> 语法糖&#xff08;底层帮你return了&#xff09; 写法如下

在线实时监测离子风机的功能

离子风机是一种能够通过释放大量负离子来净化空气并提供清新环境的设备。要实现联网实时在线监测离子风机&#xff0c;可以考虑以下几个步骤&#xff1a; 1. 设备接入互联网&#xff1a;离子风机需要具备网络连接功能&#xff0c;可以通过无线网络或者以太网接入路由器&#x…

优思学院|质量工程师和QA区别在哪?质量工程师有什么发展策略?

质量管理內容相当复杂&#xff0c;从供应商中选择SQE&#xff0c;入料检验的IQC&#xff0c;制程管控的IPQC&#xff0c;站在客户的立场&#xff0c;保证出货质量OQC&#xff0c;所以一般来说QC/QA人员必须管理从材料到出货的所有质量项目。 而质量工程师&#xff08;QE&#…

2023国赛数学建模C题模型代码

C题代码全部都完成了&#xff0c;可以看文末名片 我们先看C题的一个背景 在生鲜商超中,蔬菜类商品保鲜期短,且品相会随销售时间增加而变差。商超需要根据历史销售和需求每天进行补货。由于蔬菜品种众多、产地不同,补货时间在凌晨,商家须在不明确具体单品和价格的情况下进行补…

读书笔记:多Transformer的双向编码器表示法(Bert)-1

多Transformer的双向编码器表示法 Bidirectional Encoder Representations from Transformers&#xff0c;即Bert&#xff1b; 本笔记主要是对谷歌Bert架构的入门学习&#xff1a; 介绍Transformer架构&#xff0c;理解编码器和解码器的工作原理&#xff1b;掌握Bert模型架构…

Tableau自学四部曲_Part4:BI仪表盘搭建

文章目录 一、数据可视化原则1. 区分用户2. 主次分明、详略得当3. 真实准确4. 符合大众认知和审美习惯5. 适度原则6. 五秒原则6. 恰到好处的说明7. 少即是多8. 可视化案例 二、BI仪表盘搭建1. 仪表盘搭建原则2. 明确仪表盘主题3. 仪表盘主题拆解4. 开发设计工作表5. 构思仪表盘…

LQR 控制器

LQR&#xff08;Linear Quadratic Regulator&#xff09;控制器 LQR&#xff08;Linear Quadratic Regulator&#xff09;是一种经典的线性控制器设计方法&#xff0c;用于设计线性时不变系统的状态反馈控制器&#xff0c;以最小化系统性能指标&#xff0c;通常是二次代价函数…

软件测试/测试开发丨ChatGPT:带你进入智能对话的新时代

简介 人工智能时代来临 我们正处于AI的iPhone时刻。——黄仁勋&#xff08;英伟达CEO&#xff09; ChatGPT 好得有点可怕了&#xff0c;我们距离危险的强人工智能不远了。——马斯克&#xff08;Tesla/SpaceX/Twitter CEO&#xff09; 以上的内容说明我们现在正处于一个技术大…