数据库内核分析之GPDB and PostgreSQL Portal

2022-12-02 20:45:09 浏览数 (1)

  • 0.导论

  • 1.Portal
    • 1.1 入口层
    • 1.2 Portal层
  • 2.游标Cursor
    • 2.1 打开游标
    • 2.2 关闭游标
    • 2.3 FETCH or MOVE

GPDB and PostgreSQL Portal内核分析

0.导论

Portal(门户),也称为策略选择模块,根据sql语句类型选择不同的执行模块(ProcessUtility、Executor)。

SQL语句类型包括:可优化语句、数据定义语句。

  • 可优化语句 包括DML,像insert/update/select等语句,这类语句特点是查询满足条件的元组返回给用户或者元组操作后写入磁盘,之所以称之为可优化语句是因为这类语句通常会被优化器进行重写与优化,从而加快查询速度。
  • 数据定义语句 主要是功能性语句,例如:DDL(Create、Alter、Drop)、DCL(Grant、COMMIT、ROLLBACK)等。

1.Portal

1.1 入口层

QD执行会从exec_simple_query进入,QE执行从exec_mpp_query进入。

1.2 Portal层

1.2.1 初识Portal

首先初识Portal内部数据结构:

策略

只包含一个SELECT查询。

代码语言:javascript复制
select * from t1;

包含一个INSERT/UPDATE/DELETE查询,且带RETURNING条件。

代码语言:javascript复制
INSERT INTO ret_tbl (id) VALUES (3) RETURNING id INTO tableId; 

包含一个SELECT查询并且有修改的CTE。

代码语言:javascript复制
WITH ins AS (
  INSERT INTO t1 (t1_id) VALUES(1) RETURNING t1_id
) 
SELECT * from ins;

例如:下面这个就不是PORTAL_ONE_MOD_WITH,而是PORTAL_MULTI_QUERY。

代码语言:javascript复制
WITH ins AS (
  SELECT * from t1
) INSERT INTO t2
  (t2_id, col2)
SELECT * from ins;

包含一个utility语句,且该语句执行会返回像SELECT那样有输出结果。

代码语言:javascript复制
postgres=# explain select * from t1;
                                  QUERY PLAN                                   
-------------------------------------------------------------------------------
 Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..431.00 rows=1 width=12)
   ->  Seq Scan on t1  (cost=0.00..431.00 rows=1 width=12)
 Optimizer: Pivotal Optimizer (GPORCA)
(3 rows)

postgres=# EXECUTE t1_fn_ret(2, 'helloworld');
 t1_id |    col1    
------- ------------
     2 | helloworld
(1 row)

INSERT 0 1

其他情况,例如:PORTAL_MULTI_QUERY PORTAL_MULTI_QUERY

代码语言:javascript复制
PREPARE t1_fn (int, text) AS INSERT INTO t1 VALUES($1, $2);
  • PORTAL_MULTI_QUERY
  • PORTAL_UTIL_SELECT
  • PORTAL_ONE_MOD_WITH
  • PORTAL_ONE_RETURNING
  • PORTAL_ONE_SELECT

状态

  • PORTAL_NEW
  • PORTAL_DEFINED
  • PORTAL_READY
  • PORTAL_QUEUE
  • PORTAL_ACTIVE
  • PORTAL_DONE
  • PORTAL_FAILED

这几个状态会在下面依次引入。

1.2.2 CreatePortal

创建一个新的Portal,传入参数均为: CreatePortal("", true, true),表示创建一个匿名的Portal,允许重复且重复后保持沉默。

CreatePortal逻辑:

代码语言:javascript复制
Portal
CreatePortal(const char *name, bool allowDup, bool dupSilent)
  1. 根据传入的第一个参数name从哈希表中查找
  2. 根据传入的第二个参数allowDup,如果第一步查找到,从哈希表中决定是否删除。如果true,则删除,否则报错。在哈希表中查找到Portal且允许重复的情况下,在QD节点上会根据第三个参数dupSilent决定是否输出告警信息。
  3. 创建一个新的Portal,并初始化相应参数。

执行完毕后,便创建好了一个状态为PORTAL_NEW的Portal。

1.2.3 PortalDefineQuery

定义portal数据,包含了:查询语句sourceText、PlannedStmts、查询完成标记qc。

注意:QD上根据传递进来的stmt来设置nodeTag,但是QE上为T_Query,因为QE上不是parsed statement,所以不是 T_SelectStmt。

最终设置Portal状态为PORTAL_DEFINED。

1.2.4 PortalStart

准备好portal,主要有如下几步:

1. 设置ddesc,该信息为QD到QE上的额外信息,QD上为NULL,QE上不为NULL。

2. 设置全局参数,例如:当前活跃的portal、resourceOwner、context。

3. 设置portal参数字段:portalParams,同样QD上为NULL,QE上不为NULL。

4. 设置portal策略(ChoosePortalStrategy)。

如下图所示:输入为查询计划链表,针对PORTAL_ONE_SELECT、PORTAL_ONE_MOD_WITH、PORTAL_UTIL_SELECT、PORTAL_ONE_RETURNING都是要求一个计划,首先判断是Query还是PlannedStmt,一般情况下的查询语句基本都是PlannedStmt,对于像PREPARE st(int) as select * from t1之类utility语句第一次调用ChoosePortalStrategy返回PORTAL_MULTI_QUERY(命中PlannedStmt),第二次调用返回PORTAL_ONE_SELECT(命中Query)。

choose

5. 根据portal策略初始化portal,最重要的是初始化tupDesc与cursor postion。

例如:"QUERY PLAN"、"t1_id、col1"就是tupDesc。

代码语言:javascript复制
postgres=# explain select * from t1;
                                  QUERY PLAN                                   
-------------------------------------------------------------------------------
 Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..431.00 rows=1 width=12)
   ->  Seq Scan on t1  (cost=0.00..431.00 rows=1 width=12)
 Optimizer: Pivotal Optimizer (GPORCA)
(3 rows)

postgres=# EXECUTE t1_fn_ret(2, 'helloworld');
 t1_id |    col1    
------- ------------
     2 | helloworld
(1 row)

INSERT 0 1
  • 不同tupleDesc函数区别
    • ExecTypeFromTL
      • skip resjunk column
    • ExecCleanTypeFromTL
      • with resjunk column

6. 在执行Portal过程中发生异常,设置portal的状态为PORTAL_FAILED;否则,下一步。

7. 设置Portal状态为PORTAL_READY。

1.2.5 PortalRun

根据sql的语句类型选择不同的执行路径,获取元组数据,完成portal工作,运行完之后要么Done要么下一轮(READY,而非ACTIVE)。

portal策略执行路径如下:

  • PORTAL_ONE_SELECT
    • set result = portal->atEnd
  • PORTAL_ONE_RETURNING|PORTAL_ONE_MOD_WITH|PORTAL_UTIL_SELECT
    • 获取时数据方向包含前进/后退
    • 可以从holdStore中获取,也可以从ExectorRun中获取
    • 填充holdStore(见下方)
    • 调用PortalRunSelect返回n行数据
    • 设置状态为PORTAL_READY
    • 设置是否完成运行标记为portal->atEnd
  • PORTAL_MULTI_QUERY
    • 调用PortalRunMulti
    • 设置状态为PORTAL_Done
    • 设置是否完成运行标记为true

此外,上述图中填充holdStore逻辑如下:

  • 调用PortalCreateHoldStore填充portal->holdStore,通过工厂函数CreateDestReceiver构造DestReceiver(子类:TStoreState);
  • 根据portal策略执行查询:PORTAL_ONE_RETURNING、PORTAL_ONE_MOD_WITH 调用PortalRunMulti,PORTAL_UTIL_SELECT调用PortalRunUtility。
    • PortalRunUtility
    • PortalRunMulti
    • ProcessQuery
    • PortalRunUtility
    • utilityStmt
    • not utilityStmt
    • PORTAL_ONE_RETURNING、PORTAL_ONE_MOD_WITH
    • PORTAL_UTIL_SELECT

2.游标Cursor

2.1 打开游标

如果不想一次执行整个命令,可以设置一个封装该命令的游标(cursor), 然后每次读取几行命令结果。

代码语言:javascript复制
name [ [ NO ] SCROLL ] CURSOR [ ( arguments ) ] FOR query;

例如:

代码语言:javascript复制
DECLARE liahona SCROLL CURSOR FOR SELECT * FROM t1; 

该命令运行机制为:首先识别到是一个数据定义语句,便会调用ProcessUtility,随后解析从PlannedStmt中的utilityStmt识别出是一个T_DeclareCursorStmt节点,调用PerformCursorOpen执行Declare cursor命令。

PerformCursorOpen处理逻辑如下:

  • query重写
  • 优化器优化,生成PlannedStmt
  • 创建Portal(名字为游标名),仅调用PortalStart

2.2 关闭游标

关闭游标,实际就是关闭Portal,调用PerformPortalClose。

如下两个操作:

代码语言:javascript复制
CLOSE cursor_name;
CLOSE ALL;

如果传入的名字为空,则是CLOSE ALL关闭所有非活跃portal,否则,只关闭指定的portal(cursor)。

2.3 FETCH or MOVE

FETCH与MOVE语法分别如下:

代码语言:javascript复制
FETCH [ direction { FROM | IN } ] cursor INTO target;
MOVE [ direction { FROM | IN } ] cursor;

FETCH从游标中检索n行到目标中, 目标可以是一个行变量、记录变量、逗号分隔的普通变量列表, 就像SELECT INTO一样, 如果没有获取到数据,目标会设为NULL。

MOVE重新定位一个游标,而不需要检索任何数据,例如:一旦游标位置确定,则可以删除或更新行。

代码语言:javascript复制
MOVE cursor_variable;
UPDATE table_name 
SET column = value, ... 
WHERE CURRENT OF cursor_variable;

从实现层面两者都会进入到PerformPortalFetch,都被解析为FetchStmt,内部有个成员ismove决定是MOVE还是FETCH。

不管是哪个,都会指定cursor名,有了这个名字,便知道了portal,随后调用PortalRunFetch来获取结果。

PortalRunFetch内部会像PortalRun运行一样,首先设置portal状态为Active,随后根据策略选择不同的调用链。

  • PORTAL_ONE_SELECT
    • 调用DoPortalRunFetch
  • PORTAL_ONE_RETURNING|PORTAL_ONE_MOD_WITH|PORTAL_UTIL_SELECT
    • 首先判断portal内部是否有holdStore,如果没有会调用FillPortalStore,随后调用DoPortalRunFetch。

DoPortalRunFetch内部实现,会考虑传入的direction,决定是前向还是后向等不同方向的扫描,最后调用PortalRunSelect获取数据,注意:gpdb不支持backward scan,但是pg支持。

本节完

css

0 人点赞