转 分布式执行计划
###sample 1
https://open.oceanbase.com/blog/1100217?_gl=1*10gldye*_ga*Mjc3Nzg0NTIwLjE3MzA3ODg3NDI.*_ga_T35KTM57DZ*MTczMDc5NDE2Ny4yLjEuMTczMDc5NTk5NC42MC4wLjA.
本文介绍 OceanBase 的 SQL 执行类型。分布式数据库下数据分布在各个节点,SQL 很有可能会跨节点取数据。在分布式数据库里这个是常见的现象。常见但不简单,深入分析可以知道功能和性能都有很多讲究。
分区的位置
在前文《揭秘OceanBase的弹性伸缩和负载均衡原理》里介绍了 OceanBase 分区的分布和弹性伸缩原理。分区是表的子集,普通的表是一个分区,分区表有多个分区。分区表的全局索引也是一个独立的分区。如果全局索引还分区了,那就是独立的好几个分区。分区的位置跟 SQL请求的位置是否相同会影响 SQL 执行计划的形式。
前面几篇文章里分享的 SQL 执行计划都是默认分区的位置跟 SQL 请求的位置是相同的。本文分享如果这两个位置不同,前面的执行计划会有什么变化。
首先我们要会查看分区的位置。SQL 如下。
SELECT a.tenant_name,d.database_name, t.table_name, tg.tablegroup_name , t.part_num , t2.partition_id, t2.ZONE, t2.svr_ip, t2.role
, a.primary_zone
, t2.row_count, round(data_size/1024/1024,0) data_size_MB
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 (1001 ) AND t.table_type IN (3)
AND d.database_name = 'TPCC'
ORDER BY t.tenant_id, tg.tablegroup_name, d.database_name, t.table_name, t2.partition_id
;
+-------------+---------------+------------------+-----------------+----------+--------------+-------+---------------+------+--------------+-----------+--------------+
| tenant_name | database_name | table_name | tablegroup_name | part_num | partition_id | ZONE | svr_ip | role | primary_zone | row_count | data_size_MB |
+-------------+---------------+------------------+-----------------+----------+--------------+-------+---------------+------+--------------+-----------+--------------+
| oboracle | TPCC | BMSQL_CONFIG | NULL | 1 | 0 | zone2 | 172.30.118.74 | 1 | RANDOM | 4 | 2 |
| oboracle | TPCC | BMSQL_ITEM | NULL | 1 | 0 | zone1 | 172.30.118.75 | 1 | RANDOM | 100000 | 8 |
| oboracle | TPCC | BMSQL_CUSTOMER | tpcc_group | 1 | 0 | zone2 | 172.30.118.74 | 1 | RANDOM | 3000000 | 1284 |
| oboracle | TPCC | BMSQL_DISTRICT | tpcc_group | 1 | 0 | zone2 | 172.30.118.74 | 1 | RANDOM | 1000 | 2 |
| oboracle | TPCC | BMSQL_HISTORY | tpcc_group | 1 | 0 | zone2 | 172.30.118.74 | 1 | RANDOM | 3000000 | 72 |
| oboracle | TPCC | BMSQL_NEW_ORDER | tpcc_group | 1 | 0 | zone2 | 172.30.118.74 | 1 | RANDOM | 900000 | 2 |
| oboracle | TPCC | BMSQL_OORDER | tpcc_group | 1 | 0 | zone2 | 172.30.118.74 | 1 | RANDOM | 3000000 | 16 |
| oboracle | TPCC | BMSQL_ORDER_LINE | tpcc_group | 1 | 0 | zone2 | 172.30.118.74 | 1 | RANDOM | 29989150 | 844 |
| oboracle | TPCC | BMSQL_STOCK | tpcc_group | 1 | 0 | zone2 | 172.30.118.74 | 1 | RANDOM | 10000000 | 2382 |
| oboracle | TPCC | BMSQL_WAREHOUSE | tpcc_group | 1 | 0 | zone2 | 172.30.118.74 | 1 | RANDOM | 100 | 2 |
+-------------+---------------+------------------+-----------------+----------+--------------+-------+---------------+------+--------------+-----------+--------------+
10 rows in set (0.02 sec)
查看分区主副本位置的 SQL 倒不一定要这么复杂,这里只是顺便把相关信息都查询出来了。
SQL 路由节点
下面是应用访问 OceanBase 分区的链路示意图。应用访问的 obproxy 或者 F5 的 VIP 。

简单说明:
- OB 的每个分区至少有三个副本,其中一个主副本(
leader
)和两个备副本(follower
)。默认只有主副本提供读写服务。 - OBProxy 只提供 SQL 路由功能,不提供计算功能。OBProxy 的路由策略很丰富。简单理解就是把 SQL转发到第一个表分区的主副本所在节点。
- 如果开启事务了,OBProxy会把整个事务的 SQL 转发到开启事务的那条 SQL 被路由到的节点。
- OBServer自身也有 SQL 路由功能,如果被转发过来的 SQL 解析执行计划时发现数据在其他节点,会发起一个远程的取数据操作。
所以后续的 SQL 如果访问的数据分区不在当前节点,则该 SQL 类型就是一个远程 SQL。
为了测试生成远程执行计划,我们就不能通过 OBProxy 连接集群,而改为直连那个 OB 节点。
远程 SQL 测试
EXCHANGE IN|OUT REMOTE
首先看要访问表的主副本节点,然后直连另外一个节点。
比如说表 BMSQL_ITEM
主副本在节点 172.30.118.75
上,测试访问时故意从节点 172.30.118.74
访问。
- 示例 1
obclient -h172.30.118.74 -utpcc@oboracle -P2881 -p123456 -c -A tpcc -e "
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
obclient -h172.30.118.74 -utpcc@oboracle -P2881 -p123456 -c -A tpcc -e "
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, ?, '\')])
以上是直连 OB 节点人为构造的远程 SQL 访问,初看有点刻意为之,只是为了方便理解远程 SQL的执行计划。实际业务访问 OB 都是先通过 OBPROXY
,然后再被 OBPROXY
转发到后端 OB 节点。不过即使是只通过 OBORPXY
,也还是有可能存在一些远程 SQL 访问。
下面通过 OBPROXY
执行.
- 示例 3
CREATE INDEX bmsql_customer_idx1 ON bmsql_customer(c_last) LOCAL;
CREATE INDEX bmsql_item_idx1 ON bmsql_item(i_name) LOCAL;
EXPLAIN extended_Noaddr
SELECT /*+ test2021040502 no_use_px */ o.O_W_ID , o.O_D_ID , o.O_OL_CNT , ol.OL_NUMBER ,ol.OL_QUANTITY , ol.OL_AMOUNT , i.I_NAME , i.I_PRICE
FROM BMSQL_CUSTOMER c
JOIN BMSQL_OORDER o ON (c.c_id = o.o_c_id AND c.c_w_id=o.o_w_id AND c.c_d_id=o.o_d_id)
JOIN BMSQL_ORDER_LINE ol ON (o.O_ID = ol.OL_O_ID AND o.O_W_ID=ol.OL_W_ID AND o.O_D_ID=ol.OL_D_ID)
JOIN BMSQL_ITEM i ON (ol.OL_I_ID = i.I_ID)
WHERE c.c_last='BARBARESE' AND c.c_w_id=1 AND c.c_d_id=1
ORDER BY o.O_W_ID , o.O_D_ID , o.O_OL_CNT , ol.OL_NUMBER
;
从前面分区位置知 BMSQL_CUSTOMER
和 BMSQL_OORDER
和 BMSQL_ORDER_LINE
的主副本在节点 172.30.118.74
上, BMSQL_ITEM
的主副本在 172.30.118.75
上。这三个表的表连接如果发生在节点 74 上,对 BMSQL_ITEM
的访问就会是远程 SQL。如果发生在 75 上,对其他三个表就是远程访问。
所以这个 SQL 的执行计划可能有下面两种:
================================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
----------------------------------------------------------------
|0 |SORT | |16 |1046|
|1 | NESTED-LOOP JOIN | |16 |803 |
|2 | EXCHANGE IN DISTR | |16 |247 |
|3 | EXCHANGE OUT DISTR| |16 |197 |
|4 | NESTED-LOOP JOIN | |16 |197 |
|5 | NESTED-LOOP JOIN| |2 |126 |
|6 | TABLE SCAN |C(BMSQL_CUSTOMER_IDX1)|1 |36 |
|7 | TABLE SCAN |O(BMSQL_OORDER_IDX1) |2 |89 |
|8 | TABLE SCAN |OL |10 |39 |
|9 | TABLE GET |I |1 |36 |
================================================================
Outputs & filters:
-------------------------------------
0 - output([O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT], [OL.OL_NUMBER], [OL.OL_QUANTITY], [OL.OL_AMOUNT], [I.I_NAME], [I.I_PRICE]), filter(nil), sort_keys([O.O_OL_CNT, ASC], [OL.OL_NUMBER, ASC])
1 - output([O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT], [OL.OL_NUMBER], [OL.OL_QUANTITY], [OL.OL_AMOUNT], [I.I_NAME], [I.I_PRICE]), filter(nil),
conds(nil), nl_params_([OL.OL_I_ID]), inner_get=false, self_join=false, batch_join=true
2 - output([O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT], [OL.OL_NUMBER], [OL.OL_QUANTITY], [OL.OL_AMOUNT], [OL.OL_I_ID]), filter(nil)
3 - output([O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT], [OL.OL_NUMBER], [OL.OL_QUANTITY], [OL.OL_AMOUNT], [OL.OL_I_ID]), filter(nil)
4 - output([O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT], [OL.OL_NUMBER], [OL.OL_QUANTITY], [OL.OL_AMOUNT], [OL.OL_I_ID]), filter(nil),
conds(nil), nl_params_([O.O_ID]), inner_get=false, self_join=false, batch_join=true
5 - output([O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT], [O.O_ID]), filter(nil),
conds(nil), nl_params_([C.C_ID]), inner_get=false, self_join=false, batch_join=true
6 - output([C.C_ID]), filter(nil),
access([C.C_ID]), partitions(p0),
is_index_back=false,
range_key([C.C_LAST], [C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(BARBARESE,1,1,MIN ; BARBARESE,1,1,MAX),
range_cond([C.C_LAST = 'BARBARESE'], [C.C_W_ID = 1], [C.C_D_ID = 1])
7 - output([O.O_W_ID], [O.O_D_ID], [O.O_ID], [O.O_OL_CNT]), filter(nil),
access([O.O_W_ID], [O.O_D_ID], [O.O_ID], [O.O_OL_CNT]), partitions(p0),
is_index_back=true,
range_key([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN ; MAX),
range_cond([O.O_W_ID = 1], [O.O_D_ID = 1], [? = O.O_C_ID])
8 - output([OL.OL_I_ID], [OL.OL_NUMBER], [OL.OL_QUANTITY], [OL.OL_AMOUNT]), filter(nil),
access([OL.OL_I_ID], [OL.