MySQL性能优化建议

 原文来源:MySQL逻辑架构及性能优化原理
 
 下面会从3个不同方面给出一些优化建议。但请等等,一句忠告:不要听信自己看到的关于优化的“绝对真理”,包括本文所讨论的内容,而应该是在实际的业务场景下通过测试来验证你关于执行计划以及响应时间的假设。
 

表的设计与数据类型优化

选择数据类型只要遵循小而简单的原则就好,越小的数据类型通常会更快,占用更少的磁盘、内存,处理时需要的CPU周期也更少。越简单的数据类型在计算时需要更少的CPU周期,比如,整型就比字符操作代价低,因而会使用整型来存储ip地址,使用DATETIME来存储时间,而不是使用字符串。

这里总结几个可能容易理解错误的技巧:
1. 通常来说把可为NULL的列改为NOT NULL不会对性能提升有多少帮助,只是如果计划在列上创建索引,就应该将该列设置为NOT NULL。
2. 对整数类型指定宽度,比如INT(11),没有任何卵用。INT使用4字节存储空间,那么它所表示的数值范围已经确定,所以INT(1)和INT(20)对于存储和计算是相同的。
3. UNSIGNED表示不允许负值,大致可以使正数的上限提高一倍。比如TINYINT存储范围是-128 ~ 127,而UNSIGNED TINYINT存储的范围却是0 – 255。
4. 通常来讲,没有太大的必要使用DECIMAL数据类型。即使是在需要存储财务数据时,仍然可以使用BIGINT。比如需要精确到万分之一,那么可以将数据乘以一百万然后使用BIGINT存储。这样可以避免浮点数计算不准确和DECIMAL精确计算代价高的问题。
5. TIMESTAMP使用4个字节存储空间,DATETIME使用8个字节存储空间。因而,TIMESTAMP只能表示1970 –2038年,比DATETIME表示的范围小得多,而且TIMESTAMP的值因时区不同而不同。
6. 大多数情况下没有使用枚举类型的必要,其中一个缺点是枚举的字符串列表是固定的,添加和删除字符串(枚举选项)必须使用ALTER TABLE(如果只只是在列表末尾追加元素,不需要重建表)
7. 表的列不要太多。原因是存储引擎的API工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列,这个转换过程的代价是非常高的。如果列太多而实际使用的列又很少的话,有可能会导致CPU占用过高。
8. 大表ALTER TABLE非常耗时,MySQL执行大部分修改表结果操作的方法是用新的结构创建一个张空表,从旧表中查出所有的数据插入新表,然后再删除旧表。尤其当内存不足而表又很大,而且还有很大索引的情况下,耗时更久。

创建高性能索引

  索引是提高MySQL查询性能的一个重要途径,但过多的索引可能会导致过高的磁盘使用率以及过高的内存占用,从而影响应用程序的整体性能。应当尽量避免事后才想起添加索引,因为事后可能需要监控大量的SQL才能定位到问题所在,而且添加索引的时间肯定是远大于初始添加索引所需要的时间,可见索引的添加也是非常有技术含量的。

索引相关的数据结构和算法

  通常我们所说的索引是指B-Tree索引,它是目前关系型数据库中查找数据最为常用和有效的索引,大多数存储引擎都支持这种索引。使用B-Tree这个术语,是因为MySQL在CREATE TABLE或其它语句中使用了这个关键字,但实际上不同的存储引擎可能使用不同的数据结构,比如InnoDB存储引擎就是使用的B+Tree。
  
  B+Tree中的B是指balance,意为平衡。需要注意的是,B+树索引并不能找到一个给定键值的具体行,它找到的只是被查找数据行所在的页,接着数据库会把页读入到内存,再在内存中进行查找,最后得到要查找的数据。
  
  随着数据库中数据的增加,索引本身大小随之增加,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级。可以想象一下一棵几百万节点的二叉树的深度是多少?如果将这么大深度的一颗二叉树放磁盘上,每读取一个节点,需要一次磁盘的I/O读取,整个查找的耗时显然是不能够接受的。那么如何减少查找过程中的I/O存取次数?
  一种行之有效的解决方法是减少树的深度,将二叉树变为m叉树(多路搜索树),而B+Tree就是一种多路搜索树。理解B+Tree时,只需要理解其最重要的两个特征即可:第一,所有的关键字(可以理解为数据)都存储在叶子节点(Leaf Page),非叶子节点(Index Page)并不存储真正的数据,所有记录节点都是按键值大小顺序存放在同一层叶子节点上。其次,所有的叶子节点由指针连接。如下图为高度为3的简化了的B+Tree。
  
  B+Tree结构

对B+树可以进行两种查找运算:
  1. 从最小关键字起顺序查找
  2. 从根结点开始,进行随机查找
  在随机查找时,从根结点出发,与B树的查找方式相同,只不过即使在非终端结点上找到了待查的关键字,也不终止,而是继续向下一直到达包含待查关键字的叶子结点。因此,在B+树中,不管随机查找成功与否,每次随机查找都是走了一条从根到叶子结点的路径。如果需要进行顺序查找,从最左侧包含最小关键字的叶子结点出发,不经过分支结点(即非终端结点),沿着指向下一叶子结点的指针可遍历所有的关键字。

  下图展示了一种可能的索引方式。左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上并不是一定物理相邻的)。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在O(log2N)的复杂度内获取到相应数据。
  
  这里写图片描述

  数据库索引采用B+树的主要原因是B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)。B+树元素遍历效率极高,B+树的结构也特别适合带有范围的查找。比如查找学校18-22岁的学生人数,可以通过从根结点出发进行随机查找,找到第一个18岁的学生(此时到达了叶子结点),然后再在叶子结点出发顺序查找到符合范围的所有记录。
  
  为表设置索引是要付出代价的:一是增加了数据库的存储空间,二是在插入和修改数据时要花费较多的时间(因为索引也要随之变动)。
  

高性能策略

  通过上文,相信你对B+Tree的数据结构已经有了大致的了解,但MySQL中索引是如何组织数据的存储呢?以一个简单的示例来说明,假如有如下数据表:

CREATE TABLE People(
    last_name varchar(50) not null,
    first_name varchar(50) not null,
    dob date not null,
    gender enum(`m`,`f`) not null,
    key(last_name,first_name,dob)
);

  对于表中每一行数据,索引中包含了last_name、first_name、dob列的值,下图展示了索引是如何组织数据存储的。
  这里写图片描述

可以看到,索引首先根据第一个字段来排列顺序,当名字相同时,则根据第三个字段,即出生日期来排序,正是因为这个原因,才有了索引的“最左原则”。

1、MySQL不会在“非独立的列”上建立索引。 ”独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。比如:

select * from where id + 1 = 5

我们很容易看出其等价于 id = 4,但是MySQL无法自动解析这个表达式,使用函数是同样的道理。

2、前缀索引
  如果列很长,通常可以索引开始的部分字符,这样可以有效节约索引空间,从而提高索引效率。
3、多列索引和索引顺序
在多数情况下,在多个列上建立独立的索引并不能提高查询性能。理由非常简单,MySQL不知道选择哪个索引的查询效率更好,所以在老版本,比如MySQL 5.0之前就会随便选择一个列的索引,而新的版本会采用合并索引的策略。举个简单的例子,在一张电影演员表中,在actor_id和film_id两个列上都建立了独立的索引,然后有如下查询:

select film_id,actor_id from film_actor where actor_id = 1 or film_id = 1

老版本的MySQL会随机选择一个索引,但新版本做如下的优化:

select film_id,actor_id from film_actor where actor_id = 1  
union all 
select film_id,actor_id from film_actor where film_id = 1 and actor_id <> 1
  • 当出现多个索引做相交操作时(多个AND条件,比如上例中如果改成where film_id = 1 and actor_id = 1),通常来说一个包含所有相关列的索引要优于多个独立索引。
  • 当出现多个索引做联合操作时(多个OR条件,如上例中查询语句),对结果集的合并、排序等操作需要耗费大量的CPU和内存资源,特别是当其中的某些索引的选择性不高,需要返回合并大量数据时,查询成本更高。所以这种情况下还不如走全表扫描。
      因此explain时如果发现有索引合并(Extra字段出现Using union),应该好好检查一下查询和表结构是不是已经是最优的,如果查询和表都没有问题,那只能说明索引建的非常糟糕,应当慎重考虑索引是否合适,有可能一个包含所有相关列的多列索引更适合。
      前面我们提到过索引如何组织数据存储的,从图中可以看到多列索引时,前面我们提到过索引如何组织数据存储的,从图中可以看到多列索引时,
      理解索引选择性的概念后,就不难确定哪个字段的选择性较高了,查一下就知道了,比如:
SELECT * FROM payment where staff_id = 2 and customer_id = 584

  是应该创建(staff_id,customer_id)的索引还是应该颠倒一下顺序?执行下面的查询,哪个字段的选择性更接近1就把哪个字段索引前面就好。

select count(distinct staff_id)/count(*) as staff_id_selectivity,
       count(distinct customer_id)/count(*) as customer_id_selectivity,
       count(*) from payment

多数情况下使用这个原则没有任何问题。

4、避免多个范围条件
  实际开发中,我们会经常使用多个范围条件,比如想查询某个时间段内登录过的用户:

select user.* from user where login_time > '2017-04-01' and age between 18 and 30;

  这个查询有一个问题:它有两个范围条件,login_time列和age列,MySQL可以使用login_time列的索引或者age列的索引,但无法同时使用它们。

5、冗余和重复索引
冗余索引是指在相同的列上按照相同的顺序创建的相同类型的索引,应当尽量避免这种索引,发现后立即删除。比如有一个索引(A,B),再创建索引(A)就是冗余索引。冗余索引经常发生在为表添加新索引时,比如有人新建了索引(A,B),但这个索引不是扩展已有的索引(A)。
  大多数情况下都应该尽量扩展已有的索引而不是创建新索引。但有极少情况下出现性能方面的考虑需要冗余索引,比如扩展已有索引而导致其变得过大,从而影响到其他使用该索引的查询。

6、删除长期未使用的索引
定期删除一些长时间未使用过的索引是一个非常好的习惯。

  关于索引这个话题打算就此打住,最后要说一句,索引并不总是最好的工具,只有当索引帮助提高查询速度带来的好处大于其带来的额外工作时,索引才是有效的。对于非常小的表,简单的全表扫描更高效。对于中到大型的表,索引就非常有效。对于超大型的表,建立和维护索引的代价随之增长,这时候其他技术也许更有效,比如分区表。最后的最后,explain后再提测是一种美德。
  在工作中,我们用于捕捉性能问题最常用的就是打开慢查询,定位执行效率差的SQL,那么当我们定位到一个SQL以后还不算完事,我们还需要知道该SQL的执行计划,比如是全表扫描,还是索引扫描,这些都需要通过EXPLAIN去完成。EXPLAIN命令是查看优化器如何决定执行查询的主要方法。可以帮助我们深入了解MySQL的基于开销的优化器,还可以获得很多可能被优化器考虑到的访问策略的细节,以及当运行SQL语句时哪种策略预计会被优化器采用。

总结

  理解查询是如何执行以及时间都消耗在哪些地方,再加上一些优化过程的知识有助于更好的理解MySQL,理解常见优化技巧背后的原理。

参考资料:
[1] Baron Scbwartz 等著;宁海元 周振兴等译;高性能MySQL(第三版)
[2]由 B-/B+树看 MySQL索引结构
[3]B树、B-树、B+树、B*树
[4]MySQL的进阶实战篇
[5]深入分析MySQL的逻辑架构
[6]MySQL性能优化的最佳21条经验

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值