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

执行算子(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. 返回结果: 执行完成后,根据查询的需要,可能会返回一个结果集或者进行其他的操作,如数据插入、更新等。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Javascript 正则

基本语法 定义 JavaScript种正则表达式有两种定义方式 构造函数 var regnew RegExp(<%[^%>]%>,g);字面量 var reg/<%[^%>]%>/g;g&#xff1a; global&#xff0c;全文搜索&#xff0c;默认搜索到第一个结果接停止i&#xff1a;ingore case&#xff0c;忽略…

WebRTC | 实现数据流的一对一通信

目录 一、浏览器对WebRTC的支持 二、MediaStream与MediaStreamTrack 三、RTCPeerConnection 1. RTCPeerConnection与本地音视频数据绑定 2. 媒体协商SDP 3. ICE &#xff08;1&#xff09;Candidate信息 &#xff08;2&#xff09;WebRTC收集Candidate &#xff08;3&…

【Matlab】极限学习机-遗传算法(ELM-GA)函数极值寻优——非线性函数求极值

往期博客&#x1f449; 【Matlab】BP神经网络遗传算法(BP-GA)函数极值寻优——非线性函数求极值 【Matlab】GRNN神经网络遗传算法(GRNN-GA)函数极值寻优——非线性函数求极值 【Matlab】RBF神经网络遗传算法(RBF-GA)函数极值寻优——非线性函数求极值 【Matlab】Elman神经网络遗…

MySQL:内置函数、复合查询和内外连接

内置函数 select 函数; 日期函数 字符串函数 数学函数 其它函数 复合查询&#xff08;多表查询&#xff09; 实际开发中往往数据来自不同的表&#xff0c;所以需要多表查询。本节我们用一个简单的公司管理系统&#xff0c;有三张 表EMP,DEPT,SALGRADE来演示如何进行多表查询…

无涯教程-Perl - int函数

描述 此函数返回EXPR的整数元素,如果省略则返回$_。 int函数不进行舍入。如果需要将值四舍五入为整数,则应使用sprintf。 语法 以下是此函数的简单语法- int EXPRint返回值 此函数返回EXPR的整数部分。 例 以下是显示其基本用法的示例代码- #!/usr/bin/perl$int_valint…

使用Spring Initializr方式构建Spring Boot项目

除了可以使用Maven方式构建Spring Boot项目外&#xff0c;还可以通过Spring Initializr方式快速构建Spring Boot项目。从本质上说&#xff0c;Spring lnitializr是一个Web应用&#xff0c;它提供了一个基本的项目结构&#xff0c;能够帮助我们快速构建一个基础的Spring Boot项目…

Telegram营销,全球跨境电商都在研究的营销策略

Telegram 目前有7 亿月活跃用户。作为一个如此流行和广泛的即时通讯平台&#xff0c; Telegram 已成为企业和客户沟通的重要即时通讯工具。 为了使企业能够快速有效地覆盖目标受众&#xff0c;Telegram 不断改进平台&#xff0c;提供一系列功能&#xff0c;例如可定制的自动化…

JVM源码剖析之Java命令行参数全解

最近&#xff0c;有一位网友询问关于Java命令行参数方面的问题&#xff0c;因为在Java中参数有很多种&#xff0c;有不少的读者一直没弄明白&#xff0c;所以特意写下此篇文章。 此篇文章分2大块&#xff0c;第一块是不同参数的解释&#xff0c;第2块就是JVM源码论证&#xff…

Textnow注册防封,如何免费获取收发信息的美国手机号

TextNow和Google voice一样&#xff0c;是美国的一款免费的网络通信应用程序&#xff0c;可用于免费收发短信和无限制拨打电话&#xff0c;对于那些希望节省通讯费用的人&#xff0c;尤其是那些需要在跨境商务通讯频繁、跨境推广需要短信收发的用户来说&#xff0c;TextNow非常…

问道管理:信创概念走势活跃,恒银科技斩获四连板

信创概念9日盘中走势活泼&#xff0c;截至发稿&#xff0c;新晨科技、竞业达、恒银科技等涨停&#xff0c;宇信科技涨近10%&#xff0c;中孚信息涨近9%&#xff0c;华是科技、神州数码涨超7%。 新晨科技今天“20cm”涨停&#xff0c;公司昨日晚间公告&#xff0c;近来收到投标代…

中级课程-SSRF(CSRF进阶)

文章目录 成因危害挖掘 成因 危害 挖掘

面试热题(环形链表II)

给定一个链表&#xff0c;返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环&#xff0c;则返回 null。 为了表示给定链表中的环&#xff0c;我们使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;索引…

Java实战:高效提取PDF文件指定坐标的文本内容

前言 临时接到一个紧急需要处理的事项。业务侧一个同事有几千个PDF文件需要整理&#xff1a;需要从文件中的指定位置获取对应的编号和地址。 要的急&#xff0c;工作量大。所以就问到技术部有没有好的解决方案。 问技术的话就只能写个demo跑下了。 解决办法 1. 研究下PDF文档…

想使用cpolar内网穿透,如何下载安装?

如何下载安装并使用cpolar内网穿透 在不算久远的过去&#xff0c;哪位同学家中能有一台电脑&#xff0c;一定能收获其他同学羡慕的目光。随着科技和经济的发展&#xff0c;电脑在个人用户及商业群体中快速普及&#xff0c;也让电脑成为各类工作的中心。但想要让电脑能够发挥效…

TartanVO: A Generalizable Learning-based VO 论文阅读

论文信息 题目:TartanVO: A Generalizable Learning-based VO 作者&#xff1a;Wenshan Wang&#xff0c; Yaoyu Hu 来源&#xff1a;ICRL 时间&#xff1a;2021 代码地址&#xff1a;https://github.com/castacks/tartanvo Abstract 我们提出了第一个基于学习的视觉里程计&…

数组对象去重的几种方法

场景&#xff1a; let arrObj [{ name: "小红", id: 1 },{ name: "小橙", id: 1 },{ name: "小黄", id: 4 },{ name: "小绿", id: 3 },{ name: "小青", id: 1 },{ name: "小蓝", id: 4 } ]; 方法一&#xff1a;…

《Python入门到精通》函数详解

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;小白零基础《Python入门到精通》 函数 1、函数的调用2、函数的参数2.1、变量的就近原则2.2、传递参数2.3、形参和实…

SAP使用函数NUMBER_GET_NEXT创建流水号

1. 系统中设定流水号&#xff1b;使用T-Code&#xff1a;SNRO来创建一个流 输入Object&#xff1a;ZLC_001&#xff0c;然后单击创建。 然后输入Shorttext, Long text, Number length domain在写程序的时候应该会另外创建&#xff0c;这里测试就使用料号的Domain MATNR来做,其他…

http、https笔记

目录 HTTP 基本概念状态码&#xff1a;get和post的区别&#xff1a;http 常⻅字段&#xff1a;http的缺点&#xff1a; HTTP/1.1HTTP/3HTTPSHTTPS和HTTP区别对称加密和⾮对称加密⾮对称加密 HTTP 基本概念 状态码&#xff1a; 1xx 中间状态&#xff0c;比如post的continue 20…