# 如何诊断和调优 OceanBase SQL 执行计划

## 如何用 EXPLAIN 查看理论执行计划

查看执行计划需要一个好的客户端，这样能将注意力集中在执行计划上。

一是命令行 obclient 命令。obclient 的好处是显示会格式化，当一行结果太长的时候格式会有点不好看，这个时候可以让 SQL 以 `\G` 替换 `;`结尾时，结果集会列传行展示，可读性更好一些。
二是 OceanBase 官方客户端 ODC。 直接选中 SQL，然后点击右上角的 “执行计划”。然后会以表格形式展示执行计划。
三是 开源的 DBeaver 客户端工具。由于 DBeaver 跟 OB还没有紧密适配，所以只能下 EXPLAIN [SQL] 的方式查看执行计划。 目前还是这个途径体验最好。

### EXPLAIN 说明

EXPLAIN 命令完整的语法是：

```sql
{EXPLAIN | DESCRIBE | DESC} 
[BASIC | OUTLINE | EXTENDED | EXTENDED_NOADDR | PARTITIONS | FORMAT = {TRADITIONAL| JSON}] 
{SELECT statement | DELETE statement | INSERT statement | REPLACE statement| UPDATE statement}

```

学习遵从由简入繁原则，先从默认的 `BASIC`格式入手。不用指定。 FORMAT 有 TRADITIONAL 和 JSON ，默认是 TRADITIONAL ，人可读性好一些。 JSON 格式对程序解析比较友好。

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

```sql
EXPLAIN 
SELECT count(*) FROM BMSQL_ITEM 
;
```

```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)
```

整个 EXPLAIN 结果是一行。在 OceanBase 内部，这个结果估计是以 JSON 格式储。如下：

```json
{
  "ID": 1,
  "OPERATOR": "GROUP BY",
  "NAME": "GROUP BY",
  "EST.ROWS": 1,
  "COST": 4352454,
  "output": [
    "T_FUN_COUNT(*)"
  ],
  "CHILD_1": {
    "ID": 0,
    "OPERATOR": "TABLE SCAN",
    "NAME": "TABLE SCAN",
    "EST.ROWS": 10000000,
    "COST": 2442330,
    "output": [
      "1"
    ]
  }
}
```

这个 JSON 内容描述的是一个树，对普通用户可读性不好。所以传统格式的展示分为两部分。
第一部分是用表格形式展示执行计划这棵树。每行是一个独立的操作，操作符是 `OPERATOR`， 操作有ID。操作展示可能会缩进。缩进表示是内部操作，可以嵌套。执行顺序遵循由内到外，由上到下。操作符支持的内容也是 SQL 引擎成熟度的一个体现。

**第一部分说明：**

+ `OPERATOR` : 表示操作算子的名称，`TABLE SCAN`, 是常用操作算子，表示扫描。
+ `NAME`: 表示算子操作的对象。可以是表名或者索引名，或者内部临时视图名。需要注意的是如果是扫描主键，展示的依然是表名。因为 OceanBase 里的表和索引的本质都是索引组织表。表数据跟主键索引是一个概念。
+ `EST. ROWS`：执行当前算子输出的行数，跟统计信息有关。OB里表的统计信息目前只有在集群合并的时候才更新。
+ `COST`: 执行当前算子预估的成本。COST计算比较复杂，暂时先不深入。

**第二部分说明：**

第二部分的内容跟第一部分有关，主要是描述第一部分算子的具体信息。有一些公共的信息如下：

+ `output` : 表示当前算子输出的表达式（也包含列）。
+ `filter` : 表示当前算子的过滤表达式，`nil`表示无。如果当前算子是访问存储层，这个过滤表达式可以下推（push）。

### 常见执行计划算子

+ `TABLE GET`

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

```sql
EXPLAIN 
SELECT i_id, i_name, I_PRICE FROM BMSQL_ITEM WHERE i_id=10
\G

*************************** 1. row ***************************
Query Plan: ========================================
|ID|OPERATOR |NAME      |EST. ROWS|COST|
----------------------------------------
|0 |TABLE GET|BMSQL_ITEM|1        |53  |
========================================

Outputs & filters:
-------------------------------------
  0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE]), filter(nil),
      access([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE]), partitions(p0)

1 row in set (0.00 sec)
```

+ `TABLE SCAN`

表示全表扫描或者主键扫描、索引扫描。具体看后面的操作对象名是表还是索引。注意，扫描主键也是表名。

```sql
CREATE UNIQUE INDEX idx_item_uk ON bmsql_item(i_name);

EXPLAIN extended_noaddr
SELECT i_id, i_name, I_PRICE FROM BMSQL_ITEM WHERE i_name = 'w2uw7BJj5tG5BTlSdfT'
\G

*************************** 1. row ***************************
Query Plan: ======================================================
|ID|OPERATOR  |NAME                   |EST. ROWS|COST|
------------------------------------------------------
|0 |TABLE SCAN|BMSQL_ITEM(IDX_ITEM_UK)|1        |88  |
======================================================

Outputs & filters:
-------------------------------------
  0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE]), filter(nil),
      access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), partitions(p0),
      is_index_back=true,
      range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.shadow_pk_0]), range(w2uw7BJj5tG5BTlSdfT,MIN ; w2uw7BJj5tG5BTlSdfT,MAX),
      range_cond([BMSQL_ITEM.I_NAME = 'w2uw7BJj5tG5BTlSdfT'])

1 row in set (0.00 sec)
```

**说明：**

+ `range_cond` : 指条件中能用于索引的列。
+ `range_key` ：指索引中能用于过滤的列及其值域。

+ `TOP-N SORT` & `LIMIT`

`TOP-N SORT` ：常用的场景排序后可能只用返回最大或最小的前 N 条记录。
`LIMIT`: 这个算子限制返回的行数。

```sql
MySQL [tpccdb]> explain extended_noaddr SELECT  i_id, i_name FROM BMSQL_ITEM WHERE i_name LIKE  'w2%' ORDER BY I_PRICE  DESC limit 10 \G
*************************** 1. row ***************************
Query Plan: ========================================================
|ID|OPERATOR    |NAME                   |EST. ROWS|COST|
--------------------------------------------------------
|0 |LIMIT       |                       |10       |479 |
|1 | TOP-N SORT |                       |10       |478 |
|2 |  TABLE SCAN|bmsql_item(idx_item_uk)|61       |406 |
========================================================

Outputs & filters:
-------------------------------------
  0 - output([bmsql_item.i_id], [bmsql_item.i_name]), filter(nil), limit(10), offset(nil)
  1 - output([bmsql_item.i_id], [bmsql_item.i_name]), filter(nil), sort_keys([bmsql_item.i_price, DESC]), topn(10)
  2 - output([bmsql_item.i_name], [bmsql_item.i_id], [bmsql_item.i_price]), filter(nil),
      access([bmsql_item.i_name], [bmsql_item.i_id], [bmsql_item.i_price]), partitions(p0),
      is_index_back=true,
      range_key([bmsql_item.i_name], [bmsql_item.shadow_pk_0]), range(w2,MIN ; w2������������������������������� ,MAX),
      range_cond([(T_OP_LIKE, bmsql_item.i_name, 'w2%', '\\')])

1 row in set (0.002 sec)

MySQL [tpccdb]>
```

+ `NESTED-LOOP JOIN`

```sql
EXPLAIN extended_noaddr
SELECT w.W_ID , w.W_NAME ,w.W_CITY ,d.D_ID ,d.D_NAME ,d.D_STATE 
FROM BMSQL_WAREHOUSE w JOIN BMSQL_DISTRICT d ON (w.w_id = d.D_W_ID)
WHERE w.W_ID =1
ORDER BY D.D_name
;

====================================================
|ID|OPERATOR                   |NAME|EST. ROWS|COST|
----------------------------------------------------
|0 |SORT                       |    |10       |154 |
|1 | NESTED-LOOP JOIN CARTESIAN|    |10       |97  |
|2 |  TABLE GET                |W   |1        |53  |
|3 |  TABLE SCAN               |D   |10       |39  |
====================================================

Outputs & filters: 
-------------------------------------
  0 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), sort_keys([D.D_NAME, ASC])
  1 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      conds(nil), nl_params_(nil), inner_get=false, self_join=false, batch_join=false
  2 - output([W.W_ID], [W.W_NAME], [W.W_CITY]), filter(nil), 
      access([W.W_ID], [W.W_NAME], [W.W_CITY]), partitions(p0), 
      is_index_back=false, 
      range_key([W.W_ID]), range[1 ; 1], 
      range_cond([W.W_ID = 1])
  3 - output([D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      access([D.D_ID], [D.D_NAME], [D.D_STATE]), partitions(p0), 
      is_index_back=false, 
      range_key([D.D_W_ID], [D.D_ID]), range(1,MIN ; 1,MAX), 
      range_cond([D.D_W_ID = 1])

```

**说明：**

+ 这个算法的整体性能取决于外部表返回的记录数（循环的次数）和内部表的查询性能。
+ 通常经验是小表作为外部表，大表作为内部表。不过实际也不是以表大小看，而是看过滤条件应用后的结果集大小来定。可以对比下面 SQL 的 执行计划。
+ 两个表的访问都走了主键索引，主键就是数据，所以执行计划中每个算子后面的名称（`NAME`）里都是表名。留意 1 和 2 里的 `is_index_back=false` 。
+ 这里针对 内部表和外部表扫描时都传入了条件 `W_ID=1`，通过 2 和 3 的 `range_cond` 可知。所以这里的 `JOIN` 条件被消除，外层连接就变为笛卡儿积（`CARTESIAN`)。这也是常用的一个优化手段。

+ `MERGE JOIN`

`MERGE JOIN` 主要用于两个不是很小也很大的结果集的连接，它们没有有效的过滤条件或者这个条件上没有合适的索引。
`MERGE JOIN` 算法基本分两大阶段。一是排序，将两个结果集分别按连接字段排序。二是合并。分别从两个结果集里读取记录，比较、遍历等。如果结果集本来就是有序的，那么第一阶段可以优化。`MERGE JOIN` 可以用于等值运算，也可以用于不等值运算（小于、大于、小于等于、大于等于）。
`MERGE JOIN` 主要会利用数据主键或者索引的有序，此时它的性能很有可能会更好。很多时候数据量非常大的时候， `MERGE JOIN` 性能并不是很好，要设法规避。

```sql
EXPLAIN extended_noaddr
SELECT w.W_ID , w.W_NAME ,w.W_CITY ,d.D_ID ,d.D_NAME ,d.D_STATE 
FROM BMSQL_WAREHOUSE w JOIN BMSQL_DISTRICT d ON (w.w_id = d.D_W_ID)
ORDER BY d.D_NAME 
;

======================================
|ID|OPERATOR    |NAME|EST. ROWS|COST |
--------------------------------------
|0 |SORT        |    |1000     |10093|
|1 | MERGE JOIN |    |1000     |1312 |
|2 |  TABLE SCAN|W   |100      |59   |
|3 |  TABLE SCAN|D   |1000     |426  |
======================================

Outputs & filters: 
-------------------------------------
  0 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), sort_keys([D.D_NAME, ASC])
  1 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      equal_conds([W.W_ID = D.D_W_ID]), other_conds(nil)
  2 - output([W.W_ID], [W.W_NAME], [W.W_CITY]), filter(nil), 
      access([W.W_ID], [W.W_NAME], [W.W_CITY]), partitions(p0), 
      is_index_back=false, 
      range_key([W.W_ID]), range(MIN ; MAX)always true
  3 - output([D.D_W_ID], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), 
      access([D.D_W_ID], [D.D_ID], [D.D_NAME], [D.D_STATE]), partitions(p0), 
      is_index_back=false, 
      range_key([D.D_W_ID], [D.D_ID]), range(MIN,MIN ; MAX,MAX)always true

```

**说明：**

+ 两个表在连接条件列上都有主键索引（即数据），所以返回的数据都是有序的，这里就使用了 `MERGE JOIN`.
+ 最后在外层再根据排序条件执行排序（`SORT`）。
+ `MERGE JOIN`可以临时排序的大小受集群隐含参数（`_sort_area_size`) 限制。

+ `HASH JOIN`

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

```sql
EXPLAIN extended_noaddr      
SELECT c.C_W_ID , c.C_D_ID , c.C_FIRST || ',' || c.C_LAST , c.C_PAYMENT_CNT , c.C_YTD_PAYMENT , o.O_W_ID , o.O_D_ID ,o.O_OL_CNT 
FROM BMSQL_CUSTOMER c JOIN BMSQL_OORDER o ON (c.C_ID = o.O_C_ID)
 WHERE c.C_W_ID = 1 AND c.C_D_ID = 5 AND (c.C_W_ID <> o.O_W_ID OR c.C_D_ID <> o.O_D_ID )
ORDER BY c.C_W_ID , c.C_D_ID , c.C_LAST , o.O_W_ID , o.O_D_ID 
;

=========================================
|ID|OPERATOR    |NAME|EST. ROWS|COST    |
-----------------------------------------
|0 |SORT        |    |2238162  |45593377|
|1 | HASH JOIN  |    |2238162  |3764643 |
|2 |  TABLE SCAN|C   |2973     |3515    |
|3 |  TABLE SCAN|O   |3019348  |785737  |
=========================================

Outputs & filters: 
-------------------------------------
  0 - output([C.C_W_ID], [C.C_D_ID], [(T_OP_CNN, (T_OP_CNN, C.C_FIRST, ?), C.C_LAST)], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), filter(nil), sort_keys([C.C_LAST, ASC], [O.O_W_ID, ASC], [O.O_D_ID, ASC])
  1 - output([C.C_W_ID], [C.C_D_ID], [C.C_FIRST], [C.C_LAST], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), filter(nil), 
      equal_conds([C.C_ID = O.O_C_ID]), other_conds([C.C_W_ID != O.O_W_ID OR C.C_D_ID != O.O_D_ID])
  2 - output([C.C_ID], [C.C_W_ID], [C.C_D_ID], [C.C_FIRST], [C.C_LAST], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT]), filter(nil), 
      access([C.C_ID], [C.C_W_ID], [C.C_D_ID], [C.C_FIRST], [C.C_LAST], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT]), partitions(p0), 
      is_index_back=false, 
      range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(1,5,MIN ; 1,5,MAX), 
      range_cond([C.C_W_ID = 1], [C.C_D_ID = 5])
  3 - output([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), filter(nil), 
      access([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), partitions(p0), 
      is_index_back=false, 
      range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

```

**说明：**

+ 尽管条件 `(c.C_W_ID <> o.O_W_ID OR c.C_D_ID <> o.O_D_ID )` 不在 JOIN 的 ON 条件里，优化器还是能识别出来并自动应用。并且将条件分类为 `equal_conds` 和 `other_conds` 。
+ `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` 类似，不同的是 后者是从基表（或者索引）里扫描数据，前者是从视图（包括内部临时生成的）里读取数据。

还是前面那个分页查询场景例子。由于在 SELECT 后面增加了一列排序序号（计算出来的），执行计划就多了两步。

```sql
EXPLAIN extended_noaddr
SELECT i_id, i_name, i_price, i_data , rn
FROM (
 SELECT rownum rn, i_id, i_name, I_PRICE, I_DATA FROM (
  SELECT  i_id, i_name, i_price , i_data FROM BMSQL_ITEM 
  WHERE i_name LIKE  'wu%'
  ORDER BY i_price  DESC 
 ) t WHERE rownum <= 15
) WHERE rn > 10
;

============================================================
|ID|OPERATOR      |NAME                     |EST. ROWS|COST|
------------------------------------------------------------
|0 |COUNT         |                         |5        |332 |
|1 | SUBPLAN SCAN |T                        |5        |331 |
|2 |  LIMIT       |                         |5        |330 |
|3 |   TOP-N SORT |                         |15       |328 |
|4 |    TABLE SCAN|BMSQL_ITEM(BMSQL_ITEM_UK)|30       |245 |
============================================================

Outputs & filters: 
-------------------------------------
  0 - output([T.I_ID], [T.I_NAME], [T.I_PRICE], [T.I_DATA], [rownum() + ?]), filter(nil)
  1 - output([T.I_ID], [T.I_NAME], [T.I_PRICE], [T.I_DATA]), filter(nil), 
      access([T.I_ID], [T.I_NAME], [T.I_PRICE], [T.I_DATA])
  2 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil), limit(?), offset(?)
  3 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil), sort_keys([BMSQL_ITEM.I_PRICE, DESC]), topn(? + ?)
  4 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil), 
      access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), partitions(p0), 
      is_index_back=true, 
      range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.shadow_pk_0]), range(wu,MIN ; wu ,MAX), 
      range_cond([(T_OP_LIKE, BMSQL_ITEM.I_NAME, ?, '\')])

```

上面 `COUNT` 算子是为了兼容 ORACLE 的 rownum 功能，作用在于为 rownum 表达式进行自增操作。在一般场景下，当SQL 查询含有 rownum 时，SQL 优化器就会在生成计划的时候分配一个 `COUNT` 算子。 如果 SELECT 里不涉及到 rownum 值的展示时， `COUNT` 算子也可能会被优化为 LIMIT 算子。

```sql
EXPLAIN extended_noaddr
SELECT * FROM (
 SELECT  i_id, i_name FROM BMSQL_ITEM WHERE i_name LIKE  'w2u%'
 ORDER BY I_PRICE  DESC 
) WHERE rownum < 5
;

==========================================================
|ID|OPERATOR    |NAME                     |EST. ROWS|COST|
----------------------------------------------------------
|0 |LIMIT       |                         |1        |91  |
|1 | TOP-N SORT |                         |1        |90  |
|2 |  TABLE SCAN|BMSQL_ITEM(BMSQL_ITEM_UK)|1        |88  |
==========================================================

Outputs & filters: 
-------------------------------------
  0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME]), filter(nil), limit(?), offset(nil)
  1 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME]), filter(nil), sort_keys([BMSQL_ITEM.I_PRICE, DESC]), topn(?)
  2 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), filter(nil), 
      access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), partitions(p0), 
      is_index_back=true, 
      range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.shadow_pk_0]), range(w2u,MIN ; w2u,MAX), 
      range_cond([(T_OP_LIKE, BMSQL_ITEM.I_NAME, ?, '\')])


```

+ `EXCHANGE IN|OUT REMOTE`

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

示例 1

```bash
explain extended_Noaddr select /*+ test20210405 */ * from bmsql_item where i_id=100\G
"
obclient: [Warning] Using a password on the command line interface can be insecure.
*************************** 1. row ***************************
Query Plan: ===================================================
|ID|OPERATOR            |NAME      |EST. ROWS|COST|
---------------------------------------------------
|0 |EXCHANGE IN REMOTE  |          |1        |54  |
|1 | EXCHANGE OUT REMOTE|          |1        |53  |
|2 |  TABLE GET         |BMSQL_ITEM|1        |53  |
===================================================

Outputs & filters:
-------------------------------------
  0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil)
  1 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil)
  2 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil),
      access([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), partitions(p0),
      is_index_back=false,
      range_key([BMSQL_ITEM.I_ID]), range[100 ; 100],
      range_cond([BMSQL_ITEM.I_ID = 100])

```

+ `Exchange` 算子是分布式场景下，用于线程间进行数据交互的算子。它一般都是成对出现的，数据源端有一个 `out` 算子，目的端会有一个 `in` 算子。
+ 算子 2 是实际取数据的执行计划，`TABLE GET` 是主键访问数据的算子。
+ 算子 1 的 `EXCHANGE OUT REMOTE` 在远端机器上负责读取数据并传输出去。
+ 算子 0 的 `EXCHANGE IN REMOTE` 在本地节点接收数据。

上面是主键扫描示例，下面是索引扫描的示例。

示例 2

```sql

explain extended_Noaddr select /*+ test20210405 */ * from bmsql_item where i_name LIKE 'Ax%'\G

obclient: [Warning] Using a password on the command line interface can be insecure.
*************************** 1. row ***************************
Query Plan: ====================================================================
|ID|OPERATOR            |NAME                       |EST. ROWS|COST|
--------------------------------------------------------------------
|0 |EXCHANGE IN REMOTE  |                           |33       |294 |
|1 | EXCHANGE OUT REMOTE|                           |33       |264 |
|2 |  TABLE SCAN        |BMSQL_ITEM(BMSQL_ITEM_IDX1)|33       |264 |
====================================================================

Outputs & filters:
-------------------------------------
  0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil)
  1 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil)
  2 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil),
      access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), partitions(p0),
      is_index_back=true,
      range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID]), range(Ax,MIN ; Ax������������������������������� ,MAX),
      range_cond([(T_OP_LIKE, BMSQL_ITEM.I_NAME, ?, '\')])
      
```

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

### 主备切换如何影响执行计划

当 SQL 包含多表连接的时候，如果多表的主副本都在同一个 OBSERVER 节点上也是当前连接的节点，这就是个本地 SQL 执行计划（`plan_type` = 1）；如果多表的主副本是分散在不同的 OBSERVER 节点上，这就是分布式 SQL 执行计划（`plan_type=3`）；如果多表的主副本都在同一个 OBSERVER 节点上但不是当前连接的节点，这个就是一个远程 SQL 执行计划（`plan_type=2`）。

通常我们不直接调整分区主副本的位置，而是通过调整表或表分组、数据库或者租户的 PRIMARY_ZONE 来间接改变表主副本的位置。理论上改变了主副本的位置后，执行计划也会变化。实际情况，由于分区主副本位置变动信息 OBPROXY 并不会立即获得，所有有时候观察 EXPLAIN 执行计划也不会变，这点需要留意。

+ 查看表的主副本位置。

```sql
SELECT  a.tenant_name, d.database_name,  tg.tablegroup_name ,t.table_name,  t.part_num , t2.partition_id, t2.ZONE, t2.svr_ip , t2.svr_port, round(t2.data_size/1024/1024) data_size_mb, t2.row_count
FROM oceanbase.__all_tenant AS a  
 JOIN oceanbase.__all_virtual_database AS d ON ( a.tenant_id = d.tenant_id  ) 
 JOIN oceanbase.__all_virtual_table AS t  ON (t.tenant_id = d.tenant_id AND t.database_id = d.database_id) 
 JOIN oceanbase.__all_virtual_meta_table t2 ON (t.tenant_id = t2.tenant_id AND (t.table_id=t2.table_id OR t.tablegroup_id=t2.table_id)  AND t2.ROLE IN (1) )
 LEFT JOIN oceanbase.__all_virtual_tablegroup AS tg ON (t.tenant_id = tg.tenant_id and  t.tablegroup_id = tg.tablegroup_id) 
WHERE a.tenant_id IN (1002 ) AND  t.table_type IN (3)  
 AND d.database_name = 'tpccdb' and table_name in ('bmsql_item','bmsql_oorder')
ORDER BY t.tenant_id, tg.tablegroup_name, d.database_name, t.table_name, t2.partition_id
;

+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
| tenant_name | database_name | tablegroup_name | table_name   | part_num | partition_id | ZONE  | svr_ip        | svr_port | data_size_mb | row_count |
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
| obmysql     | tpccdb        | NULL            | bmsql_item   |        1 |            0 | zone3 | 172.20.249.51 |     2882 |            7 |    100000 |
| obmysql     | tpccdb        | tpcc_group      | bmsql_oorder |        3 |            0 | zone3 | 172.20.249.51 |     2882 |            1 |    139802 |
| obmysql     | tpccdb        | tpcc_group      | bmsql_oorder |        3 |            1 | zone2 | 172.20.249.49 |     2882 |            2 |    203152 |
| obmysql     | tpccdb        | tpcc_group      | bmsql_oorder |        3 |            2 | zone1 | 172.20.249.52 |     2882 |            2 |    150858 |
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
4 rows in set (0.053 sec)

```

从上面结果可以看出，表 `bmsql_item` 是个单分区的普通表，主副本在节点 `172.20.249.51` 上。 表 `bmsql_oorder` 是有3个分区的表，主副本分别在三个节点上。
看下面 SQL 执行计划。

```sql
MySQL [tpccdb]> explain extended_noaddr 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 \G

*************************** 1. row ***************************
Query Plan: ==========================================
|ID|OPERATOR         |NAME|EST. ROWS|COST|
------------------------------------------
|0 |LIMIT            |    |10       |407 |
|1 | NESTED-LOOP JOIN|    |10       |406 |
|2 |  TABLE SCAN     |o   |10       |39  |
|3 |  TABLE GET      |i   |1        |36  |
==========================================

Outputs & filters:
-------------------------------------
  0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
  1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
      conds(nil), nl_params_([o.o_id]), batch_join=true
  2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
      access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
      is_index_back=false,
      range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
      range_cond([o.o_w_id = 3])
  3 - output([i.i_name], [i.i_price]), filter(nil),
      access([i.i_name], [i.i_price]), partitions(p0),
      is_index_back=false,
      range_key([i.i_id]), range(MIN ; MAX),
      range_cond([? = i.i_id])

1 row in set (0.008 sec)
```

从上面可以看出 `TABLE SCAN` 访问表 `o` 的时候，访问的是分区 `partitions(p0)` 。0 号分区的主副本在 `zone3` 的节点 `172.20.249.51` 上，跟 表 `bmsql_item` 在同一个节点，所以这是个本地 JOIN 。

如果调整表 `bmsql_item` 的 PRIMARY_ZONE ，换一个节点，再看看这个  SQL 。

```sql
MySQL [tpccdb]> alter table bmsql_item primary_zone='zone1';
Query OK, 0 rows affected (0.043 sec)

```

查看 主副本实际位置。

```
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
| tenant_name | database_name | tablegroup_name | table_name   | part_num | partition_id | ZONE  | svr_ip        | svr_port | data_size_mb | row_count |
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
| obmysql     | tpccdb        | NULL            | bmsql_item   |        1 |            0 | zone1 | 172.20.249.52 |     2882 |            7 |    100000 |
| obmysql     | tpccdb        | tpcc_group      | bmsql_oorder |        3 |            0 | zone3 | 172.20.249.51 |     2882 |            1 |    139802 |
| obmysql     | tpccdb        | tpcc_group      | bmsql_oorder |        3 |            1 | zone2 | 172.20.249.49 |     2882 |            2 |    203152 |
| obmysql     | tpccdb        | tpcc_group      | bmsql_oorder |        3 |            2 | zone1 | 172.20.249.52 |     2882 |            2 |    150858 |
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
4 rows in set (0.039 sec)
```

进 SYS 租户查看 `bmsql_item` 主副本在 `zone1` 的 节点 `172.20.249.52` 上了。
再进业务租户查看 SQL 执行计划。

```sql
MySQL [tpccdb]> explain extended_noaddr 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 \G
*************************** 1. row ***************************
Query Plan: ==========================================
|ID|OPERATOR         |NAME|EST. ROWS|COST|
------------------------------------------
|0 |LIMIT            |    |10       |407 |
|1 | NESTED-LOOP JOIN|    |10       |406 |
|2 |  TABLE SCAN     |o   |10       |39  |
|3 |  TABLE GET      |i   |1        |36  |
==========================================

Outputs & filters:
-------------------------------------
  0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
  1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
      conds(nil), nl_params_([o.o_id]), batch_join=true
  2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
      access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
      is_index_back=false,
      range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
      range_cond([o.o_w_id = 3])
  3 - output([i.i_name], [i.i_price]), filter(nil),
      access([i.i_name], [i.i_price]), partitions(p0),
      is_index_back=false,
      range_key([i.i_id]), range(MIN ; MAX),
      range_cond([? = i.i_id])

1 row in set (0.004 sec)

```

跟前面执行计划比，没有发生变化！这个不符合理论。其原因就是当分区主备副本发生切换后，OBSERVER 和 OBPROXY 内部的分区 LOCATION CACHE 并不是立即更新，而是在 SQL 第一次执行的时候才触发更新。EXPLAIN 查看执行计划的时候，并不会执行 SQL，所以看到的不是实际的执行计划，而是理论的。我们可以简单访问一下表 bmsql_item 触发 OceanBase 内部 LOCATION CACHE 更新。然后再查看上面执行计划。

```sql
MySQL [tpccdb]> explain extended_noaddr 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 \G
*************************** 1. row ***************************
Query Plan: ==================================================
|ID|OPERATOR             |NAME    |EST. ROWS|COST|
--------------------------------------------------
|0 |LIMIT                |        |10       |409 |
|1 | NESTED-LOOP JOIN    |        |10       |408 |
|2 |  TABLE SCAN         |o       |10       |39  |
|3 |  PX COORDINATOR     |        |1        |37  |
|4 |   EXCHANGE OUT DISTR|:EX10000|1        |36  |
|5 |    TABLE GET        |i       |1        |36  |
==================================================

Outputs & filters:
-------------------------------------
  0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
  1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
      conds(nil), nl_params_([o.o_id]), batch_join=false
  2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
      access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
      is_index_back=false,
      range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
      range_cond([o.o_w_id = 3])
  3 - output([i.i_name], [i.i_price]), filter(nil)
  4 - output([i.i_name], [i.i_price]), filter(nil), is_single, dop=1
  5 - output([i.i_name], [i.i_price]), filter(nil),
      access([i.i_name], [i.i_price]), partitions(p0),
      is_index_back=false,
      range_key([i.i_id]), range(MIN ; MAX),
      range_cond([? = i.i_id])

1 row in set (0.004 sec)

```

从上面执行计划可以看出，对表 `bmsql_item` 的访问变成远程访问了 `EXCHANGE OUT DISTR` 。并且由于 `bmsql_oorder` 的条件过滤后会有很多记录，所以这里远程访问 `bmsql_item` 的数据还用了并行（`PX COORDINATOR`）。

上面更新的是表 `bmsql_item` 而不是 `bmsql_oorder` 也是有原因的，因为这个连接 SQL 被 OBPROXY 路由到哪个 OBSERVER 节点是由 `from` 语句后的表确定的，即 `bmsql_oorder` 的 `o.o_w_Id=3` 对应的分区的主副本。这个演示是先保证 OBPROXY 路由的节点是固定的，方便对比。

下面我们调整这个 SQL 里的表连接顺序再看看。先回滚 `bmsql_item` 的 PRIMARY_ZONE 变更。

```sql
MySQL [tpccdb]> explain extended_noaddr 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\G
*************************** 1. row ***************************
Query Plan: ==========================================
|ID|OPERATOR         |NAME|EST. ROWS|COST|
------------------------------------------
|0 |LIMIT            |    |10       |407 |
|1 | NESTED-LOOP JOIN|    |10       |406 |
|2 |  TABLE SCAN     |o   |10       |39  |
|3 |  TABLE GET      |i   |1        |36  |
==========================================

Outputs & filters:
-------------------------------------
  0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
  1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
      conds(nil), nl_params_([o.o_id]), batch_join=true
  2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
      access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
      is_index_back=false,
      range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
      range_cond([o.o_w_id = 3])
  3 - output([i.i_name], [i.i_price]), filter(nil),
      access([i.i_name], [i.i_price]), partitions(p0),
      is_index_back=false,
      range_key([i.i_id]), range(MIN ; MAX),
      range_cond([? = i.i_id])

1 row in set (0.003 sec)

```

调整表 `bmsql_item` 的 PRIMARY_ZONE 到 `zone1` 后，查看上面 SQL 的 EXPLAIN 结果，跟前面一样，没有发生变化。

那么再次使用前面的技巧，对 `bmsql_item` 访问一次。

```sql
MySQL [tpccdb]> select count(*) from bmsql_item;
+----------+
| count(*) |
+----------+
|   100000 |
+----------+
1 row in set (0.083 sec)

MySQL [tpccdb]> explain extended_noaddr 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\G
*************************** 1. row ***************************
Query Plan: ==================================================
|ID|OPERATOR             |NAME    |EST. ROWS|COST|
--------------------------------------------------
|0 |LIMIT                |        |10       |409 |
|1 | NESTED-LOOP JOIN    |        |10       |408 |
|2 |  TABLE SCAN         |o       |10       |39  |
|3 |  PX COORDINATOR     |        |1        |37  |
|4 |   EXCHANGE OUT DISTR|:EX10000|1        |36  |
|5 |    TABLE GET        |i       |1        |36  |
==================================================

Outputs & filters:
-------------------------------------
  0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
  1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
      conds(nil), nl_params_([o.o_id]), batch_join=false
  2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
      access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
      is_index_back=false,
      range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
      range_cond([o.o_w_id = 3])
  3 - output([i.i_name], [i.i_price]), filter(nil)
  4 - output([i.i_name], [i.i_price]), filter(nil), is_single, dop=1
  5 - output([i.i_name], [i.i_price]), filter(nil),
      access([i.i_name], [i.i_price]), partitions(p0),
      is_index_back=false,
      range_key([i.i_id]), range(MIN ; MAX),
      range_cond([? = i.i_id])

1 row in set (0.003 sec)

```

从上面结果可见，执行计划再次发生变化，不过跟前面那个 SQL 的变化是一样的，对表 `bmsql_item` 的访问依然是远程访问。
看到这里您可能就会奇怪，既然 SQL 被路由到表 `bmsql_item` 的主副本所在的节点了，为什么对表 `bmsql_item` 的访问还是远程访问？
这个问题就出在 EXPLAIN 被路由到的节点上。OBPROXY 对 EXPLAIN 语句的路由第一次是随机的，后面每次都跟随前面的 EXPLAIN 语句的路由。虽然上面改变了表 `bmsql_item` 的位置，但是 EXPLAIN 语句依然发往同一个节点（即 `bmsql_oorder` 所在的节点），在那个节点上生产的执行计划就永远是上面这种。
以上 SQL 被路由到哪个节点信息，可以通过 SQL审计视图查询。

```sql
SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.plan_id, s.client_Ip, s.sid,s.tenant_id, s.tenant_name, s.user_name, s.db_name, s.query_sql, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time, round(s.request_memory_used/1024/1024/1024,2) req_mem_mb, plan_type, is_executor_rpc, is_inner_sql, TRANSACTION_HASH ,trace_id
FROM gv$sql_audit s
WHERE 1=1  and s.tenant_id = 1002
 and user_name='u_tpcc' and query_sql like '%bmsql_item%'  
 and request_time >= time_to_usec(date_sub(CURRENT_TIMESTAMP, interval 30 minute ))
ORDER BY request_time DESC
LIMIT 100;
```

结果就不展示了。
下面可以换一个 OBPROXY ，或者把此前的 OBPROXY KILL 掉重新拉起。再看看上面 SQL 的执行计划，就变成我们期望的那种执行计划了。

```sql
MySQL [tpccdb]> explain extended_noaddr 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\G
*************************** 1. row ***************************
Query Plan: ==================================================
|ID|OPERATOR             |NAME    |EST. ROWS|COST|
--------------------------------------------------
|0 |LIMIT                |        |10       |409 |
|1 | NESTED-LOOP JOIN    |        |10       |408 |
|2 |  PX COORDINATOR     |        |10       |40  |
|3 |   EXCHANGE OUT DISTR|:EX10000|10       |39  |
|4 |    TABLE SCAN       |o       |10       |39  |
|5 |  TABLE GET          |i       |1        |36  |
==================================================

Outputs & filters:
-------------------------------------
  0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
  1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
      conds(nil), nl_params_([o.o_id]), batch_join=true
  2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil)
  3 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil), is_single, dop=1
  4 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
      access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
      is_index_back=false,
      range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
      range_cond([o.o_w_id = 3])
  5 - output([i.i_name], [i.i_price]), filter(nil),
      access([i.i_name], [i.i_price]), partitions(p0),
      is_index_back=false,
      range_key([i.i_id]), range(MIN ; MAX),
      range_cond([? = i.i_id])

1 row in set (0.030 sec)
```

**结论：**

+ EXPLAIN 看执行计划还是很方便的，执行计划的关键是表连接算法和顺序、索引等等。这些都能通过 EXPLAIN 看出问题。
+ EXPLAIN 的执行计划可能跟期望的执行计划有些差别，往往是 LOCATION CACHE 和 SQL 路由导致的。这两个影响因素要记得。生产环境，表被频繁访问的时候，即使分区主副本切换了，OBSERVER 也很快更新了路由。虽然是被动更新，代价就是主备副本切换后业务第一次访问的 SQL 会变慢（早期 OceanBase 版本是报错，后期版本改为 OceanBase 内部重试，所以会略微变慢。重试后，OBSERVER LOCATION CACHE 就更新了）。

## 如何查看 SQL 实际执行计划

上面说 EXPLAIN 执行计划跟期望的执行计划有差别，这个不好说，但是我们可以查看 SQL 实际执行计划来验证是不是有差异。查看 SQL 的实际执行计划要求 SQL 执行过。

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

```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 ;

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$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: 172.20.249.52
    client_Ip: 172.20.249.51
          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: 172.20.249.51
    client_Ip: 172.20.249.51
          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$plan_cache_plan_explain` 需要这个字段信息。

```sql

MySQL [tpccdb]> SELECT ip, plan_depth, plan_line_id,operator,name,rows,cost,property from oceanbase.`gv$plan_cache_plan_explain`  WHERE tenant_id=1002 AND ip = '172.20.249.52' AND port=2882 AND plan_id=3305;
 
| ip            | plan_depth | plan_line_id | operator                  | name | rows | cost | property                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
 
| 172.20.249.52 |          0 |            0 | PHY_LIMIT                 | NULL |   10 |  408 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 172.20.249.52 |          1 |            1 |  PHY_NESTED_LOOP_JOIN     | NULL |   10 |  407 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 172.20.249.52 |          2 |            2 |   PHY_PX_FIFO_COORD       | NULL |   10 |   39 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 172.20.249.52 |          3 |            3 |    PHY_PX_REDUCE_TRANSMIT | NULL |   10 |   38 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 172.20.249.52 |          4 |            4 |     PHY_TABLE_SCAN        | o    |   10 |   38 | table_rows:139802, physical_range_rows:47417, logical_range_rows:47407, index_back_rows:0, output_rows:47407, est_method:local_storage, avaiable_index_name[bmsql_oorder], estimation info[table_id:1101710651081625, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:47407, physical_rc:47407), (table_type:7, version:1633335566720232-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:5, version:1633335566720232-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:0, version:1633335792045894-1633335792045894-9223372036854775807, logical_rc:0, physical_rc:10)] |
| 172.20.249.52 |          2 |            5 |   PHY_TABLE_SCAN          | i    |    1 |   35 | table_rows:100000, physical_range_rows:1, logical_range_rows:1, index_back_rows:0, output_rows:1, est_method:local_storage, estimation info[table_id:1101710651081627, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:100000, physical_rc:100000), (table_type:7, version:1633335566970454-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:5, version:1633335566970454-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:0, version:1633335794237719-1633335794237719-9223372036854775807, logical_rc:0, physical_rc:1)]                                               |
 
6 rows in set (0.030 sec)

MySQL [tpccdb]> SELECT ip, plan_depth, plan_line_id,operator,name,rows,cost,property from oceanbase.`gv$plan_cache_plan_explain`  where tenant_id=1002 AND ip = '172.20.249.51' AND port=2882 AND plan_id=273;
 
| ip            | plan_depth | plan_line_id | operator                  | name | rows | cost | property                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
 
| 172.20.249.51 |          0 |            0 | PHY_LIMIT                 | NULL |   10 |  408 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 172.20.249.51 |          1 |            1 |  PHY_NESTED_LOOP_JOIN     | NULL |   10 |  407 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 172.20.249.51 |          2 |            2 |   PHY_TABLE_SCAN          | o    |   10 |   38 | table_rows:139802, physical_range_rows:47417, logical_range_rows:47407, index_back_rows:0, output_rows:47407, est_method:local_storage, avaiable_index_name[bmsql_oorder], estimation info[table_id:1101710651081625, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:47407, physical_rc:47407), (table_type:7, version:1633335563707246-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:5, version:1633335563707246-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:0, version:1633335792045894-1633335792045894-9223372036854775807, logical_rc:0, physical_rc:10)] |
| 172.20.249.51 |          2 |            3 |   PHY_PX_FIFO_COORD       | NULL |    1 |   36 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 172.20.249.51 |          3 |            4 |    PHY_PX_REDUCE_TRANSMIT | NULL |    1 |   35 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 172.20.249.51 |          4 |            5 |     PHY_TABLE_SCAN        | i    |    1 |   35 | table_rows:100000, physical_range_rows:1, logical_range_rows:1, index_back_rows:0, output_rows:1, est_method:local_storage, estimation info[table_id:1101710651081627, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:100000, physical_rc:100000), (table_type:7, version:1633335563894016-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:5, version:1633335563894016-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:0, version:1633335794237719-1633335794237719-9223372036854775807, logical_rc:0, physical_rc:1)]                                               |
 
6 rows in set (0.065 sec)
```

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

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

```sql
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$plan_cache_plan_stat` s  
WHERE s.tenant_id = 1002   -- 改成具体的 tenant_id 
ORDER BY avg_exe_usec desc limit 10
;

 
| tenant_id | svr_ip        | plan_Id | sql_id                           | TYPE | query_sql                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | first_load_time            | avg_exe_usec | slow_count | executions | slowest_exe_usec | outline_id |
 
|      1002 | 172.20.249.49 |     251 | FC3FED8CCB2946DE54F1C5BA3656023C |    1 | SELECT d_tax, d_next_o_id     FROM bmsql_district     WHERE d_w_id = 4 AND d_id = 3     FOR UPDATE                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | 2021-10-04 20:52:36.677973 |     25668895 |          5 |         39 |        999820024 |         -1 |
|      1002 | 172.20.249.49 |     350 | 54B5A5861DAF78F52D9ADFBEE83D35B5 |    2 | INSERT INTO bmsql_new_order (    no_o_id, no_d_id, no_w_id) VALUES (4860, 9, 5)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | 2021-10-04 20:52:38.032855 |      1231951 |          3 |          3 |          1237821 |         -1 |
|      1002 | 172.20.249.49 |     322 | B447DE16B3F42D2409B2A2BE50328E63 |    2 | UPDATE bmsql_warehouse     SET w_ytd = w_ytd + 3684.0     WHERE w_id = 8                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | 2021-10-04 20:52:36.762523 |       486163 |          2 |          2 |           703346 |         -1 |
|      1002 | 172.20.249.52 |    2596 | F4585305C4CB9B091C750826A7DEDD13 |    1 | UPDATE bmsql_district     SET d_ytd = d_ytd + 134.32     WHERE d_w_id = 2 AND d_id = 9                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | 2021-10-04 20:52:36.689589 |       237542 |          5 |      16877 |        999895631 |         -1 |
|      1002 | 172.20.249.52 |    2602 | FC3FED8CCB2946DE54F1C5BA3656023C |    1 | SELECT d_tax, d_next_o_id     FROM bmsql_district     WHERE d_w_id = 2 AND d_id = 6     FOR UPDATE                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | 2021-10-04 20:52:36.727204 |       237286 |          4 |      16889 |        999746020 |         -1 |
|      1002 | 172.20.249.51 |     272 | 9D276020142C5B8259B85C3E8966C579 |    3 | 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                                                                                                                                                                                                                                                                                                                                                                                                                                              | 2021-10-05 11:02:46.942620 |       159385 |          1 |          1 |           159385 |         -1 |
|      1002 | 172.20.249.49 |     309 | B447DE16B3F42D2409B2A2BE50328E63 |    2 | UPDATE bmsql_warehouse     SET w_ytd = w_ytd + 4878.65     WHERE w_id = 8                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | 2021-10-04 20:52:36.741234 |       159196 |          4 |          8 |           452859 |         -1 |
|      1002 | 172.20.249.51 |     273 | 274FB280E08E876819555632D6951C5A |    3 | 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                                                                                                                                                                                                                                                                                                                                                                                                                                              | 2021-10-05 11:24:46.098640 |       141562 |          1 |          1 |           141562 |         -1 |
|      1002 | 172.20.249.52 |    3367 | CDA629CFD7B13E58328149264FA50055 |    3 | 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$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   | 2021-10-05 11:25:07.245044 |       115650 |          1 |          1 |           115650 |         -1 |
|      1002 | 172.20.249.52 |    3362 | 55E820294D4EEFB396B1BAD3CE27CD47 |    3 | 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$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 60 minute ))
ORDER BY request_time DESC
LIMIT 100 | 2021-10-05 11:24:04.440565 |       111119 |          1 |          2 |           150067 |         -1 |
 
10 rows in set (0.014 sec)
```

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

执行计划可以清空，命令如下：

```sql
ALTER SYSTEM flush plan cache GLOBAL;
```

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

## 如何使用 OUTLINE 改变执行计划

OceanBase 在 SQL执行和性能诊断方面的逻辑大量参考了ORACLE的设计思路。OceanBase也支持`SQL Outline`功能，能够修改在线运行的SQL执行计划。同时也支持SQL限流功能。

Outline的用法也是通过`SQL Hint`固定SQL的执行计划，可以调整表连接算法、使用的索引等等。
创建大纲的语法如下：

```sql
CREATE [OR REPLACE] OUTLINE outline_name ON stmt [ TO target_stmt ];

或 

CREATE [OR REPLACE] OUTLINE outline_name ON 'SQLID' using 'HINTS';
```

1. 其中stmt为一个带有HINT的DML语句。限流或固定计划，通过stmt中的HINT来区分。
2. 如果期望对含有HINT的语句进行限流和固定计划，则需要TO target_stmt来指明相应的SQL。`create outline outline_name on stmt1 to stmt2;`的语意是说对`stmt2`创建outline,让`stmt2`使用`stmt1`中的`hint`。
3. 指定 `OR REPLACE` 后，可以对已经存在执行计划或限流规则进行替换。（注：限流规则和执行计划间可以彼此替换）
4. 在使用 `target_stmt` 时，严格要求 `stmt` 与 `target_stmt` 在去掉 `hint` 后完全匹配（实现中为去掉`hint`的`signature`相同）。若是在创建限流时使用 `target_stmt`，则同时要求`fix_param` 完全匹配。

通过语句文本或者 SQLID 都可以创建大纲。不过由于文本中空格和换行如果有差异就会导致匹配不上。所以，不建议通过文本来创建大纲（除非文本非常简单）。这里通过 SQLID 来创建大纲。只要 SQL 文本不变，SQLID 就不会变。

### OUTLINE 相关视图

+ 存储outline的schema信息的系统表

```sql
oceanbase.__all_outline
oceanbase.__all_outline_history
```

+ 固定计划相关虚拟表和试图

展示的均是当前租户的信息：

```sql
oceanbase.__tenant_virtual_outline  用于outline迁移使用的虚拟表，同时显示固定计划的信息。

oceanbase.gv$outline 在__tenant_virutal_outline基础上创建的视图。

information_schema.dba_outlines  在__all_table上创建的视图。
```

+ 限流相关虚拟表和试图

下表展示的均是当前租户的信息：

```sql
oceanbase.__tenant_virtual_concurrent_limit_sql： 展示限流信息
oceanbase.gv$concurrent_limit_sql： 在__tenant_virtual_concurrent_limit_sql上创建的视图。
```

### OUTLINE 调优执行计划示例

下面示例还是针对前面测试 SQL ，尝试调整一下执行计划里表连接顺序。

+ 首先执行原 SQL ，通过 视图 `gv$sql_audit` 或 `gv$plan_cache_plan_stat` 找到该 SQL 的 SQLID 。

+ 然后根据 SQLID 创建大纲，指定 HINTS 。

```sql
create outline otl_test_1 on "9D276020142C5B8259B85C3E8966C579" using hint /*+ leading(i) */ ;

MySQL [tpccdb]> select * from oceanbase.__all_outline \G
*************************** 1. row ***************************
     gmt_create: 2021-10-05 12:18:26.032586
   gmt_modified: 2021-10-05 12:18:26.032586
      tenant_id: 0
     outline_id: 1001
    database_id: 1052
 schema_version: 1633407506030264
           name: otl_test_1
      signature:
outline_content: /*+leading(i) */
       sql_text:
          owner: root
           used: 0
        version: 3-b20901e8c84d3ea774beeaca963c67d7802e4b4e
     compatible: 1
        enabled: 1
         format: 0
 outline_params: ����
 outline_target:
         sql_id: 9D276020142C5B8259B85C3E8966C579
       owner_id: NULL
1 row in set (0.006 sec)
```

+ 再次执行 SQL。

```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
;
```

+ 查看实际执行计划

```sql
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$plan_cache_plan_stat` s  
WHERE s.tenant_id = 1002   and sql_id='9D276020142C5B8259B85C3E8966C579'
ORDER BY avg_exe_usec desc limit 10
;

+-----------+---------------+---------+----------------------------------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+--------------+------------+------------+------------------+------------------+
| tenant_id | svr_ip        | plan_Id | sql_id                           | TYPE | query_sql                                                                                                                                                 | first_load_time            | avg_exe_usec | slow_count | executions | slowest_exe_usec | outline_id       |
+-----------+---------------+---------+----------------------------------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+--------------+------------+------------+------------------+------------------+
|      1002 | 172.20.249.51 |     282 | 9D276020142C5B8259B85C3E8966C579 |    3 | 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 | 2021-10-05 12:19:15.443750 |       141419 |          1 |          1 |           141419 | 1101710651032553 |
|      1002 | 172.20.249.52 |    3488 | 9D276020142C5B8259B85C3E8966C579 |    3 | 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 | 2021-10-05 12:18:36.746965 |        88434 |          0 |          1 |            88434 | 1101710651032553 |
+-----------+---------------+---------+----------------------------------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+--------------+------------+------------+------------------+------------------+
2 rows in set (0.092 sec)

SELECT ip, plan_depth, plan_line_id,operator,name,rows,cost,property
from oceanbase.`gv$plan_cache_plan_explain` 
where tenant_id=1002 AND ip = '172.20.249.51' AND port=2882 AND plan_id=282  ;

 
| ip            | plan_depth | plan_line_id | operator                  | name | rows  | cost   | property                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
 
| 172.20.249.51 |          0 |            0 | PHY_LIMIT                 | NULL |    10 | 126991 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.51 |          1 |            1 |  PHY_MERGE_JOIN           | NULL |    10 | 126990 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.51 |          2 |            2 |   PHY_PX_FIFO_COORD       | NULL |  1453 |    856 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.51 |          3 |            3 |    PHY_PX_REDUCE_TRANSMIT | NULL |  1453 |    546 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.51 |          4 |            4 |     PHY_TABLE_SCAN        | i    |  1453 |    546 | table_rows:100000, physical_range_rows:100001, logical_range_rows:100000, index_back_rows:0, output_rows:100000, est_method:local_storage, avaiable_index_name[bmsql_item], pruned_index_name[idx_item_uk], estimation info[table_id:1101710651081627, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:100000, physical_rc:100000), (table_type:7, version:1633335563894016-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:5, version:1633335563894016-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:0, version:1633335794237719-1633335794237719-9223372036854775807, logical_rc:0, physical_rc:1)] |
| 172.20.249.51 |          2 |            5 |   PHY_SORT                | NULL |   689 | 125832 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.51 |          3 |            6 |    PHY_TABLE_SCAN         | o    | 47407 |  11907 | table_rows:139802, physical_range_rows:47417, logical_range_rows:47407, index_back_rows:0, output_rows:47407, est_method:local_storage, avaiable_index_name[bmsql_oorder], estimation info[table_id:1101710651081625, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:47407, physical_rc:47407), (table_type:7, version:1633335563707246-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:5, version:1633335563707246-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:0, version:1633335792045894-1633335792045894-9223372036854775807, logical_rc:0, physical_rc:10)]                                   |
 
7 rows in set (0.026 sec)

SELECT ip, plan_depth, plan_line_id,operator,name,rows,cost,property
from oceanbase.`gv$plan_cache_plan_explain` 
where tenant_id=1002 AND ip = '172.20.249.52' AND port=2882 AND plan_id=3488  ;

 
| ip            | plan_depth | plan_line_id | operator                  | name | rows  | cost   | property                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
 
| 172.20.249.52 |          0 |            0 | PHY_LIMIT                 | NULL |    10 | 135657 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.52 |          1 |            1 |  PHY_MERGE_JOIN           | NULL |    10 | 135656 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.52 |          2 |            2 |   PHY_TABLE_SCAN          | i    |  1453 |    546 | table_rows:100000, physical_range_rows:100001, logical_range_rows:100000, index_back_rows:0, output_rows:100000, est_method:local_storage, avaiable_index_name[bmsql_item], pruned_index_name[idx_item_uk], estimation info[table_id:1101710651081627, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:100000, physical_rc:100000), (table_type:7, version:1633335566970454-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:5, version:1633335566970454-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:0, version:1633335794237719-1633335794237719-9223372036854775807, logical_rc:0, physical_rc:1)] |
| 172.20.249.52 |          2 |            3 |   PHY_PX_FIFO_COORD       | NULL | 47407 | 134808 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.52 |          3 |            4 |    PHY_PX_REDUCE_TRANSMIT | NULL | 47407 | 125832 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.52 |          4 |            5 |     PHY_SORT              | NULL | 47407 | 125832 | NULL                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 172.20.249.52 |          5 |            6 |      PHY_TABLE_SCAN       | o    | 47407 |  11907 | table_rows:139802, physical_range_rows:47417, logical_range_rows:47407, index_back_rows:0, output_rows:47407, est_method:local_storage, avaiable_index_name[bmsql_oorder], estimation info[table_id:1101710651081625, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:47407, physical_rc:47407), (table_type:7, version:1633335566720232-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:5, version:1633335566720232-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:0, version:1633335792045894-1633335792045894-9223372036854775807, logical_rc:0, physical_rc:10)]                                   |
 
7 rows in set (0.008 sec)
```

从上面结果看出，这个  SQL 执行了两次，两次被路由到的节点不同，导致远程访问的表会不一样。但是两个 SQL 都是用了同一个 OUTLINE 成功了，实际执行计划里 ，表 `bmsql_item` 表确实是 驱动表，表连接算法也从 `NESTED-LOOP JOIN` 变成了 `MERGE JOIN` 。
当然，这个只是示例，这个 HINTS 并不一定会让 SQL 性能更好。

+ 删除 OUTLINE 。

```sql
drop outline otl_test_1 ;

```

删除大纲后，该 SQLID 的执行计划重新生成，又回归到原始的执行计划了。

## 如何更新统计信息

OceanBase 当前版本（3.1）还没有手动更新表统计信息能力，表的统计信息更新是在合并（`MAJOR FREEZE`）的时候更新的。默认情况下，OceanBase 收集统计信息会扫描所有数据块，所以统计信息里的分区大小和行数估计非常准确。如果表非常大，为了节省这个时间，也可以通过参数 `merge_stat_sampling_ratio`  设置一个采样比例（默认是100）。

在 SYS 租户运行：

```sql
alter system set merge_stat_sampling_ratio = 50 ;

```

+ 查看统计信息

```sql
SELECT  t.tenant_id, a.tenant_name, t.table_name, d.database_name,  tg.tablegroup_name , t.part_num , t2.partition_id, t2.ZONE, t2.svr_ip , round(t2.data_size/1024/1024) data_size_mb, t2.row_count  
FROM oceanbase.__all_tenant AS a  
 JOIN oceanbase.__all_virtual_database AS d ON ( a.tenant_id = d.tenant_id  ) 
 JOIN oceanbase.__all_virtual_table AS t  ON (t.tenant_id = d.tenant_id AND t.database_id = d.database_id) 
 JOIN oceanbase.__all_virtual_meta_table t2 ON (t.tenant_id = t2.tenant_id AND (t.table_id=t2.table_id OR t.tablegroup_id=t2.table_id)  AND t2.ROLE IN (1) )
 LEFT JOIN oceanbase.__all_virtual_tablegroup AS tg ON (t.tenant_id = tg.tenant_id and  t.tablegroup_id = tg.tablegroup_id) 
WHERE a.tenant_id IN (1002 ) AND  t.table_type IN (3)  
 AND d.database_name = 'tpccdb' 
ORDER BY t.tenant_id, tg.tablegroup_name, d.database_name, t.table_name, t2.partition_id
;

+-----------+-------------+------------------+---------------+-----------------+----------+--------------+-------+---------------+--------------+-----------+
| tenant_id | tenant_name | table_name       | database_name | tablegroup_name | part_num | partition_id | ZONE  | svr_ip        | data_size_mb | row_count |
+-----------+-------------+------------------+---------------+-----------------+----------+--------------+-------+---------------+--------------+-----------+
|      1002 | obmysql     | bmsql_config     | tpccdb        | NULL            |        1 |            0 | zone2 | 172.20.249.49 |            0 |         4 |
|      1002 | obmysql     | bmsql_item       | tpccdb        | NULL            |        1 |            0 | zone1 | 172.20.249.52 |            7 |    100000 |
|      1002 | obmysql     | bmsql_customer   | tpccdb        | tpcc_group      |        3 |            0 | zone3 | 172.20.249.51 |           40 |     90000 |
|      1002 | obmysql     | bmsql_customer   | tpccdb        | tpcc_group      |        3 |            1 | zone2 | 172.20.249.49 |           54 |    120000 |
|      1002 | obmysql     | bmsql_customer   | tpccdb        | tpcc_group      |        3 |            2 | zone1 | 172.20.249.52 |           41 |     90000 |
|      1002 | obmysql     | bmsql_district   | tpccdb        | tpcc_group      |        3 |            0 | zone3 | 172.20.249.51 |            0 |        30 |
|      1002 | obmysql     | bmsql_district   | tpccdb        | tpcc_group      |        3 |            1 | zone2 | 172.20.249.49 |            0 |        40 |
|      1002 | obmysql     | bmsql_district   | tpccdb        | tpcc_group      |        3 |            2 | zone1 | 172.20.249.52 |            0 |        30 |
|      1002 | obmysql     | bmsql_history    | tpccdb        | tpcc_group      |        3 |            0 | zone3 | 172.20.249.51 |            3 |    137475 |
|      1002 | obmysql     | bmsql_history    | tpccdb        | tpcc_group      |        3 |            1 | zone2 | 172.20.249.49 |            5 |    200065 |
|      1002 | obmysql     | bmsql_history    | tpccdb        | tpcc_group      |        3 |            2 | zone1 | 172.20.249.52 |            4 |    149261 |
|      1002 | obmysql     | bmsql_new_order  | tpccdb        | tpcc_group      |        3 |            0 | zone3 | 172.20.249.51 |            0 |     31962 |
|      1002 | obmysql     | bmsql_new_order  | tpccdb        | tpcc_group      |        3 |            1 | zone2 | 172.20.249.49 |            0 |     45222 |
|      1002 | obmysql     | bmsql_new_order  | tpccdb        | tpcc_group      |        3 |            2 | zone1 | 172.20.249.52 |            0 |     33058 |
|      1002 | obmysql     | bmsql_oorder     | tpccdb        | tpcc_group      |        3 |            0 | zone3 | 172.20.249.51 |            1 |    139802 |
|      1002 | obmysql     | bmsql_oorder     | tpccdb        | tpcc_group      |        3 |            1 | zone2 | 172.20.249.49 |            2 |    203152 |
|      1002 | obmysql     | bmsql_oorder     | tpccdb        | tpcc_group      |        3 |            2 | zone1 | 172.20.249.52 |            2 |    150858 |
|      1002 | obmysql     | bmsql_order_line | tpccdb        | tpcc_group      |        3 |            0 | zone3 | 172.20.249.51 |           49 |   1397517 |
|      1002 | obmysql     | bmsql_order_line | tpccdb        | tpcc_group      |        3 |            1 | zone2 | 172.20.249.49 |           72 |   2029431 |
|      1002 | obmysql     | bmsql_order_line | tpccdb        | tpcc_group      |        3 |            2 | zone1 | 172.20.249.52 |           53 |   1508395 |
|      1002 | obmysql     | bmsql_stock      | tpccdb        | tpcc_group      |        3 |            0 | zone3 | 172.20.249.51 |           75 |    300000 |
|      1002 | obmysql     | bmsql_stock      | tpccdb        | tpcc_group      |        3 |            1 | zone2 | 172.20.249.49 |           99 |    400000 |
|      1002 | obmysql     | bmsql_stock      | tpccdb        | tpcc_group      |        3 |            2 | zone1 | 172.20.249.52 |           75 |    300000 |
|      1002 | obmysql     | bmsql_warehouse  | tpccdb        | tpcc_group      |        3 |            0 | zone3 | 172.20.249.51 |            0 |         3 |
|      1002 | obmysql     | bmsql_warehouse  | tpccdb        | tpcc_group      |        3 |            1 | zone2 | 172.20.249.49 |            0 |         4 |
|      1002 | obmysql     | bmsql_warehouse  | tpccdb        | tpcc_group      |        3 |            2 | zone1 | 172.20.249.52 |            0 |         3 |
+-----------+-------------+------------------+---------------+-----------------+----------+--------------+-------+---------------+--------------+-----------+
26 rows in set (0.014 sec)

```

抽查一个表数据，可以看出行数统计还是很精准的。

```sql
MySQL [tpccdb]> select 'p0', count(*) from bmsql_oorder partition (p0)
 union select 'p1', count(*) from bmsql_oorder partition (p1)
 union select 'p2', count(*) from bmsql_oorder partition (p2)
;

+------+----------+
| p0   | count(*) |
+------+----------+
| p0   |   139802 |
| p1   |   203152 |
| p2   |   150876 |
+------+----------+
3 rows in set (0.623 sec)

```
