开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十二):谈谈in常量查询的设计与优化

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核8GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十二):谈谈in常量查询的设计与优化

作者:越寒


一、介绍


如标题所示,这是一篇介绍in常量查询的源码解读文章,但又不限于in常量查询,因为其中涉及的很多设计与优化对于大多数查询都是普适的。一如往常一样,我们首先会过一遍整体的执行流程,梳理一个大致的框架。紧接着,同时也是更重要的,我们会通过一系列在真实场景中遇到的问题(说白了就是性能优化),来对各种细节处理进行增强。


温馨提醒:建议有条件有兴趣的同学可以对照着本篇文章边调试(我基本上把重要的断点位置都截了图)边学习边思考,这样印象和理解应该会更加深刻。


希望大家在读完之后,可以尝试着回答以下一些问题来进行某种测验:  


 什么是分片裁剪?为什么要进行分片裁剪?

 为什么要对物理SQL中值进行裁剪?

 什么是plan cache?为什么需要?

  为什么需要post planner ?

 XPlan是什么?为什么Xplan比物理SQL更优?

 为什么要有一个ToDrdsRelVisitor?

 什么是全局二级索引? 如何利用?

 其他散落于文章中或者阅读时的问题。


二、从大致的流程说起


:详细的执行流程请参考文章,https://zhuanlan.zhihu.com/p/457450880。我们这里只介绍其中几个比较重要的环节。我们拿一个非常简单的场景来看一下吧,一个简单的表如下,create table t(c1 int, c2 int, c3 int) dbpartition by hash(c1) tbpartition by hash(c1) tbpartitions 2,一条最简单的SQL如下:select c3 from t where c1 in (1,2)。挑了五个阶段进行了并不太详尽的说明,如果你感觉比较抽象时,也可以动手调试一下,一些概念应该就会更加清晰了。


1.阶段一


我们需要将SQL文本解析为语法树,如果不合法,则报错,关键断点如下图,其中sql为输入的查询语句,statement为经过解析后的语法树。  


         


         


需要注意的是,在这个地方,我们是只进行语法解析,而不进行语义解析。什么意思呢,比如你现在输入的SQL为select c1 from tt,此时虽然我们没有tt这张表,但是断点处还是会正常解析出一个SQLSelectStatement,有兴趣的同学可以打个断点试一下。


2.阶段二


如上分析,我们现在要进行语义的校验了,比如我怎么知道这张表存不存在,以及是否含有这个列呢?


              



             



             


3.阶段三


构建执行计划,在toRel时将由SqlNode构成的AST转换为由RelNode组成的逻辑计划。


             



             


埋一个坑把,有兴趣的同学可以结合代码思考一下,既然我们已经拿到了逻辑执行计划,那么ToDrdsRelVisitor的作用是什么呢?


4.阶段四


对执行计划进行优化,以期获得较为优异的执行效果。  


               


5.阶段五


拿到执行计划之后,紧接着我们来看一下是在哪里执行的,以及是如何执行的。


               



               


我们可以简单看一下这个plan,这是一个非常简单的plan,最上层是一个Gather用来聚合下层多个logicalView的结果,而logicalView中包含了如何与存储节点进行交互的信息。根据plan拿到相应的handler,然后进行调用就可以了。


               



               


在这个场景中,我们会递归调用logicalView的handler。


OK,以上就是一个大概的执行流程,接下来我们来真正深入到一些细节看一下,我们如何将这个大致的流程进行丰富以使其能够满足工业生产的需求。


6.现实中的使用场景


In查询列表中的值不固定,个数亦不固定。


7.优化思路


 单条SQL的优化,比如分片裁剪,物理SQL中in值的裁剪,使用XPlan代替物理SQL。

 大量执行相似的SQL时,避免重复性且不必要的工作,如避免每次重新生成plan。

 对其中一些特殊场景进行更加极致的优化,比如单分片直接下推。

 通过添加索引进行优化,在这里我们主要讨论全局二级索引。  


8.具体的优化


1)单条SQL的优化


分片裁剪:只访问必须访问的分片


Q:select * from t where c1 in (1,2) 会向所有分片下发物理SQL么?


A:不会的。通过上面的分析,我们下发的物理SQL为select * from t_physical_table where c1 in (1,2),t_physical_table为逻辑表t所对应的物理表。而由于表t的分库键和分表键均为c1,因此显然我们只需要向两张可能存在匹配记录的物理表下发物理SQL即可,获取裁剪后的分表信息如下图。  


           

                                                                            image


分片裁剪是一定需要调用分片计算,分片计算的逻辑在这里。  


             

                                                                              image


物理SQL中in值的裁剪:只留下有用的in值


Q:下发的物理SQL中,是否会对in的列表进行裁剪呢?


A:会的,而且对下发的物理SQL中的in列表中的值进行裁剪,主要有两个好处,一是尽可能避免下发的物理SQL导致不必要的全表扫描,二是减少下发物理SQL的长度。


             

                                                                                     image


             

                                                                                 image  


上图中PruneRaw即代表裁剪后的in查询列表。


2)使用XPlan代替物理SQL:避免DN节点进行物理SQL的解析优化


注:详情可参考链接文章中的执行计划传输部分,https://zhuanlan.zhihu.com/p/308173106#:~:text=PolarDB:in查询其实暂时是不支持传输执行计划的。 但我觉得可能没什么特别特殊的地方,像传输其他的plan一样,我们需要在计算层指定数据的访问方式(即指定索引),然后进行适配和对接。


3)避免每次重新构建plan


避免参数值不同而反复构建plan


Q:每次都进行plan的构建,看起来并不是非常有必要,比如select * from t where id in (1,2) 和select * from t where id in (2,3)。

A:是的,所以我们对plan进行了缓存,这就是PlanCache组件,可以理解为Map。很自然的,我们需要对上述两条SQL进行参数化以便从map中进行查找,即参数化为select * from t where id in (?,?)的形式,代码在


           

                                                                                image


           

                                                                               image


避免参数个数不同而反复构建plan


Q:细心的同学可能感觉有点奇怪,上面的select c1,c3 from t where c1 in (1,2) 参数化后为 select c1,c3 from t where c1 in (?),而非select c1,c3 from t where c1 in (?,?),这是为什么?  


A:这样做是为了避免plan cache的膨胀,因为这样参数化之后,select c1,c3 from t where c1 in (1,2) 和select c1,c3 from t where c1 in (1,2,3,4)就是共用一个plan cache了;此外,这样还可以减少参数化SQL占用的内存,想象一下,有些SQL中in列表中的值多达几十万个呢。


4)单分片场景优化


Q:对于某些场景,是否有更近一步的优化,毕竟TP是需要尽可能的高性能的。


A:有的,比如单分片的场景,in列表中的值会落在同一个物理分表上。 我们可以思考下此时下面的执行计划是否可以简化?


             


答案是显然的,在单分片场景下,上层的Gather是完全不需要的,否则我们在执行时会有额外的执行开销。


引申:我们可以再结合前面的参数化与plan cache来理解这个问题,即参数不同的SQL的最优执行计划其实并非总是相同的,但我们为了避免每次重复生成plan,又会缓存一个plan,于是我们需要一个能够对plan进行优化的能力。


我们大概可以把这种情况分成两种,一种是参数不同导致选择的join算法不同,比如是选择bka join还是hash join,为了解决这个问题,我们引入了执行计划管理模块(SPM);另一种则跟我们的架构有非常大的关系,因为我们下层的DN(可以简单理解为mysql)显然是具备执行各种SQL的能力的,而如果在某些参数下,经过裁剪后只剩下一个分片了,则该SQL经过物理表名的替换后可直接下发到DN执行,计算层只需要等待结果返回即可,无需做任何其他的操作。


为了实现第二种效果,我们在planner阶段增加了一个阶段,叫做post planner,在post planner中会判断是否能够下推到某个分片,默认为打开,上图中为了演示需要,特意使用hint进行了关闭。


           

                                                                               image


               

                                                                                  image


               

                                                                                  image


5)添加全局二级索引


:索引,本质是一种修改与查询的权衡,需要用户谨慎考虑,尤其写入全局索引会带来较大的分布式事务开销。


Q:分片建已经确定了,in查询的字段没有跟分片对齐,是不是无法做分片裁剪了,还能优化么?  


A:可以考虑增加全局二级索引。我们来举个例子吧,比如table: t3(c1 int, c2 int, c3 int) dbpartition by hash(c1); SQL为select c3 from t3 where c2 in (1,2),由执行计划可知我们无法进行分片裁剪,因此需要访问所有8个分片,如下:


                                                                                                 

                                                                                  image


现在让我们来考虑一下如何优化?


我们的目的是希望减少访问的分片数,而之所以无法进行分片的裁剪,是因为in查询的字段和分片键没有对齐。于是解决方案也很简单,我们增加一个拆分键与in查询字段对齐的全局的二级索引即可,有关全局二级索引的介绍,可参考链接,https://help.aliyun.com/document_detail/182179.html。  


比如,我们执行如下添加全局二级索引的SQL,alter table t3 add global index g_c2(c2) covering(c1, c3) dbpartition by hash(c2),然后我们再来看下此时的执行计划,发现此时已经如我们所料进行了基于全局二级索引的分片裁剪,现在只需要扫描两个分片即可。


               

                                                                                  image


三、一个小练习


In列表中包含大量重复值时,可以如何优化?(我们现在的版本没有考虑这种情况)比如,有一个很简单的做法,在参数化时加一个去重,如下图。  


             

                                                                                  image


然后大家可以思考一下,需要注意什么,以及有什么问题么?


One More:横向对比与思考


大家有兴趣,有时间的,可以对比其他友商数据库进行比较与分析。


四、总结 


其实我在这篇文章里面,抛了挺多问题,有些给了一种便于叙述却未必全面的答案,有些则完全没有回答。最后的这个总结我觉得也留给大家来写了。

相关实践学习
体验PolarDB-X透明分布式
PolarDB-X具备从单机到分布式的平滑演进能力,支持通过DDL将一张大表动态调整为分布式的分区表,结合分布式事务、以及兼容MySQL Binlog的数据回流,可完成单机到分布式的快速改造。本体验从PolarDB-X的Auto Partition,以及兼容MySQL Binlog的CDC能力两个方面演示透明分布式能力。
相关文章
|
2月前
|
关系型数据库 分布式数据库 数据库
|
2月前
|
存储 关系型数据库 分布式数据库
喜报|阿里云PolarDB数据库(分布式版)荣获国内首台(套)产品奖项
阿里云PolarDB数据库管理软件(分布式版)荣获「2024年度国内首版次软件」称号,并跻身《2024年度浙江省首台(套)推广应用典型案例》。
|
5月前
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
171 5
|
6月前
|
Cloud Native 关系型数据库 分布式数据库
登顶TPC-C|云原生数据库PolarDB技术揭秘:Limitless集群和分布式扩展篇
阿里云PolarDB云原生数据库在TPC-C基准测试中以20.55亿tpmC的成绩刷新世界纪录,展现卓越性能与性价比。其轻量版满足国产化需求,兼具高性能与低成本,适用于多种场景,推动数据库技术革新与发展。
|
3月前
|
存储 监控 关系型数据库
突破IO瓶颈:PolarDB分布式并行查询(Parallel Query)深度调优手册
在海量数据处理中,I/O瓶颈严重制约数据库性能。本文基于PolarDB MySQL 8.0.32版本,深入解析分布式并行查询技术如何提升CPU利用率至86.7%、IO吞吐达8.5GB/s,并结合20+实战案例,系统讲解并行架构、执行计划优化、资源调优与故障排查方法,助力实现高性能数据分析。
103 6
|
NoSQL 安全 调度
【📕分布式锁通关指南 10】源码剖析redisson之MultiLock的实现
Redisson 的 MultiLock 是一种分布式锁实现,支持对多个独立的 RLock 同时加锁或解锁。它通过“整锁整放”机制确保所有锁要么全部加锁成功,要么完全回滚,避免状态不一致。适用于跨多个 Redis 实例或节点的场景,如分布式任务调度。其核心逻辑基于遍历加锁列表,失败时自动释放已获取的锁,保证原子性。解锁时亦逐一操作,降低死锁风险。MultiLock 不依赖 Lua 脚本,而是封装多锁协调,满足高一致性需求的业务场景。
125 0
【📕分布式锁通关指南 10】源码剖析redisson之MultiLock的实现
|
5月前
|
Cloud Native 关系型数据库 分布式数据库
登顶TPC-C|云原生数据库PolarDB技术揭秘:Limitless集群和分布式扩展篇
云原生数据库PolarDB技术揭秘:Limitless集群和分布式扩展篇
|
6月前
|
安全
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
326 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
6月前
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
226 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
|
5月前
|
存储 安全 NoSQL
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
154 0
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现

相关产品

  • 云原生数据库 PolarDB