【华为云MySQL技术专栏】GaussDB(for MySQL) DBA运维工具—Statement Outline

举报
GaussDB 数据库 发表于 2024/07/26 09:44:44 2024/07/26
【摘要】 1.背景介绍在生产环境,MySQL数据库实例运行过程中,一些SQL语句会发生执行计划的变化,导致增加了数据库稳定性的风险,这里边有几个因素和场景,比如:表结构发生变化、某些索引的增加或删减、实例升级迁移等过程中MySQL自身优化器的行为和算法变化,以及表数据的变化等。针对上述查询语句执行计划不稳定的风险,MySQL提供了查询优化提示机制(Hints)解决了该问题。MySQL Hints,是一...

1背景介绍

在生产环境,MySQL数据库实例运行过程中,一些SQL语句会发生执行计划的变化,导致增加了数据库稳定性的风险,这里边有几个因素和场景,比如:表结构发生变化、某些索引的增加或删减、实例升级迁移等过程中MySQL自身优化器的行为和算法变化,以及表数据的变化等。

针对上述查询语句执行计划不稳定的风险,MySQL提供了查询优化提示机制(Hints)解决了该问题。MySQL Hints,是一组特殊的注释或者指令,可以直接嵌入到SQL语句中,改变MySQL优化器的默认行为。

如下所示,MySQL Hints可以添加FORCE INDEX(idx1)强制t1表使用idx1索引进行表扫描。MySQL支持两种类型的Hints,分别为Index Hints[1]和Optimizer Hints[2],详情请参考官网文档说明。

SELECT c1 FROM t1 FORCE INDEX(idx1) WHERE c2 = 1;

上述示例表明:MySQL Hints 需要对 SQL 语句进行修改,但客户业务的调整相对繁琐,因此通常不建议通过修改业务语句来解决性能下降的问题。

针对上述的问题,GaussDB(for MySQL)设计了一套利用MySQL Hints来稳定执行计划的方法,称为Statement outline。该方法解决了客户在不改变业务的情况下,通过配置对应的outline规则,来稳定语句的执行计划。


2Statement Outline原理

Statement outline是通过提前设置一些Hint规则存储在数据库系统表中。当执行某条 SQL 语句时,系统会查询相应的存储数据库。如果找到匹配的规则,系统将这些符合条件的Outline 转换为 MySQL 内部的 Hint 数据结构,并将其应用于相应的数据结构。在后续的语句优化阶段,优化器将访问这些 Hint,以选择最佳执行计划,从而使客户无需修改业务语句即可为其 SQL 语句绑定优化规则。

内部转换逻辑如下所示。

Query1 : select * from t1 where a = 5 
Outline : hint : force index(key1) for Query1.

客户端执行Query : select * from t1 where a = 5 
===> 
MySQL内部真实执行语句:select * from t1 force index(key1) where a = 5


3 Statement Outline的使用

3.1 使用场景

Statement Outline支持MySQL8.0提供的所有Hints,可以解决很多种执行计划不稳定的场景。下面是一些常见的使用场景:

可以应用在SELECT, UPDATE, INSERT, REPLACEDELETE类型的语句

可以设置表的索引选择,解决索引选择不当的问题

可以设置session级别的参数变量值,只影响该语句,不会影响该session上的其他语

可以设置JOIN的表连接顺序semijoin的优化策略选择,解决因连接顺序不佳导致的性能问题。

3.2 Statement Outline的开关

rds_opt_outline_enabled是Statement Outline功能开关。当开关设置为ON时,语句执行到解析阶段就会去系统表 "mysql.outline"中查找该语句是否有匹配的outline规则,然后将outline规则添加到对应的数据结构中,语句在优化阶段就会参考outline规则来选定执行计划。该开关不控制outline规则的添加和删除。


截图1.PNG


3.3 Outline管理接口

为了客户更加方便的管理Statement outline,我们提供了6个内置的存储过程接口:

// 添加Optimizer Hint类型的outline
dbms_outln.add_optimizer_outline();
// 添加Index Hint类型的outline 
dbms_outln.add_index_outline();
// 预览一个SQL语句命中outline的情况
dbms_outln.preview_outline();
// 展示内存中所有outline
dbms_outln.show_outline();
// 删除一条outline
dbms_outln.del_outline(); 
// 更新内存的outline,从mysql.outline表中重新加载到内存里
dbms_outln.flush_outline();

以上接口中,除了show_outline和preview_outline接口外,其他的接口操作只能在主节点上执行,主节点执行完成后会自动同步到其他节点。

为了方便介绍接口的使用方法,这里准备了两个测试表。

create table t1 (
  id int not null, col1 int, col2 varchar(100),
  primary key(id),
  key idx1(col1),
  key idx2(col2)
) engine = innodb;

create table t2 (
  id int not null, col1 int, col2 varchar(100),
  primary key(id),
  key idx1(col1),
  key idx2(col2)
) engine = innodb;

3.3.1 dbms_outln.add_index_outline

语法:

call dbms_outln.add_index_outline(<schema>,<digest>,<position>,<type>,<hint>, <scope>, <query>);

参数说明:


截图2.PNG


示例:

测试语句:

select * from t1 where t1.col1 > 5 and t1.col2 ='xxx';

使用idx1索引:

// 1. 添加对应的outline规则
call dbms_outln.add_index_outline('test_db', '', 1, 'FORCE INDEX', 'idx1', '', "select * from t1 where t1.col1 > 5 and t1.col2 = 'xxx'");
// 2. 查看是否添加成功
mysql> call dbms_outln.show_outline();
+------+---------+------------------------------------------------------------------+-------------+-------+------+------+------+----------+------------------------------------------------------------------+
| ID   | SCHEMA  | DIGEST                                                           | TYPE        | SCOPE | POS  | HINT | HIT  | OVERFLOW | DIGEST_TEXT                                                      |
+------+---------+------------------------------------------------------------------+-------------+-------+------+------+------+----------+------------------------------------------------------------------+
|    4 | test_db | ee4331923bbcb2d2c69a7200cb11ef688179f9a1e0414826b8aac1702d30f09c | FORCE INDEX |       |    1 | idx1 |    0 |        0 | SELECT * FROM `t1` WHERE `t1` . `col1` > ? AND `t1` . `col2` = ? |
+------+---------+------------------------------------------------------------------+-------------+-------+------+------+------+----------+------------------------------------------------------------------+
1 row in set (0.01 sec)

验证outline:

添加outline规则后,为了验证是否能生效,提供了两种方法:

第一种,使用dbms_outln.preview_outline()接口进行预览查看,可以查看有多少条outline匹配得上;

第二种,直接使用explain查看语句的执行计划,查看执行计划是否满足要求,并且explain执行完成后,可以通过show warnings查看具体的SQL语句,查看是否添加上对应的Hints。

// preview_outline结果可以看出有一条outline规则是符合对应语句的
mysql> call dbms_outln.preview_outline('test_db', "select * from t1 where t1.col1 > 5 and t1.col2 = 'xxx'");
+---------+------------------------------------------------------------------+------------+------------+-------+----------------------+
| SCHEMA  | DIGEST                                                           | BLOCK_TYPE | BLOCK_NAME | BLOCK | HINT                 |
+---------+------------------------------------------------------------------+------------+------------+-------+----------------------+
| test_db | ee4331923bbcb2d2c69a7200cb11ef688179f9a1e0414826b8aac1702d30f09c | TABLE      | t1         |     1 | FORCE INDEX (`idx1`) |
+---------+------------------------------------------------------------------+------------+------------+-------+----------------------+
1 row in set (0.00 sec)

// 查看执行计划,最终t1表的索引走的是idx1,
mysql> explain select * from t1 where t1.col1 > 5 and t1.col2 = 'xxx';
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | t1    | NULL       | range | idx1          | idx1 | 5       | NULL |    1 |   100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

// 查看内部执行的语句,确实将对应的Hint添加到语句中了
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                                            |
+-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `test_db`.`t1`.`id` AS `id`,`test_db`.`t1`.`col1` AS `col1`,`test_db`.`t1`.`col2` AS `col2` from `test_db`.`t1` FORCE INDEX (`idx1`) where ((`test_db`.`t1`.`col2` = 'xxx') and (`test_db`.`t1`.`col1` > 5)) |
+-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

3.3.2 dbms_outln.add_optimzier_outline

语法:

dbms_outln.add_optimizer_outline(<Schema_name>,<Digest>,<Query_block>,<Hint>,<Query>);

参数说明:

示例:

测试语句:

select * from t1 where col1 = 1 and id in (select id from t2 where col1 = 2 and col2 = 'xxx');

指定join的顺序:

// 1. 添加outline
call dbms_outln.add_optimizer_outline('test_db', '', 1, '/*+ join_order(t2, t1)*/', "select * from t1 where col1 = 1 and id in (select id from t2 where col1 = 2 and col2 = 'xxx')");

// 2. 预览是否生效
mysql> call dbms_outln.preview_outline('test_db', "select * from t1 where col1 = 1 and id in (select id from t2 where col1 = 2 and col2 = 'xxx')");
+---------+------------------------------------------------------------------+------------+------------+-------+-------------------------------------------+
| SCHEMA  | DIGEST                                                           | BLOCK_TYPE | BLOCK_NAME | BLOCK | HINT                                      |
+---------+------------------------------------------------------------------+------------+------------+-------+-------------------------------------------+
| test_db | 659a7c3116442066cdf827284c660237eb84c7edf5d966bc86aeef98fed5cd4b | QUERY      |            |     1 | /*+ JOIN_ORDER(@`select#1` `t2`,`t1`) */  |
+---------+------------------------------------------------------------------+------------+------------+-------+-------------------------------------------+
1 row in set (0.00 sec)

// 3. 查看执行计划,最终的执行计划的join顺序为t2,t1
mysql> explain select * from t1 where col1 = 1 and id in (select id from t2 where col1 = 2 and col2 = 'xxx');
+----+-------------+-------+------------+--------+-------------------+---------+---------+---------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys     | key     | key_len | ref           | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+-------------------+---------+---------+---------------+------+----------+-------------+
|  1 | SIMPLE      | t2    | NULL       | ref    | PRIMARY,idx1,idx2 | idx1    | 5       | const         |    1 |   100.00 | Using where |
|  1 | SIMPLE      | t1    | NULL       | eq_ref | PRIMARY,idx1      | PRIMARY | 4       | test_db.t2.id |    1 |   100.00 | Using where |
+----+-------------+-------+------------+--------+-------------------+---------+---------+---------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

// 4. 查看内部执行语句
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                                                                                                                                                                    |
+-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select /*+ JOIN_ORDER(@`select#1` `t2`,`t1`) */ `test_db`.`t1`.`id` AS `id`,`test_db`.`t1`.`col1` AS `col1`,`test_db`.`t1`.`col2` AS `col2` from `test_db`.`t2` join `test_db`.`t1` where ((`test_db`.`t1`.`id` = `test_db`.`t2`.`id`) and (`test_db`.`t2`.`col2` = 'xxx') and (`test_db`.`t2`.`col1` = 2) and (`test_db`.`t1`.`col1` = 1)) |
+-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

3.3.3 dbms_outln.preview_outline

dbms_outln.preview_outline()用于使用具体 SQL 语句,查看匹配Outline的情况,用于手动验证。

语法和参数:

call dbms_outln.preview_outline(<schema>, <query>);
说明:其中schema : 表示数据库名称;query:查询语句

示例如上展示,此处不再赘述。

3.3.4 dbms_outln.show_outline

dbms_outln.show_outline() 展示内存中有效的outline的命中情况。该函数是没有参数的。

示例:

mysql> call dbms_outln.show_outline();
+------+---------+------------------------------------------------------------------+-------------+-------+------+--------------------------+------+----------+-----------------------------------------------------------------------------------------------------------+
| ID   | SCHEMA  | DIGEST                                                           | TYPE        | SCOPE | POS  | HINT                     | HIT  | OVERFLOW | DIGEST_TEXT                                                                                               |
+------+---------+------------------------------------------------------------------+-------------+-------+------+--------------------------+------+----------+-----------------------------------------------------------------------------------------------------------+
|    5 | test_db | 659a7c3116442066cdf827284c660237eb84c7edf5d966bc86aeef98fed5cd4b | OPTIMIZER   |       |    1 | /*+ join_order(t2, t1)*/ |    2 |        0 | SELECT * FROM `t1` WHERE `col1` = ? AND `id` IN ( SELECT `id` FROM `t2` WHERE `col1` = ? AND `col2` = ? ) |
|    4 | test_db | ee4331923bbcb2d2c69a7200cb11ef688179f9a1e0414826b8aac1702d30f09c | FORCE INDEX |       |    1 | idx1                     |    2 |        0 | SELECT * FROM `t1` WHERE `t1` . `col1` > ? AND `t1` . `col2` = ?                                          |
+------+---------+------------------------------------------------------------------+-------------+-------+------+--------------------------+------+----------+-----------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

返回结果里边有两个字段,HIT表示outline命中的次数;OVERFLOW表示outline hint没有找到query block或者相应的table的次数。

3.3.5 dbms_outln.del_outline

dbms_outln.del_outline() 可以删除内存和表中的某一条outline。

语法和参数:

call dbms_outln.del_outline(outline_id);
说明:outlinde_id是来自mysql.outline表里的id列的值。

举例:

mysql> call dbms_outln.del_outline(4);
Query OK, 0 rows affected (0.01 sec)

mysql> call dbms_outln.del_outline(7);
Query OK, 0 rows affected, 2 warnings (0.00 sec)
mysql> show warnings;
+---------+------+-------------------------------------------+
| Level   | Code | Message                                   |
+---------+------+-------------------------------------------+
| Warning | 7534 | Statement outline 7 is not found in table |
| Warning | 7534 | Statement outline 7 is not found in cache |
+---------+------+-------------------------------------------+
2 rows in set (0.00 sec)

如果删除的outline_id不存在,系统会报warnings, 显示该id的outline是不存在的。

3.3.6 dbms_outln.flush_outline

dbms_outln.flush_outline()支持清理cache中outline,并从mysql.outline表中重新load。如果用户直接修改表来加载 outline,需要手动执行该接口重新加载表的数据到内存中。

示例:

直接修改id为5的outline的schema_name:

mysql> select id, schema_name, hint  from mysql.outline;
+----+-------------+--------------------------+
| id | schema_name | hint                     |
+----+-------------+--------------------------+
|  5 | test_db     | /*+ join_order(t2, t1)*/ |
+----+-------------+--------------------------+
1 row in set (0.00 sec)

mysql> update mysql.outline set schema_name = 'outline_db' where id = 5;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select id, schema_name, hint  from mysql.outline;
+----+-------------+--------------------------+
| id | schema_name | hint                     |
+----+-------------+--------------------------+
|  5 | outline_db  | /*+ join_order(t2, t1)*/ |
+----+-------------+--------------------------+
1 row in set (0.00 sec)

查看内存的outline情况:

mysql> call dbms_outln.show_outline();
+------+---------+------------------------------------------------------------------+-----------+-------+------+--------------------------+------+----------+-----------------------------------------------------------------------------------------------------------+
| ID   | SCHEMA  | DIGEST                                                           | TYPE      | SCOPE | POS  | HINT                     | HIT  | OVERFLOW | DIGEST_TEXT                                                                                               |
+------+---------+------------------------------------------------------------------+-----------+-------+------+--------------------------+------+----------+-----------------------------------------------------------------------------------------------------------+
|    5 | test_db | 659a7c3116442066cdf827284c660237eb84c7edf5d966bc86aeef98fed5cd4b | OPTIMIZER |       |    1 | /*+ join_order(t2, t1)*/ |    3 |        0 | SELECT * FROM `t1` WHERE `col1` = ? AND `id` IN ( SELECT `id` FROM `t2` WHERE `col1` = ? AND `col2` = ? ) |
+------+---------+------------------------------------------------------------------+-----------+-------+------+--------------------------+------+----------+-----------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

可以看到,内存的outline没有更新到最新,跟mysql.outline的规则不同。

刷新内存outline:

mysql> call dbms_outln.flush_outline();
Query OK, 0 rows affected (0.00 sec)

mysql> call dbms_outln.show_outline();
+------+------------+------------------------------------------------------------------+-----------+-------+------+--------------------------+------+----------+-----------------------------------------------------------------------------------------------------------+
| ID   | SCHEMA     | DIGEST                                                           | TYPE      | SCOPE | POS  | HINT                     | HIT  | OVERFLOW | DIGEST_TEXT                                                                                               |
+------+------------+------------------------------------------------------------------+-----------+-------+------+--------------------------+------+----------+-----------------------------------------------------------------------------------------------------------+
|    5 | outline_db | 659a7c3116442066cdf827284c660237eb84c7edf5d966bc86aeef98fed5cd4b | OPTIMIZER |       |    1 | /*+ join_order(t2, t1)*/ |    0 |        0 | SELECT * FROM `t1` WHERE `col1` = ? AND `id` IN ( SELECT `id` FROM `t2` WHERE `col1` = ? AND `col2` = ? ) |
+------+------------+------------------------------------------------------------------+-----------+-------+------+--------------------------+------+----------+-----------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

调用flush_outline()接口后,内存statement outlines就更新为最新的。

3.4 持久化

为了表示和抽象这些 Hint,持久化 outline,可以增加一个系统表 mysql.outline。其结构如下:

CREATE TABLE `outline` (
  `Id` bigint NOT NULL AUTO_INCREMENT,
  `Schema_name` varchar(64) COLLATE utf8_bin DEFAULT NULL,
  `Digest` varchar(64) COLLATE utf8_bin NOT NULL,
  `Digest_text` longtext COLLATE utf8_bin,
  `Type` enum('IGNORE INDEX','USE INDEX','FORCE INDEX','OPTIMIZER') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `Scope` enum('','FOR JOIN','FOR ORDER BY','FOR GROUP BY') CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '',
  `State` enum('N','Y') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Y',
  `Position` bigint NOT NULL,
  `Hint` text COLLATE utf8_bin NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 ROW_FORMAT=DYNAMIC COMMENT='Statement outline';

Digest/Digest_text

Outline根据语句的特征进行匹配,这一特征为 Digest Text。通过对 Digest Text 进行哈希计算,可以得到一个 64 字节的哈希字符串。在语句解析完成后,将使用 [schema + digest] 作为hash key,以查询匹配的 Outlines。

Type

Type即为Outline规则的类型。所有的Optimizer Hints的type统一为OPTIMIZER,Index Hints 分为三类,分别为IGNORE INDEX、USE INDEX和FORCE INDEX。

Scope

只针对Index Hints而言,Scope表示作用域。如果Scope参数为空,表示ALL。

Position

在Optimizer Hints中,position 表示 Query Block,因为所有的 Optimizer Hint 必须作用于 Query Block,且从 1 开始计数。而在 Index Hints 中,position 则表示表的位置,同样从 1 开始。

Hint

对于Index Hints而言,Hint可以表示索引名字的列表,比如 “ind_1, ind_2”;对于Optimizer Hints来讲,可以表示完整的 Hint 字符串,比如:“/*+ MAX_EXECUTION_TIME(1000) */”。


4 总结

生产环境中,客户的业务语句执行计划经常发生变化,导致性能下降。Statement outline功能是GaussDB(for MySQL)产品的运维工具的一员猛将,客户可以在不修改业务的情况下,解决业务语句不稳定的问题。


5 参考文献

[1] 官网Index Hints文档 :

https://dev.mysql.com/doc/refman/8.4/en/index-hints.html

[2] 官网Optimizer Hints文档:

https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。