过滤条件包含 OR 谓词,如何进行查询优化——OceanBase SQL 优化实践

这篇博客涉及两个点,一个是 “OR Expansion 改写”,另一个是 “基于代价的改写”。

背景

在写SQL查询时,难以避免在过滤条件中使用 OR 谓词,但其往往会导致索引利用效率下降的问题 。本文将分享如何通过查询改写的2种方式进行优化。

使用 Or 谓词的示例如下:

CREATE TABLE `t1` (
  `c1` varchar(255) NOT NULL,
  `c2` varchar(255) NOT NULL,
  PRIMARY KEY (`c1`),
  UNIQUE KEY (`c2`));

explain select  * from t1 where c1 = '1' or c2 = '3';
+------------------------------------------------------------------------------------+
| Query Plan                                                                         |
+------------------------------------------------------------------------------------+
| ===============================================                                    |
| |ID|OPERATOR       |NAME|EST.ROWS|EST.TIME(us)|                                    |
| -----------------------------------------------                                    |
| |0 |TABLE FULL SCAN|t1  |1       |3           |                                    |
| ===============================================                                    |
| Outputs & filters:                                                                 |
| -------------------------------------                                              |
|   0 - output([t1.c1], [t1.c2]), filter([t1.c1 = '1' OR t1.c2 = '3']), rowset=16    |
|       access([t1.c1], [t1.c2]), partitions(p0)                                     |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],  |
|       range_key([t1.c1]), range(MIN ; MAX)always true                              |
+------------------------------------------------------------------------------------+
11 rows in set (0.087 sec)

从上述计划中不难发现,当过滤条件为 where c1 = '1' or c2 = '3' 时,尽管 c1 列和 c2 列都分别建立了索引(或主键),但由于 OR 谓词的工作原理与 AND 谓词不同,导致在实际操作中无法通过索引来避免全表扫描的开销。

如果大家一时反应不过来为何无法利用索引,那这里就再多解释几句:因为在一个 table scan 最多只能选取一个索引进行使用,假如咱们选择使用建在 c1 上的主键索引,并完成了对 where c1 = '1' 的过滤,那么后面依然要在计算 where c2 = '3' 的时候进行一次全表扫描,如果选择使用建在 c2 上的索引也会出现类似的情况。所以无论怎么建索引,怎么用索引,这次全表扫总归是逃不掉的。

基础知识

OR 语句可以被改写成一条等价的带有 UNION ALL 的 SQL,例如上面的这条 SQL,就可以改写成:

explain 

SELECT *
FROM t1
WHERE t1.c1 = 1

UNION ALL

SELECT *
FROM t1
WHERE t1.c2 = 3 and lnnvl(t1.c1 = 1);

其中的 lnnvl 函数,大家可以简单的理解成是一个类似于逻辑非的操作。这个改写一般被称为 OR Expansion(OR 展开),改写的好处就是,把不能利用索引的 OR 操作,改成了等价的且可以在 union all 两侧分别利用两个索引的操作。

1731664179

优化思路

遇到这种希望利用 OR 两边的索引避免全表扫的场景时,OceanBase 支持通过 Hint /*+ use_concat */ 来强制优化器对原始 SQL 进行 OR Expansion 的改写。这样查询就会被拆分为两个部分,分别利用两个索引,在某些场景下可能可以提升查询性能

explain select /*+ use_concat */  * from t1 where c1 = '1' or c2 = '3';
+-----------------------------------------------------------------------------------------------+
| Query Plan                                                                                    |
+-----------------------------------------------------------------------------------------------+
| ====================================================                                          |
| |ID|OPERATOR          |NAME  |EST.ROWS|EST.TIME(us)|                                          |
| ----------------------------------------------------                                          |
| |0 |UNION ALL         |      |1       |7           |                                          |
| |1 |├─TABLE GET       |t1    |1       |5           |                                          |
| |2 |└─TABLE RANGE SCAN|t1(c2)|0       |3           |                                          |
| ====================================================                                          |
| Outputs & filters:                                                                            |
| -------------------------------------                                                         |
|   0 - output([UNION([1])], [UNION([2])]), filter(nil), rowset=16                              |
|   1 - output([t1.c1], [t1.c2]), filter(nil), rowset=16                                        |
|       access([t1.c1], [t1.c2]), partitions(p0)                                                |
|       is_index_back=false, is_global_index=false,                                             |
|       range_key([t1.c1]), range[1 ; 1],                                                       |
|       range_cond([t1.c1 = '1'])                                                               |
|   2 - output([t1.c1], [t1.c2]), filter([lnnvl(cast(t1.c1 = '1', TINYINT(-1, 0)))]), rowset=16 |
|       access([t1.c1], [t1.c2]), partitions(p0)                                                |
|       is_index_back=false, is_global_index=false, filter_before_indexback[false],             |
|       range_key([t1.c2], [t1.shadow_pk_0]), range(3,MIN ; 3,MAX),                             |
|       range_cond([t1.c2 = '3'])                                                               |
+-----------------------------------------------------------------------------------------------+
20 rows in set (0.008 sec)

在上面的计划中,可以看到,使用 hint 进行改写后,union all 左边是 table get,说明走了主键,右边是 table range scan,走了 c2 列上的索引。

What's more ?

上面在介绍优化思路时,提到了一句 “在某些场景下可能可以提升查询性能”,原因是优化器的改写算法分为两类:

  • 第一类叫基于规则的改写,这类改写总会有正面的效果,例如消除恒真、恒假条件。
  • 第二类叫基于代价的改写,特点是某些场景下改写后能生成更好的执行计划,另一些场景下则不能。是否可以触发这类改写,优化器会根据实际的数据分布、是否有合适的索引等因素来进行判断。基于代价的算法在完成改写后需要 “询问” 优化器:“改写后的 SQL 是否能够生成执行代价更小的计划?”,如果代价的确降低了,这次改写才会被触发。

咱们上面提到的 OR Expansion,就属于第二类 “基于代价的改写”,因为 SQL 被一分为二了,所以只有当 union all 两边都有索引,并且索引的过滤性不错的情况下,才能够达到优化的目的。

Q1: 
SELECT * FROM T1 WHERE C1 < 20000 OR C2 < 30 ;

=>

Q2: 
SELECT /*SEL_1*/ * FROM T1 WHERE C1 < 20000 UNION ALL
SELECT /*SEL_2*/ * FROM T1 WHERE C2 < 30 AND LNNVL (C1 < 20000);

针对上面这个 OR Expansion 的例子,简单解释下为什么有些改写需要基于代价才能触发:

  • 当 T1 表上在 C1 列和 C2 列上都有索引时:Q1 无法利用这两个索引,它只能走主表扫描。
  • Q2 中的 SEL_1 可以利用 C1 上的索引, SEL_2 可以利用 C2 上的索引。如果这两个过滤条件有强的过滤性,那么索引扫描可以大大减少读取数据的开销。此时,触发 OR Expansion 改写有利于生成更好的执行计划。
  • 当 T1 表上没有任何索引时:Q1 和 Q2 都需要进行全表扫描。但是 Q2 被分拆为两个 SELECT 子句,它需要进行两次全表扫描。因此,改写后的执行代价会上升。在这种情况下,不应该触发 OR Expansion。

综上所述:在没有及时收集统计信息时(即优化器不能准备计算代价时),可以根据实际情况(两边是否都有索引,以及索引过滤性),通过 Hint /*+ use_concat */ 进行 OR Expansion 改写,达到充分利用索引的目的。当然,也可以根据实际情况,通过 Hint /* +no_expand */ 来禁用 OR Expansion 改写。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/923514.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言函数递归经典题型——汉诺塔问题

一.汉诺塔问题介绍 Hanoi&#xff08;汉诺&#xff09;塔问题。古代有一个梵塔&#xff0c;塔内有3个座A、B、C&#xff0c;开始时&#xff21;座上有64个盘子&#xff0c;盘子大小不等&#xff0c;大的在下&#xff0c;小的在上。有一个老和尚想把这64个盘子从&#xff21;座移…

【Python】九大经典排序算法:从入门到精通的详解(冒泡排序、选择排序、插入排序、归并排序、快速排序、堆排序、计数排序、基数排序、桶排序)

文章目录 1. 冒泡排序&#xff08;Bubble Sort&#xff09;2. 选择排序&#xff08;Selection Sort&#xff09;3. 插入排序&#xff08;Insertion Sort&#xff09;4. 归并排序&#xff08;Merge Sort&#xff09;5. 快速排序&#xff08;Quick Sort&#xff09;6. 堆排序&…

lua除法bug

故事背景&#xff0c;新来了一个数值&#xff0c;要改公式。神奇的一幕出现了&#xff0c;公式算出一个非常大的数。排查是lua有一个除法bug,1除以大数得到一个非常大的数。 function div(a, b)return tonumber(string.format("%.2f", a/b)) end print(1/73003) pri…

STM32 USART串口发送+接收

单片机学习&#xff01; 目录 前言 一、串口发送配置步骤 二、详细步骤 2.1 RCC开启USART和GPIO时钟 2.2 GPIO初始化 2.3 配置USART 2.4 开启USART 2.5 总初始化代码 三、接收数据 3.1 查询方法 3.2 中断方法 3.2.1 中断配置 3.2.2 接收函数 总结 前言 上篇博文介…

网络安全事件管理

一、背景 信息化技术的迅速发展已经极大地改变了人们的生活&#xff0c;网络安全威胁也日益多元化和复杂化。传统的网络安全防护手段难以应对当前繁杂的网络安全问题&#xff0c;构建主动防御的安全整体解决方案将更有利于防范未知的网络安全威胁。 国内外的安全事件在不断增…

AIGC--AIGC与人机协作:新的创作模式

AIGC与人机协作&#xff1a;新的创作模式 引言 人工智能生成内容&#xff08;AIGC&#xff09;正在以惊人的速度渗透到创作的各个领域。从生成文本、音乐、到图像和视频&#xff0c;AIGC使得创作过程变得更加快捷和高效。然而&#xff0c;AIGC并非完全取代了人类的创作角色&am…

【数据结构实战篇】用C语言实现你的私有队列

&#x1f3dd;️专栏&#xff1a;【数据结构实战篇】 &#x1f305;主页&#xff1a;f狐o狸x 在前面的文章中我们用C语言实现了栈的数据结构&#xff0c;本期内容我们将实现队列的数据结构 一、队列的概念 队列&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端…

RHCSA作业

课后练习 将整个 /etc 目录下的文件全部打包并用 gzip 压缩成/back/etcback.tar.gz [rootlocalhost ~]# tar -czvf /back/etcback.tar.gz -C / etc 使当前用户永久生效的命令别名&#xff1a;写一个命令命为hello,实现的功能为每输入一次hello命令&#xff0c;就有hello&#…

java:拆箱和装箱,缓存池概念简单介绍

1.基本数据类型及其包装类&#xff1a; 举例子&#xff1a; Integer i 10; //装箱int n i; //拆箱 概念&#xff1a; 装箱就是自动将基本数据类型转换为包装器类型&#xff1b; 拆箱就是自动将包装器类型转换为基本数据类型&#xff1b; public class Main {public s…

如何选择最适合企业的ETL解决方案?

在今天的大数据时代&#xff0c;企业的数据管理和处理变得愈发重要。企业也越来越依赖于数据仓库和数据湖来提取、转换和加载&#xff08;ETL&#xff09;关键业务信息。一个高效、灵活的ETL解决方案不仅能提升数据处理能力&#xff0c;还能为企业决策提供有力支持。然而&#…

[网鼎杯 2020 朱雀组]phpweb 详细题解(反序列化绕过命令执行)

知识点: call_user_func() 函数 反序列化魔术方法 find命令查找flag 代码审计 打开题目,弹出上面的提示,是一个警告warning,而且页面每隔几秒就会刷新一次,根据warning中的信息以及信息中的时间一直在变,可以猜测是date()函数一直在被调用 查看源代码发现一些信息,但是作用…

数字图像处理(2):Verilog基础语法

&#xff08;1&#xff09;Verilog常见数据类型&#xff1a; reg型、wire型、integer型、parameter型 &#xff08;2&#xff09;Verilog 常见进制&#xff1a;二进制&#xff08;b或B&#xff09;、十进制&#xff08;d或D&#xff09;、八进制&#xff08;o或O&#xff09;、…

c++:面向对象三大特性--继承

面向对象三大特性--继承 一、继承的概念及定义&#xff08;一&#xff09;概念&#xff08;二&#xff09;继承格式1、继承方式2、格式写法3、派生类继承后访问方式的变化 &#xff08;三&#xff09;普通类继承&#xff08;四&#xff09;类模板继承 二、基类和派生类的转换&a…

数据结构 (12)串的存储实现

一、顺序存储结构 顺序存储结构是用一组连续的存储单元来存储串中的字符序列。这种存储方式类似于线性表的顺序存储结构&#xff0c;但串的存储对象仅限于字符。顺序存储结构又可以分为定长顺序存储和堆分配存储两种方式。 定长顺序存储&#xff1a; 使用静态数组存储&#xff…

在线绘制Nature Communication同款双色、四色火山图,突出感兴趣的基因

导读&#xff1a;火山图通常使用三种颜色分别表示显著上调&#xff0c;显著下调和不显著。通过为特定的数据点添加另一种颜色&#xff0c;可以创建双色或四色火山图&#xff0c;从而更直观地突出感兴趣的数据点。 《Nature Communication》文章“Molecular and functional land…

2024赣ctf-web -wp

1.你到底多想要flag??? 首先来解决第一关&#xff1a; 先了解一下stripos&#xff08;&#xff09;&#xff1b; 并且此函数处理数组返回false。而且pre_match同样遇见数组是返回false&#xff08;解释一下正则 i&#xff1a;这是正则表达式的修饰符&#xff0c;代表“不区…

计算机毕业设计Python+大模型美食推荐系统 美食可视化 美食数据分析大屏 美食爬虫 美团爬虫 机器学习 大数据毕业设计 Django Vue.js

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

Linux 查看内核日志的方法

文章目录 1. dmesg 命令一. 介绍内核环形缓冲区的特点 二. 主要功能三. dmesg 使用 2. 查看kmsg文件/dev/kmsg 的用途使用 /dev/kmsg与 dmesg 的关系 3. 内核日志消息的打印行为 1. dmesg 命令 一. 介绍 dmesg&#xff08;display message 或 display driver message 的缩写&…

Perforce SAST专家详解:自动驾驶汽车的安全与技术挑战,Klocwork、Helix QAC等静态代码分析成必备合规性工具

自动驾驶汽车安全吗&#xff1f;现代汽车的软件包含1亿多行代码&#xff0c;支持许多不同的功能&#xff0c;如巡航控制、速度辅助和泊车摄像头。而且&#xff0c;这些嵌入式系统中的代码只会越来越复杂。 随着未来汽车的互联程度越来越高&#xff0c;这一趋势还将继续。汽车越…

从Full-Text Search全文检索到RAG检索增强

从Full-Text Search全文检索到RAG检索增强 时光飞逝&#xff0c;转眼间六年过去了&#xff0c;六年前铁蛋优化单表千万级数据查询性能的场景依然历历在目&#xff0c;铁蛋也从最开始做CRUD转行去了大数据平台开发&#xff0c;混迹包装开源的业务&#xff0c;机缘巧合下做了实时…