很久之前写的一篇文章,主要是结合mysql45讲和《数据库索引设计与优化》讨论索引设计的,拿出来分享下。
选用什么引擎
对于INSERT_SELECT型数据库,如果没有事务的要求,更倾向于选择MyISAM。
因为InnoDB会维护更多的数据,包括以下几个方面:
- InnoDB的聚集索引结构在索引块中保存了数据块,因此在缓存时,要同时保存数据块和索引块;MyISAM的聚集索引则只保存索引块,叶子节点指向数据块,不需要缓存数据块的开销。
- MyISAM不需要写用来保证crash-safe能力的redo log;InnoDB需要在可重复读隔离级别时维护一致性视图(快照读),在读提交隔离级别时每个语句创建新的视图(当前读),也就是说MyISAM不需要维护undo log。
- 还有一个选择MyISAM潜在的好处是MyISAM中维护了count(*)的值,这个值在统计计算中经常被用到。而对于InnoDB来说,将会遍历一颗最小的索引数,逐行统计行数,显然比MyISAM慢很多。
下面两个图可以说明聚集索引和非聚集索引的结构。如果业务需要事务、外键、crash-safe能力,那就应当选择InnoDB。
表的结构正确吗
我们都很了解表结构的三范式:
- 符合1NF的关系中的每个属性都不可再分
- 2NF在1NF的基础之上,消除了非主属性对于码的部分函数依赖
- 3NF在2NF的基础之上,消除了非主属性对于码的传递函数依赖
如果不符合这几点,就会出现数据冗余过大,插入异常,修改异常,删除异常等的问题,所以建议表的结构应该严格遵守这几点。
在这之外,我还想说一个一般大家不会注意到的问题。
首先引入两个概念,第一个概念是谓词,谓词指的是SQL语句中的搜索参数,或者说是条件表达式或者真值表达式。
第二个概念是过滤因子,过滤因子描述了谓词的选择性,即表中满足谓词条件的行数占总行数的比例。
对于两个谓词来说,它们的过滤因子一般不能简单相乘。两个谓词相关性越小,组合谓词的过滤因子越接近两个过滤因子的乘积。所以如果有非主属性对于码的部分函数依赖,组合谓词的过滤因子将得到最差的情况。
统计结果
优化order by
第一个需求是根据条件筛选得到所有的语句,然后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是一个一维数组,性能消耗会比内存临时表更小。
然后是内部临时表。什么时候会用到内部临时表呢?
- 如果语句执行过程可以一边读数据,一边直接得到结果,是不需要额外内存的,否则就需要额外的内存,来保存中间结果;
- join_buffer 是无序数组,sort_buffer 是有序数组,临时表是二维表结构;如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。比如,union 需要用到唯一索引约束, group by 还需要用到另外一个字段来存累积计数。
内部临时表的存储方式由另一个参数:tmp_table_size设置,这个配置限制了临时表的大小,默认值是 16M。如果临时表大小超过了 tmp_table_size,那么内存临时表就会转成磁盘临时表。
对于内存临时表,排序仍然是rowid方式。
而对于磁盘临时表,同样会采用归并排序的方式。
**对于使用了limit方式的磁盘表,如果limit行数的大小不超过sort_buffer_size的大小,那么MySQL会将排序方法特化为优先队列排序也就是堆排。**这样做的好处是不需要产生临时文件。
我们可以从以上这些讨论,看出order by是一个昂贵的操作,无论是耗费的cpu时间、内存还是反复回表,我们都要尽量避免这几个操作。可以通过以下几种方式优化:
- 建立联合索引。如果order by的数据是在索引树上有序的那么就不需要sort_buffer,也不需要排序了。这时候,explain分析select语句就能够发现Extra字段中没有Using filesort也没有Tempory了。
- 覆盖索引。在索引中覆盖所有字段,那么就避免了回表的操作。这时候,explain分析select语句就能发现Extra字段中有using index,说明覆盖了索引。
- 在现有系统中增加索引往往也是非常昂贵的操作,所以可以更改数据库参数,避免产生磁盘文件。使用更大的sort_buffer_size和tmp_table_size参数避免磁盘文件进行转储。
优化group by
group by的执行流程如下图所示:
排序仍然是使用上面rowid的方式进行,那么这个过程我们可以如何优化呢?
- 使用order by null避免排序。如果你的业务不需要使用有序的结果,那么避免group by默认的排序无疑能节省巨大的开销。
- 使用SQL_BIG_RESULT字段禁止SQL优化器使用内存临时表。为了防止出现“先放到内存临时表,插入一部分数据后,发现内存临时表不够用了再转成磁盘临时表”的情况,我们可以使用SQL_BIG_RESULT告诉优化器,直接使用磁盘临时表,避免转换开销。
- 索引和数据库参数的更改仍然是有效的。
如何选好索引
一切之前…
在开始之前,我们需要了解几个概念,学习这些概念对于索引的设计是至关重要的。
索引片
索引被扫描的部分。索引片的宽度会影响对表的同步读的数量。
三星索引
- 如果必须扫描的索引片的宽度是最小的,那么得到一颗星。
- 如果索引行的顺序与查询语句的需求一致,避免了排序操作,那么得到一颗星。
- 如果索引行包含查询语句中所有的列,避免回表,那么得到一颗星。
困难谓词
无法参与定义索引片的谓词(不可索引谓词),即该谓词无法成为匹配谓词
不可索引谓词,比如<>
可索引谓词,比如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加到索引中将会是一个好主意。
增加一个索引的代价
- 每次增加一个索引,插入操作花费的时间增加10ms
- RAID 5下磁盘平均负载的贡献
磁盘平均负载 = 随机插入涉及的索引数量*插入频率/磁盘数量
现在我们可以粗略的知道阿里云DMS实例中的SQL分析倍数怎么来的了!就是性能的提高-增加索引的代价。
如何制定最合适的索引
根据我们前面提到的范围谓词和三星索引的原则,我们有两种方案可以选择:
第一种方案:
- 选出对于优化器来说不过分复杂的等值谓词列,将这些列作为索引的前导列
- 将具有最好选择性的范围谓词作为索引的下一个列
- 以正确的顺序添加order by列
- 以任意顺序将SELECT语句中其它的列添加到索引中
第二种方案:
- 选出对于优化器来说不过分复杂的等值谓词列,将这些列作为索引的前导列
- 以正确的顺序添加ORDER BY列
- 以任意顺序将SELECT中其它列添加到索引中
最后再举一个例子:
对于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的差别中是微不足道的,所以我们在这里不考虑它。
推荐的方式
- 选择正确的表结构
- 制定更好的索引
- 使用explain、慢查询日志、云服务工具(性能洞察和分析)
- …(比如高性能MySQL、数据库索引设计与优化、MySQL实战45讲等)
名词速查
码
设 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列的索引,仅当其放在范围谓词列之前时才能够满足第二颗星。否则会因为最左匹配原则,使这个索引失效,从而失去避免排序的作用。因为顺序的限制,这时候可能就无法得到最窄的扫描索引片了,因此这种情况下最佳的索引将会是一个二星索引。