10 分钟教会你如何看懂 MySQL 执行计划

from :程序员老马头 https://blog.csdn.net/weixin_74412978/article/details/144772135

通常查询慢查询SQL语句时会使用EXPLAIN命令来查看SQL语句的执行计划,通过返回的信息,可以了解到Mysql优化器是如何执行SQL语句,通过分析可以帮助我们提供优化的思路。

1、Explain 作用

explain 命令主要用于查看 SQL 语句的执行计划,该命令可以模拟优化器执行 SQL 查询语句,可以帮助我们编写和优化 SQL。那么 explain 具体可以提供哪些信息,帮助我们如何去优化 SQl 的呢?

1.1、表的读取顺序

1.2、数据读取操作的操作类型

1.3、哪些索引可以使用

1.4、哪些索引被实际使用

1.5、表之间的引用

1.6、每张表有多少行被优化器查询

2、Explain 如何使用

使用方式: explain + 待执行的sql

explain 会返回一个待执行 SQL 的执行计划列表,列表包含了 12 个字段,字段共同描述了 SQL 在执行计划中将会采取何种方式执行。以下列表详细描述了执行计划表的字段含义:

3. 关键字段分析

(1)id

执行 select 语句查询的序列号,包含一组数字,表示查询中执行 select 子句或操作表的顺序,它有三种情况:

(2)select_type

就是数据读取操作的操作类型,他一共有以下几种:

(3)table

显示的查询表名,如果查询使用了别名,那么这里显示的是别名,如果不涉及对数据表的操作,那么这显示为 null,也可以是以下之一:

(4)partitions

查询将匹配记录的分区。该值NULL用于非分区表。

(5)type

依次从好到差:

system>const>eq_ref>ref>ref_or_null>range>index>ALL

除了all之外,其他的type都可以使用到索引,除了index_merge之外,其他的type只可以用到一个索引。

我们自己创建一系列表来实验下:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
 
-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods`  (
  `id` int(11) NOT NULL,
  `sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
 
-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES (1, 'sn123456', '衣服');
 
-- ----------------------------
-- Table structure for sku
-- ----------------------------
DROP TABLE IF EXISTS `sku`;
CREATE TABLE `sku`  (
  `id` int(11) NOT NULL,
  `goods_id` int(11) NOT NULL,
  `status` int(11) NOT NULL,
  `deleted` int(11) NOT NULL,
  `barcode` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `index_2`(`name`) USING BTREE,
  INDEX `index_1`(`goods_id`, `status`, `deleted`, `barcode`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
 
-- ----------------------------
-- Records of sku
-- ----------------------------
INSERT INTO `sku` VALUES (1, 1, 1, 0, 'kt123456', '黑色');
 
SET FOREIGN_KEY_CHECKS = 1;

system

表只有一行记录(等于系统表),这是 const 类型的特例,平时不会出现,这个也可忽略不计;

const

表示通过索引一次就找到了,const 用于比较 primary key 或者 unique 索引。因为只匹配一行记录,所以很快。如果将主键置于 where 列表中,mysql 就能将该查询转换成一个常量;

EXPLAIN SELECT * FROM sku WHERE id=1;

eq_ref

唯一性索引扫描,对于每一个索引键,表中只有一条记录与之匹配,常用于主键或唯一索引扫描;此类型通常出现在多表的 join 等值查询,表示对于前表的每一个结果,都只能匹配到后表的一行结果,查询效率较高。

EXPLAIN SELECT * FROM sku,goods WHERE sku.goods_id=goods.id;

ref

非唯一性索引扫描,返回匹配某个单独值得所有行,本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以它应该属于查找和扫描的混合体;

EXPLAIN SELECT * FROM sku WHERE goods_id=1;

ref_or_null

二级索引等值比较同时限定 is null 。

EXPLAIN SELECT * FROM sku WHERE name=’123456′ or name IS NULL;

range

只检索给定范围的行,使用一个索引来选择行,key列显示使用哪个索引,一般就是在你的 where 语句中出现了 between、<、>、in 等的查询;这种范围索引扫描比全表扫描要好,因为它只需要开始于索引的某一个点,结束于另一个点,不用扫描全部索引;

EXPLAIN SELECT * FROM sku WHERE id BETWEEN 1 and 10;

index

index 和 all 区别为 index 类型只遍历索引树,这通常比 all 快,因为索引文件通常比数据文件小;也就是说虽然 all 和 index 都是读写表,但 index 是从索引中读取的,而 all 是从硬盘中读的;

EXPLAIN SELECT barcode FROM sku WHERE deleted=0;
all

也就是全表扫描;

EXPLAIN SELECT * FROM sku WHERE deleted=0;

(6)possible_keys

查询可能使用到的索引都会在这里列出来。

(7)key

查询真正使用到的索引,select_type为index_merge时,这里可能出现两个以上的索引,其他的select_type这里只会出现一个。

(8)key_len

key_len 表示该列计算查询中使用的索引的长度。例如:SELECT * FROM table where age = 1 and name like ‘xx’,假设 age 是 int 类型且不可为 null;name 是 varchar(20) 类型且可以为 null,编码为 utf8。若以这两个字段为索引查询,那么 key_len 的值为 4 + 3 * 20 + 2 + 1 = 67。具体计算规则如下表所示:

(9)ref

如果是使用的常数等值查询,这里会显示const,如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段,如果是条件使用了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示为func。

(10)rows

这里是执行计划中估算的扫描行数,不是精确值。

(11)filtered

使用explain extended时会出现这个列,5.7之后的版本默认就有这个字段,不需要使用explain extended了。这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体记录数。

(12)Extra

这个列可以显示的信息非常多,有几十种,常用的有:

1、distinct:在select部分使用了distinct关键字

2、no tables used:不带from字句的查询或者From dual查询。使用not in()形式子查询或not exists()运算符的连接查询,这种叫做反连接。即,一般连接查询是先查询内表,再查询外表,反连接就是先查询外表,再查询内表。

3、using filesort:说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。mysql中无法利用索引完成的排序操作称为“文件排序”。排序时无法使用到索引时,就会出现这个。常见于order by语句中,需要尽快优化

4、using index:查询时不需要回表查询,直接通过索引就可以获取查询的数据。

5、using join buffer(block nested loop),using join buffer(batched key accss) :5.6.x之后的版本优化关联查询的BNL,BKA特性。主要是减少内表的循环数量以及比较顺序地扫描查询。

6、using sort_union,using_union,using intersect,using sort_intersection:

using intersect:表示使用and的各个索引的条件时,该信息表示是从处理结果获取交集

using union:表示使用or连接各个使用索引的条件时,该信息表示从处理结果获取并集

using sort_union和using sort_intersection:与前面两个对应的类似,只是他们是出现在用and和or查询信息量大时,先查询主键,然后进行排序合并后,才能读取记录并返回。

using temporary:表示使用了临时表存储中间结果。临时表可以是内存临时表和磁盘临时表,执行计划中看不出来,需要查看status变量,used_tmp_table,used_tmp_disk_table才能看出来。常见于order by和分组查询group by。group by一定要遵循所建索引的顺序与个数。需要尽快优化

using where:表示存储引擎返回的记录并不是所有的都满足查询条件,需要在server层进行过滤。查询条件中分为限制条件和检查条件,5.6之前,存储引擎只能根据限制条件扫描数据并返回,然后server层根据检查条件进行过滤再返回真正符合查询的数据。5.6.x之后支持ICP特性(index condition pushdown,索引下推),可以把检查条件也下推到存储引擎层,不符合检查条件和限制条件的数据,直接不读取,这样就大大减少了存储引擎扫描的记录数量。extra列显示using index condition

firstmatch(tb_name) :5.6.x开始引入的优化子查询的新特性之一,常见于where字句含有in()类型的子查询。如果内表的数据量比较大,就可能出现这个

loosescan(m..n) :5.6.x之后引入的优化子查询的新特性之一,在in()类型的子查询中,子查询返回的可能有重复记录时,就可能出现这个。

4. Explain 主要关注点

总的来说,我们只需要关注结果中的几列:

再来看下Extra列中需要注意出现的几种情况:

学习阅读和管理 OceanBase 数据库 SQL 执行计划

from :https://www.oceanbase.com/docs/community-tutorials-cn-1000000001390068

执行计划(Execution Plan)是对一条 SQL 查询语句在数据库中执行过程的描述。

用户可以通过 EXPLAIN 命令查看优化器针对指定 SQL 生成的执行计划。如果要分析某条 SQL 的性能问题,通常需要先查看 SQL 的执行计划,排查每一步 SQL 执行是否存在问题。所以读懂执行计划是 SQL 优化的先决条件,而了解执行计划的算子是理解 EXPLAIN 命令的关键。

EXPLAIN 命令格式

该语句用于解释 SQL 语句的执行计划,可以是 SELECT、DELETE、INSERT、REPLACE 或 UPDATE 语句。

EXPLAIN 与 DESCRIBE、DESC 互为同义词。

语法

{EXPLAIN [INTO table_name ] [SET statement_id = string]}
[explain_type] [PRETTY | PRETTY_COLOR] dml_statement;

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

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

参数解释

参数描述
INTO table_name表示将 EXPLAIN 的计划信息保存到指定表内。如果没有指定 INTO table_name,默认查询到 PLAN_TABLE 表内。
SET statement_id表示当前查询使用字符串标记,以方便后续查询该 SQL 的计划信息。如果没有指定 SET statement_id,默认使用空字符串作为信息标记。
PRETTY | PRETTY_COLOR将计划树中的父节点和子节点使用树线或彩色树线连接起来,使得执行计划展示更方便阅读。
BASIC指定输出计划的基础信息,如算子 ID、算子名称、所引用的表名。
OUTLINE指定输出的计划信息包含 OUTLINE 信息。
EXTENDED展示附加信息。
EXTENDED_NOADDR以简约的方式展示附加信息。
PARTITIONS显示分区相关信息。
FORMAT = {TRADITIONAL| JSON}指定 EXPALIN 的输出格式。TRADITIONAL:表格输出格式;JSONKEY:VALUE 输出格式,JSON 显示为 JSON 字符串,包括 EXTENDEDPARTITIONS 信息。
dml_statementDML 语句。

对于 OceanBase 数据库的使用者来说,最常用的是 EXPLAIN 命令和 EXPLAIN EXTENDED_NOADDR 命令。

  • EXPLAIN 所展示的信息可以快速帮助普通用户了解整个计划的执行方式。例如:
  create table t1(c1 int, c2 int);

  create table t2(c1 int, c2 int);

  -- 向 t1 表插入 10 行测试数据,c1 列的值是从 1 到 1000 的连续整数。
  insert into t1 with recursive cte(n) as (select 1 from dual union all select n + 1 from cte where n < 1000) select n, n from cte;

  -- 向 t2 表插入 10 行测试数据,c1 列的值是从 1 到 1000 的连续整数。
  insert into t2 with recursive cte(n) as (select 1 from dual union all select n + 1 from cte where n < 1000) select n, n from cte;

  -- 收集指定表 t1 的统计信息
  analyze table t1 COMPUTE STATISTICS for all columns size 128;

  -- 收集指定表 t2 的统计信息
  analyze table t2 COMPUTE STATISTICS for all columns size 128;

  explain select * from t1, t2 where t1.c1 = t2.c1 and t1.c1 < 500;
  +------------------------------------------------------------------------------------+
  | Query Plan                                                                         |
  +------------------------------------------------------------------------------------+
  | =================================================                                  |
  | |ID|OPERATOR         |NAME|EST.ROWS|EST.TIME(us)|                                  |
  | -------------------------------------------------                                  |
  | |0 |HASH JOIN        |    |498     |315         |                                  |
  | |1 |├─TABLE FULL SCAN|t1  |499     |76          |                                  |
  | |2 |└─TABLE FULL SCAN|t2  |499     |76          |                                  |
  | =================================================                                  |
  | Outputs & filters:                                                                 |
  | -------------------------------------                                              |
  |   0 - output([t1.c1], [t1.c2], [t2.c1], [t2.c2]), filter(nil), rowset=256          |
  |       equal_conds([t1.c1 = t2.c1]), other_conds(nil)                               |
  |   1 - output([t1.c1], [t1.c2]), filter([t1.c1 < 500]), rowset=256                  |
  |       access([t1.c1], [t1.c2]), partitions(p0)                                     |
  |       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
  |       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                  |
  |   2 - output([t2.c1], [t2.c2]), filter([t2.c1 < 500]), rowset=256                  |
  |       access([t2.c1], [t2.c2]), partitions(p0)                                     |
  |       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
  |       range_key([t2.__pk_increment]), range(MIN ; MAX)always true                  |
  +------------------------------------------------------------------------------------+

OceanBase 数据库执行计划中的各列的含义如下表所示。

列名含义
ID执行树按照前序遍历的方式得到的编号(从 0 开始)。
OPERATOR操作算子的名称。
NAME对应表操作的表名(索引名)。
EST. ROWS优化器估算该操作算子的输出行数,仅作为参考。
例如上图中最下方的 1 号算子 TABLE FULL SCAN,因为过滤条件有 t1.c1 < 500,所以优化器根据这 1000 行数据的统计信息,可以估算出过滤之后需要输出的数据行数为 499 行。
EST.TIME优化器估算该操作算子的执行代价(微秒),仅作为参考。

在表操作中,NAME 字段会显示该操作涉及的表的名称(别名),如果是使用索引访问,还会在名称后的括号中展示该索引的名称,例如 t1(t1_c2) 表示使用了索引 t1_c2。如果扫描的顺序是逆序,还会在后面使用 RESERVE 关键字标识,例如 t1(t1_c2,RESERVE)

OceanBase 数据库 EXPLAIN 命令输出的第一部分是执行计划的树形结构展示。其中每一个操作在树中的层次通过其在算子中的缩进予以展示。树的层次关系用缩进来表示,层次最深的优先执行,层次相同的算子以指定算子的执行顺序为标准来执行。上述示例查询的计划展示树如下图所示:

计划展示

其中,0 号算子是一个 hash join(HJ) 算子,它有两个子节点,分别是 1 和 2 号 table scan 算子(TSC)。0 号 hash join 算子的执行逻辑如下:

  1. 读取左子节点的数据,根据联接列计算哈希值,构建一张哈希表。
  2. 读取右子节点的数据,根据联接列计算哈希值,尝试使用通过左支数据构建的哈希表进行哈希探测,完成联接计算。 OceanBase 数据库 EXPLAIN 命令输出的第二部分是各操作算子的详细信息,包括输出表达式、过滤条件、分区信息以及各算子的独有信息(包括排序键、联接键、下压条件等)。即上述计划中的下半部分:
   Outputs & filters:                                                                 
   -------------------------------------                                              
     0 - output([t1.c1], [t1.c2], [t2.c1], [t2.c2]), filter(nil), rowset=16           
         equal_conds([t1.c1 = t2.c1]), other_conds(nil)                               
     1 - output([t1.c1], [t1.c2]), filter([t1.c1 > 10]), rowset=16                    
         access([t1.c1], [t1.c2]), partitions(p0)                                     
         is_index_back=false, is_global_index=false, filter_before_indexback[false],  
         range_key([t1.__pk_increment]), range(MIN ; MAX)always true                  
     2 - output([t2.c1], [t2.c2]), filter([t2.c1 > 10]), rowset=16                    
         access([t2.c1], [t2.c2]), partitions(p0)                                     
         is_index_back=false, is_global_index=false, filter_before_indexback[false],  
         range_key([t2.__pk_increment]), range(MIN ; MAX)always true

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

  1. output : 该算子的输出表达式。
  2. filter : 该算子的过滤谓词。 如果算子没有设置过滤条件,则为 nil。 为了更好地读懂 OceanBase 数据库 EXPLAIN 计划中各个算子的第二部分,需要用户对各个算子的作用有一个初步的了解,具体介绍可参见官网《OceanBase 数据库》文档 参考指南/性能调优/SQL 调优指南/SQL 执行计划/执行计划算子 章节。 例如上面这个计划,用户需要通过官网《OceanBase 数据库》文档中 参考指南/性能调优/SQL 调优指南/SQL 执行计划/执行计划算子/TABLE SCAN 内容和 参考指南/性能调优/SQL 调优指南/SQL 执行计划/执行计划算子/JOIN 一文中的 HASH JOIN 内容的介绍来读懂上面这部分内容,大家不妨一试。

说明

上述计划中的 rowset=16 和 OceanBase 数据库执行引擎的向量化执行技术相关。表示在特定算子中,会按照 16 行一组分批进行计算。

用户可以通过 _rowsets_enabled 对向量化开关进行打开或关闭,例如通过 alter system set _rowsets_enabled = 0; 关闭向量化开关。用户也可以通过 _rowsets_max_rows 对向量化执行中一批处理的行数进行设置,例如通过 alter system set_rowsets_max_rows = 4; 将向量化执行中一批数据的行数改为 4

向量化更详细的内容不在这里展开,感兴趣的用户可以阅读 OceanBase 社区博客《OceanBase 轻量级数仓关键技术解读》中的 向量化执行技术 部分。

  • EXPLAIN EXTENDED_NOADDR 命令用于进行详细的计划展示。通常用户在排查 SQL 性能问题时,会使用这种展示模式。
  CREATE TABLE `t1` (
    `c1` int, `c2` int,
    KEY `idx` (`c1`));

  insert into t1 values(1, 2);

  explain EXTENDED_NOADDR select * from t1 where c1 > 10;
  +--------------------------------------------------------------------------+
  | Query Plan                                                               |
  +--------------------------------------------------------------------------+
  | ===================================================                      |
  | |ID|OPERATOR        |NAME   |EST.ROWS|EST.TIME(us)|                      |
  | ---------------------------------------------------                      |
  | |0 |TABLE RANGE SCAN|t1(idx)|1       |7           |                      |
  | ===================================================                      |
  | Outputs & filters:                                                       |
  | -------------------------------------                                    |
  |   0 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                   |
  |       access([t1.__pk_increment], [t1.c1], [t1.c2]), partitions(p0)      |
  |       is_index_back=true, is_global_index=false,                         |
  |       range_key([t1.c1], [t1.__pk_increment]), range(10,MAX ; MAX,MAX),  |
  |       range_cond([t1.c1 > 10])                                           |
  | Used Hint:                                                               |
  | -------------------------------------                                    |
  |   /*+                                                                    |
  |                                                                          |
  |   */                                                                     |
  | Qb name trace:                                                           |
  | -------------------------------------                                    |
  |   stmt_id:0, stmt_type:T_EXPLAIN                                         |
  |   stmt_id:1, SEL$1                                                       |
  | Outline Data:                                                            |
  | -------------------------------------                                    |
  |   /*+                                                                    |
  |       BEGIN_OUTLINE_DATA                                                 |
  |       INDEX(@"SEL$1" "test"."t1"@"SEL$1" "idx")                          |
  |       OPTIMIZER_FEATURES_ENABLE('4.0.0.0')                               |
  |       END_OUTLINE_DATA                                                   |
  |   */                                                                     |
  | Optimization Info:                                                       |
  | -------------------------------------                                    |
  |   t1:                                                                    |
  |       table_rows:1                                                       |
  |       physical_range_rows:1                                              |
  |       logical_range_rows:1                                               |
  |       index_back_rows:1                                                  |
  |       output_rows:1                                                      |
  |       table_dop:1                                                        |
  |       dop_method:Table DOP                                               |
  |       avaiable_index_name:[idx, t1]                                      |
  |       unstable_index_name:[t1]                                           |
  |       stats version:0                                                    |
  |       dynamic sampling level:0                                           |
  |   Plan Type:                                                             |
  |       LOCAL                                                              |
  |   Note:                                                                  |
  |       Degree of Parallelisim is 1 because of table property              |
  +--------------------------------------------------------------------------+
  47 rows in set

上图中的 Optimization Info 往往可以用于分析一些 SQL 的性能问题,其中重要信息的介绍如下。

Optimization Info含义解释
table_rows上一个合并版本 SSTable 中的表的行数,可以简单理解为 t1 表的行数,只具有参考意义。
physical_range_rowst1 表需要扫描的物理行数。如果走了索引的话,含义为 t1 表在索引上需要扫描的物理行数。
logical_range_rowst1 表在需要扫描的逻辑行数。如果走了索引的话,含义为 t1 表在索引上需要扫描的逻辑行数。在上面这个计划中,因为扫描了 idx 索引,所以能看到扫描范围都是 range(10,MAX ; MAX,MAX),range_cond([t1.c1 > 10])。如果没有索引,则需要扫描全表,这时扫描的范围会变为 range(MIN ; MAX)注意
physical_range_rowslogical_range_rows 这两个指标一般来说是相近的,看任意一个都可以。仅在一些特殊的 Buffer 表场景下,physical_range_rows 可能会远大于 logical_range_rows。Buffer 表表示频繁插入、删除的表。当 LSM tree 中的增量数据中积累了大量标记删除的数据时,从上层应用视角实际存在的行很少,但范围查询时可能需要处理较多的标记删除的数据,这种场景下,physical_range_rows 可能会远大于 logical_range_rows,从而导致 SQL 耗时不够理想。同时 Buffer 表场景下也容易导致优化器生成非最优执行计划。Buffer 表的介绍、检测逻辑,以及规避方法详见官网《OceanBase 数据库》文档 管理数据库/性能调优/识别组件内的瓶颈/OBServer 端性能瓶颈/非最优计划/Buffer 表
index_back_rows如果索引需要回表,这个值表示索引回表的行数。当全表扫描或者索引扫描但不需要回表时,该值为 0。索引回表的概念详见官网《OceanBase 数据库》文档 参考指南/性能调优/SQL 调优指南/SQL 执行计划/执行计划算子/TABLE SCAN。简单来说,这个计划中索引需要回主表的原因是索引 idx 中只有 c1 列的数据,需要根据索引中过滤出来的数据对应到主表中的隐藏主键信息,回到主表中查出 c2 列的数据。如果这个查询不是 select * from t1 where c1 > 10,而是 select c1 from t1 where c1 > 10,因为索引中已经包含了需要查询的全部信息,则不需要索引回表。
output_rows预估输出行数。在上面的计划中,表示 t1 表经过过滤之后的行数。
table_dopt1 表扫描时的并行度(并行执行时所使用的工作线程数)。
dop_method决定表扫描并行度的原因。可以是 TableDOP(表在定义时指定的并行度)、AutoDop(优化器基于代价选择的并行度,需要打开 auto dop 功能)、global parallel(parallel hint 或系统变量设置的并行度)。
avaiable_index_namet1 表可用的索引列表。这里除了索引表以外,还包括主表。如果没有合适的索引,计划会选择在主表上进行全表扫描。
pruned_index_name当前的查询基于优化器的规则,认为不应该使用的索引列表。OceanBase 数据库优化器的索引剪枝规则详见官网《OceanBase 数据库》文档 参考指南/性能调优/SQL 调优指南/SQL 优化/查询优化/访问路径/基于规则的路径选择
unstable_index_name这个如果存在,则一定是被裁剪的主表路径,被裁剪通常是因为存在其他的索引路径 range 行数较小。
stats versiont1 表统计信息版本号,如果值为 0,表示该表没有收集统计信息。为了保证计划生成正确,可以自动或者手动收集对应表的统计信息。
dynamic sampling level动态采样等级,如果值为 0,表示该表没有使用动态采样。
动态采样是一种优化器的优化工具,详见官网《OceanBase 数据库》文档 参考指南/性能调优/SQL 调优指南/SQL 优化/优化器统计信息/统计信息收集方式/优化器动态采样
estimation methodt1 表行数估计方式,可以是 DEFAULT(使用的默认统计信息,这种情况行数估计可能会不准,需要 DBA 介入优化)、STORAGE(使用存储层实时估行)、STATS(使用统计信息估行)。
Plan Type当前计划类型,可以是 LOCAL、REMOTE、DISTRIBUTED。具体含义详见本教程 ODP SQL 路由原理 中的 SQL 的计划类型 部分。
Note生成该计划的一些备注信息。例如上面这个计划中的:Degree of Parallelisim is 1 because of table property 表示由于当前表的并行度设置为 1,所以当前查询的并行度被设置为 1
  • EXPLAIN EXTENDED 在 EXPLAIN EXTENDED_NOADDR 的基础上,还会额外展示各个算子中涉及到的各个表达式的数据所存放的地址信息。通常技术支持同学和研发同学在排查 SQL 正确性问题时,会使用到这种展示模式。一般用户不需要关心各个表达式后面对应的地址信息,可以直接忽略。
  explain EXTENDED select * from t1 where c1 > 10;

输出如下:

  +---------------------------------------------------------------------------------------------------------------------+
  | Query Plan                                                                                                          |
  +---------------------------------------------------------------------------------------------------------------------+
  | ===================================================                                                                 |
  | |ID|OPERATOR        |NAME   |EST.ROWS|EST.TIME(us)|                                                                 |
  | ---------------------------------------------------                                                                 |
  | |0 |TABLE RANGE SCAN|t1(idx)|1       |7           |                                                                 |
  | ===================================================                                                                 |
  | Outputs & filters:                                                                                                  |
  | -------------------------------------                                                                               |
  |   0 - output([t1.c1(0x7fed4fe0df50)], [t1.c2(0x7fed4fe0e4c0)]), filter(nil), rowset=16                              |
  |       access([t1.__pk_increment(0x7fed4fe0e9d0)], [t1.c1(0x7fed4fe0df50)], [t1.c2(0x7fed4fe0e4c0)]), partitions(p0) |
  |       is_index_back=true, is_global_index=false,                                                                    |
  |       range_key([t1.c1(0x7fed4fe0df50)], [t1.__pk_increment(0x7fed4fe0e9d0)]), range(10,MAX ; MAX,MAX),             |
  |       range_cond([t1.c1(0x7fed4fe0df50) > 10(0x7fed4fe0d800)])                                                      |
  |                                                                                                                     |
  |       ......                                                                                                        |
  |                                                                                                                     |
  +---------------------------------------------------------------------------------------------------------------------+

执行计划中的算子介绍

常见的算子详见官网《OceanBase 数据库》文档 参考指南/性能调优/SQL 调优指南/SQL 执行计划/执行计划算子 章节。因为官网内容足够详细,所以本文档不再对各个算子逐一进行介绍。

说明

强烈推荐用户去了解一下其中最常见的 TABLE SCAN 算子JOIN 算子,以及 TABLE SCAN 中的 DAS 执行

本文只会重点对一个被称为 EXCHANGE 的算子进行介绍。这个 EXCHANGE 算子在 OceanBase 数据库的分布式计划中很常见,且作用和含义不像其他算子那么容易理解,因此会在官网内容的基础上,对 EXCHANGE 算子进行一些补充。

EXCHANGE 算子是用于线程间进行数据交互的算子,一般都是成对出现的,数据源端有一个 OUT 算子,目的端会有一个 IN 算子。作用主要是:gather(数据汇聚)、transmit(数据转发)、re-partition(数据重分区),下面会对这三种作用逐一进行介绍。

EXCH-IN/OUT

EXCHANGE IN/EXCHANGE OUT 被用于将多个分区上的数据汇聚到一起,发送到查询所在的主节点上。

下面的查询中访问了 5 个分区(p0-p4)的数据:

CREATE TABLE t3 (c1 INT, c2 INT) PARTITION BY HASH(c1) PARTITIONS 5;

执行 explain 命令查看执行计划

explain select * from t3 where c1 > 10;

输出如下:

+------------------------------------------------------------------------------------+
| Query Plan                                                                         |
+------------------------------------------------------------------------------------+
| =============================================================                      |
| |ID|OPERATOR                 |NAME    |EST.ROWS|EST.TIME(us)|                      |
| -------------------------------------------------------------                      |
| |0 |PX COORDINATOR           |        |1       |20          |                      |
| |1 |└─EXCHANGE OUT DISTR     |:EX10000|1       |20          |                      |
| |2 |  └─PX PARTITION ITERATOR|        |1       |19          |                      |
| |3 |    └─TABLE FULL SCAN    |t3      |1       |19          |                      |
| =============================================================                      |
| Outputs & filters:                                                                 |
| -------------------------------------                                              |
|   0 - output([INTERNAL_FUNCTION(t3.c1, t3.c2)]), filter(nil), rowset=16            |
|   1 - output([INTERNAL_FUNCTION(t3.c1, t3.c2)]), filter(nil), rowset=16            |
|       dop=1                                                                        |
|   2 - output([t3.c1], [t3.c2]), filter(nil), rowset=16                             |
|       force partition granule                                                      |
|   3 - output([t3.c1], [t3.c2]), filter([t3.c1 > 10]), rowset=16                    |
|       access([t3.c1], [t3.c2]), partitions(p[0-4])                                 |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t3.__pk_increment]), range(MIN ; MAX)always true                  |
+------------------------------------------------------------------------------------+

其中:

  • 2 号算子 PX PARTITION ITERATOR 负责按照分区粒度迭代数据。granule iterator 算子的介绍详见官网《OceanBase 数据库》文档 参考指南/性能调优/SQL 调优指南/SQL 执行计划/执行计划算子/GI
  • 1 号算子 EXCHANGE OUT DISTR 接受 2 号算子 PX PARTITION ITERATOR 产生的输出,并将数据传出。
  • 0 号算子 PX COORDINATOR 接收多个分区上 1 号算子产生的输出,并将结果汇总输出给用户。PX COORDINATOR 是一种特殊的 EXCHANGE IN 算子,除了能够拉回远程的数据外,还负责调度子计划的执行。

EXCH-IN/OUT (REMOTE)

我们在 ODP SQL 路由原理 中介绍了执行计划主要分为三种不同的类型,分别是本地(Local) 计划、远程(Remote)计划和分布式(Distributed)计划。EXCHANGE IN REMOTE 和 EXCHANGE OUT REMOTE 算子就是在远程计划中用于将远程的数据(单个分区的数据)拉回本地。

例如我们创建了一个 1 – 1 – 1(3 Zone 3 节点)的环境,三个可用区分别是:zone1zone2zone3,对应的三个节点分别叫做 ABC。某个租户的 primary_zone 是 zone1,即这个租户下的所有表的主副本都会在 zone1A 节点上。

本地计划

如果我们直连 A 节点,对两张非分区表进行计算,因为两表的主副本都在本机,就会生成如下的本地计划:

create table t1(c1 int, c2 int);

create table t2(c1 int, c2 int);

explain select * from t1, t2 where t1.c1 = t2.c1 and t1.c1 > 10;
+------------------------------------------------------------------------------------+
| Query Plan                                                                         |
+------------------------------------------------------------------------------------+
| =================================================                                  |
| |ID|OPERATOR         |NAME|EST.ROWS|EST.TIME(us)|                                  |
| -------------------------------------------------                                  |
| |0 |HASH JOIN        |    |1       |9           |                                  |
| |1 |├─TABLE FULL SCAN|t1  |1       |4           |                                  |
| |2 |└─TABLE FULL SCAN|t2  |1       |4           |                                  |
| =================================================                                  |
| Outputs & filters: 略                                                              |

远程计划

如果我们直连 B 节点,对两张非分区表进行计算,因为两表的主副本都不在本机,就会生成如下的远程计划:

explain select * from t1, t2 where t1.c1 = t2.c1 and t1.c1 > 10;

输出如下:

+------------------------------------------------------------------------------------+
| Query Plan                                                                         |
+------------------------------------------------------------------------------------+
| =====================================================                              |
| |ID|OPERATOR             |NAME|EST.ROWS|EST.TIME(us)|                              |
| -----------------------------------------------------                              |
| |0 |EXCHANGE IN REMOTE   |    |1       |11          |                              |
| |1 |└─EXCHANGE OUT REMOTE|    |1       |10          |                              |
| |2 |  └─HASH JOIN        |    |1       |9           |                              |
| |3 |    ├─TABLE FULL SCAN|t1  |1       |4           |                              |
| |4 |    └─TABLE FULL SCAN|t2  |1       |4           |                              |
| =====================================================                              |
| Outputs & filters:                                                                 |
| -------------------------------------                                              |
|   0 - output([t1.c1], [t1.c2], [t2.c1], [t2.c2]), filter(nil)                      |
|   1 - output([t1.c1], [t1.c2], [t2.c1], [t2.c2]), filter(nil)                      |
|   2 - output([t1.c1], [t1.c2], [t2.c1], [t2.c2]), filter(nil), rowset=16           |
|       equal_conds([t1.c1 = t2.c1]), other_conds(nil)                               |
|   3 - output([t1.c1], [t1.c2]), filter([t1.c1 > 10]), rowset=16                    |
|       access([t1.c1], [t1.c2]), partitions(p0)                                     |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                  |
|   4 - output([t2.c1], [t2.c2]), filter([t2.c1 > 10]), rowset=16                    |
|       access([t2.c1], [t2.c2]), partitions(p0)                                     |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t2.__pk_increment]), range(MIN ; MAX)always true                  |
+------------------------------------------------------------------------------------+
23 rows in set

远程计划中的 EXCHANGE IN REMOTEEXCHANGE OUT REMOTE 算子用于将远程的数据(单个分区的数据)拉回本地。

由于待读取的数据在远程,执行计划中分配了 0 号算子和 1 号算子来拉取远程的数据。其中:

  • 2 号 – 4 号算子在 A 机器上执行,负责读取存储层的数据,并完成 HASH JOIN 的计算。
  • 1 号算子 EXCHANGE OUT REMOTE 也在 A 机器上执行,读取 2 号算子 HASH JOIN 计算的结果数据,并将数据传出给 0 号算子。
  • 0 号算子 EXCHANGE IN REMOTEB 机器上执行,接收 1 号算子产生的输出。

EXCH-IN/OUT (PKEY)

EXCH-IN/OUT (PKEY) 算子用于数据重分区。它通常用于二元算子(例如 JOIN 算子)中,将算子其中一个孩子节点(左支)的数据,按照另外一个孩子节点(右支)的分区方式进行重分区,然后将左支重分区后的数据发送给右支对应分区所在的节点,最终完成相应的计算。

如下示例中,该查询是对两个分区表的数据进行联接。

CREATE TABLE t1 (c1 INT, c2 INT) PARTITION BY HASH(c1) PARTITIONS 5;

CREATE TABLE t2 (c1 INT PRIMARY KEY, c2 INT) PARTITION BY HASH(c1) PARTITIONS 4;

EXPLAIN SELECT * FROM t1, t2 WHERE t1.c1 = t2.c1;
+---------------------------------------------------------------------------------------+
| Query Plan                                                                            |
+---------------------------------------------------------------------------------------+
| =====================================================================                 |
| |ID|OPERATOR                         |NAME    |EST.ROWS|EST.TIME(us)|                 |
| ---------------------------------------------------------------------                 |
| |0 |PX COORDINATOR                   |        |1       |38          |                 |
| |1 |└─EXCHANGE OUT DISTR             |:EX10001|1       |37          |                 |
| |2 |  └─HASH JOIN                    |        |1       |36          |                 |
| |3 |    ├─EXCHANGE IN DISTR          |        |1       |20          |                 |
| |4 |    │ └─EXCHANGE OUT DISTR (PKEY)|:EX10000|1       |20          |                 |
| |5 |    │   └─PX PARTITION ITERATOR  |        |1       |19          |                 |
| |6 |    │     └─TABLE FULL SCAN      |t1      |1       |19          |                 |
| |7 |    └─PX PARTITION ITERATOR      |        |1       |16          |                 |
| |8 |      └─TABLE FULL SCAN          |t2      |1       |16          |                 |
| =====================================================================                 |
| Outputs & filters:                                                                    |
| -------------------------------------                                                 |
|   0 - output([INTERNAL_FUNCTION(t1.c1, t1.c2, t2.c1, t2.c2)]), filter(nil), rowset=16 |
|   1 - output([INTERNAL_FUNCTION(t1.c1, t1.c2, t2.c1, t2.c2)]), filter(nil), rowset=16 |
|       dop=1                                                                           |
|   2 - output([t1.c1], [t2.c1], [t1.c2], [t2.c2]), filter(nil), rowset=16              |
|       equal_conds([t1.c1 = t2.c1]), other_conds(nil)                                  |
|   3 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                                |
|   4 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                                |
|       (#keys=1, [t1.c1]), dop=1                                                       |
|   5 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                                |
|       force partition granule                                                         |
|   6 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                                |
|       access([t1.c1], [t1.c2]), partitions(p[0-4])                                    |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                     |
|   7 - output([t2.c1], [t2.c2]), filter(nil), rowset=16                                |
|       affinitize, force partition granule                                             |
|   8 - output([t2.c1], [t2.c2]), filter(nil), rowset=16                                |
|       access([t2.c1], [t2.c2]), partitions(p[0-3])                                    |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t2.c1]), range(MIN ; MAX)always true                                 |
+---------------------------------------------------------------------------------------+

执行计划将 t1 表的数据按照 t2 表的分区方式进行重分区:

  • 4 号算子 EXCHANGE OUT DISTR (PKEY) 负责根据 t2 表的数据分区方式,以及查询的联接条件,确定 t1 表中的每一行数据应该发送到哪个节点才能完成与 t2 表对应分区的连接操作,并将 t1 表的各行数据发送到相应的节点。
  • 3 号算子 EXCHANGE IN DISTR 负责在相应节点完成 t1 表数据的接收。
  • 2 号算子 HASH JOIN 负责在各个节点,对 7 号算子迭代出的 t2 表各分区的数据,和 3 号算子接收到的 t1 表以 t2 表分区规则重分区后的对应分区数据,进行 JOIN 计算。
  • 1 号算子 EXCHANGE OUT DISTR 负责把各节点、各分区 JOIN 之后的结果发送到 0 号算子。
  • 0 号算子 PX COORDINATOR 负责接收各个节点 JOIN 的结果,并对结果进行汇总。

除了上述的 EXCH-IN/OUT (PKEY) 以外,优化器还会根据适合不同 SQL 的不同重分区方式,生成 EXCH-IN/OUT (HASH)、EXCH-IN/OUT (BC2HOST) 等算子,这里不再一一赘述。不同的数据重分布方式,详见官网《OceanBase 数据库》文档 参考指南/性能调优/SQL 调优指南/并行执行计划/并行执行计划简介常见的并行执行中的数据分发方式 部分。

通过 Hint 生成指定计划

Hint 机制可以使优化器生成指定的执行计划。一般情况下,优化器会为用户查询选择最佳的执行计划,不需要用户使用 Hint 指定。但在某些场景下,优化器生成的执行计划可能不满足用户的要求,这时就需要用户使用 Hint 来指定生成某种执行计划。

Hint 语法

{ DELETE | INSERT | SELECT | UPDATE | REPLACE } /*+ [hint_text][,hint_text]... */

例如下面这条查询语句,通过 /*+ PARALLEL(3)*/ 指定 SQL 的执行并行度为 3,通过 explain 就可以看到计划的 dop = 3,通过 explain EXTENDED_NOADDR 还可以看到计划中会展示一个 Note: Degree of Parallelism is 3 because of hint

create t1 (c1 int, c2 int) PARTITION BY HASH(c1) PARTITIONS 5;

explain EXTENDED_NOADDR select /*+ PARALLEL(3) */* from t1 where c1 > 10;
+--------------------------------------------------------------------------+
| Query Plan                                                               |
+--------------------------------------------------------------------------+
| ==========================================================               |
| |ID|OPERATOR              |NAME    |EST.ROWS|EST.TIME(us)|               |
| ----------------------------------------------------------               |
| |0 |PX COORDINATOR        |        |1       |3           |               |
| |1 |└─EXCHANGE OUT DISTR  |:EX10000|1       |3           |               |
| |2 |  └─PX BLOCK ITERATOR |        |1       |3           |               |
| |3 |    └─TABLE RANGE SCAN|t1(idx) |1       |3           |               |
| ==========================================================               |
| Outputs & filters:                                                       |
| -------------------------------------                                    |
|   0 - output([INTERNAL_FUNCTION(t1.c1, t1.c2)]), filter(nil), rowset=16  |
|   1 - output([INTERNAL_FUNCTION(t1.c1, t1.c2)]), filter(nil), rowset=16  |
|       dop=3                                                              |
|   2 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                   |
|   3 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                   |
|       access([t1.__pk_increment], [t1.c1], [t1.c2]), partitions(p0)      |
|       is_index_back=true, is_global_index=false,                         |
|       range_key([t1.c1], [t1.__pk_increment]), range(10,MAX ; MAX,MAX),  |
|       range_cond([t1.c1 > 10])                                           |
| Used Hint:                                                               |
| -------------------------------------                                    |
|   /*+                                                                    |
|       PARALLEL(3)                                                        |
|   */                                                                     |
|                                                                          |
|   ......                                                                 |
|                                                                          |
|   Note:                                                                  |
|       Degree of Parallelism is 3 because of hint                         |
+--------------------------------------------------------------------------+

Hint 从语法上看是一种特殊的 SQL 注释,所不同的是在注释的左标记后(/* 符号)增加了一个 +。 因为 Hint 是注释,所以如果因为语法出错的原因,导致 OBServer 服务端无法识别 SQL 语句中的 Hint,优化器会选择忽略用户指定的 Hint,去生成默认的执行计划。另外,Hint 只影响优化器生成的执行计划,不会影响 SQL 语句的语义。

说明

如果使用 MySQL 的 C 客户端执行带 Hint 的 SQL 语句,需要使用 -c 选项登录,否则 MySQL 客户端会将 Hint 作为注释从用户 SQL 语句中去除,导致系统无法收到用户 Hint。

Hint 参数

Hint 相关参数名称、语义和语法如下表所示。

名称语法语义
NO_REWRITENO_REWRITE禁止 SQL 改写。
READ_CONSISTENCYREAD_CONSISTENCY(WEAK [STRONG])读一致性设置(弱/强)。
INDEX_HINTINDEX(table_name index_name)设置表索引。
QUERY_TIMEOUTQUERY_TIMEOUT(INTNUM)设置语句超时时间。
LOG_LEVELLOG_LEVEL([‘]log_level[‘])设置日志级别,当设置模块级别语句时候,以第一个单引号(')作为开始,第二个单引号(')作为结束;例如 'DEBUG'
LEADINGLEADING([qb_name] TBL_NAME_LIST)设置联接顺序。
ORDEREDORDERED设置按照 SQL 中的顺序进行联接。
FULLFULL([qb_name] TBL_NAME)设置表访问路径为主表等价于 INDEX(TBL_NAME PRIMARY)
USE_PLAN_CACHEUSE_PLAN_CACHE(NONE[DEFAULT])设置是否使用计划缓存:NONE:表示不使用计划缓存。DEFAULT:表示按照服务器本身的设置来决定是否使用计划缓存。
USE_MERGEUSE_MERGE([qb_name] TBL_NAME_LIST)设置指定表在作为右表时使用 Merge Join。
USE_HASHUSE_HASH([qb_name] TBL_NAME_LIST)设置指定表在作为右表时使用 Hash Join。
NO_USE_HASHNO_USE_HASH([qb_name] TBL_NAME_LIST)设置指定表在作为右表时不使用 Hash Join。
USE_NLUSE_NL([qb_name] TBL_NAME_LIST)设置指定表在作为右表时使用 Nested Loop Join。
USE_BNLUSE_BNL([qb_name] TBL_NAME_LIST)设置指定表在作为右表时使用 Block Nested Loop Join
USE_HASH_AGGREGATIONUSE_HASH_AGGREGATION([qb_name])设置聚合算法为 Hash。例如 Hash Group By 或者 Hash Distinct。
NO_USE_HASH_AGGREGATIONNO_USE_HASH_AGGREGATION([qb_name])设置 Aggregate 方法不使用 Hash Aggregate,使用 Merge Group By 或者 Merge Distinct。
USE_LATE_MATERIALIZATIONUSE_LATE_MATERIALIZATION设置使用晚期物化。
NO_USE_LATE_MATERIALIZATIONNO_USE_LATE_MATERIALIZATION设置不使用晚期物化。
TRACE_LOGTRACE_LOG设置收集 Trace 记录用于 SHOW TRACE 展示。
QB_NAMEQB_NAME( NAME )设置 Query Block 的名称。
PARALLELPARALLEL(INTNUM)设置分布式执行并行度。
TOPKTOPK(PRECISION MINIMUM_ROWS)设置模糊查询的精度和最小行数。 其中 PRECSION 为整型,取值范围 [0,100],表示模糊查询的行数百分比;MINIMUM_ROWS 为最小返回行数。
MAX_CONCURRENT|MAX_CONCURRENT(n)限制这个 SQL 文本的并发数。

说明

  • QB_NAME 语法是: @NAME
  • TBL_NAME 语法是: [db_name.]relation_name [qb_name]

QB_NAME 参数

在 DML 语句中,每一个 Query Block 都会有一个 QB_NAME(Query Block Name),可以由用户指定,也可以由系统自动生成。每一个 Query Block 都是一个语义上完整的查询语句,简单来看就是把 SQL 按 select、delete 这些词提取出来并结构化之后,按从左到右的方式标号。Query Block 的介绍详见官网《OceanBase 数据库》知识库 性能诊断和优化/SQL 诊断和优化/Query Block 介绍

在用户没有用 Hint 指定 QB_NAME 的时候,系统会按照 SEL$1、SEL$2、UPD$1、DEL$1 方式从左到右(实际也是 Resolver 的解析顺序)依次生成。

通过 QB_NAME 可以精确定位到每一个表,也可以在某处指定任意 Query Block 的行为。TBL_NAME 中的 QB_NAME 用于定位表,在 Hint 中最前面的 QB_NAME 用于定位 Hint 作用于哪一个 Query Block。

如下例所示,对于 SELECT *FROM t1, (SELECT* FROM t2 WHERE c2 = 1 LIMIT 5) WHERE t1.c1 = 1; 这条 SQL,按照默认规则:

  • 第一个 Query Block SEL$1 是最外层的:SELECT * FROM t1, VIEW1 WHERE t1.c1 = 1
  • 第二个 Query Block SEL$2 是 VIEW1(即计划里的 ANONYMOUS_VIEW1):SELECT * FROM t2 WHERE c2 = 1 LIMIT 5

优化器为 SEL$1 中的 t1 表选择走索引 t1_c1 路径,而为 SEL$2 中的 t2 表选择主表(Primary)访问。

CREATE TABLE t1(c1 INT, c2 INT, KEY t1_c1(c1));

CREATE TABLE t2(c1 INT, c2 INT, KEY t2_c1(c1));

EXPLAIN EXTENDED_NOADDR SELECT * FROM t1, (SELECT * FROM t2 WHERE c2 = 1 LIMIT 5) WHERE t1.c1 = 1;
+-----------------------------------------------------------------------------------------------------------+
| Query Plan                                                                                                |
+-----------------------------------------------------------------------------------------------------------+
| ======================================================================                                    |
| |ID|OPERATOR                   |NAME           |EST.ROWS|EST.TIME(us)|                                    |
| ----------------------------------------------------------------------                                    |
| |0 |NESTED-LOOP JOIN CARTESIAN |               |1       |11          |                                    |
| |1 |├─TABLE RANGE SCAN         |t1(t1_c1)      |1       |7           |                                    |
| |2 |└─MATERIAL                 |               |1       |4           |                                    |
| |3 |  └─SUBPLAN SCAN           |ANONYMOUS_VIEW1|1       |4           |                                    |
| |4 |    └─TABLE FULL SCAN      |t2             |1       |4           |                                    |
| ======================================================================                                    |
| Outputs & filters:                                                                                        |
| -------------------------------------                                                                     |
|   0 - output([t1.c1], [t1.c2], [.c1], [.c2]), filter(nil), rowset=16                                      |
|       conds(nil), nl_params_(nil), use_batch=false                                                        |
|   1 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                                                    |
|       access([t1.__pk_increment], [t1.c1], [t1.c2]), partitions(p0)                                       |
|       is_index_back=true, is_global_index=false,                                                          |
|       range_key([t1.c1], [t1.__pk_increment]), range(1,MIN ; 1,MAX),                                      |
|       range_cond([t1.c1 = 1])                                                                             |
|   2 - output([.c1], [.c2]), filter(nil), rowset=16                                                        |
|   3 - output([.c1], [.c2]), filter(nil), rowset=16                                                        |
|       access([.c1], [.c2])                                                                                |
|   4 - output([t2.c1], [t2.c2]), filter([t2.c2 = 1]), rowset=16                                            |
|       access([t2.c2], [t2.c1]), partitions(p0)                                                            |
|       limit(5), offset(nil), is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t2.__pk_increment]), range(MIN ; MAX)always true                                         |
| Used Hint:                                                                                                |
| -------------------------------------                                                                     |
|   /*+                                                                                                     |
|                                                                                                           |
|   */                                                                                                      |
| Qb name trace:                                                                                            |
| -------------------------------------                                                                     |
|   stmt_id:0, stmt_type:T_EXPLAIN                                                                          |
|   stmt_id:1, SEL$1                                                                                        |
|   stmt_id:2, SEL$2                                                                                        |
| Outline Data:                                                                                             |
| -------------------------------------                                                                     |
|   /*+                                                                                                     |
|       BEGIN_OUTLINE_DATA                                                                                  |
|       LEADING(@"SEL$1" ("test"."t1"@"SEL$1" "ANONYMOUS_VIEW1"@"SEL$1"))                                   |
|       USE_NL(@"SEL$1" "ANONYMOUS_VIEW1"@"SEL$1")                                                          |
|       USE_NL_MATERIALIZATION(@"SEL$1" "ANONYMOUS_VIEW1"@"SEL$1")                                          |
|       INDEX(@"SEL$1" "test"."t1"@"SEL$1" "t1_c1")                                                         |
|       FULL(@"SEL$2" "test"."t2"@"SEL$2")                                                                  |
|       OPTIMIZER_FEATURES_ENABLE('4.0.0.0')                                                                |
|       END_OUTLINE_DATA                                                                                    |
|   */                                                                                                      |
|   ......                                                                                                  |
+-----------------------------------------------------------------------------------------------------------+

如果 SQL 通过 Hint 来指定 SEL$1 的 t1 表走主表,SEL$2 的 t2 表走索引,示例如下:

EXPLAIN EXTENDED_NOADDR
SELECT /*+ INDEX(@SEL$1 t1 PRIMARY) INDEX(@SEL$2 t2 t2_c1) */ *
FROM t1 , (SELECT * FROM t2 WHERE c2 = 1 LIMIT 5) WHERE t1.c1 = 1;
+----------------------------------------------------------------------------------------------------------+
| Query Plan                                                                                               |
+----------------------------------------------------------------------------------------------------------+
| ======================================================================                                   |
| |ID|OPERATOR                   |NAME           |EST.ROWS|EST.TIME(us)|                                   |
| ----------------------------------------------------------------------                                   |
| |0 |NESTED-LOOP JOIN CARTESIAN |               |1       |11          |                                   |
| |1 |├─TABLE FULL SCAN          |t1             |1       |4           |                                   |
| |2 |└─MATERIAL                 |               |1       |7           |                                   |
| |3 |  └─SUBPLAN SCAN           |ANONYMOUS_VIEW1|1       |7           |                                   |
| |4 |    └─TABLE FULL SCAN      |t2(t2_c1)      |1       |7           |                                   |
| ======================================================================                                   |
| Outputs & filters:                                                                                       |
| -------------------------------------                                                                    |
|   0 - output([t1.c1], [t1.c2], [.c1], [.c2]), filter(nil), rowset=16                                     |
|       conds(nil), nl_params_(nil), use_batch=false                                                       |
|   1 - output([t1.c1], [t1.c2]), filter([t1.c1 = 1]), rowset=16                                           |
|       access([t1.c1], [t1.c2]), partitions(p0)                                                           |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],                        |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                                        |
|   2 - output([.c1], [.c2]), filter(nil), rowset=16                                                       |
|   3 - output([.c1], [.c2]), filter(nil), rowset=16                                                       |
|       access([.c1], [.c2])                                                                               |
|   4 - output([t2.c1], [t2.c2]), filter([t2.c2 = 1]), rowset=16                                           |
|       access([t2.__pk_increment], [t2.c2], [t2.c1]), partitions(p0)                                      |
|       limit(5), offset(nil), is_index_back=true, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t2.c1], [t2.__pk_increment]), range(MIN,MIN ; MAX,MAX)always true                       |
| Used Hint:                                                                                               |
| -------------------------------------                                                                    |
|   /*+                                                                                                    |
|                                                                                                          |
|       INDEX("t1" "primary")                                                                              |
|       INDEX(@"SEL$2" "t2" "t2_c1")                                                                       |
|   */                                                                                                     |
| Qb name trace:                                                                                           |
| -------------------------------------                                                                    |
|   stmt_id:0, stmt_type:T_EXPLAIN                                                                         |
|   stmt_id:1, SEL$1                                                                                       |
|   stmt_id:2, SEL$2                                                                                       |
| Outline Data:                                                                                            |
| -------------------------------------                                                                    |
|   /*+                                                                                                    |
|       BEGIN_OUTLINE_DATA                                                                                 |
|       LEADING(@"SEL$1" ("test"."t1"@"SEL$1" "ANONYMOUS_VIEW1"@"SEL$1"))                                  |
|       USE_NL(@"SEL$1" "ANONYMOUS_VIEW1"@"SEL$1")                                                         |
|       USE_NL_MATERIALIZATION(@"SEL$1" "ANONYMOUS_VIEW1"@"SEL$1")                                         |
|       FULL(@"SEL$1" "test"."t1"@"SEL$1")                                                                 |
|       INDEX(@"SEL$2" "test"."t2"@"SEL$2" "t2_c1")                                                        |
|       OPTIMIZER_FEATURES_ENABLE('4.0.0.0')                                                               |
|       END_OUTLINE_DATA                                                                                   |
|   */                                                                                                     |
|   ......                                                                                                 |
+----------------------------------------------------------------------------------------------------------+

读者可以自行研究一下,在加这个 Hint /*+ INDEX(t1 PRIMARY) INDEX(@SEL$2 t2 t2_c1)*/ 前后,计划 Query Plan 部分和 Outline Data 部分有什么变化,以及变化是否是符合预期的?

上述示例中的 Hint 也可以写成如下三种方式,这几种写法都是等价的:

  • 方式一:
  SELECT /*+INDEX(@SEL$1 t1 PRIMARY) INDEX(@SEL$2 t2 t2_c1)*/ * FROM t1 , (SELECT * FROM t2 WHERE c2 = 1 LIMIT 5) WHERE t1.c1 = 1;
  • 方式二:
  SELECT /*+INDEX(t1 PRIMARY) INDEX(@SEL$2 t2@SEL$2 t2_c1)*/ * FROM t1 , (SELECT * FROM t2 WHERE c2 = 1 LIMIT 5) WHERE t1.c1 = 1;
  • 方式三:
  SELECT /*+INDEX(t1 PRIMARY)*/ * FROM t1 , (SELECT /*+INDEX(t2 t2_c1)*/ * FROM t2 WHERE c2 = 1 LIMIT 5) WHERE t1.c1 = 1;

Hint 使用规则

Hint 的一般使用规则如下:

  • 对于没有指定 Query Block 的 Hint,表明作用于本 Query Block。
  • 示例 1:由于 Hint 写在 Query Block 1,但 Hint 中的 t2 表只出现在 Query Block 2,且优化器无法通过改写 SQL 将 SEL$2 中的 t2 表提升到 SEL$1 中,则 Hint 无法生效。 EXPLAIN SELECT /*+INDEX(t2 t2_c1)*/ * FROM t1 , (SELECT * FROM t2 WHERE c2 = 1 LIMIT 5) WHERE t1.c1 = 1; 输出如下: +-----------------------------------------------------------------------------------------------------------+ | Query Plan | +-----------------------------------------------------------------------------------------------------------+ | ====================================================================== | | |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| | | ---------------------------------------------------------------------- | | |0 |NESTED-LOOP JOIN CARTESIAN | |1 |11 | | | |1 |├─TABLE RANGE SCAN |t1(t1_c1) |1 |7 | | | |2 |└─MATERIAL | |1 |4 | | | |3 | └─SUBPLAN SCAN |ANONYMOUS_VIEW1|1 |4 | | | |4 | └─TABLE FULL SCAN |t2 |1 |4 | | | ====================================================================== | | ...... | +-----------------------------------------------------------------------------------------------------------+
  • 示例 2:如果 SQL 可以通过优化器改写将 t2 表提升到 SEL$1,则 Hint 生效。如下面这个计划所示,原本的 SQL 明显是被改写成了一条不存在匿名视图的 SQL:SELECT /*+INDEX(t2 t2_c1)*/ * FROM t1, t2 WHERE t1.c1 = 1 and t2.c2 = 1;,在这条被改写之后的 SQL 中,t2 表被提升到了最外层的 SEL$1 里,所以 Hint 会生效。 EXPLAIN SELECT /*+ INDEX(t2 t2_c1) */ * FROM t1 , (SELECT * FROM t2 WHERE c2 = 1) WHERE t1.c1 = 1; 输出如下: +------------------------------------------------------------------------------------+ | Query Plan | +------------------------------------------------------------------------------------+ | ================================================================ | | |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| | | ---------------------------------------------------------------- | | |0 |NESTED-LOOP JOIN CARTESIAN | |1 |13 | | | |1 |├─TABLE RANGE SCAN |t1(t1_c1)|1 |7 | | | |2 |└─MATERIAL | |1 |7 | | | |3 | └─TABLE FULL SCAN |t2(t2_c1)|1 |7 | | | ================================================================ | | Outputs & filters: | | ------------------------------------- | | 0 - output([t1.c1], [t1.c2], [t2.c1], [t2.c2]), filter(nil), rowset=16 | | conds(nil), nl_params_(nil), use_batch=false | | 1 - output([t1.c1], [t1.c2]), filter(nil), rowset=16 | | access([t1.__pk_increment], [t1.c1], [t1.c2]), partitions(p0) | | is_index_back=true, is_global_index=false, | | range_key([t1.c1], [t1.__pk_increment]), range(1,MIN ; 1,MAX), | | range_cond([t1.c1 = 1]) | | 2 - output([t2.c1], [t2.c2]), filter(nil), rowset=16 | | 3 - output([t2.c2], [t2.c1]), filter([t2.c2 = 1]), rowset=16 | | access([t2.__pk_increment], [t2.c2], [t2.c1]), partitions(p0) | | is_index_back=true, is_global_index=false, filter_before_indexback[false], | | range_key([t2.c1], [t2.__pk_increment]), range(MIN,MIN ; MAX,MAX)always true | +------------------------------------------------------------------------------------+
  • 如果指定表行为,但在本 Query Block 中没有找到该表,或者发生冲突,那么 Hint 都将无效。

常用 Hint

与其他数据库的行为相比,OceanBase 数据库优化器是动态规划的,已经考虑了所有可能的最优路径,Hint 主要作用是指定优化器的行为,并按照 Hint 执行 SQL 查询。下面会介绍用户最为常用的一些 Hint。

INDEX Hint

INDEX Hint 的语法如下:

SELECT/*+ INDEX(table_name index_name) */ * FROM table_name;

在 SQL 语句中,表名存在别名即 table_name [AS] alias,必须写表别名,才能使 INDEX 生效。示例如下:

create table t1(c1 int, c2 int, c3 int);

create index idx1 on t1(c1);

create index idx2 on t1(c2);

-- 插入 1000 行测试数据
insert into t1 with recursive cte(n) as (select 1 from dual union all select n + 1 from cte where n < 1000) select n, mod(n, 3), n from cte;

-- 收集指定表 t1 的统计信息
analyze table t1 COMPUTE STATISTICS for all columns size 128;

-- 对于这 1000 行测试数据的数据特征,c1 = 1 过滤性优于 c2 = 1
-- 所以在没有 index hint 或者 hint 不生效时,优化器在生成计划的过程中,默认会优先选择使用 idx1 这个索引
explain select * from t1 where c1 = 1 and c2 = 1;
+-----------------------------------------------------------------------------------+
| Query Plan                                                                        |
+-----------------------------------------------------------------------------------+
| ====================================================                              |
| |ID|OPERATOR        |NAME    |EST.ROWS|EST.TIME(us)|                              |
| ----------------------------------------------------                              |
| |0 |TABLE RANGE SCAN|t1(idx1)|1       |7           |                              |
| ====================================================                              |
| Outputs & filters:                                                                |
| -------------------------------------                                             |
|   0 - output([t1.c1], [t1.c2], [t1.c3]), filter([t1.c2 = 1]), rowset=16           |
|       access([t1.__pk_increment], [t1.c1], [t1.c2], [t1.c3]), partitions(p0)      |
|       is_index_back=true, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t1.c1], [t1.__pk_increment]), range(1,MIN ; 1,MAX),              |
|       range_cond([t1.c1 = 1])                                                     |
+-----------------------------------------------------------------------------------+

-- 生效的 index hint
explain select /*+index(t idx2)*/ * from t1 as t where c1 = 1 and c2 = 1;
+-----------------------------------------------------------------------------------+
| Query Plan                                                                        |
+-----------------------------------------------------------------------------------+
| ===================================================                               |
| |ID|OPERATOR        |NAME   |EST.ROWS|EST.TIME(us)|                               |
| ---------------------------------------------------                               |
| |0 |TABLE RANGE SCAN|t(idx2)|1       |871         |                               |
| ===================================================                               |
| Outputs & filters:                                                                |
| -------------------------------------                                             |
|   0 - output([t.c1], [t.c2], [t.c3]), filter([t.c1 = 1]), rowset=16               |
|       access([t.__pk_increment], [t.c1], [t.c2], [t.c3]), partitions(p0)          |
|       is_index_back=true, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t.c2], [t.__pk_increment]), range(1,MIN ; 1,MAX),                |
|       range_cond([t.c2 = 1])                                                      |
+-----------------------------------------------------------------------------------+

-- 不生效的 index,因为 t1 已经被赋予了一个别名 t
explain select /*+index(t1 idx2)*/ * from t1 t where c1 = 1 and c2 = 1;
+-----------------------------------------------------------------------------------+
| Query Plan                                                                        |
+-----------------------------------------------------------------------------------+
| ===================================================                               |
| |ID|OPERATOR        |NAME   |EST.ROWS|EST.TIME(us)|                               |
| ---------------------------------------------------                               |
| |0 |TABLE RANGE SCAN|t(idx1)|1       |7           |                               |
| ===================================================                               |
| Outputs & filters:                                                                |
| -------------------------------------                                             |
|   0 - output([t.c1], [t.c2], [t.c3]), filter([t.c2 = 1]), rowset=16               |
|       access([t.__pk_increment], [t.c1], [t.c2], [t.c3]), partitions(p0)          |
|       is_index_back=true, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t.c1], [t.__pk_increment]), range(1,MIN ; 1,MAX),                |
|       range_cond([t.c1 = 1])                                                      |
+-----------------------------------------------------------------------------------+

FULL Hint

FULL Hint 的语法是用于指定表使用主表扫描,等价于 INDEX Hint /*+ INDEX(table_name PRIMARY)*/

示例如下:

create table t1(c1 int, c2 int, c3 int);

create index idx1 on t1(c1);

-- c1 上有一个索引 idx1,且过滤条件中的列和结果列都是 c1,所以优化器会默认选择使用 idx1
explain select c1 from t1 where c1 = 1;
+-----------------------------------------------------------------------+
| Query Plan                                                            |
+-----------------------------------------------------------------------+
| ====================================================                  |
| |ID|OPERATOR        |NAME    |EST.ROWS|EST.TIME(us)|                  |
| ----------------------------------------------------                  |
| |0 |TABLE RANGE SCAN|t1(idx1)|1       |4           |                  |
| ====================================================                  |
| Outputs & filters:                                                    |
| -------------------------------------                                 |
|   0 - output([t1.c1]), filter(nil), rowset=4                          |
|       access([t1.c1]), partitions(p0)                                 |
|       is_index_back=false, is_global_index=false,                     |
|       range_key([t1.c1], [t1.__pk_increment]), range(1,MIN ; 1,MAX),  |
|       range_cond([t1.c1 = 1])                                         |
+-----------------------------------------------------------------------+

-- 通过 hint 让计划不走索引,变为全表扫描
explain select /*+ FULL(t1) */ c1 from t1 where c1 = 1;
+------------------------------------------------------------------------------------+
| Query Plan                                                                         |
+------------------------------------------------------------------------------------+
| ===============================================                                    |
| |ID|OPERATOR       |NAME|EST.ROWS|EST.TIME(us)|                                    |
| -----------------------------------------------                                    |
| |0 |TABLE FULL SCAN|t1  |1       |4           |                                    |
| ===============================================                                    |
| Outputs & filters:                                                                 |
| -------------------------------------                                              |
|   0 - output([t1.c1]), filter([t1.c1 = 1]), rowset=4                               |
|       access([t1.c1]), partitions(p0)                                              |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                  |
+------------------------------------------------------------------------------------+

-- 通过 hint 让计划不走索引,变为全表扫描,和上面那条 SQL 等价
explain select /*+ index(t1 PRIMARY) */ c1 from t1 where c1 = 1;
+------------------------------------------------------------------------------------+
| Query Plan                                                                         |
+------------------------------------------------------------------------------------+
| ===============================================                                    |
| |ID|OPERATOR       |NAME|EST.ROWS|EST.TIME(us)|                                    |
| -----------------------------------------------                                    |
| |0 |TABLE FULL SCAN|t1  |1       |4           |                                    |
| ===============================================                                    |
| Outputs & filters:                                                                 |
| -------------------------------------                                              |
|   0 - output([t1.c1]), filter([t1.c1 = 1]), rowset=4                               |
|       access([t1.c1]), partitions(p0)                                              |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                  |
+------------------------------------------------------------------------------------+

LEADING Hint

LEADING Hint 可以指定表的联接顺序。语法是:/*+ LEADING(table_name_list)*/。在 table_name_list 中可以使用 () 表示内部各表的联接优先级,指定复杂的联接顺序,比 ordered 有更大的灵活性。示例如下:

EXPLAIN BASIC SELECT /*+LEADING(d c b a)*/ * FROM t1 a, t1 b, t1 c, t1 d;
+------------------------------------------------------------------------------------------------------+
| Query Plan                                                                                           |
+------------------------------------------------------------------------------------------------------+
| =========================================                                                            |
| |ID|OPERATOR                       |NAME|                                                            |
| -----------------------------------------                                                            |
| |0 |NESTED-LOOP JOIN CARTESIAN     |    |                                                            |
| |1 |├─NESTED-LOOP JOIN CARTESIAN   |    |                                                            |
| |2 |│ ├─NESTED-LOOP JOIN CARTESIAN |    |                                                            |
| |3 |│ │ ├─TABLE FULL SCAN          |d   |                                                            |
| |4 |│ │ └─MATERIAL                 |    |                                                            |
| |5 |│ │   └─TABLE FULL SCAN        |c   |                                                            |
| |6 |│ └─MATERIAL                   |    |                                                            |
| |7 |│   └─TABLE FULL SCAN          |b   |                                                            |
| |8 |└─MATERIAL                     |    |                                                            |
| |9 |  └─TABLE FULL SCAN            |a   |                                                            |
| =========================================                                                            |
+------------------------------------------------------------------------------------------------------+

EXPLAIN BASIC SELECT /*+LEADING((d c) (b a))*/ * FROM  t1 a, t1 b, t1 c, t1 d;
+------------------------------------------------------------------------------------------------------+
| Query Plan                                                                                           |
+------------------------------------------------------------------------------------------------------+
| =========================================                                                            |
| |ID|OPERATOR                       |NAME|                                                            |
| -----------------------------------------                                                            |
| |0 |NESTED-LOOP JOIN CARTESIAN     |    |                                                            |
| |1 |├─NESTED-LOOP JOIN CARTESIAN   |    |                                                            |
| |2 |│ ├─TABLE FULL SCAN            |d   |                                                            |
| |3 |│ └─MATERIAL                   |    |                                                            |
| |4 |│   └─TABLE FULL SCAN          |c   |                                                            |
| |5 |└─MATERIAL                     |    |                                                            |
| |6 |  └─NESTED-LOOP JOIN CARTESIAN |    |                                                            |
| |7 |    ├─TABLE FULL SCAN          |b   |                                                            |
| |8 |    └─MATERIAL                 |    |                                                            |
| |9 |      └─TABLE FULL SCAN        |a   |                                                            |
| =========================================                                                            |
+------------------------------------------------------------------------------------------------------+

EXPLAIN BASIC SELECT /*+LEADING((d c b) a))*/ * FROM t1 a, t1 b, t1 c, t1 d;
+------------------------------------------------------------------------------------------------------+
| Query Plan                                                                                           |
+------------------------------------------------------------------------------------------------------+
| =========================================                                                            |
| |ID|OPERATOR                       |NAME|                                                            |
| -----------------------------------------                                                            |
| |0 |NESTED-LOOP JOIN CARTESIAN     |    |                                                            |
| |1 |├─NESTED-LOOP JOIN CARTESIAN   |    |                                                            |
| |2 |│ ├─NESTED-LOOP JOIN CARTESIAN |    |                                                            |
| |3 |│ │ ├─TABLE FULL SCAN          |d   |                                                            |
| |4 |│ │ └─MATERIAL                 |    |                                                            |
| |5 |│ │   └─TABLE FULL SCAN        |c   |                                                            |
| |6 |│ └─MATERIAL                   |    |                                                            |
| |7 |│   └─TABLE FULL SCAN          |b   |                                                            |
| |8 |└─MATERIAL                     |    |                                                            |
| |9 |  └─TABLE FULL SCAN            |a   |                                                            |
| =========================================                                                            |
+------------------------------------------------------------------------------------------------------+

为确保按照用户指定的顺序进行联接,LEADING Hint 的检查比较严格,如果发现 Hint 指定的 table_name 不存在,LEADING Hint 失效;如果发现 Hint 中存在重复表,LEADING Hint 失效。如果在 Optimizer 联接期间,按 table_id 无法在 From Items 中找到对应的表,则可能发生改写,那么该表及后面的表指定的 JOIN 顺序失效,该表前面的 JOIN 顺序依然有效。

说明

如果同时使用 ordered 和 leading,则仅 ordered 生效。

USE_NL Hint

和 join 相关的 Hint 的基本结构如下:join_hint_name ( @ qb_name table_name_list),基本语义为,当联接的右表匹配 table_name_list 时,按照 Hint 语义生成计划。一般需要使用 LEADING Hint 指定联接顺序,使 table_name_list 中的表为右表,否则 Hint 会随着联接顺序变化而失效。

其中 table_name_list 可有以下形式:

  • 单表 use_nl ( t1 ):以 t1 为右表时使用 Nested Loop Join。
  • 多个单表 use_nl ( t1 t2 ... ):以 t1t2 等为右表时使用 Nested Loop Join。
  • 多表 use_nl ((t1 t2)):以 t1 join t2 为右表时使用 Nested Loop Join,忽略 t1/t2 连接顺序和方法。
  • 多组表 use_nl (t1 (t2 t3) (t4 t5 t6) ... ):以 t1/t2 join t3/t4 join t5 join t6 等表为右表时使用 Nested Loop Join。

在 USE_NL 指定的表是 NLJ 的右表,在联接的时候会使用 Nested Loop Join 算法,语法是:/*+ USE_NL(table_name_list)*/。示例如下:

CREATE TABLE t0(c1 INT, c2 INT, c3 INT);

CREATE TABLE t1(c1 INT, c2 INT, c3 INT);

CREATE TABLE t2(c1 INT, c2 INT, c3 INT);

-- 如果想让 join 的顺序是 t0 join t1,join 为 nest loop join,则应该这样写 hint:
EXPLAIN BASIC SELECT /*+ LEADING(t0 t1) USE_NL(t1) */ * FROM t0, t1 WHERE t0.c1 = t1.c1;
+--------------------------------------------------------------------------------------------+
| Query Plan                                                                                 |
+--------------------------------------------------------------------------------------------+
| =============================                                                              |
| |ID|OPERATOR           |NAME|                                                              |
| -----------------------------                                                              |
| |0 |NESTED-LOOP JOIN   |    |                                                              |
| |1 |├─TABLE FULL SCAN  |t0  |                                                              |
| |2 |└─MATERIAL         |    |                                                              |
| |3 |  └─TABLE FULL SCAN|t1  |                                                              |
| =============================                                                              |
+--------------------------------------------------------------------------------------------+

-- 如果想让 join 的顺序是 t0 join (t1 join t2),且想让最外层的 join 为 nest loop join,则应该这样写 hint:
EXPLAIN BASIC SELECT /*+ LEADING(t0 (t1 t2)) USE_NL((t1 t2)) */ * FROM t0, t1, t2 WHERE t0.c1 = t1.c1 AND t0.c1 = t2.c1;
+-----------------------------------------------------------------------------------------------------------------------+
| Query Plan                                                                                                            |
+-----------------------------------------------------------------------------------------------------------------------+
| ===============================                                                                                       |
| |ID|OPERATOR             |NAME|                                                                                       |
| -------------------------------                                                                                       |
| |0 |NESTED-LOOP JOIN     |    |                                                                                       |
| |1 |├─TABLE FULL SCAN    |t0  |                                                                                       |
| |2 |└─MATERIAL           |    |                                                                                       |
| |3 |  └─HASH JOIN        |    |                                                                                       |
| |4 |    ├─TABLE FULL SCAN|t1  |                                                                                       |
| |5 |    └─TABLE FULL SCAN|t2  |                                                                                       |
| ===============================                                                                                       |
+-----------------------------------------------------------------------------------------------------------------------+

注意

USE_NL、USE_HASH、USE_MERGE 这三个 hint 往往会配合 LEADING 这个 hint 一起使用。因为当 join 的右表匹配 table_name_list 时,才会按照 hint 语义生成计划。

前面这句话理解起来可能不是那么直观,接下来,我们来举一个最简单的小例子。假设用户希望对一条 SQL:SELECT * FROM t1, t2 WHERE t1.c1 = t2.c1;t1 join t2 对应计划中的 join 计算方式进行干预。

原本的计划空间有六种,分别是:

  • t1 nest loop join t2
  • t1 hash join t2
  • t1 merge join t2
  • t2 nest loop join t1
  • t2 hash join t1
  • t2 merge join t1

如果加了 hint:/*+ USE_NL(t1)*/,则计划空间减少为四种,分别是:

  • t1 nest loop join t2
  • t1 hash join t2
  • t1 merge join t2
  • t2 nest loop join t1

因为计划空间中,只有当 t1 为 join 的右表时,才会按照 hint 生成 t2 nest loop join t1 的计划;当 t1 为 join 的左表时,则不受这个 hint 的影响。

如果加了 hint:/*+ LEADING(t2 t1) USE_NL(t1)*/,计划空间就只有确定的一种:t2 nest loop join t1

USE_HASH Hint

和 USE_NL 类似,USE_HASH 指定表作为 HJ 的右表,在联接的时候使用 Hash Join 算法,语法是:/*+ USE_HASH(table_name_list)*/。示例如下:

CREATE TABLE t0(c1 INT, c2 INT, c3 INT);

CREATE TABLE t1(c1 INT, c2 INT, c3 INT);

EXPLAIN BASIC SELECT /*+LEADING(t0 t1) USE_HASH(t1)*/ * FROM t0, t1 WHERE t0.c1 = t1.c1;
+--------------------------------------------------------------------------------------------+
| Query Plan                                                                                 |
+--------------------------------------------------------------------------------------------+
| ===========================                                                                |
| |ID|OPERATOR         |NAME|                                                                |
| ---------------------------                                                                |
| |0 |HASH JOIN        |    |                                                                |
| |1 |├─TABLE FULL SCAN|t0  |                                                                |
| |2 |└─TABLE FULL SCAN|t1  |                                                                |
| ===========================                                                                |
+--------------------------------------------------------------------------------------------+

USE_MERGE Hint

和 USE_NL 类似,USE_MERGE 指定表作为 MJ 的右表,在联接的时候使用 Merge Join 算法,语法是:/*+ USE_MERGE(table_name_list)*/。示例如下:

CREATE TABLE t0(c1 INT, c2 INT, c3 INT);

CREATE TABLE t1(c1 INT, c2 INT, c3 INT);

EXPLAIN BASIC SELECT /*+LEADING(t0 t1) USE_MERGE(t1)*/ * FROM t0, t1 WHERE t0.c1 = t1.c1;
+--------------------------------------------------------------------------------------------+
| Query Plan                                                                                 |
+--------------------------------------------------------------------------------------------+
| =============================                                                              |
| |ID|OPERATOR           |NAME|                                                              |
| -----------------------------                                                              |
| |0 |MERGE JOIN         |    |                                                              |
| |1 |├─SORT             |    |                                                              |
| |2 |│ └─TABLE FULL SCAN|t0  |                                                              |
| |3 |└─SORT             |    |                                                              |
| |4 |  └─TABLE FULL SCAN|t1  |                                                              |
| =============================                                                              |
+--------------------------------------------------------------------------------------------+

说明

OceanBase 数据库中 Merge Join 必须有等值的 join 联接条件。如果无等值条件的两个表联接,USE_MERGE 会失效。

PARALLEL Hint

PARALLEL 指定语句级别的并行度。语法是:/*+ PARALLEL(n)*/。其中 n 为整数,表示 SQL 的全局并行度。示例如下:

CREATE TABLE tbl1 (col1 INT) PARTITION BY HASH(col1) ;

EXPLAIN BASIC SELECT /*+ PARALLEL(5) */ * FROM tbl1;
+----------------------------------------------------------------------+
| Query Plan                                                           |
+----------------------------------------------------------------------+
| ===================================                                  |
| |ID|OPERATOR             |NAME    |                                  |
| -----------------------------------                                  |
| |0 |PX COORDINATOR       |        |                                  |
| |1 |└─EXCHANGE OUT DISTR |:EX10000|                                  |
| |2 |  └─PX BLOCK ITERATOR|        |                                  |
| |3 |    └─TABLE FULL SCAN|tbl1    |                                  |
| ===================================                                  |
| Outputs & filters:                                                   |
| -------------------------------------                                |
|   0 - output([INTERNAL_FUNCTION(tbl1.col1)]), filter(nil), rowset=16 |
|   1 - output([INTERNAL_FUNCTION(tbl1.col1)]), filter(nil), rowset=16 |
|       dop=5                                                          |
|   2 - output([tbl1.col1]), filter(nil), rowset=16                    |
|   3 - output([tbl1.col1]), filter(nil), rowset=16                    |
|       access([tbl1.col1]), partitions(p0)                            |
|       is_index_back=false, is_global_index=false,                    |
|       range_key([tbl1.__pk_increment]), range(MIN ; MAX)always true  |
+----------------------------------------------------------------------+

计划中显示的 dop=5 表明 hint 已经生效。

OceanBase 数据库同时也支持表级别的 PARALLEL Hint,语法是:/*+ PARALLEL(table_name n)*/。示例如下:

CREATE TABLE t1 (c1 INT, c2 INT) PARTITION BY HASH(c1) PARTITIONS 5;

CREATE TABLE t2 (c1 INT PRIMARY KEY, c2 INT) PARTITION BY HASH(c1) PARTITIONS 4;

EXPLAIN SELECT /*+ PARALLEL(3) PARALLEL(t2 5)*/* FROM t1, t2 WHERE t1.c1 = t2.c1;
EXPLAIN SELECT /*+ PARALLEL(3) PARALLEL(t1 4) PARALLEL(t2 5)*/* FROM t1, t2 WHERE t1.c1 = t2.c1;
+---------------------------------------------------------------------------------------+
| Query Plan                                                                            |
+---------------------------------------------------------------------------------------+
| =======================================================================               |
| |ID|OPERATOR                           |NAME    |EST.ROWS|EST.TIME(us)|               |
| -----------------------------------------------------------------------               |
| |0 |PX COORDINATOR                     |        |1       |9           |               |
| |1 |└─EXCHANGE OUT DISTR               |:EX10001|1       |9           |               |
| |2 |  └─HASH JOIN                      |        |1       |9           |               |
| |3 |    ├─PART JOIN FILTER CREATE      |:RF0000 |1       |5           |               |
| |4 |    │ └─PX PARTITION ITERATOR      |        |1       |5           |               |
| |5 |    │   └─TABLE FULL SCAN          |t1      |1       |5           |               |
| |6 |    └─EXCHANGE IN DISTR            |        |1       |4           |               |
| |7 |      └─EXCHANGE OUT DISTR (PKEY)  |:EX10000|1       |4           |               |
| |8 |        └─PX BLOCK HASH JOIN-FILTER|:RF0000 |1       |4           |               |
| |9 |          └─TABLE FULL SCAN        |t2      |1       |4           |               |
| =======================================================================               |
| Outputs & filters:                                                                    |
| -------------------------------------                                                 |
|   0 - output([INTERNAL_FUNCTION(t1.c1, t1.c2, t2.c1, t2.c2)]), filter(nil), rowset=16 |
|   1 - output([INTERNAL_FUNCTION(t1.c1, t1.c2, t2.c1, t2.c2)]), filter(nil), rowset=16 |
|       dop=4                                                                           |
|   2 - output([t1.c1], [t2.c1], [t1.c2], [t2.c2]), filter(nil), rowset=16              |
|       equal_conds([t1.c1 = t2.c1]), other_conds(nil)                                  |
|   3 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                                |
|       RF_TYPE(bloom), RF_EXPR[calc_tablet_id(t1.c1)]                                  |
|   4 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                                |
|       affinitize                                                                      |
|   5 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                                |
|       access([t1.c1], [t1.c2]), partitions(p[0-4])                                    |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t1.__pk_increment]), range(MIN ; MAX)always true                     |
|   6 - output([t2.c1], [t2.c2]), filter(nil), rowset=16                                |
|   7 - output([t2.c1], [t2.c2]), filter(nil), rowset=16                                |
|       (#keys=1, [t2.c1]), dop=5                                                       |
|   8 - output([t2.c1], [t2.c2]), filter(nil), rowset=16                                |
|   9 - output([t2.c1], [t2.c2]), filter(nil), rowset=16                                |
|       access([t2.c1], [t2.c2]), partitions(p[0-3])                                    |
|       is_index_back=false, is_global_index=false,                                     |
|       range_key([t2.c1]), range(MIN ; MAX)always true                                 |
+---------------------------------------------------------------------------------------+

READ_CONSISTENCY(WEAK) Hint

READ_CONSISTENCY Hint 用于指定 SQL 的读一致性级别为弱一致性,语法是:/*+ READ_CONSISTENCY(WEAK)*/。示例如下:

-- 读取 t1 表备副本上的数据
SELECT /*+ READ_CONSISTENCY(WEAK) */ * FROM t1;

对于实时性要求不高的偏 AP 类请求,为了实现 AP 业务和 TP 业务的读写分离,减少 AP 请求对主副本的影响,可以设置弱一致性读。设置弱一致性读的 hint 之后,读操作会到备副本上执行。弱一致性读的概念详见官网《OceanBase 数据库》文档 参考指南/系统原理/事务管理/事务并发和一致性/弱一致性读

QUERY_TIMEOUT Hint

QUERY_TIMEOUT Hint 用于设置查询语句的超时时间,语法是:/*+ query_timeout(n)*/,其中的 n 是整数,单位是微秒(us)。示例如下:

-- 指定这条 SQL 执行的超时时间为 100000000 微秒,即 100 秒。
SELECT /*+ query_timeout(100000000) */ * FROM t1;

说明

  • 用户可以通过执行 SHOW VARIABLES LIKE 'ob_query_timeout'; 查看默认的 SQL 超时时间。
  • OceanBase 数据库除了支持通过 hint 修改 SQL 级别的 SQL 超时时间,还支持通过执行 SET SESSION ob_query_timeout = 100000000; 修改会话(session)级别的SQL 超时时间,支持通过执行 SET GLOBAL ob_query_timeout = 100000000; 修改租户级别的 SQL 超时时间。

通过 Outline 进行计划绑定

通过对某条 SQL 创建 Outline 可实现计划绑定。

在生产系统上线前,可以直接在 SQL 语句中添加 Hint,控制优化器按 Hint 指定的行为进行计划生成。

但对于已上线的业务,如果出现优化器选择的计划不够优时,则需要在线进行计划绑定,无需业务对 SQL 进行更改。而是通过 DDL 操作将一组 Hint 加入到特定的 SQL 中,从而使优化器根据指定的一组 Hint,对该 SQL 生成更优计划。该组 Hint 称为 Outline。

Outline 相关的字典视图

Outline 视图为 DBA_OB_OUTLINES,其字段说明如下表所示。

字段名称类型(MySQL 模式)类型(Oracle 模式)描述
CREATE_TIMETIMESTAMP(6)TIMESTAMP(6)创建时间戳
MODIFY_TIMETIMESTAMP(6)TIMESTAMP(6)修改时间戳
TENANT_IDBIGINT(20)NUMBER(38)租户 ID
DATABASE_IDBIGINT(20)NUMBER(38)数据库 ID
OUTLINE_IDBIGINT(20)NUMBER(38)Outline ID
DATABASE_NAMEVARCHAR2(128)VARCHAR2(128)数据库名称
OUTLINE_NAMEVARCHAR2(128)VARCHAR2(128)Outline 名称
VISIBLE_SIGNATURELONGTEXTCLOBSignature 的反序列化结果,为了便于查看 Signature 的信息。
SQL_TEXTLONGTEXTCLOB创建 Outline 时,在 ON 子句中指定的 SQL。
OUTLINE_TARGETLONGTEXTCLOB创建 Outline 时,在 TO 子句中指定的 SQL。
OUTLINE_SQLLONGTEXTCLOB具有完整 Outline 信息的 SQL
SQL_IDVARCHAR2(32)VARCHAR2(32)SQL 标识符
OUTLINE_CONTENTLONGTEXTCLOB完整的执行计划 Outline 信息

创建 Outline

OceanBase 数据库支持通过两种方式创建 Outline,一种是通过 SQL_TEXT (用户执行的带参数的原始语句),另一种是通过 SQL_ID。

注意

创建 Outline 需要进入对应的数据库下执行。

使用 SQL_TEXT 创建 Outline

使用 SQL_TEXT 创建 Outline 后,会生成一个 Key-Value 对存储在 Map 中,其中 Key 为绑定的 SQL 参数化后的文本,Value 为绑定的 Hint。具体参数化原则,详见官网《OceanBase 数据库》文档 参考指南/性能调优/SQL 调优指南/SQL 执行计划/快速参数化

使用 SQL_TEXT 创建 Outline 的语法如下:

CREATE [OR REPLACE] OUTLINE <outline_name> ON <stmt>;
  • 指定 OR REPLACE 后,可以对已经存在执行计划进行替换。
  • 其中 stmt 一般为一个带有 Hint 和原始参数的 DML 语句。示例如下:
  CREATE OUTLINE outline1 ON 
  SELECT /*+NO_REWRITE*/ * 
  FROM tbl1
  WHERE col1 = 4 AND col2 = 6 ORDER BY 2 TO SELECT  * FROM tbl1 WHERE col1 = 4 AND col2 = 6 ORDER BY 2;

注意

在使用 target_stmt 时,严格要求 stmttarget_stmt 在去掉 Hint 后完全匹配。

如下示例中,优化器可能默认会选择走 idx_c2 索引。索引上只有索引列 c2 和主键列 c1(索引上的主键列在索引中是隐藏列,仅用于索引回表),因为结果中还需要返回 c3 列的值,所以需要索引回表查出 c3 列。

索引回表一行的代价远远大于全表扫描一行的代价(大概是十倍左右的代价)。如果用户已知 c2 列的过滤性很差,所以把该 SQL 改为强制走全表扫描,性能反倒会更优。此时可以通过创建 Outline 将该 SQL 绑定全表扫描的计划。

CREATE TABLE t1 (c1 INT PRIMARY KEY, c2 INT, c3 INT, INDEX idx_c2(c2));

INSERT INTO t1 VALUES(1, 1, 1), (2, 1, 2), (3, 1, 3);

EXPLAIN SELECT * FROM t1 WHERE c2 = 1;
+-----------------------------------------------------------------+
| Query Plan                                                      |
+-----------------------------------------------------------------+
| ======================================================          |
| |ID|OPERATOR        |NAME      |EST.ROWS|EST.TIME(us)|          |
| ------------------------------------------------------          |
| |0 |TABLE RANGE SCAN|t1(idx_c2)|1       |7           |          |
| ======================================================          |
| Outputs & filters:                                              |
| -------------------------------------                           |
|   0 - output([t1.c1], [t1.c2], [t1.c3]), filter(nil), rowset=16 |
|       access([t1.c1], [t1.c2], [t1.c3]), partitions(p0)         |
|       is_index_back=true, is_global_index=false,                |
|       range_key([t1.c2], [t1.c1]), range(1,MIN ; 1,MAX),        |
|       range_cond([t1.c2 = 1])                                   |
+-----------------------------------------------------------------+

根据如下 SQL 语句创建 Outline:

CREATE OR REPLACE OUTLINE otl_t1_full ON SELECT /*+ full(t1) */ * FROM t1 WHERE c2 = 1;

为了确认某一条 SQL 在执行时的真实计划,不应该直接执行 explain 语句,而是要通过查询上述的两个计划监控视图 GV$OB_PLAN_CACHE_PLAN_STATGV$OB_PLAN_CACHE_PLAN_EXPLAIN。详见下文 确定 Outline 创建生效 部分。

使用 SQL_ID 创建 Outline

使用 SQL_ID 创建 Outline 的语法如下:

CREATE OUTLINE outline_name ON sql_id USING HINT hint_text;

SQL_ID 为需要绑定的 SQL 对应的 SQL_ID,可以查询 GV$OB_PLAN_CACHE_PLAN_STAT 获取。

select
  TENANT_ID,
  SVR_IP,
  SVR_PORT,
  PLAN_ID,
  LAST_ACTIVE_TIME,
  QUERY_SQL,
  SQL_ID
from
  oceanbase.GV$OB_PLAN_CACHE_PLAN_STAT
where
  QUERY_SQL = 'SELECT * FROM t1 WHERE c2 = 1';

输出如下:

+-----------+--------------+----------+---------+----------------------------+-------------------------------+----------------------------------+
| TENANT_ID | SVR_IP       | SVR_PORT | PLAN_ID | LAST_ACTIVE_TIME           | QUERY_SQL                     | SQL_ID                           |
+-----------+--------------+----------+---------+----------------------------+-------------------------------+----------------------------------+
|      1002 | 10.10.10.1   |    22602 |   49820 | 2024-03-13 18:49:17.375906 | SELECT * FROM t1 WHERE c2 = 1 | ED570339F2C856BA96008A29EDF04C74 |
+-----------+--------------+----------+---------+----------------------------+-------------------------------+----------------------------------+

使用 SQL_ID 绑定 Outline,如下例所示:

DROP OUTLINE otl_t1_full;

CREATE OUTLINE otl_t1_idx_c2 ON "ED570339F2C856BA96008A29EDF04C74" USING HINT /*+ INDEX(t1 idx_c2)*/ ;

注意

  • Hint 格式为 /*+ xxx*/
  • 使用 SQL_TEXT 方式创建的 Outline 会覆盖 SQL_ID 方式创建的 Outline。SQL_TEXT 方式创建的优先级更高。
  • 如果 SQL_ID 对应的 SQL 语句已经有 Hint,则创建 Outline 指定的 Hint 会覆盖原始语句中所有 Hint。

Outline Data 是优化器为了完全复现某一计划而生成的一组 Hint 信息,以 BEGIN_OUTLINE_DATA 开始,并以 END_OUTLINE_DATA 结束。Outline Data 信息可以通过 EXPLAIN outline 命令获得,如下例所示:

EXPLAIN outline SELECT/*+ index(t1 idx_c2)*/ * FROM t1 WHERE c2 = 1;

输出如下:

-----------------------------------------------------------------+
| Query Plan                                                      |
+-----------------------------------------------------------------+
| ======================================================          |
| |ID|OPERATOR        |NAME      |EST.ROWS|EST.TIME(us)|          |
| ------------------------------------------------------          |
| |0 |TABLE RANGE SCAN|t1(idx_c2)|3       |12          |          |
| ======================================================          |
| Outputs & filters:                                              |
| -------------------------------------                           |
|   0 - output([t1.c1], [t1.c2], [t1.c3]), filter(nil), rowset=16 |
|       access([t1.c1], [t1.c2], [t1.c3]), partitions(p0)         |
|       is_index_back=true, is_global_index=false,                |
|       range_key([t1.c2], [t1.c1]), range(1,MIN ; 1,MAX),        |
|       range_cond([t1.c2 = 1])                                   |
| Outline Data:                                                   |
| -------------------------------------                           |
|   /*+                                                           |
|       BEGIN_OUTLINE_DATA                                        |
|       INDEX(@"SEL$1" "test"."t1"@"SEL$1" "idx_c2")              |
|       OPTIMIZER_FEATURES_ENABLE('4.0.0.0')                      |
|       END_OUTLINE_DATA                                          |
|   */                                                            |
+-----------------------------------------------------------------+

其中 Outline Data 信息如下所示:

Outline Data:
-------------------------------------
/*+
    BEGIN_OUTLINE_DATA
    INDEX(@"SEL$1" "test"."t1"@"SEL$1" "idx_c2")
    OPTIMIZER_FEATURES_ENABLE('4.0.0.0')
    END_OUTLINE_DATA
*/

Outline Data 也属于 Hint,因此可以用在计划绑定的过程中,如下例所示:

DROP OUTLINE otl_t1_idx_c2;

CREATE OUTLINE otl_t1_idx_c2
     ON "ED570339F2C856BA96008A29EDF04C74" USING HINT
/*+
    BEGIN_OUTLINE_DATA
    INDEX(@"SEL$1" "test"."t1"@"SEL$1" "idx_c2")
    OPTIMIZER_FEATURES_ENABLE('4.0.0.0')
    END_OUTLINE_DATA
*/;

确定 Outline 创建生效

确定创建的 Outline 是否成功且符合预期,需要进行如下三步的验证:

  1. 确定 Outline 创建成功。通过查看 DBA_OB_OUTLINES 视图,确认是否成功创建对应名称的 Outline。
   SELECT * FROM oceanbase.DBA_OB_OUTLINES WHERE OUTLINE_NAME = 'otl_t1_full'\G

输出如下:

   *************************** 1. row ***************************
         CREATE_TIME: 2024-03-13 18:38:18.807692
         MODIFY_TIME: 2024-03-13 18:39:57.210761
           TENANT_ID: 1002
         DATABASE_ID: 500001
          OUTLINE_ID: 500133
       DATABASE_NAME: test
        OUTLINE_NAME: otl_t1_full
   VISIBLE_SIGNATURE: SELECT * FROM t1 WHERE c2 = ?
            SQL_TEXT: SELECT/*+ full(t1) */ * FROM t1 WHERE c2 = 1
      OUTLINE_TARGET:
         OUTLINE_SQL: SELECT /*+BEGIN_OUTLINE_DATA FULL(@"SEL$1" "test"."t1"@"SEL$1") OPTIMIZER_FEATURES_ENABLE('4.0.0.0') END_OUTLINE_DATA*/* FROM t1 WHERE c2 = 1
              SQL_ID:
     OUTLINE_CONTENT: /*+BEGIN_OUTLINE_DATA FULL(@"SEL$1" "test"."t1"@"SEL$1") OPTIMIZER_FEATURES_ENABLE('4.0.0.0') END_OUTLINE_DATA*/
  1. 确定新的 SQL 是否通过绑定的 Outline 生成了新执行计划。当绑定 Outline 的 SQL 执行新的查询后,查询 GV$OB_PLAN_CACHE_PLAN_STAT 表中该 SQL 对应的计划信息中的 outline_id。如果 outline_id 与在 DBA_OB_OUTLINES 中查到的 outline_id 相同,则表示是按绑定的 Outline 生成的执行计划,否则不是。
   SELECT SQL_ID, PLAN_ID, STATEMENT, OUTLINE_ID, OUTLINE_DATA
   FROM oceanbase.GV$OB_PLAN_CACHE_PLAN_STAT
   WHERE STATEMENT LIKE '%SELECT * FROM t1 WHERE c2 =%'\G

输出如下:

   *************************** 1. row ***************************
         SQL_ID: ED570339F2C856BA96008A29EDF04C74
        PLAN_ID: 49820
      STATEMENT: SELECT * FROM t1 WHERE c2 = ?
     OUTLINE_ID: 500133
   OUTLINE_DATA: /*+BEGIN_OUTLINE_DATA FULL(@"SEL$1" "test"."t1"@"SEL$1") OPTIMIZER_FEATURES_ENABLE('4.0.0.0') END_OUTLINE_DATA*/
  1. 确定生成的执行计划是否符合预期。确定是通过绑定的 Outline 生成的计划后,需要确定生成的计划是否符合预期,可以通过查询 GV$OB_PLAN_CACHE_PLAN_EXPLAIN 表查看 plan_cache 中缓存的执行计划形状,具体查看方式可参见官网《OceanBase 数据库》文档 参考指南/性能调优/SQL 调优指南/SQL 执行计划/实时执行计划展示
  2. 通过 GV$OB_PLAN_CACHE_PLAN_STAT 查询上述 SQL 对应的 TENANT_IDSVR_IPSVR_PORTPLAN_ID 字段。 select TENANT_ID, SVR_IP, SVR_PORT, PLAN_ID, LAST_ACTIVE_TIME, QUERY_SQL from oceanbase.GV$OB_PLAN_CACHE_PLAN_STAT where QUERY_SQL = 'SELECT * FROM t1 WHERE c2 = 1'; 输出如下: +-----------+--------------+----------+---------+----------------------------+-------------------------------+ | TENANT_ID | SVR_IP | SVR_PORT | PLAN_ID | LAST_ACTIVE_TIME | QUERY_SQL | +-----------+--------------+----------+---------+----------------------------+-------------------------------+ | 1002 | 10.10.10.1 | 22602 | 49820 | 2024-03-13 18:49:17.375906 | SELECT * FROM t1 WHERE c2 = 1 | +-----------+--------------+----------+---------+----------------------------+-------------------------------+
  3. 根据输出,通过 oceanbase.GV$OB_PLAN_CACHE_PLAN_EXPLAIN 来看 SQL 对应的真实计划,GV$OB_PLAN_CACHE_PLAN_EXPLAIN 视图各字段含义详见官网《OceanBase 数据库》文档 参考指南/系统视图/MySQL 租户系统视图/性能视图/GV$OB_PLAN_CACHE_PLAN_EXPLAINSELECT * FROM oceanbase.GV$OB_PLAN_CACHE_PLAN_EXPLAIN WHERE tenant_id = 1002 AND SVR_IP = '10.10.10.1' AND SVR_PORT = 22602 AND PLAN_ID = 49820; 输出如下: +-----------+--------------+----------+---------+------------+--------------+----------------+------+------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | TENANT_ID | SVR_IP | SVR_PORT | PLAN_ID | PLAN_DEPTH | PLAN_LINE_ID | OPERATOR | NAME | ROWS | COST | PROPERTY | +-----------+--------------+----------+---------+------------+--------------+----------------+------+------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | 1002 | 10.10.10.1 | 22602 | 49820 | 0 | 0 | PHY_TABLE_SCAN | t1 | 1 | 3 | table_rows:3, physical_range_rows:3, logical_range_rows:3, index_back_rows:0, output_rows:1, avaiable_index_name[idx_c2,t1], pruned_index_name[idx_c2], estimation info[table_id:500131, (table_type:0, version:-1--1--1, logical_rc:3, physical_rc:3)] | +-----------+--------------+----------+---------+------------+--------------+----------------+------+------+------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 您也可以通过 DBMS_XPLAN.DISPLAY_CURSOR 来看 SQL 对应的真实计划,展示出来的信息比 oceanbase.GV$OB_PLAN_CACHE_PLAN_EXPLAIN 更像 explain 展示出来的信息,详见官网《OceanBase 数据库》文档 参考指南/SQL 参考/PL 参考/PL 参考(MySQL 模式)/PL 系统包/DBMS_XPLAN/DISPLAY_CURSORSELECT DBMS_XPLAN.DISPLAY_CURSOR( 49820, 'typical', '10.10.10.1', 22602, 1002 ) FROM DUAL; 输出如下: ================================================================================================ |ID|OPERATOR |NAME|EST.ROWS|EST.TIME(us)|REAL.ROWS|REAL.TIME(us)|IO TIME(us)|CPU TIME(us)| ------------------------------------------------------------------------------------------------ |0 |TABLE FULL SCAN|t1 |1 |4 |3 |0 |0 |0 | ================================================================================================ Outputs & filters: ------------------------------------- 0 - output([t1.c1], [t1.c2], [t1.c3]), filter([t1.c2 = :0]), rowset=16 access([t1.c1], [t1.c2], [t1.c3]), partitions(p0) is_index_back=false, is_global_index=false, filter_before_indexback[false], range_key([t1.c1]), range(MIN ; MAX)always true

删除 Outline

删除 Outline 后,对应 SQL 将不再依据所绑定的 Outline 重新生成执行计划。删除 Outline 的语法如下:

DROP OUTLINE outline_name;

计划绑定与执行计划缓存关系

  • 使用 SQL_TEXT 创建 Outline 后,SQL 请求生成新计划查找 Outline 使用的 Key 与计划缓存使用的 Key 是相同的,即均是 SQL 参数化后的文本串。
  • 当创建和删除 Outline 后,对应 SQL 有新的请求时,会触发执行计划缓存中对应执行计划失效,更新为根据绑定的 Outline 所生成的执行计划。

计划绑定与限流规则的关系

从 OceanBase 数据库 4.2.2 版本开始,数据库的 CREATE OUTLINEALTER OUTLINE 命令扩展了功能。除了原有的绑定特定查询的执行计划外,还允许用户为查询设置最大并发执行次数的限制。限流功能能够帮助大家更有效地管理数据库负载,防止因高并发查询而导致的性能问题。

例如:

  • 使用 CREATE OUTLINE 创建了一个 OUTLINE,其中包含了 USE_NL(tbl2) 提示,这告诉优化器对于表 tbl2 使用 nest loop join,同时 MAX_CONCURRENT(1) 这个 Hint 在下面这个示例中表示同一时刻,只允许一个线程执行查询:
  CREATE OUTLINE otl2 ON SELECT /*+ USE_NL(tbl2) MAX_CONCURRENT(1) */ * FROM t WHERE c1 = ?;
  • 同样地,使用 ALTER OUTLINE 修改现有 OUTLINE,同样可以达到上述效果。
  ALTER OUTLINE otl2 ON SELECT /*+ USE_NL(tbl2) MAX_CONCURRENT(1) */ * FROM t WHERE c1 = ?;