我在工作中几乎都离不开数据库优化,很多时候我只是临时报佛脚,需要用的时候现学,学了之后弃之脑后,缺乏系统性归纳总结,这样的结果就是不管我学了多少次每次都觉得是新的。痛定思痛,决定把逼自己所学全面整理出来以供参考。
在面试内容中也几乎避免不了MySQL优化的问题,一般情况下回答的基本上是SQL语句层面的优化、建表时的优化,但是在工作中面对特定业务中SQL语句优化的空间很小甚至几乎不可能优化,这个时候就要从架构方面进行考虑了。
所以,综上所述,MySQL优化可以从五个方面进行入手:
- 建表优化
- 查询优化
- 架构优化
- 硬件优化
- 参数优化
建表优化
建表优化就是在针对业务设计表的时候进行合理建表,既不能设计过度也不能设计不足。根据个人工作经验,在建表的时候需要从以下几个方面进行思考:
数据量
对业务进行大致估计,评估数据量,如果数据量过大,就需要对表进行垂直分割。
根据业务合理建立索引
建立索引既不能过度也不能不足,如果过度建立索引,不仅会占用过多的存储空间,同时也会增加更新索引时的开销。因此在家里索引时我们要对业务进行拆分,清楚根据哪些条件进行查询,可能会使用哪些表进行连表查询,然后再对这些条件建立索引。
需要注意的是对多个条件查询时我们可能要建立联合索引,联合索引有最左匹配原则,如idx_uid_order_no_create_time(uid,order_no,create_time),其实已经对uid、uid,order_no、uid,order_no,create_time这三列都建了索引,因此就没有必要对uid和uid,order_no单独建立索引。
建立索引时我们不经要从业务方面进行考虑,同时也需要从数据统计方面进行考虑进行建立索引。每条记录尽可能的加上创建时间、修改时间,并且对创建时间加上索引,这样在数据统计时加上时间范围能有效降低扫描数据量。
选择正确的存储引擎
MySQL有两个常用的存储引擎,MyISAM和nnoDB,在此不对两者的区别进行详细分析,只是阐述选择存储引擎的一般标准。
- MyISAM适用于大量查询,但对大量写操作并不友好。在写或更新操作时,MyISAM会锁定整个表,那么其他进程便无法进行知道锁解除。
- InnoDB比MyISAM复杂得多,重量级得多。它支持“行锁”,因此在大量写操作的时候表现要比MyUSAM好要得多,且它支持事务。
那我们在建表的时候就需要考虑表中的数据是固定的还是经常需要修改的,比如存储品类的表、学生信息表这类我们最好选择MyISAM引擎,因为这类数据很少变动;相反,对于经常修改的数据我们最好选择InnoDB。
熟悉建表一般规则
从广义范围来讲,mysql优化不止性能优化,建表规范也是一种优化,从一定程度上也会影响mysql语句的执行效率。
- 所有表都需要建id自增列作为主键,类型为unsigned。
- 库名、表名、字段名全都使用小些字母加下划线,不超过12个字母,使用名词,并且见名知意。
- 对于varchar类型必须设置长度,注:在新版本中,varchar(n)中的n表示最大能存的字符个数,在低版本中n表示最大存储字节个数,比如varchar(50)不论汉字或字母都可以存储50个。
- 对索引类型使用前缀区分,比如普通索引使用idx_前缀,唯一索引使用uniq_前缀,且全部小写
- 联合索引中的字段度不超过5个,单表索引数量控制在5个以内,
- 不要使用外键
- 所有字段需要设置not null
- 对于整形字段,如果不可能为负值就把类型设置为unsigned。
- 表的字符集选用UTF-8
- 使用decimal替代float和double
- 避免使用enum,建议使用tinyint,然后在业务中进行映射。
- 避免使用text,blob 类型
- 使用int unsigned存储ipv4地址,使用inet_aton('10.20.17.90') 转换为整形,使用inet_ntoa(3520061480)转换为IP。
- 对于短数据类型字段,建议使用tinyint unsigned替代int
- 尽量设置默认值
- 定义整形时不要添加长度,不论是int(4)还是int(11)都是用4个字节进行存储,能存储的最大值是pow(2,31);唯一的区别是当类型是int(4)时不足4位会用0填充。
查询优化
我们在写mysql语句时,除了遵循一般规则,还要考虑如何优化查询。
写sql语句一般规则
- 时刻记得explain!!!
- 任何地方避免出现select *
- 注意每个字段的类型,避免发生隐式转换
- 避免使用!=或<>、IS NULL或IS NOT NULL、IN ,NOT IN等这样的操作符
- 避免在where字句中进行函数操作,这会导致引擎放弃使用索引而进行 全表扫描。
- 避免在where子句中对字段进行表达式操作,这将会使搜索引擎放弃索引而进行全表扫描
- 能够使用distinct就避免使用group by
- 避免子查询并合理使用exists进行改写。
- 慎用like,对于帖子、文章的标题、内容检索时,如果要使用like,避免在keyword前面加%
- 避免在where子句中使用or来连接条件。如果一个字段有索引,一个字段没有索引,引擎则会放弃使用索引而进行全表扫描,这时应该使用union all或union来进行查询组合
mysql语句优化方法
count()优化
在使用count统计时我们应该考虑目标范围与非目标范围的大小进行优化。比如:
统计班里学生成绩低于80的人数:
select count(uid) from grade where score < 80;
正常情况下一个班中的学生成绩低于80肯定比高于80的要多,因此可以优化成:
select (select count(*) from grade) – count(*) from grade where score >= 80;
强制走索引
有些情况下即使建了索引也不一定会走索引,这个时候我们可以试试强制走索引:
select * from student where gender=1 and province_id in (22,33);
优化:
select * from student (INDEX = idx_province_id) where gender=1 and province_id in (22,33);
少用OR
如果遇到OR最好考虑下能够避免,如下:
select * from grade where score >=90 or gender =1;
优化如下:
select* from grade where score >=90 UNION ALL select* from grade where gender =1;
避免对字段进行函数操作:
在正常情况下应该禁止在sql语句中对字段进行函数操作或表达式操作,这样不仅牵扯业务还影响执行效率,如:
select id from t where substring(name ,1,3)='abc';
select id from t where datediff(day, createdate,'2005-11-30')=0;
select id from t where num/2=100
可以优化成:
select id from t where name like 'abc%';
select id from t where createdate >='2015-11-30' and createdate < '2015-12-01';
select id from t where num=2*100;
适当情况下使用exist替代in
如:
select num from a where num in (select num from b);
优化成:
select num from a where num exist (select 1 from b where num=a.num);
limit优化
一般情况下limit不回出现性能短板,但是当对数量达到千万级别的表使用limit分页就会出现明显的性能问题,如:
select * from orders order by id limit10000000,10;
我们可以优化成:
Select * from orders where id>=(Select id from orders limit 10000000,1) limit 10;
还可以优化成:
select * from orders where id between limit 10000000 and 10000010;
尽量使用union all 替代union
union和union all的差异主要是前者需要将两个(或者多个)结果集合并后再进行唯一性过滤操作,这就会涉及到排序,增加大量的cpu运算,加大资源消耗及延迟。所以当 我们可以确认不可能出现重复结果集或者不在乎重复结果集的时候,尽量使用union all而不是union。
架构层面优化
当数据量达到一定级别,前面三种优化就显得微不足道。在大数据面前,索引的作用微乎其微,甚至没有索引比有索引还要好点,这个时候我们就要曾架构方面来进行优化了。
根据我的工作经验,一般会从一下几个方面进行考虑:
读写分离
建立主库实例,所有的写操作全部进入主库,所有的读操作全部走从库,然后通过binlog将主库的数据同步到从库(为防止主从数据同步延迟,最好在读数据的接口中支持查询主库)
垂直分库分表
把与核心业务相关的表作为一个整体分别按照某种规则映射到不同的数据库,这句话有点抽象,举个例子:
背景:某个商城的订单系统数据量很庞大,必须对数据进行切分,不然单个实例无法支撑。
数据库名称:jm_orders
相关表:
orders:交易单主表
order_additional:交易单附加表
order_items:交易单商品表
数据库sharding规则:uid后三位与128进行取模,然后在除以8下取整,这样我们就建立了0-15的数据库实例,建立的数据库分别是jm_order_0,jm_orders_1,jm_orders_2...jm_orders_15,对于数据库jm_orders_n,里面的表分别是:
orders_(n*8),orders_(n*8+1),orders_(n*8+2),...,orders_(n*8+8-1);
order_additional_(n*8),order_additional_(n*8+1),order_additional_(n*8+2),...,order_additional_(n*8+8-1);
order_items_(n*8),order_items_(n*8+1),order_items_(n*8+2),...,order_items_(n*8+8-1)
如果查询uid=6543243用户的交易单信息,根据sharding规则:应该查询14号sharding库的115相关表:
select * from jm_orders_14.orders_115 where uid=6543243;
建立冷热数据库
虽然建立sharding库可以对数据进行垂直切分,但是随着业务推进,有很多老数据不会被查询,但是这些数据却会影响对新数据的查询。这个时候就要考虑把旧数据剥离出去放入另外一个数据库中,我们称之为冷库,相反存放近段时间的新数据的数据库我们称之为热库。
一般情况下三个月前的数据就可以称之为老数据,需要用脚本把热裤中的数据删除(冷库的数据也是通过binlog自动同步的,因此只需从热库中删除即可)。
业务系统根据具体业务来确定查冷库还是热库,有时候我们无法确定查冷库还是查热裤,比如按uid进行查询。这个时候DB层需要提供一个兼容热裤和冷库查询的接口,也就是说优先查询热裤,如果热裤中查不到再查冷库,否则直接返回查询接口。
硬件优化
参数优化
以上两点参考:
https://www.cnblogs.com/devinzhang/p/8192064.html