当前位置: 首页 > news >正文

如何处理 collation 导致的索引失效 | OceanBase SQL调优实践

OceanBase 的社区论坛中常有关于性能调优方面的问题。为了方便更多用户能掌握相关的调优技能,将论坛中遇到的相关问题记录下来并进行总结和分享。期待我们能够一起学习,共同成长!

基础背景知识

在聊这个调优问题之前,需要先简单介绍几个和数据库相关的基础知识,就是 charset(字符集)和 collation(字符排序规则或者叫比较规则)的概念,以及 leading 和 use_nl 这两种常见 hint。已经了解的同学可以直接跳过这段内容。

charset

简单来说,charset 会定义字符如何被编码和存储。例如

  • 当 charset 为 utf-8 时,大写字母 "A" 会被编码为 1 个字节:0100 0001(也就是十六进制的 0x41);
  • 当 charset 为 utf-16 时,大写字母 "A" 被编码为 2 个字节:0000 0100 0000 0001(也就是十六进制 0x0041)。

不同的字符集支持存储的字符种类和范围也有所不同,例如 utf-8 字符集可以存储 Unicode 字符,而 latin1 字符集只能存储西欧语言字符。

collation

collation 是 charset 的一个属性,用来定义字符的比较和排序方式。例如当 charset 被设置为 utf8mb4 时,可选的 collation 就会有 utf8mb4_general_ci、utf8mb4_bin、utf8mb4_unicode_ci 等等。

  • utf8mb4_general_ci:不区分大小写的通用排序规则(ci 是 case insensitive 的意思,即不区分大小写);
  • utf8mb4_bin:基于二进制比较,区分大小写的排序规则;
  • utf8mb4_unicode_ci:基于 Unicode 的,不区分大小写的排序规则。
  • utf8 下还有很多针对各种不同语言的排序规则,例如根据汉语拼音排序的 utf8mb4_zh_pinyin_ci 等等。

charset 和 collation 是一对多的关系,一种 collation 属于且仅属于一种 charset。例如定义一个列为 c3 varchar(200) COLLATE utf8mb4_bin 时,这个列的 charset 就会自动被设置为 utf8mb4。

常用 Hint

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

这里只简单介绍两个最常用的 Hint,一个叫 leading,一个叫 USE_NL:

  • LEADING Hint 可以指定表的联接顺序。语法是:/*+ LEADING(table_name_list)*/。在 table_name_list 中可以使用 () 表示内部各表的联接优先级,指定复杂的联接顺序,比 ordered 有更大的灵活性。示例如下:
-- 注意:为确保按照用户指定的顺序进行联接,LEADING Hint 的检查比较严格:
-- 如果发现 Hint 指定的 table_name 不存在,LEADING Hint 不会生效。
-- 如果发现 Hint 中存在重复表,LEADING Hint 也不会生效。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   |                                                            |
| =========================================                                                            |
+------------------------------------------------------------------------------------------------------+

  • 在 USE_NL 中指定的表如果是 Join 的右表,在 Join 的时候会使用 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

问题描述

铺垫了这么久,终于可以描述下这个前两天遇到的比较有意思的问题了!下面为方便大家理解,会对问题进行一些简化(原问题链接在这里)。问题大致是说有两张表 t1 和 t2:

  • t1 表的 c3 列是 varchar,charset 是 utf8mb4,collation 是 utf8mb4_bin;
  • t2 表的 c3 列也是 varchar, charset 也是 utf8mb4,但 collation 是 charset 默认的 collation utf8mb4_general_ci。
create table t1(c1 int primary key,c2 int unique key,c3 varchar(200) COLLATE utf8mb4_bin) DEFAULT CHARSET = utf8mb4;create table t2(c1 int primary key,c2 int,c3 varchar(200),key idx(c3)) DEFAULT CHARSET = utf8mb4;

然后需要对两张表的 c3 列进行 join,但 join 之后发现没能利用上 t2 表的索引 idx。加/*+leading(t1 t2) use_nl(t2)*/ 这个 hint 是为了还原用户描述的计划,含义是强制让这个 join 是 nest loop join,并且连接顺序是 t1 join t2(即 t2 是 join 的右表)。

explain
select /*+leading(t1 t2) use_nl(t2)*/ * from t1 join t2 on t1.c3 = t2.c3where t1.c2 = 123;
+--------------------------------------------------------------------------------------------+
| Query Plan                                                                                 |
+--------------------------------------------------------------------------------------------+
| =====================================================                                      |
| |ID|OPERATOR           |NAME  |EST.ROWS|EST.TIME(us)|                                      |
| -----------------------------------------------------                                      |
| |0 |NESTED-LOOP JOIN   |      |1       |11          |                                      |
| |1 |├─TABLE RANGE SCAN |t1(c2)|1       |7           |                                      |
| |2 |└─MATERIAL         |      |1       |5           |                                      |
| |3 |  └─TABLE FULL SCAN|t2    |1       |4           |                                      |
| =====================================================                                      |
| Outputs & filters:                                                                         |
| -------------------------------------                                                      |
|   0 - output([t1.c1], [t1.c2], [t1.c3], [t2.c1], [t2.c2], [t2.c3]), filter(nil), rowset=16 |
|       conds([t1.c3 = cast(t2.c3, VARCHAR(1048576))]), nl_params_(nil), use_batch=false     |
|   1 - output([t1.c1], [t1.c3], [t1.c2]), filter(nil), rowset=16                            |
|       access([t1.c1], [t1.c3], [t1.c2]), partitions(p0)                                    |
|       is_index_back=true, is_global_index=false,                                           |
|       range_key([t1.c2], [t1.shadow_pk_0]), range(123,MIN ; 123,MAX),                      |
|       range_cond([t1.c2 = 123])                                                            |
|   2 - output([t2.c1], [t2.c2], [t2.c3]), filter(nil), rowset=16                            |
|   3 - output([t2.c1], [t2.c3], [t2.c2]), filter(nil), rowset=16                            |
|       access([t2.c1], [t2.c3], [t2.c2]), partitions(p0)                                    |
|       is_index_back=false, is_global_index=false,                                          |
|       range_key([t2.c1]), range(MIN ; MAX)always true                                      |
+--------------------------------------------------------------------------------------------+

大家可以看到,在执行 nest loop join 的过程中,左表 t1 每拿出一行数据后,t2 表无法利用 idx 这个索引快速定位这一行数据,而是需要进行全表扫(TABLE FULL SCAN),导致 SQL 执行效率很低。

问题分析

两个表的 c3 列都是 varchar 类型,charset 也相同,那为啥走不上索引?

这个问题猛一看确实有点儿奇怪。但仔细想想也能解释的通,就是因为这个 collation 搞的鬼!例如向两张表里各插入相同的四行数据 A、a、B、b,那么由于 collation 不同,就会导致排序结果不同:

-- t1 表认为:A < B < a < b
obclient [test]> select c3 from t1 order by c3;
+------+
| c3   |
+------+
| A    |
| B    |
| a    |
| b    |
+------+
4 rows in set (0.002 sec)-- t2 表认为:A <= a < B <= b
obclient [test]> select c3 from t2 order by c3;
+------+
| c3   |
+------+
| A    |
| a    |
| B    |
| b    |
+------+
4 rows in set (0.001 sec)

那么 t2 表上的索引 idx 上的数据就会是 A、a、B、b 这个顺序。如果拿着 t1 表的 B 值去 t2 表的索引 idx 上进行探测,就会先和 idx 上的 A 值比较,发现 B > A,那么就需要继续比较 idx 上的下一个值 a,按照 t1 表 B 值的 collation,就会发现 B < a,那就没必要继续探测了,直接返回 t2 表中没有 B 值。

1729240959

这样 join 之后的结果显然是错误的!所以数据库不会利用 t2 表索引的有序性快速定位 t1 表中的数据,只能对 t2 进行全表扫。

问题解决

方法一

SQL 调优专家絮语老哥给了个解决方案:把 join 条件里的 t1.c3 改成 convert(t1.c3 using utf8mb4)。

这样就会在进行 join 之前,把从 t1 过滤出来的值的 collation 转成 utf8mb4 默认的 collation utf8mb4_general_ci,统一了 join 双方的排序规则,这样就可以利用上索引了。

explainselect /*+leading(t1 t2) use_nl(t2)*/ * from t1 join t2 on convert(t1.c3 using utf8mb4) = t2.c3where t1.c2 = 123;
+--------------------------------------------------------------------------------------------+
| Query Plan                                                                                 |
+--------------------------------------------------------------------------------------------+
| =================================================================                          |
| |ID|OPERATOR                      |NAME   |EST.ROWS|EST.TIME(us)|                          |
| -----------------------------------------------------------------                          |
| |0 |NESTED-LOOP JOIN              |       |4       |36          |                          |
| |1 |├─TABLE RANGE SCAN            |t1(c2) |1       |7           |                          |
| |2 |└─DISTRIBUTED TABLE RANGE SCAN|t2(idx)|4       |29          |                          |
| =================================================================                          |
| Outputs & filters:                                                                         |
| -------------------------------------                                                      |
|   0 - output([t1.c1], [t1.c2], [t1.c3], [t2.c1], [t2.c2], [t2.c3]), filter(nil), rowset=16 |
|       conds(nil), nl_params_([t1.c3(:0)]), use_batch=true                                  |
|   1 - output([t1.c1], [t1.c3], [t1.c2]), filter(nil), rowset=16                            |
|       access([t1.c1], [t1.c3], [t1.c2]), partitions(p0)                                    |
|       is_index_back=true, is_global_index=false,                                           |
|       range_key([t1.c2], [t1.shadow_pk_0]), range(123,MIN ; 123,MAX),                      |
|       range_cond([t1.c2 = 123])                                                            |
|   2 - output([t2.c1], [t2.c3], [t2.c2]), filter(nil), rowset=16                            |
|       access([GROUP_ID], [t2.c1], [t2.c3], [t2.c2]), partitions(p0)                        |
|       is_index_back=true, is_global_index=false,                                           |
|       range_key([t2.c3], [t2.c1]), range(MIN ; MAX),                                       |
|       range_cond([convert(cast(:0, VARCHAR(1048576)), 'utf8mb4') = t2.c3])                 |
+--------------------------------------------------------------------------------------------+

计划里也可以看到,之前 t2 表的 table full scan 变成了 table range scan,终于用上了 idx 这个索引!用户的问题也得到了解决~

方法二

上面这种方法一并不是通用的方法。OceanBase 的 convert 只能转 charset,然后附带把 collation 转成对应 charset 默认的 collation。碰巧 utf8mb4 的默认 collation 是 general_ci,直接把 collation 给转成想要的了。万一想转的 collation 不是对应 charset 默认的 collation,用方法一就不成了!

最好的方法是把 join 条件中的 t1.c3 改成 t1.c3 collate utf8mb4_general_ci,这样就可以随心所欲地转换成你真正想要的 collation 了。

explainselect /*+leading(t1 t2) use_nl(t2)*/ * from t1 join t2 on t1.c3 collate utf8mb4_general_ci = t2.c3where t1.c2 = 123
+--------------------------------------------------------------------------------------------+
| Query Plan                                                                                 |
+--------------------------------------------------------------------------------------------+
| =================================================================                          |
| |ID|OPERATOR                      |NAME   |EST.ROWS|EST.TIME(us)|                          |
| -----------------------------------------------------------------                          |
| |0 |NESTED-LOOP JOIN              |       |4       |36          |                          |
| |1 |├─TABLE RANGE SCAN            |t1(c2) |1       |7           |                          |
| |2 |└─DISTRIBUTED TABLE RANGE SCAN|t2(idx)|4       |29          |                          |
| =================================================================                          |
| Outputs & filters:                                                                         |
| -------------------------------------                                                      |
|   0 - output([t1.c1], [t1.c2], [t1.c3], [t2.c1], [t2.c2], [t2.c3]), filter(nil), rowset=16 |
|       conds(nil), nl_params_([t1.c3(:0)]), use_batch=true                                  |
|   1 - output([t1.c1], [t1.c3], [t1.c2]), filter(nil), rowset=16                            |
|       access([t1.c1], [t1.c3], [t1.c2]), partitions(p0)                                    |
|       is_index_back=true, is_global_index=false,                                           |
|       range_key([t1.c2], [t1.shadow_pk_0]), range(123,MIN ; 123,MAX),                      |
|       range_cond([t1.c2 = 123])                                                            |
|   2 - output([t2.c1], [t2.c3], [t2.c2]), filter(nil), rowset=16                            |
|       access([GROUP_ID], [t2.c1], [t2.c3], [t2.c2]), partitions(p0)                        |
|       is_index_back=true, is_global_index=false,                                           |
|       range_key([t2.c3], [t2.c1]), range(MIN ; MAX),                                       |
|       range_cond([convert(cast(:0, VARCHAR(1048576)), 'utf8mb4') = t2.c3])                 |
+--------------------------------------------------------------------------------------------+

What's more?

推荐使用论坛小助手

在OceanBase社区中,可以直接 @论坛小助手 机器人问问题,对于本文的问题,目测效果还不错,提出了好几种可行的方案(还包括了絮语老哥的那种)。与义说其实直接在问答帖下艾特他就可以,不用多余问他什么,他会自己去爬楼看问题。

1729241043

推荐阅读

《DBA 入门教程 —— 诊断和调优 —— 阅读和管理 OceanBase 数据库 SQL 执行计划》

相关文章:

  • 信奥赛-刷题笔记-栈篇-T3-P4387验证栈序列0520
  • 13 分钟讲解所有知名 Python 库/模块
  • Linux探秘:驾驭开源,解锁高效能——基础指令
  • 数据仓库是什么?常见问题解答
  • 彭博社聚焦Coinbase数据泄露,CertiK联创顾荣辉警示私钥风险与物理攻击
  • Java从入门到精通 - 案例专题
  • 【RK3588嵌入式图形编程】-Cairo-形状与填充
  • 瑞萨单片机笔记
  • JS 中 var、let、const 的区别联系
  • 奥威BI:打破AI数据分析伪场景,赋能企业真实决策价值
  • CesiumEarth v1.15 更新
  • 虚拟主机:经济便捷的 “云端小屋”
  • MySQL 8.0窗口函数详解
  • Java 09Stream流与File类
  • C++显式声明explicit
  • Android trace presentFence屏幕显示的帧
  • 挖o心得(2)
  • 深入解析Node.js文件系统(fs模块):从基础到进阶实践
  • [创业之路-370]:企业战略管理案例分析-10-战略制定-差距分析的案例之小米
  • 关闭 Ubuntu 20.04 的 GNOME Shell和PulseAudio
  • 广东“龙舟水”开启:降水少于去年,但仍比常年多两成
  • 国家话剧院发讣告悼念朱媛媛:始终秉持“戏比天大”的信念
  • 破局之路,阳光保险何以向“新”而行
  • 总数再更新!我国新增三项全球重要农业文化遗产
  • 最高法:政府信息公开案件审理应避免泄露国家秘密、商业秘密
  • 昆明一小区火灾调查报告:七旬夫妇身亡,生前拾荒存放大量可燃物