前言
在程序员或DBA的日常工作中,编写并执行SQL语句如同日常饮食中的一餐一饭,再寻常不过。然而,在使用命令行或黑屏客户端处理SQL时,常会遇到编写难、错误排查缓慢以及查询结果可读性不佳等难题,因此,图形化工具成为了我们调试与执行SQL的首选。那么,你是否曾好奇,当我们借助直观易用的开发者工具执行一条SQL语句时,从按下执行按钮的那一刻起,直至结果呈现、查询执行计划展现的过程,这条SQL语句究竟经历了怎样的旅程?倘若执行响应迟缓,又是哪个环节遭遇了瓶颈呢?
ODC 作为OceanBase的数据库开发者工具,致力于为用户提供更高效、更易用的 SQL 执行与诊断方案,在 4.1.0 版本,对 SQL 执行阶段的耗时也做了进一步的细化展示,以方便用户更清晰的了解 SQL 耗时详情。本文以 OceanBase Oracle 模式下一条 SQL 在 ODC 以及 OBServer 上的生命周期为例,介绍 ODC SQL 执行过程及耗时计算方案。
1 SQL 的执行过程
前置处理
在 SQL 被实际执行前,首先会被解析,获取其操作对象类型、是否加写锁(for update)、是否为多表查询等基础信息。之后根据解析结果,若其满足一定的条件,可能会对 SQL 进行改写操作。
Apply SQL
改写包括两点,一是为了提升查询性能,当语句为 SELECT 类型,查询字段中包含 * 且非 .* 的语句时,会尝试替换为 table.*
;二是会在 SELECT 后增加 ROWID AS __ODC_INTERNAL_ROWID__
的字段,这是因为结果集编辑时,若查询中未指定 ROWID 字段,那么可能会出现数据误更新的情况。
以SELECT * FROM DEMO;
为例,在发出执行请求后,会被改写为:
SELECT ROWID AS "__ODC_INTERNAL_ROWID__", DEMO.* FROM DEMO;
Validate SQL semantics
尽管对是否改写 SQL 已经经过了一定的判定,但仍然可能出现改写后无法执行的情况,例如查询 GV$SYSSTAT 等不支持 ROWID 的系统视图,那么即便语法不存在错误,也会导致执行失败。
为了避免改写导致的执行失败,在真实执行 SQL 之前,会执行 EXPLAIN + SQL
,若执行成功则说明改写成功,执行改写后的 SQL 并返回结果,否则仍然会采用原 SQL。
SQL 执行
SQL 执行阶段是 SQL 生命周期中最主要的阶段,其经过内部处理和准备后,通过网络协议一次或多次发送至 OceanBase Server 端,由 OBServer 进行具体执行操作,之后将结果以同样的方式返回客户端。
一条 SQL 在调用 JDBC 驱动执行语句后,驱动首先会进行一系列准备,例如设置查询行数限制、判断是否通过代理连接 Server 、设置执行超时计时器 等,这个阶段记作 Jdbc prepare 。
SQL 在从客户端发送至 OceanBase Server 端后,会进入等待队列,若租户 CPU 不足或服务器负载过高,则此过程可能会耗费大量时间。结束排队后,将会对其进行解析、改写和获取 SELECT 语句的执行计划,该过程若未命中缓存,也可能占用较长时间。拿到物理执行计划后,执行器才会调用线程执行该条 SQL ,并将计划保存至缓存中。为简化理解成本,上述一系列操作合并记作 OBServer wait 阶段。
OBServer 执行完成后,将数据返回客户端,通过 Get result-set 阶段,从返回结果中解析出要展示的数据,至此,便是一条 SQL 的实际执行过程。
后置处理
在获取到执行结果后,为了客户端的展示、编辑及错误提示,还会对该条 SQL 和执行结果进行几项后置处理。
Init SQL type
针对不同类型的 SQL,Navicat、SQL Developer 等数据库开发工具一般会给出不同的结果展示方式,例如 Navicat 对于 DML(INSERT、DELETE、UPDATE)类型语句会展示 Affected rows
影响行数,而对于 DDL、DQL 等仅提示 OK
执行成功,且展示 DQL 类型的查询结果。
ODC 对 SQL 类型的结果展示逻辑与 Navicat 基本相同,而解析类型的过程即被记作 Init SQL type 阶段,具体解析工作通过 OceanBase sql-parser 工具完成。
Init column comment 与 Init editable Info
在解析完 SQL 类型后,如果执行结果包含一个或多个表的列数据,则 ODC 会从字典视图 ALL_TAB_COLUMNS 中尝试获取相关列和表信息,并将其暂存在缓存中,以供接下来获取列注释和判断结果集是否可编辑。
若当前 SQL 涉及到对表或视图对象的查询,那么在展示时会显示列注释。而且,若当前 SQL 为单表查询,那么 ODC 能够允许用户对结果集进行编辑;若为多表查询,有且只有其中一张表的 ROWID 被指定,则指定 ROWID 的表的列可编辑,其余列不可。
Init warning message
OceanBase 数据库提供了字典视图 ALL_ERRORS,用于查看当前用户可访问的存储对象的错误。由于 SQL 窗口同样可以执行 PL,因此在执行结束后,ODC 将尝试查询 ALL_ERRORS 视图,通过对象名名称、Schema 名称和对象类型进行标识。若存在错误信息,则将其结构化处理后返回给用户。
至此,SQL 和结果集的后置处理也结束了,我们通过在 执行记录 界面中的 DB 耗时,查看上述各阶段的具体耗时。
2 OBServer 上的执行时间
SQL_AUDIT 视图
OceanBase 数据库将每一次 SQL 执行的来源、执行状态、详细耗时等信息存储在 GV$SQL_AUDIT 系统视图中,您可以通过该视图来查询 SQL 在 OBServer 端的耗时详情。
SQL_AUDIT 视图相关字段如下所示:
字段名称 | 类型 | 描述 |
---|---|---|
TRACE_ID | VARCHAR2(128) | 该语句的 trace_id |
TENANT_ID | NUMBER(38) | 发送请求的租户 ID |
REQUEST_TIME | NUMBER(38) | 开始执行时间点,单位:微秒 |
ELAPSED_TIME | NUMBER(38) | 接收到请求到执行结束所消耗的总时间 |
EXECUTE_TIME | NUMBER(38) | 实际计划执行所消耗的时间 |
注:在 OceanBase 4.0 版本之后,GVSQL_AUDIT 命名更改为 GVOB_SQL_AUDIT 。该视图按照租户拆分,除了系统租户,其他租户不能跨租户查询。
SQL TRACE
此外, OceanBase 支持 Trace 功能,通过变量 OB_ENABLE_TRACE_LOG
控制。
开启该功能后,可以使用SHOW TRACE
命令来快速获取最近一次 SQL 请求的完整日志。该命令获取的数据格式如下所示:
示例结果说明如下:
- Title 列包含整个 SQL 执行经历的各个阶段的信息以及该 SQL 真实的执行路径。若结果中有经过 Resolve、Transform、Optimizer 和 Code Generate 四个流程,则说明该 SQL 重新生成了计划,没有命中 Plan Cache。
- KeyValue 列包含一些执行信息,可以用于排查问题:
- TRACE_ID 可以作为 SQL_AUDIT 视图中的过滤条件,快速找到该 SQL 执行信息,同时也可以通过该 TRACE_ID 快速查找相关的 OBServer 日志。
- plan_id 可以用于在 v$plan_cache_plan_explain 中查看 Plan Cache 中缓存的具体执行计划。
- phy_plan_type 指出该次执行计划的类型(1 表示本地计划/2 表示远程计划/3 表示分布式计划),可以辅助 SQL 诊断。
- Time 列显示上一个阶段点到这次阶段点执行耗时。如果某个 SQL 执行很慢,则通过查看 TIME 列,能够快速定位出具体是哪个阶段执行较慢,然后再进行具体分析。假设执行耗时主要在生成计划过程中,则只需要分析没有命中 Plan Cache 的原因,可能是计划淘汰后第一次执行该 SQL,或是 Plan Cache 不支持的 SQL。
3 ODC 如何计算耗时
Jdbc 准备耗时
JDBC驱动记录了执行请求发送和接收结果的时间节点,通过api来获取 JDBC 网络开销的时间,ODC 通过调用接口获取两个关键时间戳,将其作为计算耗时的数据来源。
Jdbc prepare 阶段耗时通过计算真实发送网络请求 与调用执行方法 之间的时间差得到。当 Server 端的查询结果较多时,会分多次网络通信返回数据,我们使用 Jdbc 记录第一个数据包接收的时间戳,用于后续计算 Network consumption 阶段的耗时。
实际执行耗时
在 SQL 执行完成后,ODC 会通过 SHOW TRACE
命令获取查询该条 SQL 在 OBServer 端的实际执行日志。通过分析计算该日志,可得到该次请求的 ELAPSED_TIME (接收到请求到执行结束消耗的总时间)和 EXECUTE_TIME (实际执行物理计划所消耗的时间)。
其中 EXECUTE_TIME 即为 OBServer Execute SQL 阶段耗时,且被展示为执行记录中做外层的 DB 耗时 。解析、改写、获取执行计划等预处理操作的耗时为除 EXECUTE_TIME 之外时间之和,合并记作 OBServer wait 阶段。
网络耗时
虽然 GV$SQL_AUDIT 视图中记录了每条 SQL 执行请求的 REQUEST_TIME ,理论上可通过该时间戳与请求发送之间的时间差获取到网络延时。但实际上由于该字段取自 OBServer 本地服务器时钟,而不同服务器间的·,时钟存在时间差异,因此难以单独计算请求发送或接收的耗时。
ODC 将接收到第一个回包 的时间节点与请求发送 的时间节点之间的差值作为请求往返时间,并减去上一步获取到的 ELAPSED_TIME 即 OBServer 端总耗时,从而获取到请求发送与接收的总网络耗时,记作 Network consumption 阶段。
4 ODC 执行耗时展示
在 ODC 中,我们可以通过"执行记录"界面的 "DB 耗时"功能查看 SQL 执行各个阶段的耗时,进而定位问题所在。ODC 在 4.1.0 版本,将每个阶段的执行耗时时间单位进行了自适应处理,保留小数点后两位,且对 SQL 执行阶段的耗时也做了进一步的细化展示,以方便用户更清晰的了解 SQL 耗时详情。
文中的例子是在 OB-Oracle 模式下运行,OB-MySQL 模式与之相比,不会对 SQL 进行解析与改写,而是直接交由驱动和 OBServer 执行,其余阶段基本相同,因此不再赘述。
总结
SQL 的一生很长,本文仅仅是简要描述了其被 ODC 执行时,各个生命周期中的主要工作以及耗时是如何得到的,对 SQL 在 OceanBase Server 端内部的执行过程也仅是粗略介绍。
后续的文章中将会带大家了解 ODC 的权限管理模型,敬请期待