执行计划

执行计划 #

执行计划是对一条 SQL 查询语句在数据库中执行过程的描述,通常用于分析某条 SQL 的性能问题,读懂执行计划是 SQL 优化的先决条件。本文介绍如何查看 SQL 的逻辑执行计划和实际执行计划,并介绍一些常用的执行计划算子。

逻辑执行计划 #

使用 EXPLAIN 命令可以查看优化器针对指定 SQL 生成的逻辑执行计划。EXPLAIN 命令完整的语法如下:

{EXPLAIN | DESCRIBE | DESC} [explain_type] dml_statement;

explain_type
    BASIC 
  | OUTLINE
  | EXTENDED
  | EXTENDED_NOADDR
  | PARTITIONS 
  | FORMAT = {TRADITIONAL| JSON}

dml_statement:
    SELECT statement 
  | DELETE statement
  | INSERT statement
  | REPLACE statement

其中,FORMAT 有 TRADITIONAL 和 JSON 两种格式,默认是 TRADITIONAL 格式,可读性更好,JSON 格式对程序解析比较友好。

先看一个简单的 SQL 执行计划格式:

obclient> EXPLAIN
    -> SELECT count(*) FROM BMSQL_ITEM \G
*************************** 1. row ***************************
Query Plan: ===============================================
|ID|OPERATOR       |NAME      |EST. ROWS|COST |
-----------------------------------------------
|0 |SCALAR GROUP BY|          |1        |78754|
|1 | TABLE SCAN    |BMSQL_ITEM|99995    |59653|
===============================================

Outputs & filters:
-------------------------------------
  0 - output([T_FUN_COUNT(*)]), filter(nil),
      group(nil), agg_func([T_FUN_COUNT(*)])
  1 - output([1]), filter(nil),
      access([BMSQL_ITEM.I_ID]), partitions(p0)

1 row in set (0.01 sec)

执行计划的输出展示分为两部分:

  • 第一部分是用表格形式展示执行计划这棵树。每行是一个独立的操作,操作符是 OPERATOR,操作有 ID。操作展示可能会缩进。缩进表示是内部操作,可以嵌套。执行顺序遵循由内到外,由上到下。操作符支持的内容也是 SQL 引擎成熟度的一个体现。

    • OPERATOR:表示操作算子的名称,TABLE SCAN 是常用操作算子,表示扫描。

    • NAME:表示算子操作的对象。可以是表名、索引名、内部临时视图名。需要注意的是,如果扫描主键,依然展示表名。因为 OceanBase 数据库里的表和索引的本质都是索引组织表,表数据跟主键索引是一个概念。

    • EST. ROWS:执行当前算子输出的行数,跟统计信息有关。OceanBase 数据库里表的统计信息目前只有在集群合并的时候才更新。

    • COST:执行当前算子预估的成本。COST 计算比较复杂,暂时先不深入。

  • 第二部分的内容跟第一部分有关,主要是描述第一部分算子的具体信息。

    • output:表示当前算子输出的表达式(包含列)。

    • filter:表示当前算子的过滤表达式,nil 表示无。如果当前算子是访问存储层,这个过滤表达式可以下推(push)。

在 OceanBase 数据库内部,这个结果是以 JSON 格式存储。示例如下:

{
  "ID": 1,
  "OPERATOR": "SCALAR GROUP BY",
  "NAME": "SCALAR GROUP BY",
  "EST.ROWS": 1,
  "COST": 78754,
  "output": [
    "T_FUN_COUNT(*)"
  ],
  "CHILD_1": {
    "ID": 0,
    "OPERATOR": "TABLE SCAN",
    "NAME": "TABLE SCAN",
    "EST.ROWS": 99995,
    "COST": 59653,
    "output": [
      "1"
    ]
  }
}

该 JSON 内容描述的是一个树,对普通用户可读性不好。

实际执行计划 #

我们可以通过如下命令查看 SQL 实际执行计划,查看 SQL 的实际执行计划要求 SQL 被执行过。

运行下面两个 SQL,查看 SQL 审计视图,获取执行节点和 PLAN_ID 信息。

select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id  from bmsql_oorder o , bmsql_item i  where o.o_id = i.i_id  and o.o_w_id  = 3 limit 10 ;

select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id  from bmsql_item i , bmsql_oorder o  where o.o_id = i.i_id  and o.o_w_id  = 3 limit 10 ;

SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip,  s.client_Ip, s.sid,s.tenant_id, s.tenant_name, s.user_name, s.db_name, s.query_sql, s.plan_id, s.plan_type, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time
FROM oceanbase.gv$ob_sql_audit s
WHERE 1=1  and s.tenant_id = 1002
 and user_name='u_tpcc' and query_sql like 'select o.o_w_id%'
 and request_time >= time_to_usec(date_sub(CURRENT_TIMESTAMP, interval 5 minute ))
ORDER BY request_time DESC
LIMIT 10 \G

# 输出:

*************************** 1. row ***************************
request_time_: 2021-10-05 11:24:50
       svr_ip: x.x.x.x
    client_Ip: x.x.x.x
          sid: 3221668666
    tenant_id: 1002
  tenant_name: obmysql
    user_name: u_tpcc
      db_name: tpccdb
    query_sql: select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id  from bmsql_item i , bmsql_oorder o  where o.o_id = i.i_id  and o.o_w_id  = 3 limit 10
      plan_id: 3305
    plan_type: 3
affected_rows: 0
  return_rows: 10
     ret_code: 0
        event: default condition wait
 elapsed_time: 20058
   queue_time: 73
 execute_time: 19726
*************************** 2. row ***************************
request_time_: 2021-10-05 11:24:46
       svr_ip: x.x.x.x
    client_Ip: x.x.x.x
          sid: 3222238517
    tenant_id: 1002
  tenant_name: obmysql
    user_name: u_tpcc
      db_name: tpccdb
    query_sql: select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id  from bmsql_oorder o , bmsql_item i  where o.o_id = i.i_id  and o.o_w_id  = 3 limit 10
      plan_id: 273
    plan_type: 3
affected_rows: 0
  return_rows: 10
     ret_code: 0
        event: system internal wait
 elapsed_time: 141562
   queue_time: 48
 execute_time: 139714
2 rows in set (0.119 sec)

其中 tenant_id、svr_ip、svr_port 和 plan_id 列信息很重要。查看视图 gv$ob_plan_cache_plan_explain 需要这些字段信息。

如果是在网页上,且以上输出结果格式化正确,对比 2 个 SQL 的实际执行计划可以看出分别是对那个表进行远程访问。

除了通过 SQL 审计视图定位具体的 SQL 及其执行计划外,还可以通过查看缓存的执行计划汇总视图 gv$ob_plan_cache_plan_stat。

SELECT s.tenant_id, svr_ip,plan_Id,sql_id,TYPE, query_sql, first_load_time, avg_exe_usec, slow_count,executions, slowest_exe_usec,s.outline_id
FROM oceanbase.`gv$ob_plan_cache_plan_stat` s  
WHERE s.tenant_id = 1002   -- 改成具体的 tenant_id
ORDER BY avg_exe_usec desc limit 10
;

从这个视图里可以看到全局的 SQL 执行汇总。适合找 TOP N 慢 SQL。根据里面的节点信息、SQLID 和 PLANID 信息,既可以到 SQL 审计视图里定位具体的 SQL 信息,也可以查看实际运行的执行计划信息。

执行计划可以清空,命令如下:

ALTER SYSTEM flush plan cache GLOBAL;

仅用于测试环境研究,生产环境的 SQL 执行计划缓存通常不可随便清空。清空执行计划会导致所有 SQL 要重新进行一次硬解析。

常见执行计划算子 #

TABLE GET

表示主键直接等值访问,后面接表名。OceanBase 数据库里主键就是表数据。

TABLE SCAN

表示全表扫描、主键扫描或索引扫描。具体需根据该执行计划算子后面的操作对象名是表还是索引判断。

注意

主键扫描时执行计划算子后面跟的操作对象也是表名。

TOP-N SORT

常用的场景排序后可能只返回最大或最小的前 N 条记录。

NESTED-LOOP JOIN

这个算法的整体性能取决于外部表返回的记录数(循环的次数)和内部表的查询性能。

个人经验是小表作为外部表,大表作为内部表。不过实际并不是按照表的大小区分,而是由过滤条件应用后的结果集大小来定。可以对比下面 SQL 的执行计划。

MERGE JOIN

MERGE JOIN 主要用于两个不是很小或很大的结果集的连接,它们没有有效的过滤条件或者这个条件上没有合适的索引。

MERGE JOIN 算法基本分两大阶段:

  • 排序,将两个结果集分别按连接字段排序。

  • 合并,分别从两个结果集里读取记录,进行比较、遍历等。

如果结果集本来就是有序的,那么第一阶段可以优化。MERGE JOIN 可以用于等值运算,也可以用于不等值运算(小于、大于、小于等于、大于等于)。

MERGE JOIN 主要利用数据主键或者索引的有序,此时它的性能有可能会更好。数据量非常大的时候,MERGE JOIN 性能并不是很好,要设法规避。

HASH JOIN

HASH JOIN 用于两个比较大的结果集之间的连接,通常没有比较好的过滤条件或者过滤条件上没有索引。

说明

  • HASH JOIN 也分外部表和内部表,内部表是 probe table,外部表是 hash table。通常数据库会挑选结果集相对小的表作为外部表,并在连接条件上用哈希函数构建 hash table,然后循环遍历 probe table,对连接条件列用哈希函数,探测是否在 hash table 中存在,如果存在,则返回匹配的记录。该算子和 NESTED-LOOP JOIN 很类似,不同之处是 HASH JOIN 会在连接条件列上用哈希函数,并在内存中构建 hash table。

  • OceanBase 优化器一次能构建的最大 hash table 受内部参数(_hash_table_size)限制。如果外部表的结果集比这个大,就需要分多次构建 hash table,这个也叫 multiple pass,会涉及到一些内存和文件数据交换,以及多次哈希探测,性能相对会下降一些。

  • HASH JOIN 的细节比较复杂,此处不详细讨论。目前只要能识别出 HASH JOIN,以及掌握产生后如何规避 HASH JOIN 算法。

SUBPLAN SCAN 和 COUNT

算子 SUBPLAN SCAN 跟 TABLE SCAN 类似,不同的是:

  • SUBPLAN SCAN 是从视图(包括内部临时生成的)里读取数据。

  • TABLE SCAN 是从基表(或者索引)里扫描数据。

EXCHANGE IN|OUT REMOTE

首先看要访问表的主副本节点,然后直连另外一个节点。人为构造一个远程执行计划。

说明

Exchange 算子是分布式场景下,用于线程间进行数据交互的算子。它一般成对出现,数据源端有一个 out 算子,目的端会有一个 in 算子。

实际上业务都是通过 OBProxy 连接,能正确路由到 OBServer 节点上,很大程度规避了远程执行计划,不过并不能从根本上避免。后面还会举例说明。