很久之前写的一篇文章,主要是结合mysql45讲和《数据库索引设计与优化》讨论索引设计的,拿出来分享下。
对于INSERT_SELECT型数据库,如果没有事务的要求,更倾向于选择MyISAM。
因为InnoDB会维护更多的数据,包括以下几个方面:
下面两个图可以说明聚集索引和非聚集索引的结构。如果业务需要事务、外键、crash-safe能力,那就应当选择InnoDB。
我们都很了解表结构的三范式:
如果不符合这几点,就会出现数据冗余过大,插入异常,修改异常,删除异常等的问题,所以建议表的结构应该严格遵守这几点。
在这之外,我还想说一个一般大家不会注意到的问题。
首先引入两个概念,第一个概念是谓词,谓词指的是SQL语句中的搜索参数,或者说是条件表达式或者真值表达式。
第二个概念是过滤因子,过滤因子描述了谓词的选择性,即表中满足谓词条件的行数占总行数的比例。
对于两个谓词来说,它们的过滤因子一般不能简单相乘。两个谓词相关性越小,组合谓词的过滤因子越接近两个过滤因子的乘积。所以如果有非主属性对于码的部分函数依赖,组合谓词的过滤因子将得到最差的情况。
第一个需求是根据条件筛选得到所有的语句,然后order by通过src_id排序返回。是一个很简单的需求。
想要优化order by语句,首先要知道这个语句是如何执行的。
order by排序MySQL会根据使用的表结构的不同、参数的不同、内存大小的不同,选择不同的排序方法。
首先来看引擎表,如果单行长度不大,就会采用全字段排序的方法。单行长度的限制是由数据库的max_length_for_sort_data参数限制的。如果单行小于这个参数,那就会采用全字段排序。全字段排序的过程如下图所示:
全字段排序的排序过程也不一定全在内存中完成,sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
如果使用了磁盘临时文件,mysql将不得不使用归并排序,将需要排序的数据分成N份,每一份单独排序后存在这些临时文件中。然后把这N个有序文件再合并成一个有序的大文件。
此处多出来的开销是读写临时文件的IO时间,这个开销是相当大的。
如果单行长度太大,超过了max_length_for_sort_data的大小,就会使用row_id的排序方法:
可以看到,row_id会多出一次回表的操作,而且将物化结果集的过程提前了,一般认为物化结果集越晚进行越好,因为引擎会为了一致性做出很多牺牲并且必须要等待io。
另外提一点,sort_buffer是一个一维数组,性能消耗会比内存临时表更小。
然后是内部临时表。什么时候会用到内部临时表呢?
内部临时表的存储方式由另一个参数:tmp_table_size设置,这个配置限制了临时表的大小,默认值是 16M。如果临时表大小超过了 tmp_table_size,那么内存临时表就会转成磁盘临时表。
对于内存临时表,排序仍然是rowid方式。
而对于磁盘临时表,同样会采用归并排序的方式。
**对于使用了limit方式的磁盘表,如果limit行数的大小不超过sort_buffer_size的大小,那么MySQL会将排序方法特化为优先队列排序也就是堆排。**这样做的好处是不需要产生临时文件。
我们可以从以上这些讨论,看出order by是一个昂贵的操作,无论是耗费的cpu时间、内存还是反复回表,我们都要尽量避免这几个操作。可以通过以下几种方式优化:
group by的执行流程如下图所示:
排序仍然是使用上面rowid的方式进行,那么这个过程我们可以如何优化呢?
在开始之前,我们需要了解几个概念,学习这些概念对于索引的设计是至关重要的。
索引片
索引被扫描的部分。索引片的宽度会影响对表的同步读的数量。
三星索引
困难谓词
无法参与定义索引片的谓词(不可索引谓词),即该谓词无法成为匹配谓词
不可索引谓词,比如<>
可索引谓词,比如C1=5,in条也是可索引谓词,而or条件就是不可索引谓词
最小的索引片宽度
对于如何得到最小的扫描索引片宽度,可以使用以下的步骤:
取出对优化器来说不过分复杂的等值谓词(非困难谓词)列作为前导列(任意顺序都可,因为对于任意顺序在B+树上的查找时间都将是相同的),将选择性最好的范围谓词作为索引的下一列。
注意:最小的索引片宽度并不一定是最好的。
半宽索引
一个包含WHERE子句中所有列的索引,使用半宽索引将使得访问路径在必要时才访问表。
宽索引
至少满足第三条的索引称为宽索引
最左匹配原则
最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配
最佳索引
为一个select设计出的最好的索引,可能是二星索引也可能是三星索引。
范围谓词和三星索引
对于order by列的索引,仅当其放在范围谓词列之前时才能够满足第二颗星。否则会因为最左匹配原则,使这个索引失效,从而失去避免排序的作用。因为顺序的限制,这时候可能就无法得到最窄的扫描索引片了,因此这种情况下最佳的索引将会是一个二星索引。
我们现在已经可以通过一些工具,比如explain得到的rows,慢查询日志中的总扫描行数来查看访问表的次数。但是对于语句执行时间的评估或者说概念上的理解仍然是很重要的,它能够帮助我们更好的定位问题,并且是一个非常低成本的工具。
这里介绍一种比较直观的估算方法:QUBE(快速上限估算法)。
LRT指的是本地响应时间,TR指的是随机访问的数量,TS指的是顺序访问的数量,F指的是有效FETCH的数量。
下面依次介绍这几个概念:
访问:DBMS读取一个索引行或者表行的成本称为一次访问
随机访问:一次对于表行的查找访问
顺序访问:读取物理上连续的下一行
Fetch:游标操作中用于提取数据的SQL调用,相当于物化表行的成本
举几个简单的例子:(针对InnoDB引擎)
SELECT CNO,LNAME,FNAME
FROM CUST
WHERE CNO = :CNO
其中CNO字段加了主键索引。
我们假设符合要求的结果只有一个。
那么LRT = 210ms+10.1ms=20ms
包括对于主键索引的一次随机访问和对于表行(缓存数据块)的随机访问加上一个字段的Fetch成本。
SELECT CNO,LNAME,FNAME
FROM CUST
WHERE ZIP = :ZIP AND LNAME = :LNAME
ORDER BY FNAME
其中ZIP、LNAME、FNAME字段加了组合索引。CNO字段加了主键索引。
我们假设符合要求的结果共有1000个。
因为FNAME字段在索引中,所以就避免了排序的开销,但因为没有ZIP字段,所以仍然需要回表。
首先访问索引行(找到第一个符合要求的数据)需要一次随机访问;然后顺序访问1000次索引行(直到找到最后一个不符合要求的数据为止);之后访问主键索引需要一次随机访问,和999次顺序访问主键索引。之后提取1000个数据。
所以LRT=210ms+19990.01ms+1000*0.1ms=140ms。
如果CNO不是主键呢?也就是说CNO字段上没有任何索引的时候会发生什么?
1000次对于主键索引的顺序访问将会变成1000次对于字段的随机访问,
LRT=10s+10ms+100ms 这个时候将CNO加到索引中将会是一个好主意。
磁盘平均负载 = 随机插入涉及的索引数量*插入频率/磁盘数量
现在我们可以粗略的知道阿里云DMS实例中的SQL分析倍数怎么来的了!就是性能的提高-增加索引的代价。
根据我们前面提到的范围谓词和三星索引的原则,我们有两种方案可以选择:
第一种方案:
第二种方案:
最后再举一个例子:
对于SELECT语句:
SELECT cno,fname FROM CUST WHERE city = :city AND lname IN ( lname1,lname2 )
ORDER BY fname
我们如何设计它的索引呢?
我们假设过滤因子最大大小为0.1,数据集为1 000 000
按照两种方案分别设计的索引包括:
A:(city,lname,fname,cno)
B:(city,fname,lname,cno)
A的方式因为最左匹配所以根据city和lname取出后需要进行排序
但是A的索引片厚度只有B的十分之一,也就是A:1 000 0000.10.1 B:1 000 0000.1
LRT(A)=110ms+10 0000.01ms+10 0000.1ms
LRT(B)=110ms+100 0000.01ms+10 000*0.1ms
A大约为1s,B大约为2s
排序成本在1s的差别中是微不足道的,所以我们在这里不考虑它。
码
设 K 为某表中的一个属性或属性组,若除 K 之外的所有属性都完全函数依赖于 K(这个“完全”不要漏了),那么我们称 K 为候选码,简称为码。在实际中我们通常可以理解为:假如当 K 确定的情况下,该表除 K 之外的所有属性的值也就随之确定,那么 K 就是码。一张表中可以有超过一个码。(实际应用中为了方便,通常选择其中的一个码作为主码)
主属
包含在任意一个码中的属性称为主属性。
非主属性
不包含在任何一个码中的属性称为非主属性。
完全函数依赖
在一张表中,若 X → Y,且对于 X 的任何一个真子集(假如属性组 X 包含超过一个属性的话),X ’ → Y 不成立,那么我们称 Y 对于 X 完全函数依赖,记作 X F→ Y。
部分函数依赖
假如 Y 函数依赖于 X,但同时 Y 并不完全函数依赖于 X,那么我们就称 Y 部分函数依赖于 X,记作 X P→ Y
传递函数依赖
假如 Z 函数依赖于 Y,且 Y 函数依赖于 X (严格来说还有一个X 不包含于Y,且 Y 不函数依赖于Z的前提条件),那么我们就称 Z 传递函数依赖于 X ,记作 X T→ Z
索引片
索引被扫描的部分。索引片的宽度会影响对表的同步读的数量。
三星索引
困难谓词
无法参与定义索引片的谓词,即该谓词无法成为匹配谓词。
最小的索引片宽度
对于如何得到最小的扫描索引片宽度,可以使用以下的步骤:
取出对优化器来说不过分复杂的等值谓词(非困难谓词)列作为前导列(任意顺序都可,因为对于任意顺序在B+树上的查找时间都将是相同的),将选择性最好的范围谓词作为索引的下一列。
注意:最小的索引片宽度并不一定是最好的。
半宽索引
一个包含WHERE子句中所有列的索引,使用半宽索引将使得访问路径在必要时才访问表。
宽索引
至少满足第三条的索引称为宽索引
最左匹配原则
最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配
最佳索引
为一个select设计出的最好的索引,可能是二星索引也可能是三星索引。
范围谓词和三星索引
对于order by列的索引,仅当其放在范围谓词列之前时才能够满足第二颗星。否则会因为最左匹配原则,使这个索引失效,从而失去避免排序的作用。因为顺序的限制,这时候可能就无法得到最窄的扫描索引片了,因此这种情况下最佳的索引将会是一个二星索引。