当前位置: 首页 > news >正文

建设银行新乡分行城南支行网站/广东seo网站推广代运营

建设银行新乡分行城南支行网站,广东seo网站推广代运营,wordpress调用搜索结果,wordpress 4.5.2 漏洞执行算子(SeqScan算子) 执行算子概述扫描算子SeqScan算子ExecInitSeqScan函数InitScanRelation函数ExecSeqScan函数 总结 声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵…

执行算子(SeqScan算子)

  • 执行算子概述
  • 扫描算子
  • SeqScan算子
    • ExecInitSeqScan函数
    • InitScanRelation函数
    • ExecSeqScan函数
  • 总结

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

执行算子概述

  在OpenGauss中,执行算子是用于执行数据库查询计划的基本操作单元。执行算子负责处理查询计划中的每个节点,执行各种操作,从而实现用户查询的功能。
  执行算子模块包含多种计划执行算子,是计划执行的独立单元,用于实现具体的计划动作。执行计划包含4类算子,分别是控制算子、扫描算子、物化算子和连接算子。这些算子统一使用节点(node)表示,具有统一的接口,执行流程采用递归模式。整体执行流程是:首先根据计划节点的类型初始化状态节点(函数名为“ExecInit+算子名”),然后再回调执行函数(函数名为“Exec+算子名”),最后是清理状态节点(函数名为“ExecEnd+算子名”)。本文主要介绍扫描算子,因为最近在做这方面的工作。
  执行算子类型如下所示:

算子类型说 明
控制算子处理特殊执行流程,如Union语句
扫描算子用于扫描表对象,从表中获取数据
物化算子缓存中间执行结果到临时存储
连接算子用于实现SQL中的各类连接操作,通常包含 nested loop join、bash join、merge-sort join等

扫描算子

  扫描算子用于表、结果集、链表子查询等结果遍历每次获取一条元组作为上层节点的输入。控制算子中的 BitmapAnd/BitmapOr 函数所需的位图与扫描算子(索引扫描算子)密切相关。主要包括顺序扫描(SeqScan)、索引扫描(IndexScan)、位图扫描(BitmapHeapScan)、位图索引扫描(BitmapIndexScan)、元组TID扫描(TIDScan)、子查询扫描(SubqueryScan)、函数扫描(FunctionScan)等。扫描算子如下表所示。

算子名称说 明
SeqScan算子用于扫描基础表
IndexScan算子对表的扫描使用索引加速元组获取
BitmapIndexScan算子通过位图索引做扫描操作
BitMapHeapScan算子通过位图获取实际元组
TIDScan算子遍历元组的物理存储位置获取一个元组
SubqueryScan算子子查询生成的子执行计划
FunctionScan算子用于从函数返回的数据集中获取元组
ValuesScan算子用于处理“Values(···),(···),···”类型语句,从值列表中输出元组
CteScan算子用于处理With表达式对应的子查询
WorkTableScan算子用于递归工作表元组输出
PartIteratorScan算子用于支持分区表的 wise join

SeqScan算子

  我们首先来看一下SeqScan算子,SeqScan 算子是最基本的扫描算子,对应 SeqScan 执行节点,对应的代码源文件是“src/gausskernel/runtime/executor/nodeSeqScan.cpp”,用于对基础表做顺序扫描。算子对应的主要函数如下表所示。

主要函数说 明
ExecInitSeqScan初始化SeqScan状态节点
ExecSeqScan迭代获取元组
ExecEndSeqScan清理SeqScan状态节点
ExecSeqMarkPos标记扫描位置
ExecSeqRestrPos重置扫描位置
ExecReScanSeqScan重置SeqScan
InitScanRelation初始化扫描表

  下面我们以一个案例来调试一下代码吧,首先执行sql语句:

postgres=# create table t2 (id int not null, name varchar);
CREATE TABLE
postgres=# insert into t2 values(1, 'Postgres');
INSERT 0 1
postgres=# insert into t2 values(2, 'OpenGauss');
INSERT 0 1
postgres=# select * from t2;

ExecInitSeqScan函数

  函数 ExecInitSeqScanOpenGauss 数据库中用于初始化顺序扫描算子(SeqScan)的函数。它是执行计划中实际执行算子的初始化过程,用于准备执行算子所需的状态和资源。在函数 ExecInitSeqScan 中打上断点进行调试
在这里插入图片描述
  我们来解读一下 ExecInitSeqScan 函数的源码吧:(路径:src/gausskernel/runtime/executor/nodeSeqscan.cpp
  在ExecInitSeqScan 函数中,有三个入参SeqScan* node, EState* estate, int eflags,下面分别来解释一下他们是什么:

  1. SeqScan* node:这是一个指向查询计划节点的指针,表示要初始化的顺序扫描算子节点。这个节点包含了构建查询计划所需的信息,如表信息、谓词条件等。
  2. EState* estate:这是一个指向执行状态的指针,表示当前查询的执行状态。执行状态包含了在查询执行过程中需要的各种上下文信息,如内存管理、参数信息等。
  3. int eflags:这是一个标志位,表示执行的选项和参数。可以用来控制执行算子的行为,如是否要在执行过程中收集统计信息等。
/* ----------------------------------------------------------------*		ExecInitSeqScan* ----------------------------------------------------------------*/
SeqScanState* ExecInitSeqScan(SeqScan* node, EState* estate, int eflags)
{TableSampleClause* tsc = NULL; /* 表样本子句 *//** 曾经有可能在 SeqScan 的 outerPlan 中,但现在不再允许。*/Assert(outerPlan(node) == NULL);Assert(innerPlan(node) == NULL);/** 创建状态结构体*/SeqScanState* scanstate = makeNode(SeqScanState);scanstate->ps.plan = (Plan*)node; /* 计划节点 */scanstate->ps.state = estate; /* 执行状态 */scanstate->isPartTbl = node->isPartTbl; /* 是否是分区表 */scanstate->currentSlot = 0; /* 当前迭代位置 */scanstate->partScanDirection = node->partScanDirection; /* 分区扫描方向 */scanstate->rangeScanInRedis = {false, 0, 0}; /* 分布时间的范围扫描 */if (!node->tablesample) {scanstate->isSampleScan = false; /* 是否是表样本扫描 */} else {scanstate->isSampleScan = true;tsc = node->tablesample;}/** 杂项初始化** 为节点创建表达式上下文*/ExecAssignExprContext(estate, &scanstate->ps);/** 初始化子表达式*/scanstate->ps.targetlist = (List*)ExecInitExpr((Expr*)node->plan.targetlist, (PlanState*)scanstate);// 判断是否需要进行表样本扫描,如果 node 结构中的 tablesample 成员非空,就说明需要进行表样本扫描if (node->tablesample) {scanstate->sampleScanInfo.args = (List*)ExecInitExpr((Expr*)tsc->args, (PlanState*)scanstate);scanstate->sampleScanInfo.repeatable = ExecInitExpr(tsc->repeatable, (PlanState*)scanstate);scanstate->sampleScanInfo.sampleType = tsc->sampleType; //设置表样本类型 sampleType/** 初始化 RowTableSample*/if (scanstate->sampleScanInfo.tsm_state == NULL) {scanstate->sampleScanInfo.tsm_state = New(CurrentMemoryContext) RowTableSample(scanstate);}}/** 元组表初始化*/ExecInitResultTupleSlot(estate, &scanstate->ps);ExecInitScanTupleSlot(estate, scanstate);InitScanRelation(scanstate, estate);ADIO_RUN(){/* 添加预取相关信息 */scanstate->ss_scanaccessor = (SeqScanAccessor*)palloc(sizeof(SeqScanAccessor));SeqScan_Init(scanstate->ss_currentScanDesc, scanstate->ss_scanaccessor, scanstate->ss_currentRelation);}ADIO_END();/** 初始化扫描关系*/// 调用InitSeqNextMtd函数初始化ScanState结构体中的ScanNextMtd函数指针,用于执行具体的扫描操作。InitSeqNextMtd(node, scanstate);// 判断是否存在扫描描述符if (IsValidScanDesc(scanstate->ss_currentScanDesc)) {// 初始化并行扫描的参数,包括指定的并行度、分区扫描方向等scan_handler_tbl_init_parallel_seqscan(scanstate->ss_currentScanDesc, scanstate->ps.plan->dop, scanstate->partScanDirection);} else {scanstate->ps.stubType = PST_Scan;}// 表示扫描结果并不是直接从目标列表(即查询的目标列)中获取的,而是从其他地方获取的scanstate->ps.ps_TupFromTlist = false;/** 初始化结果元组类型和投影信息。*/ExecAssignResultTypeFromTL(&scanstate->ps,scanstate->ss_currentRelation->rd_tam_type);ExecAssignScanProjectionInfo(scanstate);return scanstate;
}

  以为下SeqScan* node的结构,可以看到,执行节点(Node)中的 type 字段的值为 T_SeqScan ,这表示这个执行节点是一个顺序扫描(Sequential Scan)节点。
在这里插入图片描述
  以为下
EState* estate
的结构,而 EState* estate 中的 type 字段的值为 T_EState,它表示这个结构体是一个执行状态(Execution State)的对象。在数据库查询执行过程中,EState 存储了查询执行过程中的中间状态。
在这里插入图片描述
  可以看到,“ExecInitSeqScan” 函数的返回类型是 “SeqScanState” 结构,SeqScanState 结构体是 OpenGauss 数据库中用于执行顺序扫描操作的状态信息的结构体。在数据库查询执行过程中,每个执行节点都会有相应的状态结构体来存储节点执行过程中的中间状态结果等信息。对于顺序扫描操作,就是使用 SeqScanState 结构体来存储顺序扫描的状态信息。
  SeqScanState 结构体定义如下:(路径:src/include/nodes/execnodes.h

typedef struct ScanState {PlanState ps;                     /* 计划状态,首字段是NodeTag */Relation ss_currentRelation;      /* 当前关系 */TableScanDesc ss_currentScanDesc; /* 当前扫描描述符 */TupleTableSlot* ss_ScanTupleSlot;  /* 扫描元组插槽 */bool ss_ReScan;                   /* 是否重扫描 */Relation ss_currentPartition;     /* 当前分区关系 */bool isPartTbl;                   /* 是否是分区表 */int currentSlot;                  /* 当前迭代位置 */ScanDirection partScanDirection;  /* 分区扫描方向 */List* partitions;                 /* 分区列表 */LOCKMODE lockMode;                /* 锁模式 */List* runTimeParamPredicates;     /* 运行时参数谓词 */bool runTimePredicatesReady;      /* 运行时谓词是否准备好 */bool is_scan_end;                 /* 是否结束扫描,用于扫描使用信息性约束的情况 */SeqScanAccessor* ss_scanaccessor; /* 预取相关 */int part_id;                      /* 分区ID */int startPartitionId;             /* 并行线程的起始分区ID */int endPartitionId;               /* 并行线程的结束分区ID */RangeScanInRedis rangeScanInRedis; /* 是否为分布时间的范围扫描 */bool isSampleScan;                /* 是否是表样本扫描 */SampleScanParams sampleScanInfo;  /* TABLESAMPLE 参数,包括类型/种子/可重复性 */ExecScanAccessMtd ScanNextMtd;    /* 扫描下一个方法 */
} ScanState;/** SeqScan uses a bare ScanState as its state node, since it needs* no additional fields.*/
typedef ScanState SeqScanState;

  其中,ExecInitSeqScan 函数中的这段代码是在判断是否需要进行表样本扫描,如果 node 结构中的 tablesample 成员非空,就说明需要进行表样本扫描。

    if (node->tablesample) {scanstate->sampleScanInfo.args = (List*)ExecInitExpr((Expr*)tsc->args, (PlanState*)scanstate);scanstate->sampleScanInfo.repeatable = ExecInitExpr(tsc->repeatable, (PlanState*)scanstate);scanstate->sampleScanInfo.sampleType = tsc->sampleType; //设置表样本类型 sampleType/** 初始化 RowTableSample*/if (scanstate->sampleScanInfo.tsm_state == NULL) {scanstate->sampleScanInfo.tsm_state = New(CurrentMemoryContext) RowTableSample(scanstate);}}

注解:什么是表样本扫描?
  表样本扫描(Table Sample Scan)是一种数据库查询优化技术,用于在大型表中进行随机或者有规律的采样,以减少查询的执行时间。它适用于那些数据量庞大的表,通过从表中抽取一小部分数据进行查询,可以在不影响查询结果准确性的前提下,显著减少查询所需的时间和资源消耗。

InitScanRelation函数

  接着,进入到 InitScanRelation 函数中进行初始化扫描表。InitScanRelation 函数源码如下:(路径:src/gausskernel/runtime/executor/nodeSeqscan.cpp

/* ----------------------------------------------------------------*		InitScanRelation**		此函数对扫描关系和扫描的子计划进行初始化。* ----------------------------------------------------------------*/
void InitScanRelation(SeqScanState* node, EState* estate)
{Relation current_relation;                      // 当前关系Relation current_part_rel = NULL;               // 当前分区关系SeqScan* plan = NULL;                           // SeqScan计划节点bool is_target_rel = false;                     // 是否为目标关系LOCKMODE lockmode = AccessShareLock;            // 锁模式TableScanDesc current_scan_desc = NULL;         // 当前扫描描述// 判断当前扫描操作是否针对目标关系(即是否用于更新、删除等操作的目标表)is_target_rel = ExecRelationIsTargetRelation(estate, ((SeqScan*)node->ps.plan)->scanrelid);/** 从范围表的第scanrelid项获取关系对象id,打开该关系并在其上获取适当的锁。*/current_relation = ExecOpenScanRelation(estate, ((SeqScan*)node->ps.plan)->scanrelid);/** 元组表初始化*/ExecInitResultTupleSlot(estate, &node->ps, current_relation->rd_tam_type);ExecInitScanTupleSlot(estate, node, current_relation->rd_tam_type);// 判断当前查询是否针对分区表(partitioned table)进行的。// node->isPartTbl 是一个标志,用于指示当前查询是否是针对一个分区表。if (!node->isPartTbl) {/* 为redis添加限制条件 */// 初始化表扫描的描述符(TableScanDesc),以便开始执行表的扫描操作。current_scan_desc = InitBeginScan(node, current_relation);} else {// 将节点的计划部分转换为SeqScan类型,以便访问相关属性plan = (SeqScan*)node->ps.plan;/* 初始化分区列表 */node->partitions = NULL;// 如果当前关系不是目标关系,则将锁模式设置为AccessShareLockif (!is_target_rel) {lockmode = AccessShareLock;} else {switch (estate->es_plannedstmt->commandType) {case CMD_UPDATE:case CMD_DELETE:case CMD_MERGE:lockmode = RowExclusiveLock;break;case CMD_SELECT:lockmode = AccessShareLock;break;default:ereport(ERROR,(errcode(ERRCODE_INVALID_OPERATION),errmodule(MOD_EXECUTOR),errmsg("无效操作 %d 用于序列扫描的分区,允许的操作为 UPDATE/DELETE/SELECT",estate->es_plannedstmt->commandType)));break;}}node->lockMode = lockmode;/* 生成node->partitions(如果存在) */if (plan->itrs > 0) { // 如果有分区迭代数Partition part = NULL;PruningResult* resultPlan = NULL;// 如果分区裁剪信息中的表达式不为空,获取分区信息if (plan->pruningInfo->expr != NULL) {resultPlan = GetPartitionInfo(plan->pruningInfo, estate, current_relation);} else {resultPlan = plan->pruningInfo;}ListCell* cell = NULL;List* part_seqs = resultPlan->ls_rangeSelectedPartitions;// 遍历选中的分区序号foreach (cell, part_seqs) {Oid tablepartitionid = InvalidOid;int partSeq = lfirst_int(cell);// 根据序号获取分区的Oidtablepartitionid = getPartitionOidFromSequence(current_relation, partSeq);// 打开分区part = partitionOpen(current_relation, tablepartitionid, lockmode);// 将打开的分区加入到分区列表中node->partitions = lappend(node->partitions, part);}// 设置分区数量if (resultPlan->ls_rangeSelectedPartitions != NULL) {node->part_id = resultPlan->ls_rangeSelectedPartitions->length;} else {node->part_id = 0;}}// 如果有分区if (node->partitions != NIL) {/* 构造第一个分区的HeapScanDesc */// 获取第一个分区Partition currentPart = (Partition)list_nth(node->partitions, 0);// 获取分区关系current_part_rel = partitionGetRelation(current_relation, currentPart);// 设置当前分区关系node->ss_currentPartition = current_part_rel;/* 为redis添加限制条件 */// 初始化第一个分区的扫描描述符current_scan_desc = InitBeginScan(node, current_part_rel);} else {// 如果没有分区,则设置当前分区为NULLnode->ss_currentPartition = NULL;// 初始化扫描条件node->ps.qual = (List*)ExecInitExpr((Expr*)node->ps.plan->qual, (PlanState*)&node->ps);}}// 将当前正在扫描的关系对象(表)赋值给SeqScanState结构体中的ss_currentRelation成员变量,以表示当前扫描的是哪个关系。node->ss_currentRelation = current_relation;// 将扫描描述符赋值给SeqScanState结构体中的ss_currentScanDesc成员变量,以便后续的扫描操作可以使用该描述符。node->ss_currentScanDesc = current_scan_desc;// 对扫描到的数据进行正确的解析和存储ExecAssignScanType(node, RelationGetDescr(current_relation));
}

  在上述代码中,ExecRelationIsTargetRelation 函数作用是判断当前扫描操作是否针对目标关系(即是否用于更新、删除等操作的目标表)。ExecRelationIsTargetRelation 会检查给定的扫描计划节点是否是一个目标关系,如果是的话返回true,否则返回false。这里打印结果为false,表示不是一个目标关系。
在这里插入图片描述

注解:在数据库操作中,一个目标关系通常是指执行更新(UPDATE)删除(DELETE)插入(INSERT)等修改数据操作的表目标关系是被操作的主要表,对其进行的修改操作会直接影响到表中的数据。而非目标关系则是在操作过程中可能涉及到的其他表,但这些表不是直接受到操作影响的对象。
  在本案例中,执行 SQL 查询语句 SELECT * FROM t2; 时,如果查询的是表 t2 并且没有进行更新、删除等修改操作,那么这条查询语句并不会修改表的数据,因此表 t2 就不是一个目标关系。因此打印结果为false

  而 InitBeginScan(node, current_relation) 函数作用是初始化表扫描的描述符(TableScanDesc),以便开始执行表的扫描操作。具体来说,它会根据传入的参数 nodecurrent_relation ,生成一个用于表扫描的描述符。这个描述符包含了扫描相关的信息,比如扫描的表、锁模式等。然后,它将这个描述符返回,以便后续的表扫描操作可以使用。
  在这段代码中,如果 node 不是分区表(即 node->isPartTblfalse),则调用 InitBeginScan 来初始化扫描描述符,并将结果赋值给current_scan_desc。这意味着这段代码是在非分区表的情况下初始化表扫描描述符。如果是分区表,就不执行这个操作。
  源码及注释如下:(路径:src/gausskernel/runtime/executor/nodeSeqscan.cpp

static TableScanDesc InitBeginScan(SeqScanState* node, Relation current_relation)
{// 声明一个变量来存储表扫描描述符TableScanDesc current_scan_desc = NULL;// 如果不是表样本扫描if (!node->isSampleScan) {// 调用scan_handler_tbl_beginscan函数来初始化表扫描描述符// 参数包括要扫描的关系、当前事务的快照、初始块号、分布式查询所需的信息,以及扫描状态current_scan_desc = scan_handler_tbl_beginscan(current_relation, node->ps.state->es_snapshot, 0, NULL, (ScanState*)node);} else {// 如果是表样本扫描,则调用InitSampleScanDesc函数来初始化表样本扫描描述符current_scan_desc = InitSampleScanDesc((ScanState*)node, current_relation);}// 返回初始化后的表扫描描述符return current_scan_desc;
}

ExecSeqScan函数

  ExecSeqScan 函数是 OpenGauss 中用于执行序列扫描(Sequential Scan)操作的函数。它执行基于序列扫描的查询计划,获取满足扫描条件的数据,并将结果放入一个元组表槽(TupleTableSlot)中返回。
  其函数调用关系如下:
在这里插入图片描述
   ExecSeqScan 函数实际上是通过调用通用的扫描函数 ExecScan 来执行具体的序列扫描操作。源码如下:(路径:src/gausskernel/runtime/executor/nodeSeqscan.cpp

/* ----------------------------------------------------------------*		ExecSeqScan(node)**		Scans the relation sequentially and returns the next qualifying*		tuple.*		We call the ExecScan() routine and pass it the appropriate*		access method functions.* ----------------------------------------------------------------*/
TupleTableSlot* ExecSeqScan(SeqScanState* node)
{return ExecScan((ScanState*)node, node->ScanNextMtd, (ExecScanRecheckMtd)SeqRecheck);
}

  其三个入参的描述如下所示:

  1. node: SeqScanState* 类型,表示序列扫描操作的状态信息,其中包含了查询计划的相关信息以及当前扫描的状态。
  2. node->ScanNextMtd: 是一个函数指针,指向具体的序列扫描操作的实现函数,用于获取下一个满足条件的元组。
  3. (ExecScanRecheckMtd)SeqRecheck: 一个函数指针,指向重新检查操作的函数,用于在扫描过程中需要重新检查的情况下执行。

  TupleTableSlot 结构体是在数据库查询执行过程中用于存储元组数据的数据结构。它是一个用于临时存储从表中检索到的数据行(元组)的容器,通常在查询执行的不同阶段中用于传递处理数据。来看一看 TupleTableSlot 结构体长什么样吧:(路径:src/include/executor/tuptable.h

typedef struct TupleTableSlot {NodeTag type;                  /* 节点类型,用于标识数据结构类型 */bool tts_isempty;              /* true = 槽为空 */bool tts_shouldFree;           /* 是否应该释放 tts_tuple 内存? */bool tts_shouldFreeMin;        /* 是否应该释放 tts_mintuple 内存? */bool tts_slow;                 /* 用于 slot_deform_tuple 的保存状态 */Tuple tts_tuple;               /* 物理元组,如果是虚拟的则为 NULL */#ifdef PGXC/** PGXC 扩展,用于支持从远程 Datanode 发送的元组。*/char* tts_dataRow;             /* DataRow 格式的元组数据 */int tts_dataLen;               /* 数据行的实际长度 */bool tts_shouldFreeRow;        /* 是否应该释放 tts_dataRow 内存? */struct AttInMetadata* tts_attinmeta; /* 存储从 DataRow 中提取值所需的信息 */Oid tts_xcnodeoid;             /* 从哪个节点获取的数据行的 Oid */MemoryContext tts_per_tuple_mcxt;
#endifTupleDesc tts_tupleDescriptor; /* 槽的元组描述 */MemoryContext tts_mcxt;        /* 槽本身所在的内存上下文 */Buffer tts_buffer;             /* 元组的缓冲区,如果没有则为 InvalidBuffer */int tts_nvalid;                /* tts_values 中的有效值数 */Datum* tts_values;             /* 当前每个属性的值 */bool* tts_isnull;              /* 当前每个属性的是否为 NULL 的标志 */MinimalTuple tts_mintuple;     /* 最小元组,如果没有则为 NULL */HeapTupleData tts_minhdr;      /* 仅用于最小元组的情况下的工作空间 */long tts_off;                  /* 用于 slot_deform_tuple 的保存状态 */long tts_meta_off;             /* 用于 slot_deform_cmpr_tuple 的保存状态 */TableAmType tts_tupslotTableAm; /* 槽的元组表类型 */
} TupleTableSlot;

  调试信息如下:
在这里插入图片描述

  其中,TupleTableSlot 结构体中的属性 tts_values 可以用于存储元组的具体值。这是一个指向 Datum 数组的指针,其中每个元素对应于元组的每个属性的值。在 PostgreSQL 中,Datum 是一个通用的数据类型,可以表示各种数据类型的值。
  tts_isnull 是一个 bool 数组的指针,用于标识对应属性是否为 NULL。如果 tts_isnull[i]true,表示属性值是 NULL;如果为 false,则表示属性值不是 NULL
  这两个数组一起,构成了一个完整的元组的值和 NULL 信息的表示。通过遍历 tts_values 数组和 tts_isnull 数组,可以访问元组的每个属性的值和是否为 NULL 的信息。

不理解??没关系,来看一个案例吧:

  假设有一个关系表 students 包含以下几个属性:student_id(整数)、name(字符串)、age(整数)和 grade(字符串)。我们可以使用 TupleTableSlot 结构体来表示从该表中获取的元组。
  假设我们从 students 表中获取了一个元组,具体的值如下:

属性
student_id101
nameAlice
age25
gradeA

  我们可以将这个元组的值存储在 TupleTableSlot 结构体的 tts_valuestts_isnull 数组中,以便后续处理。具体表示如下:

tts_values 数组:

索引
索引 0101(student_id)
索引 1Alice(name)
索引 225(age)
索引 3A(grade)

tts_isnull 数组:

索引
索引 0false(student_id 不为 NULL)
索引 1false(name 不为 NULL)
索引 2false(age 不为 NULL)
索引 3false(grade 不为 NULL)

  通过这两个数组,我们可以知道元组的每个属性的值和是否为 NULL,进而在执行操作时进行处理和判断。

  ExecScan 函数源码如下:(路径:src/gausskernel/runtime/executor/execScan.cpp

/* ----------------------------------------------------------------*		ExecScan**		使用指定的 '访问方法' 扫描关系,根据全局变量 ExecDirection 返回下一个符合条件的元组。*		访问方法返回下一个元组,execScan() 负责将返回的元组与限定条件进行检查。**		还必须提供一个 '重新检查方法',它可以检查关系的任意元组是否符合实现在访问方法内部的任何限定条件。**		条件:*		  -- AMI 维护的 "游标" 定位在之前返回的元组。**		初始状态:*		  -- 指定的关系已打开进行扫描,以便 "游标" 定位在第一个符合条件的元组之前。* ----------------------------------------------------------------*/
TupleTableSlot* ExecScan(ScanState* node, ExecScanAccessMtd access_mtd, /* 返回元组的函数 */ExecScanRecheckMtd recheck_mtd)
{ExprContext* econtext = NULL;  /* 表达式计算上下文 */List* qual = NIL;              /* 限定条件 */ProjectionInfo* proj_info = NULL;  /* 投影信息 */ExprDoneCond is_done;          /* 判断投影是否结束的标志 */TupleTableSlot* result_slot = NULL;  /* 存储结果的槽 */if (node->isPartTbl && !PointerIsValid(node->partitions))return NULL;/** 获取来自节点的数据*/qual = node->ps.qual;proj_info = node->ps.ps_ProjInfo;econtext = node->ps.ps_ExprContext;/** 如果既没有需要检查的限定条件,也没有需要进行投影的操作,直接跳过全部开销,返回原始扫描元组。*/if (qual == NULL && proj_info == NULL) {ResetExprContext(econtext);return ExecScanFetch(node, access_mtd, recheck_mtd);}/** 检查是否仍在从之前的扫描元组中投影出元组(因为在投影表达式中存在函数返回集合的情况)。如果是,则尝试投影另一个。*/if (node->ps.ps_TupFromTlist) {Assert(proj_info); /* 如果不进行投影,不会到达这里 */result_slot = ExecProject(proj_info, &is_done);if (is_done == ExprMultipleResult)return result_slot;/* 完成了这个源元组... */node->ps.ps_TupFromTlist = false;}/** @hdfs* 通过使用信息性约束来优化扫描。* 如果 is_scan_end 为 true,则迭代结束。*/if (node->is_scan_end) {return NULL;}/** 重置每个元组内存上下文,以释放在上一个元组循环中分配的表达式计算存储。* 注意,这只有在我们完成了从扫描元组投影出元组后才能发生。*/ResetExprContext(econtext);/** 从访问方法中获取一个元组。循环直到获取到符合条件的元组为止。*/for (;;) {TupleTableSlot* slot = NULL;CHECK_FOR_INTERRUPTS();slot = ExecScanFetch(node, access_mtd, recheck_mtd);/* 在每次循环中刷新限定条件 */qual = node->ps.qual;/** 如果 accessMtd 返回的 slot 包含 NULL,那么意味着没有更多要扫描的元组,因此我们只返回一个空的槽,* 要小心使用投影结果槽,以便其具有正确的 tupleDesc。*/if (TupIsNull(slot) || unlikely(executorEarlyStop())) {if (proj_info != NULL)return ExecClearTuple(proj_info->pi_slot);elsereturn slot;}/** 将当前元组放入表达式上下文中*/econtext->ecxt_scantuple = slot;/** 检查当前元组是否符合限定条件** 在此检查非空限定条件,以避免在限定条件为空时调用 ExecQual() 函数的开销... 节省一些循环周期,但它们会累积...*/if (qual == NULL || ExecQual(qual, econtext, false)) {/** 找到了满足条件的扫描元组。*/if (proj_info != NULL) {/** 构造投影元组,将其存储在结果元组槽中并返回,除非我们发现无法从这个扫描元组中投影出元组,* 在这种情况下继续扫描。*/result_slot = ExecProject(proj_info, &is_done);
#ifdef PGXC/* 如果底层扫描的槽具有 xcnodeoid,则复制 xcnodeoid */result_slot->tts_xcnodeoid = slot->tts_xcnodeoid;
#endif /* PGXC */if (is_done != ExprEndResult) {node->ps.ps_TupFromTlist = (is_done == ExprMultipleResult);/** @hdfs* 通过使用信息性约束来优化外部扫描。*/if (IsA(node->ps.plan, ForeignScan)) {ForeignScan* foreign_scan = (ForeignScan*)(node->ps.plan);if (foreign_scan->scan.scan_qual_optimized) {/** 如果找到了合适的元组,则设置 is_scan_end 值为 true。* 这意味着在下一次迭代中找不到合适的元组,迭代结束。*/node->is_scan_end = true;}}return result_slot;}} else {/** 通过使用信息性约束来优化外部扫描。*/if (IsA(node->ps.plan, ForeignScan)) {ForeignScan* foreign_scan = (ForeignScan*)(node->ps.plan);if (foreign_scan->scan.scan_qual_optimized) {/** 如果找到了合适的元组,则设置 is_scan_end 值为 true。* 这意味着在下一次迭代中找不到合适的元组,迭代结束。*/node->is_scan_end = true;}}/** 在这里,我们不进行投影,因此直接返回扫描元组。*/return slot;}} elseInstrCountFiltered1(node, 1);/** 元组不符合限定条件,因此释放每个元组内存并重试。*/ResetExprContext(econtext);}
}

  在执行查询计划时,扫描节点会通过 ExecScan 函数调用 ExecScanFetch 函数来获取符合条件的元组,然后再进行投影过滤等操作。
  ExecScanFetch 函数的核心任务是从关系中获取下一个元组,以及根据重新检查方法检查该元组是否满足内部的限定条件。这个函数的逻辑会根据当前的扫描方向(正向或反向)和访问方法的实现来决定如何获取下一个元组。
  我们来详细的看一看 ExecScanFetch 函数到底是怎样运行的吧, ExecScanFetch 函数源码如下:(路径:src/gausskernel/runtime/executor/execScan.cpp

/** ExecScanFetch -- 获取下一个潜在元组** 这个函数主要用于在 EvalPlanQual 重新检查时替换测试元组。* 如果不是在 EvalPlanQual 中,就执行访问方法的下一个元组操作。*/
static TupleTableSlot* ExecScanFetch(ScanState* node, ExecScanAccessMtd access_mtd, ExecScanRecheckMtd recheck_mtd)
{// 从node的执行状态结构体中获取执行状态estateEState* estate = node->ps.state;// 检查是否存在 EvalPlanQual(执行计划中的计划块),即是否正在执行 EvalPlanQual 重新检查if (estate->es_epqTuple != NULL) {/** 我们在 EvalPlanQual 重新检查中。如果有测试元组可用,就在重新检查任何访问方法特定条件后返回该测试元组。*/Index scan_rel_id = ((Scan*)node->ps.plan)->scanrelid;Assert(scan_rel_id > 0);if (estate->es_epqTupleSet[scan_rel_id - 1]) {TupleTableSlot* slot = node->ss_ScanTupleSlot;/* 如果我们已经返回了一个元组,则返回空槽 */if (estate->es_epqScanDone[scan_rel_id - 1])return ExecClearTuple(slot);/* 否则标记以记住不应该再返回更多元组 */estate->es_epqScanDone[scan_rel_id - 1] = true;/* 如果没有测试元组,就返回空槽 */if (estate->es_epqTuple[scan_rel_id - 1] == NULL)return ExecClearTuple(slot);/* 将测试元组存储在计划节点的扫描槽中 */(void)ExecStoreTuple(estate->es_epqTuple[scan_rel_id - 1], slot, InvalidBuffer, false);/* 检查是否满足访问方法的条件 */if (!(*recheck_mtd)(node, slot))(void)ExecClearTuple(slot); /* 不会被扫描返回 */return slot;}}/** 运行节点类型特定的访问方法函数来获取下一个元组*/return (*access_mtd)(node);
}

  再来看看 (*access_mtd)(node); 做了什么:SeqNext 函数是执行顺序扫描的核心工作函数。它从扫描描述中获取扫描信息和状态,然后通过调用 scan_handler_tbl_getnext 函数从表中获取下一个元组。获取到的元组会被保存到扫描元组槽中,并返回该槽。在这个过程中,还涉及到预取操作,以及缓冲区引用计数的处理。
  函数指针定义如下:(路径:src/gausskernel/runtime/executor/nodeSeqscan.cpp

static TupleTableSlot* SeqNext(SeqScanState* node);
/* ----------------------------------------------------------------*						Scan Support* ----------------------------------------------------------------*/
/* ----------------------------------------------------------------*		SeqNext**		这是 ExecSeqScan 的工作函数* ----------------------------------------------------------------*/
static TupleTableSlot* SeqNext(SeqScanState* node)
{Tuple tuple;TableScanDesc scanDesc;EState* estate = NULL;ScanDirection direction;TupleTableSlot* slot = NULL;/** 从 estate 和扫描状态中获取信息*/scanDesc = node->ss_currentScanDesc;estate = node->ps.state;direction = estate->es_direction;slot = node->ss_ScanTupleSlot;GetTableScanDesc(scanDesc, node->ss_currentRelation)->rs_ss_accessor = node->ss_scanaccessor;/** 从表中获取下一个元组用于顺序扫描。*/tuple = scan_handler_tbl_getnext(scanDesc, direction, node->ss_currentRelation);ADIO_RUN(){Start_Prefetch(GetTableScanDesc(scanDesc, node->ss_currentRelation), node->ss_scanaccessor, direction);}ADIO_END();/** 将返回的元组和缓冲区保存在扫描元组槽中,并返回该槽。* 注意:我们传递 'false',因为由 heap_getnext() 返回的元组是指向磁盘页上的指针,不是使用 palloc() 创建的,* 因此不应使用 pfree_ext() 进行释放。另外注意,ExecStoreTuple 将增加缓冲区的引用计数;引用计数将不会减少,* 直到清除元组表槽为止。*/return ExecMakeTupleSlot(tuple, GetTableScanDesc(scanDesc, node->ss_currentRelation), slot, node->ss_currentRelation->rd_tam_type);
}

  SeqNext 函数中 使用 GetTableScanDesc 函数来处理分桶表(Bucketed Table)的情况GetTableScanDesc 获取表扫描描述符(TableScanDesc)的函数,它根据输入的扫描描述符和关系(表)来返回对应的有效的扫描描述符。

注解:分桶表Bucketed Table)是一种数据库表的存储组织方式,主要用于在分布式数据库系统中提高查询性能和并行处理能力。分桶表将表的数据按照某个列的值分成多个桶(bucket),每个桶包含相近的数据,从而可以使查询和分析操作更加高效。

函数源码如下:(路径:src/gausskernel/storage/access/hbstore/hbucket_am.cpp

TableScanDesc GetTableScanDesc(TableScanDesc scan, Relation rel)
{if (scan != NULL && rel != NULL && RELATION_CREATE_BUCKET(scan->rs_rd)) {return (TableScanDesc)((HBktTblScanDesc)scan)->currBktScan;} else {return scan;}
}

  该函数返回一个 TableScanDesc 类型的指针,表示有效的扫描描述符。调试结果如下:
在这里插入图片描述
  SeqNext 函数调用 ExecMakeTupleSlot 函数主要用于将获取到的元组(tuple)存储到一个元组表槽(TupleTableSlot)中,同时关联元组的信息,如元组来自的扫描描述符关联的缓冲区等。这个函数在数据库查询执行过程中扮演了重要的角色,用于将从数据库表中获取的数据以一种可管理的形式存储在内存中,以便后续的处理和返回。
  函数定义如下:(路径:src/gausskernel/runtime/executor/execTuples.cpp

TupleTableSlot* ExecMakeTupleSlot(Tuple tuple, TableScanDesc tableScan, TupleTableSlot* slot, TableAmType tableAm)
{if (unlikely(RELATION_CREATE_BUCKET(tableScan->rs_rd))) {tableScan = ((HBktTblScanDesc)tableScan)->currBktScan;}if (tuple != NULL) {Assert(tableScan != NULL);slot->tts_tupslotTableAm = tableAm;return ExecStoreTuple(tuple, /* tuple to store */slot, /* slot to store in */tableScan->rs_cbuf, /* buffer associated with this tuple */false); /* don't pfree this pointer */}return ExecClearTuple(slot);
}

  在函数 ExecMakeTupleSlot 中,ExecStoreTuple 函数用于将一个元组存储到一个 TupleTableSlot 中。TupleTableSlot 是一种用于存储元组数据的数据结构,通常用于在查询计划的不同节点之间传递元组数据。
  源码如下:(路径:src/gausskernel/runtime/executor/execTuples.cpp

/* --------------------------------*		ExecStoreTuple**		This function is used to store a physical tuple into a specified*		slot in the tuple table.**		tuple:	tuple to store*		slot:	slot to store it in*		buffer: disk buffer if tuple is in a disk page, else InvalidBuffer*		shouldFree: true if ExecClearTuple should pfree_ext() the tuple*					when done with it** If 'buffer' is not InvalidBuffer, the tuple table code acquires a pin* on the buffer which is held until the slot is cleared, so that the tuple* won't go away on us.** shouldFree is normally set 'true' for tuples constructed on-the-fly.* It must always be 'false' for tuples that are stored in disk pages,* since we don't want to try to pfree those.** Another case where it is 'false' is when the referenced tuple is held* in a tuple table slot belonging to a lower-level executor Proc node.* In this case the lower-level slot retains ownership and responsibility* for eventually releasing the tuple.	When this method is used, we must* be certain that the upper-level Proc node will lose interest in the tuple* sooner than the lower-level one does!  If you're not certain, copy the* lower-level tuple with heap_copytuple and let the upper-level table* slot assume ownership of the copy!** Return value is just the passed-in slot pointer.** NOTE: before PostgreSQL 8.1, this function would accept a NULL tuple* pointer and effectively behave like ExecClearTuple (though you could* still specify a buffer to pin, which would be an odd combination).* This saved a couple lines of code in a few places, but seemed more likely* to mask logic errors than to be really useful, so it's now disallowed.* --------------------------------*/
TupleTableSlot* ExecStoreTuple(Tuple tuple, TupleTableSlot* slot, Buffer buffer, bool should_free)
{/** sanity checks*/Assert(tuple != NULL);Assert(slot != NULL);Assert(slot->tts_tupleDescriptor != NULL);tableam_tslot_store_tuple(tuple, slot, buffer, should_free);return slot;
}

总结

  ExecInitSeqScan 函数初始化 SeqScan 状态节点,负责节点状态结构构造,并初始化用于存储结果的元组表。
  ExecSeqScan 函数是 SeqScan 算子执行的主体函数,用于迭代获取每一个元组ExecSeqScan 函数通过回调函数调用SeqNext 函数、HbktSeqSampleNext 函数、SeqSampleNext 函数实现获取元组。非采样获取元组时调用 SeqNext 函数;如果需要采样且对应的表采用哈希桶方式存储则调用 HbktSeqSampleNext 函数,否则调用 SeqSampleNext 函数。
  以下是 SeqScan 算子的执行过程的完整阐述:

  1. 查询解析和分析: 在 PostgreSQL 中,查询首先经过解析和分析阶段,确定了要查询的表、选择的列以及过滤条件等。
  2. 执行计划生成: 根据解析和分析的结果,系统生成一个查询执行计划。在这个阶段,PostgreSQL 的查询优化器会考虑各种访问路径、连接顺序和操作顺序,以生成一个优化的执行计划。
  3. 初始化执行环境: 执行计划生成后,会为查询执行创建一个执行环境(EState),其中包括查询的状态信息、内存上下文等。此时还会初始化一些查询中使用到的辅助数据结构。
  4. 初始化 SeqScanState: 对于 SeqScan 算子,会创建一个 SeqScanState 结构,用于存储该算子的执行状态。这个结构包含了 ScanState 的通用字段,以及一些特定于 SeqScan 的字段,如当前的扫描状态、分区信息等。
  5. 初始化扫描关系: SeqScan 需要初始化扫描关系,即要从哪个表中扫描数据。这包括打开表、获取相关的元数据等操作。
  6. 初始化元组插槽: 在查询执行过程中,需要使用 TupleTableSlot 来存储和传递元组数据。SeqScanState 中会初始化一个 TupleTableSlot,用于存储从表中读取的元组数据。
  7. 执行扫描: 正式进入执行阶段,SeqScan 开始逐行地扫描数据。具体步骤如下:
  • 获取下一个扫描元组:通过底层的存储访问接口获取表中的下一个元组。
  • 将元组存储到插槽:将扫描到的元组存储到初始化的 TupleTableSlot 中
    以便后续操作使用。
  • 应用过滤条件:如果查询中存在过滤条件(WHERE 子句),会对当前扫描到的元组应用过滤条件,确定是否满足条件。
  • 如果满足条件,元组会被返回给上层的操作节点进行进一步处理,例如投影、排序等。
  • 如果不满足条件,会继续扫描下一个元组。
  1. 结束扫描: 一旦所有的元组都被扫描完毕,或者查询的其他操作已经得到了结果,SeqScan 算子的执行就会结束。这时会释放相关的资源,关闭表,清理内存等操作。
  2. 返回结果: 执行完成后,根据查询的需要,可能会返回一个结果集或者进行其他的操作,如数据插入、更新等。

相关文章:

【OpenGauss源码学习 —— 执行算子(SeqScan算子)】

执行算子(SeqScan算子) 执行算子概述扫描算子SeqScan算子ExecInitSeqScan函数InitScanRelation函数ExecSeqScan函数 总结 声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵…...

Postman中,既想传递文件,还想传递多个参数(后端)

需求:既想传文件又想传多个参数可以用以下方式实现...

跨境干货|TikTok变现的9种方法

在这个流量为王的时代,哪里有流量,哪里就有商机。TikTok作为近几年最火爆的社媒平台之一,在全球范围都具有一定的影响力。随着TikTok Shop等商务功能加持上线,更是称为跨境电商的新主场之一。 在这样的UGC平台,想要变…...

Grafana 曲线图报错“parse_exception: Encountered...”

问题现象 配置的Grafana图报错如下: 原因分析 点开报错,可以看到报错详细信息,是查询语句的语法出现了异常。 变量pool的取值为None 解决方案 需要修改变量pool的查询SQL,修改效果如下: 修改后&#x…...

idea中提示Unsupported characters for the charset ‘ISO-8859-1‘

application.properties中文注释拉黄线 ,提示Unsupported characters for the charset ISO-8859-1 解决办法: 注意: 改完之后之前输入的中文就变成“ ???”了,建议备份一下 1、打开setti…...

通过signtool进行数字签名和验证签名

(一)如何签名 SignTool.exe (Sign Tool) - .NET Framework | Microsoft Learn Using SignTool to Sign a File - Win32 apps | Microsoft Learn 签名命令行: signtool.exe sign /f xxx.pfx /t http://timestamp.digicert.com yyy.dll xx…...

geeemap学习总结(2)——地图底图应用

1. 加载库中已有图层 import os os.environ[HTTP_PROXY] http://127.0.0.1:8001 os.environ[HTTPS_PROXY] http://127.0.0.1:8001 # 设置中心位置/地图层级/图层加载高度,加载图层 import geemap Mapgeemap.Map(center[40, 100], zoom4, height600) Map# 添加已经…...

flutter 手写日历组件

先看效果 直接上代码 calendar_popup_view.dart import package:flutter/material.dart; import package:intl/intl.dart;import custom_calendar.dart; import hotel_app_theme.dart;class CalendarPopupView extends StatefulWidget {const CalendarPopupView({required th…...

C++动态规划经典试题解析之打家劫舍系列

1.前言 力扣上有几道与打家劫舍相关的题目,算是学习动态规划时常被提及的经典试题,很有代表性,常在因内大大小小的社区内看到众人对此类问题的讨论。 学习最好的方式便是归纳总结、借鉴消化,基于这个目的,本文对此类问题也做了讲解,在一些优秀思想的基础上添加了个人观…...

24届近5年东南大学自动化考研院校分析

今天给大家带来的是东南大学控制考研分析 满满干货~还不快快点赞收藏 一、东南大学 学校简介 东南大学是我国最早建立的高等学府之一,素有“学府圣地”和“东南学府第一流”之美誉。东南大学前身是创建于1902年的三江师范学堂。1921年经近代著名教育家…...

electron、electron-forge 安装

npm修改了registry,安装依旧无效 使用cnpm 倒是可以解决,但是 npx electron-forge import 中 Installing dependencies 使用的是npm 给出一次性解决方案: step1:切换npm的下载源,可以使用nrm 进行管理,有…...

go的strings用法

strings 是 Go 语言标准库中提供的一个包,用于处理字符串相关的操作。这个包包含了许多函数,可以用于字符串的切割、拼接、替换、查找等操作。下面是一些常用的 strings 包函数和用法示例: package mainimport ("fmt""string…...

echo用法、linxu课堂练习题、作业题

一、课堂练习 练习一: 4、普通用户修改密码: root修改密码: 5、修改主机名:hostnamectl hostname 主机名 查看:hostnamectl或者cat etc/hostname 练习二: 1、 mkdir /root/html touch /root/html/index.…...

WordPress使用【前端投稿】功能时为用户怎么添加插入文章标签

在使用Wordpress做前端投稿功能的时候,可能需要用户填写文章标签,在插入文章的时候很多人不知道怎么把这些标签插入进去,下面这篇文章来为大家带来WordPress使用前端投稿功能时插入文章标签方法。 在Wordpress里 wp_insert_post 此函数的作…...

第二章:CSS基础进阶-part1:CSS高级选择器

文章目录 一、 组合选择器二、属性选择器三、伪类选择器1、动态伪类选择器2、状态伪类选择器3、结构性伪类选择器4、否定伪类选择器 一、 组合选择器 后代选择器:E F子元素选择器: E>F相邻兄弟选择器:EF群组选择器:多个选择器…...

js 正则表达式 限制input元素内容必须以abc开头,123结尾

要通过正则表达式验证一个输入元素的内容是否以"abc"开头且以"123"结尾,您可以使用 ^ 表示开头,$ 表示结尾,以及适当的字符类或具体字符。以下是一个示例正则表达式: var regex /^abc.*123$/;上面的正则表达…...

Linux下安装nginx (tar解压版安装)

Linux下安装nginx (tar安装) 1、下载nginx 官方下载地址https://nginx.org/en/download.html 在这里插入图片描述 2.解压 解压‘nginx-1.16.1.tar.gz’到指定目录(/usr/local/myWorkSpace)并且重命名 命令: tar -xvf nginx-1.16.1.tar.gz …...

不同组件之间相互传递信息的方式(拓展知识)

文章目录 🐒个人主页🏅JavaEE系列专栏📖前言:🏨补充知识:不同组件之间通过get()方式传递信息 🎀父组件与子组件之间的信息交互 $emit 方法🏅父组件给子组件发…...

idea找不到DataBase

一、我想把数据库跟我的idea链接,结果发现找不到。如图。 二、解决方案 找到 file ---setting 找到plugin------找到marketplace 我的已经出现了...

研发工程师玩转Kubernetes——PVC使用Label和storage选择PV

在《研发工程师玩转Kubernetes——local型PV和PVC绑定过程中的状态变化》和《研发工程师玩转Kubernetes——使用local型PV在不同Pod上共享数据》中,我们介绍了指定VPC的spec.volumeName为PV名称来绑定它们的方法。本文将介绍PVC在创建时,系统自动选择绑定…...

【VUE】localStorage、indexedDB跨域数据操作实战笔记

由于业务需求&#xff0c;最近研究localStorage、indexedDB等如何跨域进行CRUD管理&#xff0c;经过一番研究&#xff0c;封装了如下代码并做个笔记 环境 vue: ^3.3.4 实战 发送端(即触发站点) 在App.vue中引入CrossDomainStorage组件(后面有实现过程) <script setup&g…...

四、web应用程序技术——HTTP

文章目录 1 HTTP请求2 HTTP响应3 HTTP方法4 URL5 HTTP消息头5.1 常用消息头5.2 请求消息头5.3 响应消息头 6 cookie7 状态码8 HTTP代理9 HTTP身份验证 HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;是访问万维网使用的核心通信协议&…...

B2B2C小程序商城系统--跨境电商后台数据采集功能开发

搭建一个B2B2C小程序商城系统涉及到多个步骤和功能开发&#xff0c;其中包括跨境电商后台数据采集功能的开发。具体搭建步骤如下&#xff1a; 一、系统搭建 1. 确定需求和功能&#xff1a;根据B2B2C商城的需求&#xff0c;确定系统的功能和模块&#xff0c;包括商品管理、订单…...

Python-OpenCV中的图像处理-形态学转换

Python-OpenCV中的图像处理-形态学转换 形态学转换腐蚀膨胀开运算闭运算形态学梯度礼帽黑帽形态学操作之间的关系 形态学代码例程 形态学转换 形态学操作:腐蚀&#xff0c;膨胀&#xff0c;开运算&#xff0c;闭运算&#xff0c;形态学梯度&#xff0c;礼帽&#xff0c;黑帽等…...

理解 Python 的 for 循环

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 在本篇博客中&#xff0c;我们将讨论 Python 中 for 循环的原理。 我们将从一组基本例子和它的语法开始&#xff0c;还将讨论与 for 循环关联的 else 代码块的用处。 然后我们将介绍迭代对象、迭代器和迭代器协议&…...

携程验证码

今日话题&#xff1a;凑字数水文章。大表哥们感兴趣可以看看。 携程验证类型总共有3种。无感&#xff0c;滑块&#xff0c;点选。 process_type&#xff1a;None为无感 验证接口&#xff1a;https://ic.ctrip.com/captcha/v4/risk_inspect process_type&#xff1a;JIGSAW为…...

资深媒体人宋繁银加入《数据猿》任总编辑,全面负责公司整体内容工作

大数据产业创新服务媒体 ——聚焦数据 改变商业 2023年7月北京&#xff0c;《数据猿》宣布正式任命宋繁银为总编辑&#xff0c;全面负责公司整体内容工作。此次重要的人事任命标志着《数据猿》的发展迈上了一个新的台阶&#xff0c;对于《数据猿》团队而言&#xff0c;不仅是一…...

【Unity实战100例】人物状态栏UI数据刷新—MVC观察者模式

目录 一.创建Model层数据模型 二.创建View层关联UI组件 三.创建Controller层使得V和M数据关联 源码:htt...

8路AD采集FMC子卡【产品资料】

FMC148是一款基于VITA57.4标准的JESD204B接口FMC子卡模块&#xff0c;该模块可以实现8路14-bit、500MSPS/1GSPS/1.25GSPS ADC采集功能。该板卡ADC器件采用ADI公司的AD9680芯片,全功率-3dB模拟输入带宽可达2GHz。该ADC与FPGA的主机接口通过16通道的高速串行GTX收发器进行互联。 …...

文章三:团队协作实践 - 协作高手:Git团队开发最佳实践

开始本篇文章之前先推荐一个好用的学习工具&#xff0c;AIRIght&#xff0c;借助于AI助手工具&#xff0c;学习事半功倍。欢迎访问&#xff1a;http://airight.fun 概述 在现代软件开发中&#xff0c;团队协作是必不可少的环节。而Git作为目前最受欢迎的分布式版本控制系统&a…...