SQL Server 优化器内幕【上篇】

本文涉及的产品
云数据库 RDS SQL Server,基础系列 2核4GB
RDS SQL Server Serverless,2-4RCU 50GB 3个月
推荐场景:
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 我记得我还在上一家公司的时候,有一次和主管一起做1:1,主管问我,将来你的技术方向是什么,我说我想往HA方向发展,因为是我的强项。主管问我还有别的吗?我犹豫地说,我也想做优化器方向,但是智商不够。主管大笑,说如果有兴趣可以钻研看看。

作者:马弓手三菜

我记得我还在上一家公司的时候,有一次和主管一起做1:1,主管问我,将来你的技术方向是什么,我说我想往HA方向发展,因为是我的强项。主管问我还有别的吗?我犹豫地说,我也想做优化器方向,但是智商不够。主管大笑,说如果有兴趣可以钻研看看。

现在想想,优化器确实比HA好玩多了。HA只是数据库的一个理念,实现这个理念的一些手段方式而已。而优化器则是算法实现,是关系型数据库的整体实现方式。借此来了解图灵奖得主James Gray的遗作,就算学不会,也可以吹吹这位传奇人物的神秘去世故事

上篇

什么是执行计划? 怎么生成执行计划?
数据访问的基本操作:Scan, Seek, bookmark lookup
表连接的三种基本操作: Nested Loop join, Merge Join, Hash Join

后续计划中的坑

子查询的种种
并发线程和我们的hybrid 并发
隔离级别 Isolation的实现
其他杂七杂八的待定

先挖好坑,逐步来填。

1. 什么是执行计划? 怎么生成执行计划?

1.1 执行计划的基本单位 Operator

在说执行计划之前,我们要知道执行计划的基本单位。 SQL Server中,一个执行计划的基本单位是iterator(迭代器)或者叫 operator,iterator主要做一件事,就是从input,可能是一个表,可能是一个计算结果集,读入数据,然后进行一定操作之后,返回结果集给他的父节点。

在SQL Server的图形执行计划中,每一个小图标,就是一个operator
image.png

1.2 怎么生成执行计划

SQL Server有两个执行计划,一个是Estimated EP, 一个是 Actual EP。

所谓Estimated EP,就是指,在真正执行SQL语句之前,解析一次语句,看看可能生成的执行计划时什么

而Actual EP则是一条SQL语句执行时真正采用的执行计划。 在SQL Server和 Oracle中,两者往往因为种种原因(比如统计信息不准)会和estimated EP不一样,也是商业数据库经常优化的一个点。

Actual EP 相比于 Estimated EP,每个operator上会多一些统计结果,包括,这个operator实际执行了几次,返回了多少行?这和estimated的是否一致,是我们调优的一个重要点,statistics是否准确。

下图左边是AEP,右边是EEP。
image.png

那么为什么需要Estimated EP呢 ,有些大语句跑一次要一个小时甚至更多,没法获得Actual EP,这个时候我们就需要 Estimated EP.

生成Estimated EP的方法非常简单,SQL Server中,点击这个按钮“Display Estimated Execution Plan”,或者使用showplan语句。

image.png

1.3 operator的三属性

所以这下我们知道了,如果要想读懂一个执行计划,最关键的是读懂operator。这也是为什么我一开始说,mysql的执行计划看上去简单,容易理解,实际上,没法深入阅读。对于学习优化器,是一个天然壁垒。

1.3.1 内存

每一个operator,都需要使用一定的内存来完成执行动作。所以通常情况,数据库引擎会预留一些内存给他们。但有些操作,需要消耗大量内存, 所以会出现使用tempdb(sqlserver) 来弥补内存的不足。

1.3.2 阻塞式和交互式

有些operator必须执行完成,才能返回结果,这种称之为阻塞式,比如排序,hash操作,和一些组合函数,max,min,count等,都是在执行完成才返回结果。

有些operator,可以一边执行,一边返回结果。一个非常好的例子就是top(sql server) 和 limit(mysql), 一个亿级别的表,也可以很快取到top 5 行。 当然,如果之后有sort条件,就另当别论了。

1.3.3 动态游标(cursor)

并不是所有数据结果都是可以顺序/倒序读取的,一个好的例子就是索引检索,索引扫描的本质就是一个可以正过来倒过去的游标。所以合适的索引非常重要,如果是一个堆(heap),operator就没法支持cursor。

MySQL中默认会解决掉heap结构,这点和SQL server很像,和Oracle不一样。Oracle的索引组织表并不那么常用,且叶节点也和我们的clustered index有些许不同。

好了,以上就是基础中的基础。不论你是初学者还是高玩,相信都能从上述总结对比中,有所收获。

2. 数据访问的基本操作:Scan, Seek, bookmark lookup

2.1 几个基本概念

Predicate,谓词。 一个SQL语句中,where条件之后的列,通常我们叫过滤条件,它的学名叫做谓词。

output list, 返回列。一个SQL语句中, select 之后 from之前的那些列,真正需要展示给客户的列。

select id,name from table_a where id =3;

这个例子中, id就是谓词, 同时output list是 id 和 name 两个列。

2.2 Scan 和 Seek

一条简单的语句

select * from test where id=2

想要找到id为2的所有记录,这就像在字典中翻一个字,你有两种翻法:

第一种,逐页翻,从第一页翻到最后一页,这个就叫SCAN
image.png

第二种到目录检索(有个有序指引)里快速查到,这个就叫SEEK。在树形结构中,SEEK的IO接近于2.
image.png

当然,就像你翻字典一样,如果你知道,你在翻得这个字是唯一的,当你翻到第一个的时候,你就不会继续往后翻了。数据库在操作unique key的时候,也是这样,所以常说,哲学指导世界。

在商业数据库里,堆表(heap)和集群索引(clustered index)构成了两种数据组织结构,前者无序,所以只能做scan(table scan)操作,后者有序,可以做scan(clustred index scan)和seek(clustered index seek)操作。如果你单独创建了某(几)个列的索引,它也可以做(non-clustered)index scan和(non-clustered)index seek

MySQL默认都会有clustered index,SQL Server也十分强调创建clustered index,Oracle则不同。所以我们SQL Server所有的调优都基于clustered index这个优秀的前提。

2.3 Bookmark Lookup

如果你只接触过mysql, 这可能是你第一次听说这个词,在Oracle和SQL Server中,它有一个简单的中文名: 回表。 当然,这个名字并不准确。

考虑下如下这两个简单查询,执行计划会有什么不同:

select lastname from test where lastname='Simth'
select id,lastname,firstname,other from test where lastname='Simth'

第一个查询就是大家熟悉的覆盖索引,第二个查询就是大家口中的回表。
image.png

第一个非常简单,走了索引 idx_l,predicate(谓词)是 lastname, output list(输出列) 也是 lastname,当output list完全从索引中就能取出,俗称覆盖索引查询。

第二个就不一样了, 先走了索引 idx_l, predicate 是 lastname, 因为这个索引只有3个列,所以output list 只有, 、lastname, firstname, id(clustered index的列会在所有索引里), 没有other列,所以要根据 clustered index的id, 回到 clustered index,拿到对应的other。

如果没有clustered index,就是根据row ID 回表,这就是Oracle时代广为流传的“回表”一词的来源。

下图非常好的再现了这个逻辑
image.png

这里必须要补充一下,bookmark lookup可不是什么廉价的操作.

上图非常好的看见,non-clustered index seek是一个连续读,而lookup的时候,回到clustered index中,变成了随机读(random pickup),这是非常昂贵的。如果seek的结果是20,回表操作就是20次random IO。

所以,当回表操作非常大的时候,还不如直接走clustered index,眼尖的同学一定看见,我在上述例子中使用了index hint强迫他走了索引,实际上,优化器默认会聪明地调整到clustered index scan

image.png

2.4 Seek Predicate

并不是所有情况,所有谓词都能轻而易举的走到索引,尤其是在组合索引的时候。这也是网上经常能看到的零散知识点总结,大家着迷一般分享着神秘的索引判断条件,而忽视了整个优化器架构。

这里我就不太多赘述,

2.4.1 单列索引

下述条件可以走

a = 3

a > 100

a between 0 and 99

a like ‘abc%’

a in (2, 3, 5, 7)

下述条件不可以走

ABS(a) = 1

a + 1 = 9

a like ‘%abc’

2.4.2 组合索引

下列条件可以走,前者等值查询,后者范围查询

a = 3 and b = ‘pi’

a = ‘xyzzy’ and b <= 0

2.4.3 一个例子

从网上找了一个例子,非常好的说明了索引和cover的列的多少。具体数据存储,以后我们再开文章来说。

下述语法中,include是SQL Server特有,mysql可以忽略。

create table T_clu (a int, b int, c int, d int, e int, f int)
create unique clustered index T_clu_a on T_clu (a) -- mysql 中不允许单独创建,等价于主键如果加上非空的话
create index T_clu_b on T_clu (b)
create index T_clu_ac on T_clu (a, c)
create index T_clu_d on T_clu (d) include (e) -- SQL Server特有
create unique index T_clu_f on T_clu (f)
image.png

T_clu_a a a, b, c, d, e
T_clu_b b, a a, b
T_clu_ac a, c a, c
T_clu_d d, a a, d, e
T_clu_f f a, f

2.5 MySQL 索引下推

不得不说,MySQL在有些场景下是充满创造力的,MySQL的索引下推,就是一个非常好的例子。这点,SQL Server还需要不断进步。在学习SQL Server的道路上,还要对友商的先进之处,保持技术敏感。

考虑下这个语句的执行计划

select * from test where lastname = 'Smith' and firstname > 'Goe' and other < 'abc';

现在,有了SCAN, SEEK和bookmark lookup的基础,我们知道,这个语句,前两个判断条件可以走一次索引的seek, 之后会对结果集进行bookmark lookup, 回表后,再进行第三个判断条件过滤。 比如SQL Server就是这样执行的。

image.png

这里提一句,如果是索引里面seek走的谓词,叫做seek predicate,如果是scan走的,就是predicate。

ICP的好处就是,在回表之前,先过滤第三个条件,减少回表量,确保每个回表数据就是要进output list的。因为我们说了,回表是非常expensive的操作

当执行计划里,Extra写了Using Index Condition,就表示ICP(Index Condition Pushdown).

image.png

3.表连接的三种基本操作: Nested Loop join, Merge Join, Hash Join

掌握了数据的访问方式,我们终于可以来探讨表(数据集)的连接(join)的基本形态了。

3.1 先说Join

Join本身是一个关键字,并不是一个operator。

Join基本分类就是inner join(默认)和outer join。inner join就是找交集的过程。而outer join则可以参考下面这个表格

Join 结果集 …
A left outer join B all A rows
A right outer join B all B rows
A full outer join B -- MySQL没有 all A and B rows

除此之外,还有半连接(semi-join) 。这和我们之前说的unique key的半扫描很像,就是找到第一个就会退出当前循环,比如带exists的连接。

select a.id from test a where exists ( select 1 from test2 b where a.id = b.id);

3.2 Nested Loop

考虑下这个查询会怎么走

select a.id,a.lastname,b.firstname from test a join test2 b on a.id=b.id;

id是主键, (lastname,firstname)组成组合索引。

我们来看下SQL Server的执行计划
image.png

Nested Loop 的算法伪代码如下

for each row R1 in the outer table
    for each row R2 in the inner table
        if R1 joins with R2
            return (R1, R2)

可以看见这是一个典型的双层循环,想想学C语言时肯定会举得例子,内存循环(inner table) 外层循环(outer table)整个效率就取决于内层循环每次的执行速度。

所以这个模型往往需要一个小的驱动表,驱动表上有索引,每次快速扫描一次。外循环是大表,一共扫描一次,这是比较理想的情况。

当我们把test2表撑到10万行,并且和test表大量重复,这个时候,Nested Loop就不太适用了。有一个误区,大部分人会认为,大量数据的等值查询会使用hash join,那么事实上,并不完全是这样的。

3.3 Merge Join

在MySQL中并没有这种join 方式,所以,我们可以认为Merge Join是在一些特殊场景提高join性能的一种方法。 大部分场景Merge Join 并不快。

让我们看下merge join的算法伪代码。

get first row R1 from input 1
get first row R2 from input 2
while not at the end of either input
    begin
        if R1 joins with R2
            begin
                return (R1, R2)
                get next row R2 from input 2
            end
        else if R1 < R2
            get next row R1 from input 1
        else
            get next row R2 from input 2
    end

什么时候会走merge join呢? 当两个结果集做有序列map的时候,merge是按顺序读取并合并。而如果是两个无序结果,则效率会比较差。

有序结果的merge join肯定比hash要廉价,因为hash join的时间复杂度是o(1),结果集越大,join 成本越高。此例中我强制使用hash之后得到的执行计划。
image.png

我们可以看下merge join 和 hash join 在大量有序结果集的性能差别。
image.png

3.4 Hash Join

Hash 算法是一个通用算法,在计算机领域大量使用。还记得我们前面说过,阻塞式查询和交互式查询吗? Nested Loop 和 Merge Join 是交互式,而Hash是阻塞式,必须等待Hash结果全部做完,才会返回数据。MySQL目前也没有这个join方法。

hash join的算法 伪代码如下

for each row R1 in the build table
    begin
        calculate hash value on R1 join key(s)
        insert R1 into the appropriate hash bucket
    end
for each row R2 in the probe table
    begin
        calculate hash value on R2 join key(s)
        for each row R1 in the corresponding hash bucket
            if R1 joins with R2
                return (R1, R2)
    end

在Hash Join执行之前,数据库引擎会根据表的Cardinality (mysql) / Cardinality Estimate(CE, SQLServer) 来估算大概需要多少内存。如果是多表hash join,引擎会选择最小的两个表进行Hash,进而减小内存的使用。

hash中有两个基本概念,一个是probe,一个是buckets list。

如上述伪代码所描述的,hash join分两个阶段,第一个阶段叫做build,就是创建buckets list,这是一个维护在内存的hash table, 如果内存不够大,会溢出到tempdb上。

第二个阶段叫probe,就是用第二个表生成好的hash 结果来对应第一阶段的buckets list。

但是hash都有一个问题,就是hash 冲突(collision),所以在实现的时候,引擎在buckets size上是需要计算的,细节的部分,我们将来在in-memory OLTP 中再细聊。

再多说一句左hash和右hash

image.png

算法上,左hash是指,表1表2生成出第一个hash结果HJ1,而表3是下一个probe,在生成完下一个HJ2之后,HJ1已经没用了, 可以释放掉。所以左hash是一个有阻塞的,但是内存开销较小的算法,内存开销是 max(hj1 + hj2, hj2 +hj3)

右hash是指,表1表2生成的HJ1是下一个probe,会参与到表3的probe,所以没有阻塞,并且内存不能释放,内存开销是(hj1+hj2+hj3),但速度更好

3.5 Join 小结

知道你们喜欢这种总结表格,一劳永逸。但是请不要忘记,懂得过程,比懂得结果更有意义。

image.png

因为篇幅原因,方便大家阅读,所以先写一个上篇,聊一些基础的。

后续会逐步聊一些更上层的东西。毕竟优化器是数据库中最核心也是最难实现的部分。需要足够的毅力和能力,去理解它。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
SQL 算法 数据库
SQL优化器原理 - Join重排
保证等价性:不同的Join顺序可能产生相同的结果集,但执行成本可能不同。因此,在重排Join顺序时,必须确保结果集的等价性。
|
4月前
|
SQL 算法 数据库
SQL优化器原理 - Join重排。
保证等价性:不同的Join顺序可能产生相同的结果集,但执行成本可能不同。因此,在重排Join顺序时,必须确保结果集的等价性。
|
SQL 存储 分布式计算
AnalyticDB MySQL带你深入浅出SQL优化器原理
SQL优化器是数据库、数据仓库、大数据等相关领域中最复杂的内核模块之一,它是影响查询性能的关键因素。比如大家熟知的开源产品 MySQL、PostgreSQL、Greenplum DB、Hive、Spark、Presto,都有自己的优化器。本文将由浅入深地带读者了解其中技术原理。
|
SQL JSON 数据可视化
Explain的四种格式与查看优化器重写SQL
Explain的四种格式与查看优化器重写SQL
139 0
|
SQL 存储 缓存
关于数据仓库的Hive的Hive架构的Driver的SQL的解析器、编译器、执行器、优化器
数据仓库是一个面向分析的数据存储系统,其中包含了大量的历史数据,可以用于数据分析和报表生成。Hive是一个开源的数据仓库系统,基于Hadoop平台,可以存储和处理大规模的数据。在Hive中,SQL语句被解析器解析成抽象语法树(AST),然后编译器将其转换成物理执行计划,包括执行器和优化器的参与。本文将介绍Hive中SQL解析器、编译器、执行器和优化器的作用和原理。
375 0
|
SQL 存储 Cloud Native
深入浅出SQL优化器原理
SQL优化器是数据库、数据仓库、大数据等相关领域中最复杂的内核模块之一,它是影响查询性能的关键因素。比如大家熟知的开源产品 MySQL、PostgreSQL、Greenplum DB、Hive、Spark、Presto,都有自己的优化器。本文将由浅入深地带读者了解其中技术原理。
472 0
深入浅出SQL优化器原理
|
SQL 存储 分布式计算
深入浅出SQL优化器原理
SQL优化器是数据库、数据仓库、大数据等相关领域中最复杂的内核模块之一,它是影响查询性能的关键因素。比如大家熟知的开源产品 MySQL、PostgreSQL、Greenplum DB、Hive、Spark、Presto,都有自己的优化器。本文将由浅入深地带读者了解其中技术原理。 作者:阿里云 AnalyticDB MySQL 团队 — 郭泽晖(索月)
258 0
深入浅出SQL优化器原理
|
6月前
|
达摩院 开发者 容器
「达摩院MindOpt」优化形状切割问题(MILP)
在制造业,高效地利用材料不仅是节约成本的重要环节,也是可持续发展的关键因素。无论是在金属加工、家具制造还是纺织品生产中,原材料的有效利用都直接影响了整体效率和环境影响。
「达摩院MindOpt」优化形状切割问题(MILP)
|
6月前
|
人工智能 自然语言处理 达摩院
MindOpt 云上建模求解平台:多求解器协同优化
数学规划是一种数学优化方法,主要是寻找变量的取值在特定的约束情况下,使我们的决策目标得到一个最大或者最小值的决策。
|
1月前
|
机器学习/深度学习 算法 数据可视化
如果你的PyTorch优化器效果欠佳,试试这4种深度学习中的高级优化技术吧
在深度学习领域,优化器的选择对模型性能至关重要。尽管PyTorch中的标准优化器如SGD、Adam和AdamW被广泛应用,但在某些复杂优化问题中,这些方法未必是最优选择。本文介绍了四种高级优化技术:序列最小二乘规划(SLSQP)、粒子群优化(PSO)、协方差矩阵自适应进化策略(CMA-ES)和模拟退火(SA)。这些方法具备无梯度优化、仅需前向传播及全局优化能力等优点,尤其适合非可微操作和参数数量较少的情况。通过实验对比发现,对于特定问题,非传统优化方法可能比标准梯度下降算法表现更好。文章详细描述了这些优化技术的实现过程及结果分析,并提出了未来的研究方向。
27 1
下一篇
无影云桌面