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

列存储(analyze)

  • acquire_sample_rows 函数
    • RelationGetNumberOfBlocks 函数
    • BlockSampler_Init 函数
    • anl_init_selection_state 函数
    • BlockSampler_GetBlock 函数
    • ReadBufferExtended
    • PageGetMaxOffsetNumber 函数
    • HeapTupleSatisfiesVacuum 函数
    • heapCopyTuple 函数
      • heapCopyCompressedTuple 函数
      • heap_copytuple 函数
    • anl_get_next_S 函数

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

acquire_sample_rows 函数

  前两章(analyze(一)和 analyze(二))中我们分别介绍了 analyze 中所设计的一些函数,如 analyze_get_relationanalyze_rel_internal 函数分别负责打开指定的关系(表),以便进行分析操作执行实际的表分析操作,包括收集统计信息、更新系统表等做了足够的铺垫,我们在本章则来看看 analyze 功能中的 acquire_sample_rows 函数,其函数用于获取样本行集合,以便估算表的统计信息。
  其中,函数 acquire_sample_rows 的调用关系如下:do_analyze_rel -> get_target_rows -> acquire_sample_rows
  以上三个函数的关系描述如下:

  1. acquire_sample_rows:这是一个核心函数,用于获取样本行集合,并估算表的统计信息,包括总行数和死亡行数。它是分析操作的关键部分,用于收集关于表数据分布的信息。
  2. get_target_rows:这个函数用于获取目标行数(targrows,这个值是在分析操作中用于确定要采样的行数的一个关键参数。get_target_rows 函数通常会考虑表的大小和其他因素,以确定适当的 targrows 值。在内部,它可能会调用 acquire_sample_rows
  3. do_analyze_rel:这个函数是执行分析操作的入口点。它负责调用 get_target_rows 来获取目标行数,并接着调用 acquire_sample_rows 来收集样本行和估算表的统计信息。因此,do_analyze_rel 包含了对 get_target_rowsacquire_sample_rows 的调用,并协调它们的工作。

  总结:do_analyze_rel 是执行表分析操作的主函数,它通过调用 get_target_rows 获取目标行数,然后再调用 acquire_sample_rows 来收集样本行和估算统计信息。这三个函数共同协作以实现表的分析操作。

  acquire_sample_rows 函数是用于表抽样的一个重要函数。它的主要作用是在执行表分析(ANALYZE)操作时,从数据库表中抽取一部分数据行,以便估算表中的统计信息,从而帮助查询优化器生成更好的查询计划acquire_sample_rows 函数源码如下:(路径:src/gausskernel/optimizer/commands/analyze.cpp

template <bool estimate_table_rownum>
static int64 acquire_sample_rows(
    Relation onerel, int elevel, HeapTuple* rows, int64 targrows, double* totalrows, double* totaldeadrows)
{
    int64 numrows = 0;      /* # rows now in reservoir */  // 用于记录当前样本集合中的行数
    double samplerows = 0;  /* total # rows collected */   // 用于记录总共采集的行数
    double liverows = 0;    /* # live rows seen */         // 用于记录活跃的行数
    double deadrows = 0;    /* # dead rows seen */         // 用于记录已删除的行数
    double rowstoskip = -1; /* -1 means not set yet */     // 表示要跳过的行数,初始值为-1表示尚未设置
    BlockNumber totalblocks;  // 用于记录表的总块数
    TransactionId OldestXmin; // 用于记录最老的事务ID
    BlockSamplerData bs;      // 用于块抽样的数据结构
    double rstate;            // 用于伪随机数生成器的状态
    BlockNumber targblock = 0; // 目标块的编号
    BlockNumber sampleblock = 0; // 采样块的编号
    BlockNumber retrycount = 1; // 重试计数,初始值为1
    AnlPrefetch anlprefetch;   // 用于预取的数据结构
    int64 ori_targrows = targrows; // 原始目标行数,用于后续的估算
    anlprefetch.blocklist = NULL; // 预取块列表,初始为空
    bool isAnalyzing = true;    // 是否正在分析的标志,初始为true

    AssertEreport(targrows > 0, MOD_OPT, "Target row number must be greater than 0 when sampling.");

    totalblocks = RelationGetNumberOfBlocks(onerel); // 获取表的总块数

    /*
     * 在 "pretty analyze" 模式下,估算总行数以确保采样数据密度均匀
     */
    if (SUPPORT_PRETTY_ANALYZE && estimate_table_rownum) {
        /*
         * 如果 targrows 大于 MAX_ESTIMATE_SAMPLE_PAGES,则将 targrows 设置为 MAX_ESTIMATE_SAMPLE_PAGES,
         * 以确保采样的数据密度尽可能均匀。
         */
        if (targrows > MAX_ESTIMATE_SAMPLE_PAGES)
            targrows = MAX_ESTIMATE_SAMPLE_PAGES;

        /*
         * 如果表是分区表,则将 targrows 设置为 MAX_ESTIMATE_PART_SAMPLE_PAGES,
         * 以确保在每个分区中进行均匀的采样。
         */
        if (RelationIsPartition(onerel))
            targrows = MAX_ESTIMATE_PART_SAMPLE_PAGES;
    }

    /*
     * 获取 HeapTupleSatisfiesVacuum 需要的最旧事务ID(xmin)
     */
    OldestXmin = GetOldestXmin(onerel);

retry:
    /* 初始化块采样器数据结构,用于采样块号 */
    BlockSampler_Init(&bs, totalblocks, targrows);
    /* 初始化采样行的状态,用于采样行号 */
    rstate = anl_init_selection_state(targrows);

    /*
     * 在 ADIO 运行模式下执行以下代码块,用于数据预取
     */
    ADIO_RUN()
    {
        if (1 == retrycount) {
        	/*
             * 计算预取数量,取最小值,确保不超过缓冲区数量的四分之一
             */
            uint32 quantity =
                Min(u_sess->attr.attr_storage.prefetch_quantity, (g_instance.attr.attr_storage.NBuffers / 4));
            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(DEBUG2,
            (errmodule(MOD_ADIO),
                errmsg("analyze prefetch for %s prefetch quantity(%u)",
                    RelationGetRelationName(onerel),
                    anlprefetch.fetchlist1.size)));
    }
    /*
     * ADIO 运行模式结束
     */
    ADIO_END();

    /*
     * 当 BlockSampler_GetBlock 函数返回的块号不等于 InvalidBlockNumber 时,进入循环
     * BlockSampler_GetBlock 用于获取下一个要分析的块的块号
     */
    while (InvalidBlockNumber !=
           (targblock = BlockSampler_GetBlock<false>(onerel, &bs, &anlprefetch, 0, NULL, estimate_table_rownum))) {
        Buffer targbuffer;
        Page targpage;
        OffsetNumber targoffset, maxoffset;

        /*
         * 延迟清理点,用于检查是否需要延迟清理,以确保分析不会过于耗时
         */
        vacuum_delay_point();
        
        /*
         * 增加已经采样的块数
         */
        sampleblock++;


        /*
         * We must maintain a pin on the target page's buffer to ensure that
         * the maxoffset value stays good (else concurrent VACUUM might delete
         * tuples out from under us).  Hence, pin the page until we are done
         * looking at it.  We also choose to hold sharelock on the buffer
         * throughout --- we could release and re-acquire sharelock for each
         * tuple, but since we aren't doing much work per tuple, the extra
         * lock traffic is probably better avoided.
         */
        /*
         * 为了确保在查看目标页时 maxoffset 值保持正确,我们必须在目标页的缓冲区上保持引用锁定。
         * 因此,我们首先使用 ReadBufferExtended 函数获取目标块的缓冲区,
         * 然后使用 LockBuffer 函数对缓冲区进行共享锁定,以防止其他进程修改该块。
         * 接着,使用 BufferGetPage 函数将缓冲区转换为目标页的指针,以便我们可以查看页的内容。
         * 最后,使用 PageGetMaxOffsetNumber 函数获取目标页的最大偏移号码,以便后续处理元组。
         */
        targbuffer = ReadBufferExtended(onerel, MAIN_FORKNUM, targblock, RBM_NORMAL, u_sess->analyze_cxt.vac_strategy);
        LockBuffer(targbuffer, BUFFER_LOCK_SHARE);
        targpage = BufferGetPage(targbuffer);
        maxoffset = PageGetMaxOffsetNumber(targpage);

        /*
         * 遍历选定页上的所有元组,内部循环处理元组。
         */
        for (targoffset = FirstOffsetNumber; targoffset <= maxoffset; targoffset++) {
            ItemId itemid;
            HeapTupleData targtuple;
            bool sample_it = false;

            /*
             * 如果启用了工作负载控制 (ENABLE_WORKLOAD_CONTROL),则通过
             * IOSchedulerAndUpdate 函数进行IO调度以及记录IO操作。
             */
            if (ENABLE_WORKLOAD_CONTROL)
                IOSchedulerAndUpdate(IO_TYPE_READ, 10, IO_TYPE_ROW);

            targtuple.t_tableOid = InvalidOid;
            targtuple.t_bucketId = InvalidBktId;
            HeapTupleCopyBaseFromPage(&targtuple, targpage);

#ifdef PGXC
            targtuple.t_xc_node_id = InvalidOid;
#endif
            itemid = PageGetItemId(targpage, targoffset);
            /*
             * 忽略未使用和重定向行指针。已死的行指针应被视为已死,
             * 因为我们需要运行 VACUUM 来消除它们。
             * 需要注意的是,这一规则与 heap_page_prune() 函数统计行指针的方式一致。
             */
            if (!ItemIdIsNormal(itemid)) {
                if (ItemIdIsDead(itemid))
                    deadrows += 1;
                continue;
            }

            ItemPointerSet(&targtuple.t_self, targblock, targoffset);

            targtuple.t_tableOid = RelationGetRelid(onerel);
            targtuple.t_bucketId = RelationGetBktid(onerel);
            targtuple.t_data = (HeapTupleHeader)PageGetItem(targpage, itemid);
            targtuple.t_len = ItemIdGetLength(itemid);

            /*
             * 如果启用了调试选项(u_sess->attr.attr_storage.enable_debug_vacuum),
             * 则设置 t_thrd.utils_cxt.pRelatedRel 为当前的关系(onerel)。
             */
            if (u_sess->attr.attr_storage.enable_debug_vacuum)
                t_thrd.utils_cxt.pRelatedRel = onerel;

            switch (HeapTupleSatisfiesVacuum(&targtuple, OldestXmin, targbuffer, isAnalyzing)) {
			    case HEAPTUPLE_LIVE:
			        /*
			         * 当前元组是活跃的(live),将其标记为样本中的一部分,同时增加
			         * 活跃行数(liverows)的计数。
			         */
			        sample_it = true;
			        liverows += 1;
			        break;
			
			    case HEAPTUPLE_DEAD:
			    case HEAPTUPLE_RECENTLY_DEAD:
			        /*
			         * 当前元组已死亡或最近死亡,将其标记为死亡行(dead),
			         * 同时增加已死亡行数(deadrows)的计数。
			         */
			        deadrows += 1;
			        break;
			
			    case HEAPTUPLE_INSERT_IN_PROGRESS:
			        /*
			         * 正在插入新行的事务不计数。我们假定当插入事务提交或中止时,
			         * 会发送一个统计消息来增加适当的计数。这只有在我们完成分析表之后插入
			         * 事务才能正常工作;如果情况发生在不同的顺序,它的统计更新将被我们的
			         * 统计更新覆盖。然而,只有当其他事务运行足够长时间插入了很多元组时,
			         * 才会出现较大的错误,因此假设它将在我们之后完成是较安全的选择。
			         *
			         * 特殊情况是插入事务可能是我们自己的事务。在这种情况下,
			         * 我们应该计数并采样该行,以适应在一个事务中加载表并分析表的用户。
			         * (pgstat_report_analyze 必须调整我们发送给统计收集器的数字,以使其正常工作。)
			         */
			        if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(targpage, targtuple.t_data))) {
			            sample_it = true;
			            liverows += 1;
			        }
			        break;
			
			    case HEAPTUPLE_DELETE_IN_PROGRESS:
			        /*
			         * 我们将正在删除的行视为仍然活跃,使用与上述相同的原因;但我们不需要
			         * 包括它们在样本中。
			         *
			         * 但是,如果删除是由我们自己的事务执行的,那么我们必须将该行标记为死亡,
			         * 以使 pgstat_report_analyze 的统计调整正常工作。
			         * (注意:这在行在我们的事务中既插入又删除时可以正常工作。)
			         *
			         * 采用这些选择的净效果是,我们假设一个 IN_PROGRESS 事务还没有发生,
			         * 除非它是我们自己的事务,我们假设它已经发生。
			         *
			         * 这种方法确保我们在看到由并发事务更新的行的预映像和后映像时表现正常:
			         * 我们会采样预映像但不会采样后映像。如果并发事务从不提交,我们也会获得正常的结果。
			         */
			        if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmax(targpage, targtuple.t_data)))
			            deadrows += 1;
			        else {
			            sample_it = true;
			            liverows += 1;
			        }
			        break;
			
			    default:
			        /*
			         * 未预料到的 HeapTupleSatisfiesVacuum 结果,报告错误。
			         */
			        ereport(
			            ERROR, (errcode(ERRCODE_CASE_NOT_FOUND), errmsg("unexpected HeapTupleSatisfiesVacuum result")));
			        break;
			}


            if (sample_it) {
                /*
                 * The first targrows sample rows are simply copied into the
                 * reservoir. Then we start replacing tuples in the sample
                 * until we reach the end of the relation.	This algorithm is
                 * from Jeff Vitter's paper (see full citation below). It
                 * works by repeatedly computing the number of tuples to skip
                 * before selecting a tuple, which replaces a randomly chosen
                 * element of the reservoir (current set of tuples).  At all
                 * times the reservoir is a true random sample of the tuples
                 * we've passed over so far, so when we fall off the end of
                 * the relation we're done.
                 */
                /*
     			 * 如果需要采样当前的元组(sample_it 为 true),则执行以下操作:
     			 */
                if (numrows < targrows) {
			        /*
			         * 如果样本行数(numrows)还没有达到目标行数(targrows),则直接将
			         * 当前元组复制到样本中,并增加 numrows 计数。
			         */
                    if (!estimate_table_rownum)
                        rows[numrows++] = heapCopyTuple(&targtuple, onerel->rd_att, targpage);
                    else
                        numrows++;
                } else {
			        /*
			         * 如果样本行数已经达到目标行数,则采用 Vitter's 算法进行采样。
			         * 该算法通过重复计算在选择元组之前要跳过的元组数,然后用选定的元组替换
			         * 池中随机选择的元组。在所有时刻,池都是已经遍历的元组的真正随机样本,
			         * 因此当我们遍历完整个关系时,我们就完成了采样。
			         */
                    if (rowstoskip < 0)
                        rowstoskip = anl_get_next_S(samplerows, targrows, &rstate);

                    if (rowstoskip <= 0) {
			            /*
			             * 找到了适合采样的元组,因此保存它,替换随机选择的旧元组。
			             */
                        int64 k = (int64)(targrows * anl_random_fract());

                        AssertEreport(
                            k >= 0 && k < targrows, MOD_OPT, "Index number out of range when replacing tuples.");

                        if (!estimate_table_rownum) {
						    /*
						     * 如果不是在估算表的行数(`estimate_table_rownum` 为 false),则执行以下操作:
						     * 1. 释放旧的元组占用的内存(`tableam_tops_free_tuple` 函数)。
						     * 2. 用当前的目标元组(`&targtuple`)创建一个新的元组,并将其存储在样本数组(`rows`)的第 `k` 个位置。
						     * 注意:`heapCopyTuple` 用于创建一个与 `targtuple` 一样的新元组。
						     */
                            tableam_tops_free_tuple(rows[k]);
                            rows[k] = heapCopyTuple(&targtuple, onerel->rd_att, targpage);
                        }
                    }
				    /*
				     * 将 `rowstoskip` 减 1,表示下一个要跳过的元组数。
				     */
                    rowstoskip -= 1;
                }
				/*
				 * 增加采样的元组计数(`samplerows`)。
				 */
                samplerows += 1;
            }
        }

        if (u_sess->attr.attr_storage.enable_debug_vacuum)
            t_thrd.utils_cxt.pRelatedRel = NULL;

        /* Now release the lock and pin on the page */
		/*
		 * 释放对目标页面的锁和引用,以确保在后续迭代中可以再次访问页面。
		 * `UnlockReleaseBuffer` 函数执行这些操作。
		 */
        UnlockReleaseBuffer(targbuffer);

		/*
		 * 如果 estimate_table_rownum 为真且不支持 pretty analyze 模式,
 		 * 并且没有发现任何存活的行,则退出循环。
	 	 * 这意味着不再需要继续估算总行数,因为没有存活的行。
	  	 * 在这种情况下,总块数减一,并输出一条日志信息,指示哪个块没有存活的行。
	     */
		if (estimate_table_rownum && !SUPPORT_PRETTY_ANALYZE) {
		    if (liverows > 0)
		        break;
		
		    totalblocks--;
		    ereport(LOG,
		        (errmsg("ANALYZE INFO : estimate total rows of \"%s\" - no lived rows in blockno: %u",
		            RelationGetRelationName(onerel),
		            targblock)));
		}
    }

	if (estimate_table_rownum) {
	    if (liverows > 0) {
	        /* 如果采样到了存活的行,只需估算总存活元组数量 */
	        AssertEreport(0 < sampleblock && 0 < totalblocks, MOD_OPT, "This should not happend when liverows>0");
	        *totalrows = totalblocks * (liverows / sampleblock);
	    } else if (sampleblock >= totalblocks || retrycount > MAX_ESTIMATE_RETRY_TIMES) {
	        /*
	         * 如果所有元组都已死亡或者我们尝试次数太多(MAX_ESTIMATE_RETRY_TIMES + 1),则返回0
	         */
	        *totalrows = 0.0;
	    } else if (SUPPORT_PRETTY_ANALYZE) {
	        /*
	         * 如果我们尝试了 MAX_ESTIMATE_RETRY_TIMES 次,则按照普通分析样本采样元组
	         */
	        if (MAX_ESTIMATE_RETRY_TIMES == retrycount)
	            targrows = ori_targrows;
	
	        /*
	         * 除非以下情况,否则在 pretty analyze 模式下重试获取存活的行:
	         * 1. 获取到了存活元组(liverows > 0)
	         * 2. 重试次数已足够多(MAX_ESTIMATE_RETRY_TIMES)
	         */
	        retrycount++;
	        goto retry;
	    }

        ADIO_RUN()
        {
        	// 释放预取的块列表内存
            pfree_ext(anlprefetch.fetchlist1.blocklist);
            pfree_ext(anlprefetch.fetchlist2.blocklist);
        }
        ADIO_END();

        ereport(elevel,
            (errmsg("ANALYZE INFO : estimate total rows of \"%s\": scanned %u pages of "
                    "total %u pages with %u retry times, containing %.0f "
                    "live rows and %.0f dead rows,  estimated %.0f total rows",
                RelationGetRelationName(onerel),
                sampleblock,
                totalblocks,
                retrycount,
                liverows,
                deadrows,
                *totalrows)));

        *totaldeadrows = 0.0;

		/*
		 * 我们只计算存活行和死亡行的数量,从未采样元组来填充 rows[],
		 * 因此在这里返回0以避免不必要的内存释放
		 */
        return 0;
    }

	/*
	 * 如果我们没有找到与预期的元组数量一样多的元组,那么分析完成。
	 * 无需排序,因为它们已经按顺序排列。
	 *
	 * 否则,我们需要按位置(itempointer)对收集到的元组进行排序。
	 * 不值得担心元组已经按顺序排列的边界情况。
	 */
	if (numrows == targrows)
	    qsort((void*)rows, numrows, sizeof(HeapTuple), compare_rows);
	
	/*
	 * 估算关系中的总活行数和死行数,假设我们没有扫描的页的平均元组密度
	 * 与我们扫描的页相同。由于我们扫描的是关系页的随机样本,这应该是一个很好的假设。
	 */
	if (bs.m > 0) {
	    *totalrows = floor((liverows / bs.m) * totalblocks + 0.5);
	    *totaldeadrows = floor((deadrows / bs.m) * totalblocks + 0.5);
	} else {
	    *totalrows = 0.0;
	    *totaldeadrows = 0.0;
	}

    ADIO_RUN()
    {
    	// 释放抓取列表的内存
        pfree_ext(anlprefetch.fetchlist1.blocklist);
        pfree_ext(anlprefetch.fetchlist2.blocklist);
    }
    ADIO_END();

	/*
	 * 发出一些有趣的关系信息,仅在数据节点中
	 */
    if (IS_PGXC_DATANODE) {
        ereport(elevel,
            (errmsg("ANALYZE INFO : \"%s\": scanned %d of %u pages, "
                    "containing %.0f live rows and %.0f dead rows; "
                    "%ld rows in sample, %.0f estimated total rows",
                RelationGetRelationName(onerel),
                bs.m,
                totalblocks,
                liverows,
                deadrows,
                numrows,
                *totalrows)));
    }

    return numrows;
}

  以上函数是用于执行表分析(ANALYZE)操作的关键功能之一。ANALYZE操作的主要目的是为了帮助查询优化器更好地估算查询计划的成本,以提高查询性能。这个函数的具体功能是从指定的关系(表)中抽样一定数量的行,并估算表的总行数、存活行数和已删除行数。以下是 acquire_sample_rows 函数的主要步骤和功能:

  1. 初始化一些变量和数据结构,包括存储样本行的数组 rows,用于记录总共采集的行数活跃行数已删除行数块采样器等等。
  2. 获取表的总块数totalblocks)和最旧的事务IDOldestXmin),后者用于确定哪些行已被删除。
  3. 进入一个主要的循环,循环从表中不断抽取块(blocks),然后在每个块中遍历行(tuples)。
  4. 对于每个块,采用块抽样器(BlockSampler)来决定是否抽样该块。如果块被选中,则在该块中遍历行,并根据行的状态将它们标记为活跃已删除。同时,将标记为活跃的行添加到样本数组 rows 中。
  5. 样本数组 rows 达到目标行数targrows)时,采用随机采样算法Vitter’s算法)来维护样本集合,以确保样本具有均匀的分布。这些操作将有助于更好地估算表的统计信息。
  6. 如果启用了调试选项,则在处理每个块之前设置一个相关关系的标志。处理完块后,释放块的锁和引用。
  7. 样本抽取的过程中,如果发现所有的行都已被删除(liverows == 0),并且估算表行数(estimate_table_rownum)并且不支持"pretty analyze"模式(SUPPORT_PRETTY_ANALYZE),则提前退出循环。
  8. 根据不同的条件计算表的总行数totalrows)和总已删除行数totaldeadrows)。这些估算是通过对采样数据进行推断来完成的。
  9. 如果不需要估算表的行数(estimate_table_rownumfalse),则返回样本数组中的样本行数。
  10. 如果需要估算表的行数(estimate_table_rownumtrue),则根据不同的情况计算总行数。如果采样到了存活的行,直接估算总行数;如果所有元组都已死亡或尝试次数过多,则估算总行数为0;如果支持"pretty analyze"模式,可以选择在这些情况下重试获取存活的行。
  11. 释放相关的内存,并输出一些关于分析操作的日志信息。
  12. 如果是在数据节点上执行的操作(IS_PGXC_DATANODE),则输出关于分析结果的信息
  13. 最后,返回样本数组中的样本行数,或者0,具体取决于估算模式

  此函数的具体用途是为了创建一个有限的样本集合,以估算表的统计信息。这些统计信息对于查询优化非常重要,因为它们可以帮助优化器选择更有效的查询计划。该函数的核心是在表的随机样本上执行分析操作,以推断表的整体特征。这对于大型表格特别有用,因为它避免了对整个表的完全扫描,从而提高了分析的性能。
  本文所使用的案例为列存储(analyze)(一)中的案例,但是表 sales 需要创建为行存模式,本节先考虑行存模式。调试信息如下:
【 OpenGauss源码学习 —— 列存储(analyze)(三)】_第1张图片
  返回样本数组中的样本行数
【 OpenGauss源码学习 —— 列存储(analyze)(三)】_第2张图片

RelationGetNumberOfBlocks 函数

  RelationGetNumberOfBlocks 函数用于获取一个关系(表)中的块数(即页面数量),是一个核心函数,用于管理表空间的大小和估算关系的存储占用

注:在数据库系统中,一个关系(通常指的是被存储在多个数据块或页面中。每个数据块页面都具有固定的大小,通常以字节为单位。关系的数据被分布存储在这些页面上,而每个页面可以容纳一定数量的行数据。
  “表中的块数” 指的是该表在存储层面上占据的页面数量。这些页面包括了表的实际数据页面,以及可能包括了索引、TOAST表(用于存储大对象值的表)等相关的页面。这个数量可以用来估算表的存储空间占用和性能特征。
  通常情况下,数据库管理员或优化器使用这个信息来评估表的大小、性能以及执行查询计划的优化。例如,它可以用来估算表的大小,以便规划备份和恢复策略,或者估算表的大小以决定何时进行重新组织(VACUUM)操作以优化性能。
  总之,“表中的块数” 表示了表在存储层面上所占用的页面数量,是数据库管理和优化的一个重要指标。

   RelationGetNumberOfBlocks 函数被定义为一个宏,用于简化了获取关系(表)中主要 Fork 的块数(页面数量)的操作。它将传递给 RelationGetNumberOfBlocksInFork 函数的 Fork 参数设置为 MAIN_FORKNUM,因此可以通过传递一个关系(reln)来快速获取主要 Fork 的块数,而无需显式指定 Fork 号。这个宏的效果就像是一个快速的查找主要Fork 块数的捷径。

#define RelationGetNumberOfBlocks(reln) RelationGetNumberOfBlocksInFork(reln, MAIN_FORKNUM)

  函数 RelationGetNumberOfBlocksInFork 源码如下:(路径:src/gausskernel/storage/buffer/bufmgr.cpp

函数入参:

  1. Relation relation:要获取块数的关系(表)。
  2. ForkNumber fork_num:要获取块数的 Fork 号码,通常是主ForkMAIN_FORKNUM)或 VM(可见性映射)ForkVISIBILITYMAP_FORKNUM)。
  3. bool estimate:一个布尔值,指示是否估算块数。如果为true,则进行估算;如果为false,则获取实际块数。
/*
 * RelationGetNumberOfBlocksInFork
 *     确定关系中当前的块数(页面数量)。
 */
BlockNumber RelationGetNumberOfBlocksInFork(Relation relation, ForkNumber fork_num, bool estimate)
{
    BlockNumber result = 0;  // 用于存储最终的块数结果

    /*
     * 当此后端没有初始化 GTT 存储时,返回 0
     */
    if (RELATION_IS_GLOBAL_TEMP(relation) &&
        !gtt_storage_attached(RelationGetRelid(relation))) {
        return result;
    }

    // 对于列存储关系,只需返回 pg_class 中的 relpages 数量
    // 未来:应该实现新的接口来计算块数。
    if (RelationIsColStore(relation)) {
        return (BlockNumber)relation->rd_rel->relpages;
    }

    if (RELATION_CREATE_BUCKET(relation)) {
        Relation buckRel = NULL;

        oidvector *bucketlist = searchHashBucketByOid(relation->rd_bucketoid);

        if (estimate) {
            /*
             * 估算关系大小使用第一个桶关系,仅供规划器使用。
             */
            buckRel = bucketGetRelation(relation, NULL, bucketlist->values[0]);
            result += smgrnblocks(buckRel->rd_smgr, fork_num) * bucketlist->dim1;
            bucketCloseRelation(buckRel);

#define ESTIMATED_MIN_BLOCKS 10000

            /* 简单地估算,如果结果小于 ESTIMATED_MIN_BLOCKS,则将其设置为 ESTIMATED_MIN_BLOCKS */
            if (result < ESTIMATED_MIN_BLOCKS) {
                result = ESTIMATED_MIN_BLOCKS;
            }
        } else {
            for (int i = 0; i < bucketlist->dim1; i++) {
                buckRel = bucketGetRelation(relation, NULL, bucketlist->values[i]);
                result += smgrnblocks(buckRel->rd_smgr, fork_num);
                bucketCloseRelation(buckRel);
            }
        }
        return result;
    } else {
        /* 如果还没有在 smgr 层面打开,则打开关系 */
        RelationOpenSmgr(relation);
        return smgrnblocks(relation->rd_smgr, fork_num);
    }
}

  函数功能如下:

  1. 首先,函数检查关系是否为全局临时表Global Temporary TableGTT)。如果是,并且没有初始化GTT存储,它将返回0。否则,它将继续执行后续操作。
  2. 接着,函数检查关系是否是列存储关系Column-Store Relation)。如果是,它将直接返回 rd_rel->relpages,即在 pg_class 系统表中存储的页面数量。这是因为列存储关系的页面数量可以直接从 pg_class 中获取,不需要实际计算。
  3. 如果关系不是列存储关系,则它根据是否是分桶关系来执行不同的逻辑。
  • 如果是分桶关系bucket relation),则根据是否需要估算块数进行处理。如果需要估算,则选择第一个桶(bucket)的大小,将其乘以桶的数量,作为估算值。如果估算值小于某个最小值(ESTIMATED_MIN_BLOCKS),则将其设置为该最小值。
  • 如果不需要估算,则遍历所有桶,累加各个桶的块数。
      这个分桶关系的概念用于分布式表,其中一个关系被分成多个桶,每个桶都是一个独立的关系。这个函数会遍历每个桶,获取它们的块数。
  1. 最后,如果关系既不是全局临时表,也不是列存储关系或分桶关系,函数将在存储管理器层面Storage Manager,smgr)打开关系,然后获取指定 Fork 的块数。

BlockSampler_Init 函数

  BlockSampler_Init 函数的作用和意义是块抽样(Block Sampling)过程做初始化准备工作块抽样是数据库系统中用于分析表数据的一种技术,它用于在大表中随机选择一部分数据块以进行分析,而不必分析整个表。这有助于提高分析效率并降低系统资源的消耗。

具体来说,BlockSampler_Init 函数完成以下任务:

  1. 初始化块抽样器数据结构 BlockSampler,该结构用于记录和控制块抽样的状态
  2. 接受表的总块数 nblocks 和要抽样的块数 samplesize 作为参数,以便后续的块抽样过程中使用。
  3. 设置块抽样器的状态,包括已扫描的块数 bs->t 和已选择的块数 bs->m,初始值都为0。
  4. 块抽样的目标是从表中选择 samplesize 个块进行分析。如果表的块数量少于 samplesize,则选择所有块。

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

/*
 * BlockSampler_Init -- 准备随机抽样块编号
 *
 * BlockSampler_Init 是 PostgreSQL 数据库系统中用于准备从表的数据页中进行块编号随机抽样的函数。
 * 这个函数是两阶段元组抽样机制的一部分,用于第一阶段。其目标是从表中随机选择数据块的样本,
 * 这对于各种数据库管理和优化任务非常有用。
 *
 * 在这个函数中,我们使用所需的信息初始化一个 BlockSampler 数据结构(bs),该结构用于随机块抽样。
 * BlockSampler 旨在从表中选择一个代表性子集的数据块。如果表的块数量少于 samplesize,我们将选择所有块。
 *
 * 用于抽样的算法是 Knuth 3.4.2 中的算法 S,这是一种用于随机抽样的简单方法。
 */
void BlockSampler_Init(BlockSampler bs, BlockNumber nblocks, int samplesize)
{
    bs->N = nblocks; /* 测得的表大小 */

    /*
     * 如果我们决定减少对于拥有少于或接近于 samplesize 块的表的样本大小,
     * 那么这就是执行此操作的地方。
     */
    bs->n = samplesize;
    bs->t = 0; /* 到目前为止扫描的块数 */
    bs->m = 0; /* 到目前为止选择的块数 */
}

  调试信息如下:
【 OpenGauss源码学习 —— 列存储(analyze)(三)】_第3张图片

anl_init_selection_state 函数

  anl_init_selection_state 函数是关于随机抽样算法的实现,具体是基于 Jeffrey S. Vitter 在其论文 “Random sampling with a reservoir” 中描述的算法。该算法用于在大数据集中进行随机抽样,以选取代表性的样本数据,而不需要将整个数据集全部加载到内存中anl_init_selection_state 函数源码如下:(路径:src/gausskernel/optimizer/commands/analyze.cpp

/*
 * 这两个例程体现了 Jeffrey S. Vitter 在 ACM Trans. Math. Softw. 1985 年的论文中描述的
 * 算法 Z,题为 "Random sampling with a reservoir"("带水库的随机抽样")。
 * Vitter 的算法以 S 记录的计数为基础,用于记录在处理另一条记录之前要跳过多少记录。
 * 它主要基于 t,已读取的记录数来计算。两次调用之间唯一需要的额外状态是 W,一个随机状态变量。
 *
 * anl_init_selection_state 计算初始的 W 值。
 *
 * 假设我们已经读取了 t 条记录(t >= n),anl_get_next_S 确定下一条记录在处理之前需要跳过的记录数。
 */
double anl_init_selection_state(int n)
{
    if (unlikely(n == 0)) {
        ereport(ERROR, 
            (errcode(ERRCODE_DIVISION_BY_ZERO), 
                errmsg("records n should not be zero")));
    }
    /* 初始的 W 值(在首次应用算法 Z 时使用) */
    return exp(-log(anl_random_fract()) / n);
}

具体来说,BlockSampler_Init 函数完成以下任务:

  1. anl_init_selection_state 函数用于计算初始的 W 值。W 是算法 Z 中的一个随机状态变量,用于确定在记录集中跳过多少记录。在首次应用算法 Z 时,需要初始化 W
  2. 函数接受一个整数参数 n,表示已经读取的记录数。这个参数用于计算初始的 W 值。
  3. 函数首先检查 n 是否为零,如果是零,则抛出一个错误,因为在抽样过程中不能有零个记录。
  4. 然后,函数计算并返回初始的 W 值。计算公式为 exp(-log(anl_random_fract()) / n),其中 anl_random_fract() 用于生成一个随机小数

  调试信息如下:
【 OpenGauss源码学习 —— 列存储(analyze)(三)】_第4张图片

BlockSampler_GetBlock 函数

  BlockSampler_GetBlock 函数用于从数据表中获取一个块(页面)以供分析使用。其函数源码如下:(路径:src/gausskernel/optimizer/commands/analyze.cpp

/*
 * @Description: 获取用于分析的一个块
 * @Param[IN/OUT] anlprefetch: 如果使用 ADIO,则是 ADIO 预取上下文
 * @Param[IN/OUT] bs: BlockSampler 结构体,用于随机抽样
 * @Param[IN/OUT] estimate_table_rownum: 是否估算表中的行数
 * @Param[IN/OUT] onerel: 数据表
 * @Return: 块的标识号,InvalidBlockNumber 表示没有块可供分析
 * @See also: 相关函数
 */
template <bool isColumnStore>
BlockNumber BlockSampler_GetBlock(void* rel_or_cuDesc, BlockSampler bs, AnlPrefetch* anlprefetch, int analyzeAttrNum,
    int32* attrSeq, bool estimate_table_rownum)
{
    // 如果需要估算表中的行数,且 BlockSampler 还有更多块可供选择,则获取下一个块
    if (estimate_table_rownum) {
        if (BlockSampler_HasMore(bs)) {
            return BlockSampler_Next(bs);
        }
    }

    ADIO_RUN()
    {
        // 使用 ADIO 运行块抽样,并返回下一个块的标识号
        return BlockSampler_NextBlock<isColumnStore>(rel_or_cuDesc, bs, anlprefetch, analyzeAttrNum, attrSeq);
    }
    ADIO_ELSE()
    {
        // 如果不使用 ADIO 且 BlockSampler 还有更多块可供选择,则获取下一个块
        if (BlockSampler_HasMore(bs)) {
            return BlockSampler_Next(bs);
        }
    }
    ADIO_END();

    // 如果没有更多的块可供分析,返回 InvalidBlockNumber
    return InvalidBlockNumber;
}

具体来说,BlockSampler_GetBlock 函数完成以下任务:

  1. 该函数用于从数据表中获取一个块(页面)以供分析使用。在大数据集的分析过程中,通常需要逐个处理数据块,而不是一次性加载整个数据集。
  2. 函数接受多个参数,包括 ADIO 预取上下文(如果使用 ADIO)、BlockSampler 结构体(用于随机抽样)、是否估算表中的行数、数据表等。
  3. 首先,函数检查是否需要估算表中的行数,并且检查 BlockSampler 是否还有更多块可供选择。如果需要估算行数且块抽样器还有块可用,它将获取下一个块的标识号并返回。
  4. 在宏 ADIO_RUNADIO_ELSE 内部,根据是否使用 ADIO 运行块抽样。如果使用 ADIO,则调用 BlockSampler_NextBlock 函数来获取下一个块的标识号
  5. 最后,在 ADIO_END 外部,如果没有更多的块可供分析,函数返回 InvalidBlockNumber,表示没有块可供分析

ReadBufferExtended

  函数 ReadBufferExtended 用于读取一个关系(表)的指定块的缓冲区。该函数还支持在需要时扩展关系文件并分配新块。函数 ReadBufferExtended 接受多个参数,包括要读取的关系要读取的分支(如主分支、 FSM 分支等)、要读取的块号读取模式以及缓冲区访问策略。其函数源码如下:(路径:src/gausskernel/storage/buffer/bufmgr.cpp

/*
 * ReadBufferExtended -- 返回包含请求的关系的指定块的缓冲区。
 * 如果请求的块号为 P_NEW,则扩展关系文件并分配一个新的块。
 * (调用方负责确保只有一个后台进程尝试同时扩展一个关系!)
 *
 * 返回值:包含读取块的缓冲区的缓冲区号。返回的缓冲区已被固定。
 * 在发生错误时不返回,而是生成错误日志(elog)。
 *
 * 假定在调用此函数时,reln 已经被打开。
 *
 * 在 RBM_NORMAL 模式下,从磁盘读取页面,并验证页面头。
 * 如果页面头无效,将引发错误。(但请注意,全零页面被视为“有效”;参见 PageIsVerified()。)
 *
 * RBM_ZERO_ON_ERROR 类似于正常模式,但如果页面头无效,则将页面清零而不是引发错误。
 * 这适用于非关键数据,调用方准备修复错误。
 *
 * 在 RBM_ZERO_AND_LOCK 模式下,如果页面尚未在缓冲区缓存中,
 * 则填充页面为零,而不是从磁盘读取它。当调用方打算从头开始填充页面时,这可以节省 I/O,
 * 并避免在磁盘上的页面具有损坏的页面头时引发不必要的错误。
 * 页面以锁定状态返回,以确保调用方有机会在对其他用户可见之前初始化页面。
 * 注意:不要使用此模式来读取超出关系当前物理 EOF 的页面;
 * 当修改并写出页面时,这可能会在 md.c 中引发问题。但 P_NEW 是可以的。
 *
 * RBM_ZERO_AND_CLEANUP_LOCK 与 RBM_ZERO_AND_LOCK 相同,
 * 但以清理级别的锁定方式获取页面。
 *
 * RBM_NORMAL_NO_LOG 模式在此处与 RBM_NORMAL 一样处理。
 *
 * RBM_FOR_REMOTE 与正常模式类似,但在 PageIsVerified 失败时不进行远程读取。
 *
 * 如果 strategy 不为 NULL,则使用非默认的缓冲区访问策略。
 * 有关详细信息,请参阅 buffer/README。
 */
Buffer ReadBufferExtended(Relation reln, ForkNumber fork_num, BlockNumber block_num, ReadBufferMode mode,
                          BufferAccessStrategy strategy)
{
    bool hit = false;
    Buffer buf;

    if (block_num == P_NEW) {
        STORAGE_SPACE_OPERATION(reln, BLCKSZ);
    }

    /* 如果尚未打开,请在 smgr 级别打开关系 */
    RelationOpenSmgr(reln);

    /*
     * 拒绝尝试读取非本地临时关系;由于我们无法查看所属会话的本地缓冲区,因此可能会获取错误的数据。
     */
    if (RELATION_IS_OTHER_TEMP(reln) && fork_num <= INIT_FORKNUM)
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot access temporary tables of other sessions")));

    /*
     * 读取缓冲区,并更新 pgstat 计数器以反映缓存命中或未命中。
     */
    pgstat_count_buffer_read(reln);
    pgstatCountBlocksFetched4SessionLevel();
    buf = ReadBuffer_common(reln->rd_smgr, reln->rd_rel->relpersistence, fork_num, block_num, mode, strategy, &hit);
    if (hit) {
        pgstat_count_buffer_hit(reln);
    }
    return buf;
}

具体来说,ReadBufferExtended 函数完成以下任务:

  1. 如果请求的块号是 P_NEW,则会执行 STORAGE_SPACE_OPERATION,该操作用于扩展关系文件并分配新块
  2. 在函数内部,首先打开关系的存储管理器smgr),以确保可以操作关系的物理文件
  3. 接下来,检查是否可以访问非本地临时关系,因为在不同会话中无法访问它们。
  4. 然后,函数调用 pgstat_count_buffer_readpgstatCountBlocksFetched4SessionLevel更新统计信息,以记录缓冲区的读取操作
  5. 最后,调用 ReadBuffer_common 函数来读取指定块的缓冲区,并根据情况设置 hit 标志以表示是否缓存命中。如果缓存命中,还会调用 pgstat_count_buffer_hit 更新统计信息。

  总之,ReadBufferExtended 是一个用于读取关系块的函数,具有多种读取模式和选项,并在需要时支持文件扩展和缓存命中统计。

PageGetMaxOffsetNumber 函数

  内联函数 PageGetMaxOffsetNumber 用于获取给定页面Page上使用的最大偏移量编号。由于偏移量编号是从1开始的,因此这个值也表示了页面上的项目数量

  1. 参数char* pghr,表示一个页面的指针,通常是页面的内存表示。
  2. 返回值OffsetNumber,表示最大的偏移量编号(或项目数量)。如果页面未初始化(pd_lower == 0),则返回 0,以确保行为合理。

PageGetMaxOffsetNumber 函数源码如下:(路径:src/include/storage/buf/bufpage.h

/*
 * PageGetMaxOffsetNumber
 *		获取给定页面上使用的最大偏移量编号。
 *		由于偏移量编号从1开始,因此此值也表示页面上的项目数量。
 *
 * 注意:如果页面未初始化(pd_lower == 0),我们必须返回零,以确保行为合理。
 * 接受参数的双重评估,以确保这一点。
 */
inline OffsetNumber PageGetMaxOffsetNumber(char* pghr)
{
    OffsetNumber maxoff = InvalidOffsetNumber; // 初始化最大偏移量编号为无效值
    Size pageheadersize = GetPageHeaderSize(pghr); // 获取页面头的大小

    if (((PageHeader)pghr)->pd_lower <= pageheadersize) // 如果pd_lower小于等于页面头大小
        maxoff = 0; // 则最大偏移量编号为0,表示页面未初始化
    else
        maxoff = (((PageHeader)pghr)->pd_lower - pageheadersize) / sizeof(ItemIdData); // 否则计算最大偏移量编号

    return maxoff; // 返回最大偏移量编号
}

  调试信息如下:
【 OpenGauss源码学习 —— 列存储(analyze)(三)】_第5张图片

HeapTupleSatisfiesVacuum 函数

  HeapTupleSatisfiesVacuum 函数的主要作用是确定给定的堆元组在进行 VACUUM 操作时的状态。具体来说,它判断了堆元组是否可能对任何正在运行的事务可见。这个信息对于 VACUUM 过程非常重要,因为它决定了是否可以删除或保留堆中的特定元组
  HeapTupleSatisfiesVacuum 函数源码如下:(路径:src/gausskernel/storage/access/heap/heapam_visibility.cpp

/*
 * HeapTupleSatisfiesVacuum
 *
 * 为了VACUUM的目的,确定元组的状态。在这里,我们主要想知道的是元组是否潜在地对*任何*正在运行的事务可见。
 * 如果是,那么在VACUUM之前不能删除它。
 *
 * OldestXmin是一个截止的XID(从GetOldestXmin()获得)。被XID >= OldestXmin删除的元组被视为“最近死亡”;
 * 它们可能仍然对某些打开的事务可见,因此我们不能删除它们,即使我们看到删除事务已经提交。
 */
HTSV_Result HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin, Buffer buffer, bool isAnalyzing)
{
    HeapTupleHeader tuple = htup->t_data;
    TransactionIdStatus xidstatus;
    Assert(ItemPointerIsValid(&htup->t_self));
    Assert(htup->t_tableOid != InvalidOid);

    /* 不需要同步,因为不使用快照 */
    Page page = BufferGetPage(buffer);
    HeapTupleCopyBaseFromPage(htup, page);

    if (SHOW_DEBUG_MESSAGE()) {
        ereport(DEBUG1,
            (errmsg("HeapTupleSatisfiesVacuum self(%d,%d) ctid(%d,%d) cur_xid %lu xmin %ld"
                    " xmax %ld OldestXmin %ld",
                ItemPointerGetBlockNumber(&htup->t_self),
                ItemPointerGetOffsetNumber(&htup->t_self),
                ItemPointerGetBlockNumber(&tuple->t_ctid),
                ItemPointerGetOffsetNumber(&tuple->t_ctid),
                GetCurrentTransactionIdIfAny(),
                HeapTupleHeaderGetXmin(page, tuple),
                HeapTupleHeaderGetXmax(page, tuple),
                OldestXmin)));
    }

    /*
     * 插入事务是否已经提交?
     *
     * 如果插入事务中止,那么元组从未对任何其他事务可见,所以我们可以立即删除它。
     */
    if (!HeapTupleHeaderXminCommitted(tuple)) {
        if (HeapTupleHeaderXminInvalid(tuple))
            return HEAPTUPLE_DEAD;
        xidstatus = TransactionIdGetStatus(HeapTupleGetRawXmin(htup));
        if (xidstatus == XID_INPROGRESS && TransactionIdIsInProgress(HeapTupleGetRawXmin(htup))) {
            if (tuple->t_infomask & HEAP_XMAX_INVALID) /* xid 无效 */
                return HEAPTUPLE_INSERT_IN_PROGRESS;
            if (tuple->t_infomask & HEAP_IS_LOCKED)
                return HEAPTUPLE_INSERT_IN_PROGRESS;
            /* 插入后由相同xact删除 */
            return HEAPTUPLE_DELETE_IN_PROGRESS;
        } else if (xidstatus == XID_COMMITTED ||
            (xidstatus == XID_INPROGRESS && TransactionIdDidCommit(HeapTupleGetRawXmin(htup)))) {
            /* 必须重新检查clog,因为csn可能在检查TransactionIdIsInProgress之前被提交 */
            if (!isAnalyzing) {
                SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, HeapTupleGetRawXmin(htup));
            }
        } else {
            /*
             * 不在进行中,未提交,因此要么已中止要么已崩溃
             */
            if (u_sess->attr.attr_storage.enable_debug_vacuum && t_thrd.utils_cxt.pRelatedRel) {
                elogVacuumInfo(
                    t_thrd.utils_cxt.pRelatedRel, htup, "HeapTupleSatisfiedVacuum set HEAP_XMIN_INVALID", OldestXmin);
            }
            if (!LatestFetchTransactionIdDidAbort(HeapTupleHeaderGetXmin(page, tuple)))
                LatestTransactionStatusError(HeapTupleHeaderGetXmin(page, tuple),
                    NULL,
                    "HeapTupleSatisfiedVacuum set HEAP_XMIN_INVALID xid don't abort");
            SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, InvalidTransactionId);
            return ((!t_thrd.xact_cxt.useLocalSnapshot || IsInitdb) ? HEAPTUPLE_DEAD : HEAPTUPLE_LIVE);
        }

        /*
         * 此时已知xmin已提交,但我们可能无法设置提示位,因此不能再断言它已设置。
         */
    }

    /*
     * 好的,插入者已提交,所以它在某个时候是有效的。现在看看删除事务呢?
     */
    if (tuple->t_infomask & HEAP_XMAX_INVALID)
        return HEAPTUPLE_LIVE;

    if (tuple->t_infomask & HEAP_IS_LOCKED) {
        /*
         * “删除”事务实际上只锁定了它,因此无论如何元组都是有效的。
         * 但是,我们应该确保在xact消失后设置XMAX_COMMITTED或XMAX_INVALID之一,以减少将来为xact检查元组的成本。
         * 同样,将已死亡的MultiXacts标记为无效。
         */
        if (!(tuple->t_infomask & HEAP_XMAX_COMMITTED)) {
            if (tuple->t_infomask & HEAP_XMAX_IS_MULTI) {
                if (MultiXactIdIsRunning(HeapTupleGetRawXmax(htup)))
                    return HEAPTUPLE_LIVE;
            } else {
                xidstatus = TransactionIdGetStatus(HeapTupleGetRawXmax(htup));
                if (xidstatus == XID_INPROGRESS && TransactionIdIsInProgress(HeapTupleGetRawXmax(htup))) {
                    return HEAPTUPLE_LIVE;
                }
            }

            /*
             * 我们实际上并不关心xmax是否已提交、中止还是崩溃。
             * 我们知道xmax锁定了元组,但它永远不会实际更新它。
             */
            if (u_sess->attr.attr_storage.enable_debug_vacuum && t_thrd.utils_cxt.pRelatedRel) {
                elogVacuumInfo(
                    t_thrd.utils_cxt.pRelatedRel, htup, "HeapTupleSatisfiedVacuum set HEAP_XMAX_INVALID ", OldestXmin);
            }
            SetHintBits(tuple, buffer, HEAP_XMAX_INVALID, InvalidTransactionId);
        }
        return HEAPTUPLE_LIVE;
    }

    if (tuple->t_infomask & HEAP_XMAX_IS_MULTI) {
        /* MultiXacts目前只允许锁定元组 */
        Assert(tuple->t_infomask & HEAP_IS_LOCKED);
        return HEAPTUPLE_LIVE;
    }

    if (!(tuple->t_infomask & HEAP_XMAX_COMMITTED)) {
        xidstatus = TransactionIdGetStatus(HeapTupleGetRawXmax(htup));
        if (xidstatus == XID_INPROGRESS && TransactionIdIsInProgress(HeapTupleGetRawXmax(htup))) {
            return HEAPTUPLE_DELETE_IN_PROGRESS;
        } else if (xidstatus == XID_COMMITTED ||
            (xidstatus == XID_INPROGRESS && TransactionIdDidCommit(HeapTupleGetRawXmax(htup)))) {
            /* 必须重新检查clog,因为csn可能在检查TransactionIdIsInProgress之前被提交 */
            SetHintBits(tuple, buffer, HEAP_XMAX_COMMITTED, HeapTupleGetRawXmax(htup));
        } else {
            /*
             * 不在进行中,未提交,因此要么已中止要么已崩溃
             */
            if (!LatestFetchTransactionIdDidAbort(HeapTupleHeaderGetXmax(page, tuple)))
                LatestTransactionStatusError(HeapTupleHeaderGetXmax(page, tuple),
                    NULL,
                    "HeapTupleSatisfiedVacuum set HEAP_XMAX_INVALID xid don't abort");

            SetHintBits(tuple, buffer, HEAP_XMAX_INVALID, InvalidTransactionId);
            return HEAPTUPLE_LIVE;
        }

        /*
         * 此时已知xmax已提交,但我们可能无法设置提示位,因此不能再断言它已设置。
         */
    }

    /*
     * 删除者已提交,但也许是最近的,一些打开的事务仍然可以看到元组。
     */
    if (!TransactionIdPrecedes(HeapTupleGetRawXmax(htup), OldestXmin))
        return ((!t_thrd.xact_cxt.useLocalSnapshot || IsInitdb) ? HEAPTUPLE_RECENTLY_DEAD : HEAPTUPLE_LIVE);

    /* 否则,它已死且可移除 */
    return ((!t_thrd.xact_cxt.useLocalSnapshot || IsInitdb) ? HEAPTUPLE_DEAD : HEAPTUPLE_LIVE);
}

  HeapTupleSatisfiesVacuum 函数的作用可以总结如下:

1. 判断堆元组是否已被插入的事务提交。
2. 判断堆元组是否已被删除的事务提交。
3. 判断堆元组是否仍然对某些打开的事务可见。
  这些信息帮助 VACUUM 确定哪些元组可以安全地删除,哪些需要保留,以及哪些需要进一步的检查。
  此外,函数还根据参数 OldestXmin 来考虑正在运行的最旧事务,以确定是否将元组标记为“最近死亡”或“活跃”。函数的结果类型 HTSV_Result 包含了不同的状态,表示元组的不同状态,如已删除、正在插入、已锁定等。

heapCopyTuple 函数

  heapCopyTuple 函数是对 heap_copytupleheapCopyCompressedTuple 函数的包装器。该函数的主要作用是根据输入的堆元组以及元组描述符,选择性地调用不同的元组复制函数,以便根据元组的存储方式和属性描述符进行复制。这有助于确保正确地复制堆元组的数据,并处理元组的默认值。其函数源码如下所示:(路径:src/gausskernel/storage/access/common/heaptuple.cpp

/*
 * heapCopyTuple
 *
 * heap_copytuple 和 heapCopyCompressedTuple 的包装器函数
 */
HeapTuple heapCopyTuple(HeapTuple tuple, TupleDesc tupDesc, Page page)
{
    // 检查输入的元组是否有效,以及是否包含数据
    if (!HeapTupleIsValid(tuple) || tuple->t_data == NULL) {
        ereport(WARNING, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
                          (errmsg("tuple copy failed, because tuple is invalid or tuple data is null "))));
        return NULL;
    }

    // 如果输入元组使用压缩格式存储,调用 heapCopyCompressedTuple 函数
    if (HEAP_TUPLE_IS_COMPRESSED(tuple->t_data)) {
        return heapCopyCompressedTuple(tuple, tupDesc, page);
    }

    // 如果元组描述符(tupDesc)包含默认值,并且输入元组的属性数量少于描述符所描述的属性数量,调用 HeapCopyInitdefvalTup 函数
    if (tupDesc->initdefvals && tupDesc->natts > (int)HeapTupleHeaderGetNatts(tuple->t_data, tupDesc)) {
        return HeapCopyInitdefvalTup(tuple, tupDesc);
    }

    // 否则,调用 heap_copytuple 函数进行普通的元组复制
    return heap_copytuple(tuple);
}

heapCopyCompressedTuple 函数

  heapCopyCompressedTuple 该函数的主要作用是解压缩一个压缩的堆元组,并返回解压缩后的元组的副本。根据元组是否包含 NULL 值和表的属性描述,它可以选择性地调用不同的解压缩函数。这有助于确保正确地处理压缩的元组数据,并生成解压缩后的元组副本。函数源码如下:(src/gausskernel/storage/access/common/heaptuple.cpp

/*
 * heapCopyCompressedTuple
 *
 * 解压缩一个压缩的堆元组并返回解压缩后的元组的副本
 */
HeapTuple heapCopyCompressedTuple(HeapTuple tuple, TupleDesc tupleDesc, Page page, HeapTuple destTup)
{
    HeapTuple newTuple = NULL;
    Assert(HeapTupleIsValid(tuple) && (tuple->t_data != NULL));
    Assert(HEAP_TUPLE_IS_COMPRESSED(tuple->t_data));
    Assert((tupleDesc != NULL) && (page != NULL));

    /* 处理修改表时的特殊情况,因为 HeapUncompressTup() 不考虑该情况 */
    if (tupleDesc->initdefvals && tupleDesc->natts > (int)HeapTupleHeaderGetNatts(tuple->t_data, tupleDesc)) {
        // 调用 HeapUncompressTup2 函数解压缩元组
        newTuple = HeapUncompressTup2(tuple, tupleDesc, (Page)getPageDict(page));
        
        if (destTup) {
            errno_t retno = EOK;
            Assert(MAXALIGN(newTuple->t_len) <= MaxHeapTupleSize);

            // 将新的元组数据复制到现有元组的空间中
            destTup->t_len = newTuple->t_len;
            destTup->t_self = newTuple->t_self;
            destTup->t_tableOid = newTuple->t_tableOid;
            destTup->t_bucketId = newTuple->t_bucketId;
            destTup->t_xc_node_id = newTuple->t_xc_node_id;
            retno = memcpy_s(destTup->t_data, destTup->t_len, newTuple->t_data, newTuple->t_len);
            securec_check(retno, "\0", "\0");

            // 释放未使用的空间并使 newTuple 指向 destTup
            heap_freetuple(newTuple);
            newTuple = destTup;
        }
    } else {
        if (!HeapTupleHasNulls(tuple)) {
            // 解压缩不包含 NULL 值的元组
            newTuple = HeapUncompressTup<false>(tuple, tupleDesc, (char *)getPageDict(page), destTup);
        } else {
            // 解压缩包含 NULL 值的元组
            newTuple = HeapUncompressTup<true>(tuple, tupleDesc, (char *)getPageDict(page), destTup);
        }
    }

    return newTuple;
}

  调试结果如下:
在这里插入图片描述

heap_copytuple 函数

heap_copytuple 用于复制元组数据,并根据需要进行解压缩,其中 heap_copytuple 用于未压缩的元组。先前的 heapCopyCompressedTuple 用于压缩的元组。它们都分配了新的内存块,将原始元组的数据复制到新的元组中,以便生成元组的副本。函数源码如下:(路径:src/gausskernel/storage/access/common/heaptuple.cpp

/*
 * heap_copytuple && heapCopyCompressedTuple
 *
 * 返回一个完整元组的副本,如果是行压缩的,则首先对其进行解压缩。
 * HeapTuple 结构、元组头部和元组数据都分配在单个 palloc() 块中。
 * heap_copytuple 用于未压缩的元组,对于所有 PostgreSQL 系统关系,它们的元组必须是未压缩的。
 * 这也适用于那些根本没有压缩的元组,例如索引元组。
 * heapCopyCompressedTuple 用于压缩的元组,需要传递 。
 * 同时提供了宏 HEAP_COPY_TUPLE,用于包装未压缩和压缩的元组。
 */
HeapTuple heap_copytuple(HeapTuple tuple)
{
    HeapTuple newTuple;
    errno_t rc = EOK;

    if (!HeapTupleIsValid(tuple) || tuple->t_data == NULL) {
        return NULL;
    }

    Assert(!HEAP_TUPLE_IS_COMPRESSED(tuple->t_data));
	newTuple = (HeapTuple)heaptup_alloc(HEAPTUPLESIZE + tuple->t_len);
    newTuple->t_len = tuple->t_len;
    newTuple->t_self = tuple->t_self;
    newTuple->t_tableOid = tuple->t_tableOid;
    newTuple->t_bucketId = tuple->t_bucketId;
    HeapTupleCopyBase(newTuple, tuple);
#ifdef PGXC
    newTuple->t_xc_node_id = tuple->t_xc_node_id;
#endif
    newTuple->t_data = (HeapTupleHeader)((char *)newTuple + HEAPTUPLESIZE);
    rc = memcpy_s((char *)newTuple->t_data, tuple->t_len, (char *)tuple->t_data, tuple->t_len);
    securec_check(rc, "\0", "\0");
    return newTuple;
}

anl_get_next_S 函数

  anl_get_next_S 函数实现了 Jeffrey S. Vitter 的抽样算法,该算法用于在一系列记录中随机选择一个记录。该算法的目的是从 n 条记录中抽样 t 条记录。

以下是 anl_get_next_S 函数的核心步骤:

  1. 如果 t 小于或等于 22.0 * n,则使用 “Algorithm X” 进行抽样。在此过程中,它生成一个随机数 V,然后迭代地计算一个值 S,直到满足一定的条件。这个条件涉及到 V、t、nS 的比较。
  2. 如果 t 大于 22.0 * n,则应用 “Algorithm Z” 进行抽样。在此过程中,它使用一个状态变量 W,并迭代地计算 S 的值,以满足一定的条件。这个条件同样涉及到 W、t、nS 的比较。

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

double anl_get_next_S(double t, int n, double* stateptr)
{
    double S;

    /* The magic constant here is T from Vitter's paper */
    if (t <= (22.0 * n)) {
        /* Process records using Algorithm X until t is large enough */
        double V, quot;

        V = anl_random_fract(); /* Generate V */
        S = 0;
        t += 1;
        /* Note: "num" in Vitter's code is always equal to t - n */
        quot = (t - (double)n) / t;
        /* Find min S satisfying (4.1) */
        while (quot > V) {
            S += 1;
            t += 1;
            quot *= (t - (double)n) / t;
        }
    } else {
        /* Now apply Algorithm Z */
        double W = *stateptr;
        double term = t - (double)n + 1;

        for (;;) {
            double numer, numer_lim, denom;
            double U, X, lhs, rhs, y, tmp;

            /* Generate U and X */
            U = anl_random_fract();
            X = t * (W - 1.0);
            S = floor(X); /* S is tentatively set to floor(X) */
            /* Test if U <= h(S)/cg(X) in the manner of (6.3) */
            tmp = (t + 1) / term;
            lhs = exp(log(((U * tmp * tmp) * (term + S)) / (t + X)) / n);
            rhs = (((t + X) / (term + S)) * term) / t;
            if (lhs <= rhs) {
                W = rhs / lhs;
                break;
            }
            /* Test if U <= f(S)/cg(X) */
            y = (((U * (t + 1)) / term) * (t + S + 1)) / (t + X);
            if ((double)n < S) {
                denom = t;
                numer_lim = term + S;
            } else {
                denom = t - (double)n + S;
                numer_lim = t + 1;
            }
            for (numer = t + S; numer >= numer_lim; numer -= 1) {
                y *= numer / denom;
                denom -= 1;
            }
            W = exp(-log(anl_random_fract()) / n); /* Generate W in advance */
            if (exp(log(y) / n) <= (t + X) / t)
                break;
        }
        *stateptr = W;
    }
    return S;
}

  以上算法的细节部分较为复杂,涉及到许多随机数生成、数学计算和迭代。它的目标是选择一定数量的记录,以便对数据进行随机抽样,而不需要遍历整个数据集。这对于性能优化和统计分析非常有用。函数的返回值 S 表示抽样结果,即所选记录的数量

你可能感兴趣的:(OpenGauss,postgresql,gaussdb,数据库)