mysql索引设计的注意事项(大量示例,收藏再看)

mysql索引设计的注意事项(大量示例,收藏再看)

目录

  • 1、索引的重要性
  • 2、执行计划上的重要关注点
  • (1).全表扫描,检索行数
  • (2).key,using index(覆盖索引)
  • (3).经过key_len肯定究竟使用了复合索引的几个索引字段
  • (4) order by和Using filesort
  • 3、索引设计的注意事项
  • (1). 关于INNODB表PRIMARY KEY的建议
  • (2). 什么列上适合建索引,什么列上不适合建索引
  • (3). 索引必定是有益的吗?
  • (4). where条件中不要在索引字段侧进行任何运算(包括隐式运算),不然会致使索引不可用,致使全表扫描
  • (5). 不要使用%xxx%这种模糊匹配,会致使全表扫描/索引全扫描
  • (6). 关于前缀索引和冗余索引
  • (7). 关于索引定义中的字段顺序
  • (8). 关于排序查询的优化
  • (9). 关于单列索引和复合索引
  • (10). 关于多表关联
  • 4、慢查询日志的分析以及关注点
  • (1). 使用pt-query-digest工具来统计
  • (2). 对统计输出进行分析
  • 5、几个优化案例
  • 优化案例1
  • 优化案例2
  • 优化案例3

1、索引的重要性

索引对于MySQL数据库的重要性是不言而喻的:
由于缺少合适的索引,一个稍大的表全表扫描,稍微来些并发,就可能致使DB响应时间急剧飙升,甚至致使DB性能的雪崩;
如今你们广泛使用的Innodb引擎的锁机制依赖于索引,缺少适合的索引,会致使锁范围的扩大,甚至致使锁表的效果,严重影响业务SQL的并行执行,影响业务的可伸缩性,只有在合适的索引条件下,才是行锁的效果.
既然索引对MySQL数据库这么重要,那么在索引的设计上有什么须要注意的事项吗? 这篇文章就来聊聊这个.html

2、执行计划上的重要关注点

既然涉及到索引,避免不了执行计划的对比,先简单说一下执行计划上的重要关注点mysql

(1).全表扫描,检索行数

mysql> show create table novel_agg_info\G *************************** 1. row ***************************
       Table: novel_agg_info Create Table: CREATE TABLE `novel_agg_info` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `rid` bigint(20) unsigned NOT NULL, `book_name` varchar(128) NOT NULL, `tag` varchar(128) NOT NULL, `dir_id` bigint(20) unsigned NOT NULL DEFAULT '0', `dir_url` varchar(512) NOT NULL DEFAULT '', `public_status` int(2) NOT NULL DEFAULT '1', PRIMARY KEY (`id`), UNIQUE KEY `rid` (`rid`), KEY `book_name` (`book_name`) ) ENGINE=InnoDB AUTO_INCREMENT=12096483 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) mysql> select count(1) from novel_agg_info; +----------+
| count(1) |
+----------+
|  4298257 |
+----------+
1 row in set (0.00 sec) mysql> show table status like 'novel_agg_info'\G *************************** 1. row *************************** Name: novel_agg_info Engine: InnoDB Version: 10 Row_format: Compact Rows: 4321842 Avg_row_length: 130 Data_length: 565182464 Max_data_length: 0 Index_length: 374095872 Data_free: 35651584 Auto_increment: 12096483 Create_time: 2017-05-10 11:55:30 Update_time: NULL Check_time: NULL Collation: utf8_general_ci Checksum: NULL Create_options: Comment: 1 row in set (0.00 sec)

 

实际数据行数近430W,优化器估算Rows: 4321842 行记录(这是一个估算值,来自于动态采样,数量级没有大的偏差便可,实际上屡次执行show table status,获得的数据也是不一样的)


where dir_id = 13301689388199959972 由于dir_id字段上没有索引可用,致使了全表扫描(type:ALL),优化器估算检索行数为(rows:4290581)sql

避免全表扫描 全表扫描(type:ALL),大的检索行数(rows:N,为估算值),这些都是咱们应该尽可能避免的.数据库

(2).key,using index(覆盖索引)

再看下面的执行计划对比:

key:book_name表明执行使用了KEY book_name (book_name),检索行数为1,这很好,是咱们想要的效果.
为何第1个执行计划中出现了Using index,而第2个执行计划中却没有呢?
由于:第1个SQL中只须要检索id,book_name字段,这在KEY book_name (book_name)中都存在了(索引叶节点中都会存储PRIMARY KEY字段ID),不须要回访表去获取其它字段了,Using index即表明这个含义;而第2个SQL中还须要检索tag字段,这在KEY book_name (book_name)中并不存在,就须要回访表会获取这个字段内容,因此没有出现Using index.
缓存

key,Using index
key: 表明使用的索引名称
Extra部分的Using index,表明只使用了索引便完成了查询,并无回访表去获取索引外的字段,也就是咱们一般所说的使用了“覆盖索引”;若是使用了key,但没有出现Using index,说明索引并不能覆盖检索和核对的全部字段,须要回访表去获取其它字段内容,这相对于覆盖索引增长了回访表的成本,增长了随机IO的成本安全

 

(3).经过key_len肯定究竟使用了复合索引的几个索引字段

对于复合索引INDEX(a,b,c) 我如何肯定执行计划到底使用了几个索引字段呢? 这个须要经过key_len去肯定.网络

*************************** 1. row ***************************
       Table: operationMenuInfo Create Table: CREATE TABLE `operationMenuInfo` ( `id` int(50) NOT NULL AUTO_INCREMENT, `operationMenuName` varchar(200) NOT NULL, `createTime` int(50) DEFAULT NULL, `startTime` int(50) DEFAULT NULL, `endTime` int(50) DEFAULT NULL, `appId` int(50) NOT NULL, `status` int(50) NOT NULL, `fromPlat` varchar(200) DEFAULT NULL, `appName` varchar(200) DEFAULT NULL, `packageId` int(20) DEFAULT NULL, `menuType` smallint(5) NOT NULL DEFAULT '0' COMMENT 'type', `entityId` int(11) NOT NULL DEFAULT '0' COMMENT 'entityId', `productId` int(11) NOT NULL DEFAULT '0' COMMENT 'pid', PRIMARY KEY (`id`), KEY `time_appid` (`appId`,`createTime`), KEY `idx_startTime` (`startTime`), KEY `idx_endTime` (`endTime`), KEY `t_eId_pId` (`entityId`,`menuType`,`productId`), KEY `idx_appId_createTime_fromPlat` (`appId`,`createTime`,`fromPlat`) ) ENGINE=InnoDB AUTO_INCREMENT=4656258 DEFAULT CHARSET=utf8 1 row in set (0.00 sec)

 

对比下面这两个SQL和它们的执行计划
mysql优化

where appId=927 and createTime=1494492062 按咱们的理解,应该是使用KEY idx_appId_createTime_fromPlat (appId,createTime,fromPlat)的前2个字段.
where appId=927 and fromPlat='dataman' 按咱们的理解,应该是使用KEY idx_appId_createTime_fromPlat (appId,createTime,fromPlat)的第1个字段.由于where条件中缺乏createTime字段,因此只能使用索引的第1个字段来access.
其实key_len反映的就是这些信息,不过没有那么直接(其实直接显示使用哪些字段来access了会更好),要对应到字段上还须要一些换算:
并发


key_len的计算
经过key_len能够知道复合索引都使用了哪些字段.key_len的计算上:
当字段定义能够为空时,须要额外的1个字节来记录它是否为空,当字段定义为not null时,这额外的1个字节是不须要的.
当字段定义为变长数据类型(好比说varchar)时,须要额外的2个字节来记录它的长度; 当字段定义为定长数据类型(好比说int,char,datetime等),这额外的2个字节是不须要的.
对于字符型数据,varchar(n),char(n), n都是定义的最大字符长度, gbk的话:2*n ,utf8的话:3*n
int 4个字节,bigint 8个字节,这些定长类型占用的字节数,这里只列举这2个吧.
索引使用哪些字段,上述计算公式计算出的字节的和就是ken_len,就能够肯定索引使用了哪些字段 
app

 

第1个SQL,使用了索引的前2个字段,appId(4) + createTime(4+1 这个字段定义为能够为空,因此是4+1) =9 ,因此ken_len是9,标识索引使用了这2个字段.
第2个SQL,只使用了索引的第1个字段appId(4) =4,因此ken_len是4,标识索引只使用了第1个字段.

(4) order by和Using filesort

业务SQL常常会有order by,通常来讲这须要真实的物理排序才能达到这个效果, 这就是咱们所说的Using filesort,通常来讲它须要检索出全部的符合where条件的数据记录,然后在内存/文件层面进行物理排序,因此通常是一个很耗时的操做,是咱们极力想要避免的.
但其实对于MySQL来讲,却不必定非得物理排序才能达到order by的效果,也能够经过索引达到order by的效果,却不须要物理排序.
由于索引经过叶节点上的双向链表实现了逻辑有序性,好比说对于where a=? order by b limit 1; 能够直接使用index(a,b)来达到效果,不须要物理排序,从索引的根节点,走到叶节点,找到a=?的位置,由于这时b是有序的,只要顺着链表向右走,扫描1个位置,就能够找到想要的1条记录,这样既达到了业务SQL的要求,也避免了物理的排序操做。这种状况下,执行计划的Extra部分就不会出现Using filesort,由于它只扫描了极少许的索引叶节点就返回告终果,因此通常而言,执行很快,资源消耗不多,是咱们想要的效果.

由于存在KEY time_appid (appId,createTime), 第1个SQL能够经过它快速的返回结果,由于没有物理排序,因此执行计划的Extra部分没有出现Using filesort.
而第2个SQL是没法经过任何索引达到上述效果的,必须扫描出全部的符合条件的记录行后物理排序再返回TOP1的记录,由于存在物理排序,因此执行计划的Extra部分出现了Using filesort.
执行时间上,第1个SQL瞬间返回结果,第2个SQL须要0.7秒左右才能返回结果(由于它要检索出符合条件的40W记录,然后还要排序,这2个操做致使了它执行时间偏长).


order by和Using filesort
索引自己是逻辑有序的,因此能够经过索引达到order by的效果要求,却不须要真正的物理排序操做. 若是业务SQL中有order by,但执行计划的Extra部分中却没有出现Using filesort,说明经过索引避免了物理的排序操做,对于TOPN SQL而言,这每每意味着经过索引快速的返回告终果,是咱们想要的.
若是执行计划的Extra部分中出现了Using filesort,说明没法经过索引达到效果,而使用了物理排序操做,对TOPN SQL而言,这意味着虽然只是返回极少的N条记录,但须要检索出符合where条件的全部记录,然后物理排序,最终才能返回业务想要的N条记录,若是符合where条件的记录不少,这2个操做每每是很耗时的,是咱们极力想要避免的.

 

3、索引设计的注意事项

关于索引的2个知识点 关于索引,首先说2个应该知道的事项(其实上面也已经提到了): 1.如今广泛使用的innodb存储引擎中,索引的叶节点中除了存储了索引定义中的字段外,还存储了primary key,从而能够找到对应的行记录,这样才能访问索引外的字段. 2.索引的叶节点经过双向链表实现了逻辑上的有序性,使得索引是有序的.

(1). 关于INNODB表PRIMARY KEY的建议

表设计层面,咱们通常建议使用自增ID作PRIMARY KEY,业务主键作UNIQUE KEY,缘由以下:
1.若是业务主键作PRIMARY KEY,业务主键的插入顺序比较随机,这样会致使插入时间偏长,并且聚簇索引叶节点分裂严重,致使碎片严重,浪费空间;而自增ID作PRIMARY KEY的状况下,顺序插入,插入快,并且聚簇索引比较紧凑,空间浪费小。
2.通常表设计上除了PRIMARY KEY外,还会有几个索引用来优化读写.而这些非PK索引叶节点中都要存储PRIMARY KEY,以指向数据行,从而关联非索引中的字段内容.这样自增ID(定义为bigint才占用8个字节)和业务主键(一般字符串,多字段,空间占用大)相比,作PRIMARY KEY在索引空间层面的优点也是很明显的(同时也会转换为时间成本层面的优点),表定义中的索引越多,这种优点越明显。

综上所述,咱们通常建议使用自增ID作PRIMARY KEY,业务主键作UNIQUE KEY。

 

(2). 什么列上适合建索引,什么列上不适合建索引

这里涉及到一个重要的概念:字段的选择性
select count(1)/count(distinct col) 这个结果越接近数据总行数,那么这个字段的选择性越低; 越接近1,那么这个字段的选择性越高. 简单举例说就是:身份证ID字段的选择性很高,而性别字段的选择性很低.

通常来讲,高选择性字段上是适合建立索引的,而低选择性字段上是不适合建立索引的


通常来讲,status,type这类枚举值不多的字段,就是低选择性字段(或者说低基数字段),是不适合单独做为索引字段的.
例外的状况就是: 这类字段数据分布特别不均衡,而你常常要定位的是数据量极少的字段值,这种状况下,仍是适合在这个字段上建立索引的.

 

mysql> show create table novel_agg_info\G *************************** 1. row ***************************
       Table: novel_agg_info Create Table: CREATE TABLE `novel_agg_info` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `rid` bigint(20) unsigned NOT NULL, `book_name` varchar(128) NOT NULL, `tag` varchar(128) NOT NULL, `dir_id` bigint(20) unsigned NOT NULL DEFAULT '0', `dir_url` varchar(512) NOT NULL DEFAULT '', `public_status` int(2) NOT NULL DEFAULT '1', PRIMARY KEY (`id`), UNIQUE KEY `rid` (`rid`), KEY `book_name` (`book_name`), KEY `idx_public_status` (`public_status`) ) ENGINE=InnoDB AUTO_INCREMENT=12096483 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) mysql> select public_status,count(1) from novel_agg_info group by public_status; +---------------+----------+
| public_status | count(1) |
+---------------+----------+
|             0 |  3511945 |
|             1 |   367234 |
|             2 |   419062 |
|            12 |       16 |
+---------------+----------+
4 rows in set (1.35 sec) mysql> explain select * from novel_agg_info where public_status = 12; +----+-------------+----------------+------+-------------------+-------------------+---------+-------+------+-------+
| id | select_type | table          | type | possible_keys     | key               | key_len | ref   | rows | Extra |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+------+-------+
|  1 | SIMPLE      | novel_agg_info | ref  | idx_public_status | idx_public_status | 4       | const |   15 | NULL  |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+------+-------+
1 row in set (0.00 sec) mysql> explain select * from novel_agg_info where public_status = 0; +----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
| id | select_type | table          | type | possible_keys     | key               | key_len | ref   | rows    | Extra |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
|  1 | SIMPLE      | novel_agg_info | ref  | idx_public_status | idx_public_status | 4       | const | 1955112 | NULL  |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
1 row in set (0.00 sec) mysql> select sql_no_cache count(1) from (select * from novel_agg_info where public_status = 12 ) tmp; +----------+
| count(1) |
+----------+
|       16 |
+----------+
1 row in set (0.00 sec) mysql> select sql_no_cache count(1) from (select * from novel_agg_info where public_status = 0 ) tmp; +----------+
| count(1) |
+----------+
|  3511945 |
+----------+
1 row in set (11.60 sec)

 

能够看到状态值为12的数据量极少,因此where public_status = 12 使用索引,快速的返回告终果. 但where public_status = 0 彻底是另一种状况了.
其实下面能够看到 where public_status = 0 不使用索引,使用全表扫描会更好些,但这里也依然是选择了使用索引的执行计划. 优化器应该基于数据分布的统计信息,对于不一样的输入值,使用更合理的执行计划,而不是使用一个统一的执行计划,这也是优化器层面须要继续智能化,提高的地方.
它的一个典型的应用场景,就是任务处理表:
不断有新任务插入进来,任务状态初始化为"未处理",后台不断的扫描出"未处理"的任务,进行调度处理,完成后,更新任务状态为"已处理",任务数据仍然保留下来.
这里任务状态字段就是这种状况,不一样值不多,但频繁查询的"未处理"状态极少,绝大部分为"已处理"状态,它们又基本上不会被查询,这种状况下,就适合在任务状态字段上建立索引.

为何低选择性字段上不适合建立索引呢? 其实也涉及到另外一个问题: 使用索引必定比全表扫描要好吗? 答案是否认的.
继续进行测试:

mysql> explain select * from novel_agg_info where public_status = 0;    ---默认走索引idx_public_status
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
| id | select_type | table          | type | possible_keys     | key               | key_len | ref   | rows    | Extra |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
|  1 | SIMPLE      | novel_agg_info | ref  | idx_public_status | idx_public_status | 4       | const | 1955112 | NULL  |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
1 row in set (0.00 sec) mysql> explain select * from novel_agg_info ignore index(idx_public_status) where public_status = 0; ---强制忽略索引idx_public_status,走全表扫描.
+----+-------------+----------------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table          | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+----------------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | novel_agg_info | ALL  | NULL          | NULL | NULL    | NULL | 3910225 | Using where |
+----+-------------+----------------+------+---------------+------+---------+------+---------+-------------+
1 row in set (0.00 sec) mysql> select sql_no_cache count(1) from (select * from novel_agg_info where public_status = 0) tmp; +----------+
| count(1) |
+----------+
|  3511945 |
+----------+
1 row in set (11.59 sec) mysql> select sql_no_cache count(1) from (select * from novel_agg_info ignore index(idx_public_status)  where public_status = 0) tmp; +----------+
| count(1) |
+----------+
|  3511945 |
+----------+
1 row in set (8.46 sec)

 

上面2个SQL的执行时间均取屡次执行的平均执行时间,能够忽略BUFFER POOL的影响.

为何全表扫描反而快了,使用索引反而慢了呢?
必定程度上是由于回访表的操做,使用索引,但提取了索引字段外的数据,因此须要回访表数据,这里符合条件的数据量特别大,因此致使了大量的回表操做,带来了大量的随机IO; 而全表扫描的话,虽说表空间比索引空间大,但可使用多块读特性,必定程度上使用顺序读; 此消彼长,致使全表扫描反而比使用索引还要快了.
这也解释了低选择性字段(低基数字段)为何不适合建立索引(固然,使用覆盖索引,不须要回访表是另一种状况了).

(3). 索引必定是有益的吗?

答案是否认的,由于索引是有代价的:
每次的写操做,都要维护索引,相应的调整索引数据,会在必定程度上下降写操做的速度.因此大量的索引必然会下降写性能,索引的建立要从总体考虑,在读写性能之间找到一个好的平衡点,在主要矛盾和次要矛盾之间找到平衡点.
因此说,索引并非越多越好,无用的索引要删除,冗余的索引(这在后面会提到)要删除,由于它们只有维护上的开销,却没有益处,因此在业务逻辑,SQL,索引结构变动的时候,要及时删除无用/冗余的索引.
索引使用不合理的状况下,使用索引也不必定会比全表扫描快,上面也提到了.
总结说,索引不是万能的,要合理的建立索引.

(4). where条件中不要在索引字段侧进行任何运算(包括隐式运算),不然会致使索引不可用,致使全表扫描

select * from tab where id + 1 = 1000; 会致使全表扫描,应该修改成select * from tab where id = 1000 -1; 才能够高效返回.

select * from tab where from_unixtime(addtime) = '2017-05-11 00:00:00' 会致使index(addtime)不可用
应该调整为select * from tab where addtime = unix_timestamp('2017-05-11 00:00:00') 这样才可使用index(addtime)

再好比说:

SELECT COUNT(*) FROM message WHERE (token = 'bed21e35b19fe40e71b3ba2ad080b10a') AND (date(create_time) = curdate());

会致使create_time上的索引不可用, 为了使得create_time上的索引可用,应转化为以下的等效形式:

SELECT COUNT(*) FROM message WHERE (token = 'bed21e35b19fe40e71b3ba2ad080b10a') AND create_time>=curdate() and create_time<adddate(curdate(),1)

这里的运算也包括隐式的运算,好比说隐式的类型转换..业务上常常有类型不匹配致使隐式的类型转换的状况.这里常常出现的状况是字符串和整型比较.
好比说表定义字段类型为BIGINT,但业务上传进来一个字符串的; 或者是表定义字段类型为varchar,但业务上传进来一个整型的.这个字段上存在索引时,索引也许是不可用的.
为何说也许呢?这取决于这种隐式的类型转换发生在了哪侧?是表字段侧,仍是业务传入数据侧?
整型和字符串比较,DB中和许多程序语言中的处理方式是同样的,都是字符串转换为整型后和整型比较.
因此表定义字段类型为BIGINT,但业务上传进来一个字符串,字段上的索引依然可用,由于隐式的类型转换发生在业务传入数据侧(这只能说是索引依然可用,没有大的性能影响,但隐式的类型转换照样是有性能损耗的,因此仍是一致的好)。


表定义字段类型为varchar,但业务上传进来一个整型,会致使索引不可用,全表扫描.由于隐式的类型转换发生在表字段侧。
建议可使用INT/BIGINT存储的,尽可能定义为INT/BIGINT,这样相对于长的纯数字字符串的VARCHAR定义,INT/BIGINT不只更节省空间(INT 4个字节,BIGINT 8个字节),性能更好;并且即便类型不匹配了,也不会致使索引不可用的问题.


还有表关联,关联字段上类型不一致,这种状况下,索引是否可用,是否存在严重的性能问题,取决于哪一个表是驱动表,哪一个表是被驱动表.这里不细论这个问题了.关联字段类型定义一致了,什么问题都没有.这也是表设计阶段须要注意的.
总结起来仍是一句话,类型一致了,什么问题都没有,不然可能存在严重的性能问题.


索引字段类型定义改变时的调整顺序
这里单独的说一下这个,由于业务上确实存在字段类型调整的状况,存在int/bigint和varchar定义转换的状况,若是这个字段上还存在着高效索引的话,必定要注意是业务代码侧先调整,仍是DB侧先调整,若是顺序弄反了,会致使这里提到的全表扫描问题的:
原定义为int/bigint,要修改成varchar的: 业务代码侧先调整,传入数据都按字符串处理,确认都调整完毕后,DB端再修改表定义.
原定义为varchar,要修改成 int/bigint的: DB端先修改表定义,DB端调整完毕,且确认从库也同步完毕以后,业务代码再调整,传入数据都按整型处理


再说一下区分大小写的字段比较
mysql的字符串比较默认是不区分大小写的.因此有些业务上为了严格匹配,区分大小写,在SQL中使用了binary,确实达到了区分大小写的目的,但致使索引不可用了(由于在字段侧进行了运算)

 

表定义中存在合适的索引  KEY `idx_app_name_status` (`appname`,`status`)
mysql> explain select * from tbl_rtlc_conf where binary appname='LbsPCommon' and status = 1; +----+-------------+---------------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table         | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+---------------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | tbl_rtlc_conf | ALL  | NULL          | NULL | NULL    | NULL | 9156 | Using where |
+----+-------------+---------------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

 

但由于binary的使用,致使了全表扫描.

那如何达到目的,又能高效呢?
mysql的字符串比较默认不区分大小写,是由于它们默认的collation是不区分大小写的

mysql> pager egrep -i "utf8|gbk|Default collation" PAGER set to 'egrep -i "utf8|gbk|Default collation"' mysql> show character set; | Charset  | Description                 | Default collation   | Maxlen |
| gbk      | GBK Simplified Chinese      | gbk_chinese_ci      |      2 |
| utf8     | UTF-8 Unicode               | utf8_general_ci     |      3 |
| utf8mb4  | UTF-8 Unicode               | utf8mb4_general_ci  |      4 |

 

gbk,utf8 字符集默认的collation分别为gbk_chinese_ci,utf8_general_ci, caseignore 它们都是忽略大小写的,致使字符串比较默认不区分大小写了.


区分大小写,且索引可用
解决的方案就是修改特定表/字段的collation,表collation的修改会影响到这个表的全部字段,因此通常都是只修改特定目标字段的collation
表字符集为utf8的话:
appname varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT 'appname'
表字符集为gbk的话:
appname varchar(255) CHARACTER SET gbk COLLATE gbk_bin NOT NULL COMMENT 'appname'
同时保证SQL中没有包含binary, 这样既达到了严格匹配的目的(utf8_bin ,gbk_bin这2个collation都是严格匹配的),也保证了索引的可用性.

 

(5). 不要使用%xxx%这种模糊匹配,会致使全表扫描/索引全扫描

where name like '%zhao%' 这种先后统配的模糊查询,会致使索引不可用,全表扫描,稍好的状况是,能使用覆盖索引的话,是索引全扫描,但也高效不了.
若是确实存在这样高频执行的模糊匹配的业务需求,建议走全文检索系统,不要使用MySQL来作这个事情.
但其实不少业务,使用模糊匹配是带有很大的随意性的,彻底能够改成精确匹配,从而使用字段上的索引快速定位数据的.

另外where name like 'xxx%'这种,不前统配,只后统配的,确实是可使用索引的.
但它实际上是一个范围匹配,下文会提到,这种范围匹配(非等值匹配)会致使后面的索引字段不能(高效)使用,会致使索引不能用于避免物理排序等问题.
因此仍是要谨慎使用,若是能够改成精确匹配的话,仍是建议使用精确匹配的好.

(6). 关于前缀索引和冗余索引

index(a,b,c) 能同时优化下面几类查询:
where a=? and b=? and c=?
where a=? and b=?
where a=?
也能优化以下的排序查询:
where a=? order by b[,c] limit 
where a=? and b=? order by c limit 
但不能优化 where b=? and c=? 由于索引定义index(a,b,c) 的前缀列a没有出如今where条件中.
更不能优化where c=?

对于where a=? and c=? 查询,它只能使用index(a,b,c)的第1个索引字段a.

因此,若是业务查询为以下2类:
where b=? and c=?
where c=?
**那么就应该定义索引为index(c,b),它能同时优化上面2类查询 **,而不该该定义索引index(b,c)的,由于索引index(b,c)优化不了where c=? 由于这个索引的前缀列b没有出如今where条件中.
也不建议建立2个索引: index(b,c) 和index(c) 由于前面提到了索引越少越好,能够用一个index(c,b) 来完成的,就不要建立2个索引来完成.

在存在索引index(a,b,c)的状况下,绝大多数状况下,下面的这些索引就冗余了,能够DROP掉的:
index(a)
index(a,b)
上面提到了,这2个索引能优化的查询,index(a,b,c)绝大多数状况下也都能优化,因此它们就冗余了,本着索引越少越好的原则,均可以DROP掉的.

上面提到了绝大多数状况下,冗余了,能够DROP了,但也存在例外的状况,它们的存在仍是必要的:
那就是存在下面的查询:
where a=? order by id limit
这里index(a) ( 实际为index(a,id) ) 能够优化上面的查询,经过使用这个索引,避免物理排序而达到排序的实际效果.
但index(a,b,c) ( 实际为index(a,b,c,id) ) 和index(a,b) (实际为index(a,b,id)) 却达不到这样的效果.
这种状况下,存在index(a,b,c)的状况下,index(a) 是不冗余的,是须要保留的.
若是不存在这种状况,存在index(a,b,c)的状况下,index(a) ,index(a,b) 都是冗余的,建议drop掉.

但若是where a=? 后返回的数据行已经不多,也就是说对不多的数据进行order by id排序的话,也是可使用index(a,b)或者index(a,b,c) 来过滤行的,只不过还须要进行物理排序,但代价已经很小了,是否还须要建立一个index(a)须要业务折中考虑了.

(7). 关于索引定义中的字段顺序

建议where条件中等值匹配的字段放到索引定义的前部,范围匹配的字段(> < between in等为范围匹配)放到索引定义的后面.
由于前缀索引字段使用了范围匹配后,会致使后续的索引字段不能高效的用于优化查询.
来看一个例子:

mysql> show create table opLog\G *************************** 1. row ***************************
       Table: opLog Create Table: CREATE TABLE `opLog` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', `listId` int(11) unsigned NOT NULL COMMENT '对应id', `listType` varchar(255) NOT NULL COMMENT '对应类型', `opName` varchar(255) NOT NULL COMMENT '操做人id', `operation` varchar(255) NOT NULL COMMENT '具体操做', `content` varchar(255) NOT NULL COMMENT '内容', `createTime` int(10) NOT NULL COMMENT '时间', PRIMARY KEY (`id`), KEY `idx_opName_createTime` (`opName`,`createTime`), KEY `idx_createTime_opName` (`createTime`,`opName`) ) ENGINE=InnoDB AUTO_INCREMENT=2515923 DEFAULT CHARSET=utf8 COMMENT='操做记录表'

 

查询2017-04-23到2017-05-23 这一个月内某个op发起的操做数量:
select sql_no_cache count(1) from opLog where opName='zhangyu21' and createTime between 1492876800 and 1495468800;
+----------+
| count(1) |
+----------+
|        0 |
+----------+
 
这1个月内共有2.2W次的操做记录,对应2.2W行记录.
mysql> select count(1) from opLog  where createTime between 1492876800 and 1495468800;         
+----------+
| count(1) |
+----------+
|    22211 |
+----------+
 
我下面使用force index的hint强制走某个索引:
# Query_time: 0.009124  Lock_time: 0.000093 Rows_sent: 1  Rows_examined: 22211
select sql_no_cache count(1) from opLog force index(idx_createTime_opName) where opName='zhangyu21' and createTime between 1492876800 and 1495468800; 
 
# Query_time: 0.000220  Lock_time: 0.000077 Rows_sent: 1  Rows_examined: 0
select sql_no_cache count(1) from opLog force index(idx_opName_createTime) where opName='zhangyu21' and createTime between 1492876800 and 1495468800;
 
能够看到第1个SQL,强制走KEY `idx_createTime_opName`(`createTime`,`opName`)时,检索的行数是22211行,这个行数恰好是这个时间段内的总行数.为何是这样呢?
由于在前缀索引字段createTime上使用了范围匹配,因此致使索引定义中后面的字段opName不能做为高效的检索字段(Access),只能做为低效的过滤字段(Filter)了.
(在5.6推出ICP以前,这一点都很难知足,致使范围匹配后的索引字段基本是无用的)
说白了,就是说索引上定位到createTime的起止,对期间的索引条目一行行的检查是否知足opName='zhangyu21'的条件,知足的返回.
而第2个SQL,强制走KEY `idx_opName_createTime` (`opName`,`createTime`)时,这2个索引字段都是能够做为高效的Access条件的.
经过索引定位到opName='zhangyu21',createTime =1492876800 条目,向后扫描,直至opName='zhangyu21',createTime>1495468800或者opName!='zhangyu21'为止.
它是至关高效的,扫描的条目就是返回的条目.
没有带force index这类hint的话,mysql优化器会默认使用idx_opName_createTime这个索引.

(8). 关于排序查询的优化

前面提到了index(a,b) 逻辑上是有序的,因此能够用于优化where a=? order by b [asc/desc] [limit n] 特别是对这种topN操做的优化效果很是好.

mysql> show create table opLog\G *************************** 1. row ***************************
       Table: opLog Create Table: CREATE TABLE `opLog` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', `listId` int(11) unsigned NOT NULL COMMENT '对应id', `listType` varchar(255) NOT NULL COMMENT '对应类型', `opName` varchar(255) NOT NULL COMMENT '操做人id', `operation` varchar(255) NOT NULL COMMENT '具体操做', `content` varchar(255) NOT NULL COMMENT '内容', `createTime` int(10) NOT NULL COMMENT '时间', PRIMARY KEY (`id`), KEY `idx_opName_createTime` (`opName`,`createTime`) ) ENGINE=InnoDB AUTO_INCREMENT=2515923 DEFAULT CHARSET=utf8 COMMENT='操做记录表' mysql> select count(1) from opLog where opName=''; +----------+
| count(1) |
+----------+
|  2511443 |
+----------+
1 row in set (1.08 sec)

 

一共有251W的匿名用户,要查找他们最近的5个操做记录:
# Query_time: 0.001566  Lock_time: 0.000084 Rows_sent: 5  Rows_examined: 5
select * from opLog where opName='' order by createTime desc limit 5;
  
从实际执行的统计信息看,它并无扫描出251W的记录,排序,最终输出5条记录,而是只扫描了5条记录,就直接输出了,执行时间很短的.
看一下执行计划:
mysql> explain select * from opLog where opName='' order by createTime desc limit 5;
+----+-------------+-------+------+-----------------------+-----------------------+---------+-------+---------+-------------+
| id | select_type | table | type | possible_keys         | key                   | key_len | ref   | rows    | Extra       |
+----+-------------+-------+------+-----------------------+-----------------------+---------+-------+---------+-------------+
|  1 | SIMPLE      | opLog | ref  | idx_opName_createTime | idx_opName_createTime | 767     | const | 1252639 | Using where |
+----+-------------+-------+------+-----------------------+-----------------------+---------+-------+---------+-------------+
1 row in set (0.01 sec)
sql中有order by,但执行计划的Extra部分并无出现Using filesort,说明经过KEY `idx_opName_createTime` (`opName`,`createTime`)这个索引达到了排序的效果,但避免了物理排序的操做.(rows部分的估算值能够忽略呀)
若是没有这个索引,就真的须要检索出251W记录(如何检索出这些记录,取决于其余的索引,若是没有合适的索引,可能须要全表扫描),对他们进行物理排序,并输出须要的5行记录.执行代价很大,执行时间很长.
但这里经过索引,利用索引自己的逻辑有序性,避免了物理排序操做,快速的返回了topN行记录.
mysql> show create table opLog\G *************************** 1. row ***************************
       Table: opLog Create Table: CREATE TABLE `opLog` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', `listId` int(11) unsigned NOT NULL COMMENT '对应id', `listType` varchar(255) NOT NULL COMMENT '对应类型', `opName` varchar(255) NOT NULL COMMENT '操做人id', `operation` varchar(255) NOT NULL COMMENT '具体操做', `content` varchar(255) NOT NULL COMMENT '内容', `createTime` int(10) NOT NULL COMMENT '时间', PRIMARY KEY (`id`), KEY `idx_opName_listType_createTime` (`opName`,`listType`,`createTime`) ) ENGINE=InnoDB AUTO_INCREMENT=2515923 DEFAULT CHARSET=utf8 COMMENT='操做记录表' 仍是上面的表数据,我修改了一下表的索引结构. mysql> select count(1) from opLog where opName=''; +----------+
| count(1) |
+----------+
|  2511443 |
+----------+
1 row in set (0.91 sec) 

 

# Query_time: 3.188810  Lock_time: 0.000088 Rows_sent: 1  Rows_examined: 2511444
select * from opLog where opName='' and listType in ('cronJob','cronJobNew') order by createTime desc limit 1;

 

从执行统计信息看,这个查询并无经过索引快速的返回结果.
mysql> explain select * from opLog where opName='' and listType in ('cronJob','cronJobNew') order by createTime desc limit 1; +----+-------------+-------+------+--------------------------------+--------------------------------+---------+-------+---------+----------------------------------------------------+
| id | select_type | table | type | possible_keys                  | key                            | key_len | ref   | rows    | Extra                                              |
+----+-------------+-------+------+--------------------------------+--------------------------------+---------+-------+---------+----------------------------------------------------+
|  1 | SIMPLE      | opLog | ref  | idx_opName_listType_createTime | idx_opName_listType_createTime | 767     | const | 1252640 | Using index condition; Using where; Using filesort |
+----+-------------+-------+------+--------------------------------+--------------------------------+---------+-------+---------+----------------------------------------------------+

 

执行计划来看,仍是有Using filesort,仍是须要物理排序的. 为何不能经过这个索引避免物理排序,快速的返回结果呢?
缘由就在于listType in ('cronJob','cronJobNew')  在这个索引字段上使用了范围匹配,从而致使索引层面上总体再也不有序了.

在排序字段前的全部索引字段上都必须是等值匹配,才能经过索引保证有序性,才能经过索引避免物理排序,快速的返回结果.

因此上面的查询必须改造为等效的等值匹配才能够经过索引快速的返回结果的:
mysql> select *
    -> from
    -> ( -> select * from opLog where opName='' and listType = 'cronJob' order by createTime desc limit 1
    -> union all
    -> select * from opLog where opName='' and listType = 'cronJobNew' order by createTime desc limit 1
    -> ) tmp -> order by createTime desc limit 1; ERROR 1221 (HY000): Incorrect usage of UNION and ORDER BY

 

这样还不行,必须再嵌套个外层,使用临时表才能够的:
select *
from ( select * from ( select * from opLog where opName='' and listType = 'cronJob' order by createTime desc limit 1 ) tmp_1 union all
    select * from ( select * from opLog where opName='' and listType = 'cronJobNew' order by createTime desc limit 1 ) tmp_2 ) tmp order by createTime desc limit 1;

 

这样就能够了.
  
改造后的SQL对应的执行统计信息以下:
# Query_time: 0.000765  Lock_time: 0.000332 Rows_sent: 1  Rows_examined: 4
通过改造为等效的等值匹配,使用索引避免了大的物理排序操做,快速的返回告终果.

说到经过索引优化排序查询,特别是TOPN操做,必须说一下MySQL在优化器层面的一个问题:
就是说在遇到order by时,myql会优先选择一个能够避免物理排序的索引来优化这个查询,有时候,这种优先选择是不合理的,会致使性能不好.
(特别在涉及到order by id limit N, 这里id是primary key,优化器选择使用PRIMARY KEY来避免物理排序时尤为要注意是否合理了)

mysql> show create table layer\G *************************** 1. row ***************************
       Table: layer Create Table: CREATE TABLE `layer` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'layer的id', `uuid` varchar(255) NOT NULL COMMENT 'layer的惟一标识', `type` tinyint(4) NOT NULL COMMENT 'layer的类型', `status` tinyint(4) NOT NULL COMMENT 'layer的状态', `app_id` bigint(20) NOT NULL COMMENT 'layer所属的app id', `src` varchar(1024) NOT NULL COMMENT 'layer的源地址', `oais_src` varchar(1024) NOT NULL DEFAULT '' COMMENT 'layer存在于oais的地址', `cmd` varchar(1024) NOT NULL DEFAULT '' COMMENT 'layer执行的命令', `skip_download` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默认为0,不跳过中转', `extra` text NOT NULL COMMENT 'layer的额外信息', `create_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'layer建立时间戳', `last_update_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'layer更新时间戳', `finish_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'layer完成时间戳', `merge_latest_layer_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '该baseLayer最新merge的layerid', PRIMARY KEY (`id`), KEY `idx_uuid` (`uuid`), KEY `idx_app_id` (`app_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2866980 DEFAULT CHARSET=utf8 COMMENT='layer表' # Query_time: 2.586674  Lock_time: 0.000084 Rows_sent: 1  Rows_examined: 1986479
SELECT * FROM `layer`  WHERE (app_id = 2183) ORDER BY `layer`.`id` ASC LIMIT 1; 输出的ID:1998941 # Query_time: 1.442171  Lock_time: 0.000071 Rows_sent: 1  Rows_examined: 1095035
SELECT * FROM `layer`  WHERE (app_id = 139) ORDER BY `layer`.`id` ASC LIMIT 1; 输出的ID: 1107497 # Query_time: 0.597380  Lock_time: 0.000077 Rows_sent: 1  Rows_examined: 464929
SELECT * FROM `layer`  WHERE (app_id = 1241) ORDER BY `layer`.`id` ASC LIMIT 1; 输出的ID:465532 mysql> explain SELECT sql_no_cache* FROM `layer`  WHERE (app_id = 2183) ORDER BY `layer`.`id` ASC LIMIT 1; +----+-------------+-------+-------+-----------------------------+---------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys               | key     | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+-----------------------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | layer | index | idx_app_id | PRIMARY | 8       | NULL |  151 | Using where |
+----+-------------+-------+-------+-----------------------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)

 

能够看到扫描的数据行数是大不一样的.为何呢? 源于它的执行计划,使用了PRIMARY KEY (`id`)来避免物理排序操做.
说白了,就是顺着PRIMARY KEY (`id`)的索引链表,从小往大扫描,找到第1条知足app_id = ?的记录就返回了.
因此执行的时间长短,扫描的记录行数的多少,彻底取决于app_id = ? 的整体数据量,数据分布状况.若是查找1个不存在的app_id最终的结果是扫描了整个表的数据行,也没有找到数据,返回0行记录,执行时间确定长.
下面也能够验证这1点:
mysql> SELECT count(1) FROM `layer`  WHERE (app_id = 2183)  and id<1998941; +----------+
| count(1) |
+----------+
|        0 |
+----------+
1 row in set (0.01 sec) mysql> SELECT count(1) FROM `layer`  WHERE id<=1998941; +----------+
| count(1) |
+----------+
|  1986479 |
+----------+
1 row in set (0.79 sec) 就是检索的数据行数 mysql> SELECT count(1) FROM `layer`  WHERE (app_id = 139)  and id<1107497; +----------+
| count(1) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec) mysql> SELECT count(1) FROM `layer`  WHERE id<= 1107497; +----------+
| count(1) |
+----------+
|  1095035 |
+----------+
1 row in set (0.43 sec) 就是检索的数据行数 mysql> SELECT count(1) FROM `layer`  WHERE (app_id = 1241)  and id<465532; +----------+
| count(1) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec) mysql> SELECT count(1) FROM `layer`  WHERE id<= 465532; +----------+
| count(1) |
+----------+
|   464929 |
+----------+
1 row in set (0.18 sec)  就是检索的数据行数

 

这里虽然经过索引避免了物理排序,但扫描的行数很大,实际执行时间很长,执行效果不好.
那这个SQL应该如何优化呢?
KEY idx_app_id (app_id) 等价于index(app_id,id) 彻底能够经过它来高效的返回前N行记录呀.但由于MySQL默认不选择它,只能使用force index这个hint来强制mysql选择这个索引了.

mysql> explain SELECT * FROM `layer` force index(idx_app_id) WHERE (app_id = 1241) ORDER BY `layer`.`id` ASC LIMIT 1; +----+-------------+-------+------+---------------+------------+---------+-------+--------+-------------+
| id | select_type | table | type | possible_keys | key        | key_len | ref   | rows   | Extra       |
+----+-------------+-------+------+---------------+------------+---------+-------+--------+-------------+
|  1 | SIMPLE      | layer | ref  | idx_app_id    | idx_app_id | 8       | const | 111142 | Using where |
+----+-------------+-------+------+---------------+------------+---------+-------+--------+-------------+
1 row in set (0.00 sec) 表名后跟 force index(idx_app_id) 提示mysql强制选择这个索引. 经过这个索引也是能够避免物理排序的,并且真的能够快速的返回结果(即便这个app_id不存在,也会快速返回结果) # Query_time: 0.000213  Lock_time: 0.000082 Rows_sent: 1  Rows_examined: 1
SELECT * FROM `layer` force index(idx_app_id) WHERE (app_id = 2183) ORDER BY `layer`.`id` ASC LIMIT 1; # Query_time: 0.000202  Lock_time: 0.000075 Rows_sent: 1  Rows_examined: 1
SELECT * FROM `layer`force index(idx_app_id) WHERE (app_id = 139) ORDER BY `layer`.`id` ASC LIMIT 1; # Query_time: 0.000222  Lock_time: 0.000075 Rows_sent: 1  Rows_examined: 1
SELECT * FROM `layer`force index(idx_app_id) WHERE (app_id = 1241) ORDER BY `layer`.`id` ASC LIMIT 1;

 

使用force index 这个hint强制走某个索引后,真的高效返回了.
在遇到MYSQL蒙圈,选择错误的执行计划时,须要使用一些hint给mysql一些提示,使用频率较高的hint有:
force index(index_name) 强制走某个索引
ignore index(index_name) 建议忽略某个索引不使用
但通常不建议使用这种hint,缘由以下:
hint是和索引名称而不是索引字段绑定的,之后存在着很大的风险,把索引更名了,会致使提示无效的.
业务存在拼接SQL的状况下,代码考虑不周全,会致使一些不该该使用这种HINT的SQL也使用了这种HINT,致使它们的执行计划变差.
随着版本的升级,优化器的提高,数据量,数据分布特色的变化,MYSQL本能够选择更好的执行计划,但由于HINT致使MYSQL不能选择更好的执行计划.
因此使用这些提示前,请先和DBA沟通,也要进行详尽的测试,确认HINT的引入只带来了益处,没有带来坏处.

(9). 关于单列索引和复合索引

有时候会看到业务SQL是where a=? and b=? and c=? 
但3个列上分别建立了一个单列索引:
index(a) index(b) index(c)
这种建立是否合理呢?
前面提到高选择性字段上适合建立索引,低选择性字段上不适合建立单列索引(但能够考虑做为复合索引定义的一部分)
**若是a字段上的选择性足够高,b,c的选择性低,彻底能够只建立索引index(a) **, 这种状况下,固然也能够只建立index(a,b) 或者只建立index(a,b,c). (不要建立index(b), index(c) 这2个低选择性字段上的单列索引了).
须要考虑到index(a,b) index(a,b,c) 相对于index(a),提高的收益并不大,但可能空间占用却大出很多去,须要业务在时空的矛盾中作出平衡,看建立哪一个索引更合适.

若是实际状况是a,b,c单独的选择性通常,都不是很高,但3个组合到一块儿的选择性很高的话,那就建议建立index(a,b,c)的组合索引,不要3个字段上都建立一个单列索引.
为何呢? mysql确实可使用index merge来使用多个索引,但不少时候是否比得上复合索引效率高呢?
简化一下: where a=? and b=?

a=? 返回1W行记录, b=? 返回1W行记录, where a=? and b=? 返回100行记录.
若是是两个单列索引: index(a) index(b) 的状况下,index_merge会是一个什么样的执行计划呢?
针对a=? 经过使用index(a) 返回1W行记录,带PRMIARY KEY
针对b=? 经过使用index(b) 返回1W行记录,带PRMIARY KEY
而后对primary key 取交集,无论是排序后取交集也好,仍是经过嵌套循环,关联的方式取交集也好.都会是一个耗时耗费资源的操做.
综合来讲,扫描各自的索引返回1W行记录,然后对这2W行记录取交集,确定是一个耗时耗费资源的操做了.
但若是存在复合索引index(a,b) 经过索引的扫描定位,能够快速的返回这100行记录的.
因此针对这种状况,建议建立复合索引,不要建立多个单列索引.

补充说一下:
where a=? or b=? 这种查询, a列,b列上的选择性都很高,这时候须要index(a) index(b),缺乏一个,都会致使全表扫描的.

(10). 关于多表关联

ORACLE中有三种主要的表关联方式:NESTED LOOP , HASH JOIN 和 SORT MERGE JOIN
其中最经常使用的仍是前两种,ORACLE的优化器会根据统计获得的表行数,数据分布状况等信息,对各类关联方式,关联顺序下的多个执行计划进行评估,分别计算它们的cost,最后选择一个cost最低(优化器认为的最优)执行计划做为最终的执行计划去执行.
至少到mysql官方的5.6版本,依然只有NESTED LOOP(嵌套循环)这样一种关联方式.
NESTED LOOP说白了就是FOR循环实现:

好比说针对下面的关联查询: select a.*, b *  
from EMP a,DEPT b where a.DEPTNO = b.DEPTNO; 它的嵌套循环的伪代码大意是这样的: declare 
begin 
  for outer_table in (select * from dept) loop for inner_table in (select * 
                          from emp where DEPTNO = outer_table.DEPTNO) loop dbms_output.put_line(inner_table.*, outer_table.*); end loop; end loop; end;

 

NESTED LOOP的适用场景是什么?
外表(驱动表)通过过滤后返回较少的数据行(最好也能够经过索引快递的定位这些数据行,和表自己的数据行多少无关,只要求通过条件的过滤后返回较少的数据行),而内表(被驱动表)在表的关联字段上存在着高效的索引可用.
由于这种状况下,FOR循环的代价是小的,是适用NESTED LOOP的.
其它状况,使用NESTED LOOP都不合适,好比内外表通过过滤后都返回上万行甚至数十万,百万的记录,这种状况下,FOR循环的成本过高了(其实这种状况下,HASH JION是适用的)
由于这个缘由(固然还有其它缘由了,好比说mysql没有bitmap index等),mysql不适合作OLAP系统,不适合作复杂的多表关联:
多表关联,关联的表越多,返回的行数越多,他们做为外表,FOR循环的成本会愈来愈高,执行时间愈来愈长,很容易就超过业务设置的读超时时间,或者超过DB端设置的超时时间,稍微来点儿并发,就可能会耗尽DB的资源,会致使雪崩,DB响应不了任何的业务请求.
因此不建议在MySQL上进行复杂的多表关联查询,低频,基本无并发的查询,能够在线下库进行;执行频率稍高,存在并发的,就必须到hadoop,hbase等环境进行了.
由于mysql的表关联实现就是for循环,因此简单的表关联,业务也能够本身for循环实现.

4、慢查询日志的分析以及关注点

(1). 使用pt-query-digest工具来统计

可使用percona公司的开源工具pt-query-digest来进行统计,它能够支持多种类型日志文件的分析,包括binlog,genlog,slowlog,tcpdump的输出进行统计.默认就是对slowlog进行分析的.
它也支持多种过滤条件,好比说执行时间,检索行数等的过滤输出,也支持过滤后裸数据的输出,支持多种聚合排序输出.
通常使用最简单的调用形式便可,都使用默认定义:
/usr/local/bin/pt-query-digest slow.log > slow.log.fenxi
slow.log 是待分析的慢查询日志文件,将分析的结果重定向到文件slow.log.fenxi中.
它是去除字面值后对SQL进行分类汇总,而后按照每类SQL总的执行时间降序排序输出的.而且每类SQL都给出了一个字面值SQL(期间执行时间最长的SQL).

(2). 对统计输出进行分析

咱们通常重点分析执行时间占比大的SQL,也就是前排的一些SQL,它们的执行时间长,系统资源消耗大,对业务的影响也大.
以一个输出为例:

# Profile # Rank Query ID Response time Calls R/Call  V/M Item # ==== ================== =============== ===== ======= ===== ============ # 1 0x426D0452190D3B9C 9629.1622 55.8%  5204  1.8503  0.01 SELECT queue_count # 2 0x52A6A31F2F3F0692 2989.7074 17.3%  2224  1.3443  0.03 SELECT server_info # 3 0x959209F179E16B2A  819.3819  4.8%   759  1.0796  0.00 SELECT server_info

 

第1类SQL总共耗时9629s,总的执行时间占日志中全部SQL执行时间的55.8%,在慢查询日志中出现了5204次,平均每次执行耗时为1.85s
下面有这类SQL的详尽信息,显示的字面值SQL是其中执行时间最长的SQL

# Query 1: 0.11 QPS, 0.20x concurrency, ID 0x426D0452190D3B9C at byte 4615533 # This item is included in the report because it matches --limit.
# Scores: V/M = 0.01 # Time range: 2017-05-24 23:56:03 to 2017-05-25 13:37:15 # Attribute pct total min     max     avg     95% stddev median # ============ === ======= ======= ======= ======= ======= ======= ======= # Count         52    5204 # Exec time     55 9629s 2s 3s 2s 2s 110ms 2s # Lock time 23 185ms 20us 23ms 35us 40us 331us 25us # Rows sent 0   5.65k       0       2    1.11    1.96    0.45    0.99 # Rows examine 83  18.54G   3.65M   3.65M   3.65M   3.50M       0   3.50M # Query size 20 665.75k     131     131     131     131       0     131 # String: # Databases queue_center # Hosts 10.36.31.52 (696/13%), 10.36.31.31 (694/13%)... 6 more # Users queue_center_w # Query_time distribution # 1us # 10us # 100us # 1ms # 10ms # 100ms # 1s ################################################################ # 10s+ # Tables # SHOW TABLE STATUS FROM `queue_center` LIKE 'queue_count'\G # SHOW CREATE TABLE `queue_center`.`queue_count`\G # EXPLAIN /*!50100 PARTITIONS*/
select * from `queue_count` where `app_id` = '1' and `created_at` > '2017-05-25 11:42:01' and `created_at` <= '2017-05-25 11:43:01'\G

 

咱们重点关注avg,95分位的 #Rows examine 和 # Rows sent
Rows examine / Rows sent 对非聚合SQL而言,表明返回1行数据所要检索的数据行数, 1 是想要的效果.
Rows examine检索行数偏大的,若是同时Rows sent返回的数据行数不多(聚合函数除外),通常是能够经过索引优化的。
对于update/delete类的写操做,慢查询日志中Rows_examined仍是SQL执行过程当中检索的行数,Rows_sent: 0 没有意义,慢查询日志中没有体现出来匹配/影响的行数来。
若是写操做Rows_examined很大,同时匹配/影响的行数极少,通常是全表扫描,写操做过程当中持有表锁,影响并发的,并且执行时间长,容易致使同步延迟。但实际上是能够经过索引优化这类写操做的。
若是Rows examin,Rows sent都很小,但整体执行时间长的话,特别是读取操做,极可能是受其它慢查询影响的,能够暂时先无论,把其它慢查询优化完毕以后,这类慢查询极可能也就消失了。
像上面这个SQL,3.65M/1.11 = 3.29M,也就是说平均须要扫描329W行数据才能返回1行记录,过低效了.
表结构中除了主键ID外没有任何的索引,其实业务都是查询最近1分钟内的数据,确实能够经过index(app_id,created_at)或者index(created_at)来优化这类查询。

5、几个优化案例

优化案例1

mysql> show create table lc_day_channel_version\G *************************** 1. row ***************************
       Table: lc_day_channel_version Create Table: CREATE TABLE `lc_day_channel_version` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `prodline` varchar(50) NOT NULL DEFAULT '' COMMENT '产品线标识', `os` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '平台类型,1:Android_Phone 2:Android_Pad 3:IPhone', `original_type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '母包类型,1主线 3非主线', `dtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'date time,yyyymmdd', `version_name` varchar(50) NOT NULL DEFAULT '' COMMENT '来源版本号', `channel` varchar(50) NOT NULL DEFAULT '' COMMENT '渠道号', `request_pv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '请求量', `request_uv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '请求用户量', `response_pv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '请求成功量', `response_uv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '请求成功用户量', `download_pv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '下载量', `download_uv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '下载用户量', PRIMARY KEY (`id`), UNIQUE KEY `UNIQUE_poouvc` (`prodline`,`os`,`original_type`,`dtime`,`version_name`,`channel`), KEY `INDEX_d` (`dtime`) ) ENGINE=InnoDB AUTO_INCREMENT=135293125 DEFAULT CHARSET=utf8 COMMENT='升级版本渠道汇总信息'
1 row in set (0.00 sec) mysql> explain select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime>=20170504 and dtime<=20170510 group by version_name order by request_pv desc; +----+-------------+------------------------+-------+-----------------------+---------+---------+------+---------+--------------------------------------------------------+
| id | select_type | table                  | type  | possible_keys         | key     | key_len | ref  | rows    | Extra                                                  |
+----+-------------+------------------------+-------+-----------------------+---------+---------+------+---------+--------------------------------------------------------+
|  1 | SIMPLE      | lc_day_channel_version | range | UNIQUE_poouvc,INDEX_d | INDEX_d | 4       | NULL | 2779470 | Using index condition; Using temporary; Using filesort |
+----+-------------+------------------------+-------+-----------------------+---------+---------+------+---------+--------------------------------------------------------+
1 row in set (0.00 sec) 

 

业务反馈执行上面的SQL,有索引可用呀,为何还这么慢呢?
  
问题在于:
mysql> select count(1) from lc_day_channel_version where dtime>=20170504 and dtime<=20170510; +----------+
| count(1) |
+----------+
|  1462991 |
+----------+
1 row in set (0.58 sec)

 

对应146W记录,使用index(dtime),须要回访表获取version_name,request_pv字段,这样要对应146W的随机IO + 扫描的索引块数量的随机IO,
然后还要对这146W的结果集 group by version_name order by request_pv desc,代价仍是很高的. 屡次测试执行4.7s左右.
一种优化方案就是走覆盖索引,避免回访表:alter table lc_day_channel_version add key idx_dtime_version_name_request_pv(dtime,version_name,request_pv);
再看执行计划:
mysql> explain select sql_no_cache version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime>=20170504 and dtime<=20170510 group by version_name order by request_pv desc; +----+-------------+------------------------+-------+---------------------------------------------------------+-----------------------------------+---------+------+---------+-----------------------------------------------------------+
| id | select_type | table                  | type  | possible_keys                                           | key                               | key_len | ref  | rows    | Extra                                                     |
+----+-------------+------------------------+-------+---------------------------------------------------------+-----------------------------------+---------+------+---------+-----------------------------------------------------------+
|  1 | SIMPLE      | lc_day_channel_version | range | UNIQUE_poouvc,INDEX_d,idx_dtime_version_name_request_pv | idx_dtime_version_name_request_pv | 4       | NULL | 2681154 | Using where; Using index; Using temporary; Using filesort |
+----+-------------+------------------------+-------+---------------------------------------------------------+-----------------------------------+---------+------+---------+-----------------------------------------------------------+
1 row in set (0.00 sec)

 

Using index 已经不须要回访表了,总体的执行时间也下降了一半,平均执行时间为2.35s左右.
  
再继续优化下去,可否避免对这么大量的数据(146W行记录)进行排序操做呀? 能利用索引避免排序操做吗? index(dtime,version_name,request_pv)为何不能避免物理排序操做呢?(Using filesort显示确实存在物理排序动做) 
缘由就在于dtime上使用了范围匹配,使得索引数据总体上再也不有序了. 那我改为等值匹配看看呢?
mysql> explain select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170504 group by version_name; +----+-------------+------------------------+------+---------------------------------------------------------+-----------------------------------+---------+-------+--------+--------------------------+
| id | select_type | table                  | type | possible_keys                                           | key                               | key_len | ref   | rows   | Extra                    |
+----+-------------+------------------------+------+---------------------------------------------------------+-----------------------------------+---------+-------+--------+--------------------------+
|  1 | SIMPLE      | lc_day_channel_version | ref  | UNIQUE_poouvc,INDEX_d,idx_dtime_version_name_request_pv | idx_dtime_version_name_request_pv | 4       | const | 402616 | Using where; Using index |
+----+-------------+------------------------+------+---------------------------------------------------------+-----------------------------------+---------+-------+--------+--------------------------+
1 row in set (0.00 sec)

 

确实没有出现Using filesort,说明经过这个索引能避免物理排序操做.
固然,业务逻辑仍是不能变的,最终最初的SQL能够修改成以下等效的SQL:
select version_name,sum(request_pv) as request_pv from ( select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170504 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170505 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170506 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170507 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170508 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170509 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170510 group by version_name ) tmp group by version_name order by request_pv desc;

 

天天对应21W左右的记录,天天的记录group by后对应2500行左右的记录,这样就将原来对146W记录排序,变成了对2500*7 行记录排序,排序量大幅降低,因此执行时间也有了提高,如今执行时间已经变为1.01s左右了
  
单纯从SQL的角度优化,彷佛只能优化到这个地步了.而实际的业务SQL要比上面的还要复杂多变,好比说:
select version_name, sum(request_uv) as request_uv from `lc_day_channel_version` where `dtime` >= 20170505 and `dtime` <= 20170511
group by version_name order by request_uv desc; select sql_calc_found_rows version_name, channel, sum(request_pv) as request_pv, sum(request_uv) as request_uv, sum(response_pv) as response_pv, sum(response_uv) as response_uv, sum(download_pv) as download_pv, sum(download_uv) as download_uv from `lc_day_channel_version` where `dtime` >= 20170505 and `dtime` <= 20170511
group by version_name, channel order by request_uv desc limit 0, 15
 
select version_name, sum(response_pv) as response_pv from `lc_day_channel_version` where `dtime` >= 20170505 and `dtime` <= 20170511
group by version_name order by response_pv desc
 
select sql_calc_found_rows version_name, channel, sum(request_pv) as request_pv, sum(request_uv) as request_uv, sum(response_pv) as response_pv, sum(response_uv) as response_uv, sum(download_pv) as download_pv, sum(download_uv) as download_uv from `lc_day_channel_version` where `dtime` >= 20170505 and `dtime` <= 20170511
 group by version_name, channel order by response_pv desc limit 0, 15

 

针对每类SQL都添加一个对应的索引? 那索引太多了,会严重影响写入性能的.
index(dtime,version_name,全部的统计项字段)
index(dtime,version_name,channel,全部的统计项字段)
这样全家桶式的索引,包含了全部的统计项字段,问题是每一个索引太大了.
  
不要光想着SQL优化,其实最大的杀手锏: 业务优化尚未考虑呢。 那业务层面是否有优化的空间呢? 固然是有的,并且优化空间还不小.
天天的统计数据在插入后,基本就再也不变更了.天天插入21W左右的记录,天天的数据统计后也就是2000多行的记录,这样在天天凌晨对前1天的数据进行异步统计,
统计结果放到一个中间表中去,天天的这种统计报表,再也不扫描原始数据表,而扫描这类中间表,天天扫描的记录行数能够减小到1/100的数量级,再配以SQL层面的优化才是王道呀!

优化案例2

mysql> show create table mc_state\G *************************** 1. row ***************************
       Table: mc_state Create Table: CREATE TABLE `mc_state` ( `state_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '机器的状态ID', `transaction_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '维修周期ID', `ip` int(10) unsigned NOT NULL COMMENT '机器的IP', `state_name` varchar(255) NOT NULL COMMENT '状态名称', `start_time` datetime NOT NULL COMMENT '状态开始时间', `update_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '状态信息的更新时间', `end_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '状态结束时间', `create_ip` int(10) unsigned NOT NULL COMMENT '建立该状态的IP', `error_status` text NOT NULL COMMENT '机器当前的状态错误信息(JSON)', PRIMARY KEY (`state_id`), KEY `idx_name` (`state_name`), KEY `idx_time` (`start_time`), KEY `idx_transaction_id` (`transaction_id`), KEY `idx_ip` (`ip`), KEY `idx_end_time` (`end_time`) ) ENGINE=InnoDB AUTO_INCREMENT=7614257 DEFAULT CHARSET=utf8 COMMENT='机器维修状态表' mysql> show create table mc_machine\G *************************** 1. row ***************************
       Table: mc_machine Create Table: CREATE TABLE `mc_machine` ( `ip` int(10) unsigned NOT NULL COMMENT '机器的IP', `pool` varchar(255) NOT NULL COMMENT '机器所属维修策略', `params` varchar(2047) NOT NULL DEFAULT '' COMMENT '其余PER机器的维修参数信息', `create_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '机器信息的建立时间', `update_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '机器信息的更新时间', PRIMARY KEY (`ip`), KEY `idx_pool` (`pool`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='机器信息表'

 

天天存在以下的慢查询:
# Query_time: 6.799199  Lock_time: 0.000124 Rows_sent: 148  Rows_examined: 8394700
SELECT mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_stat e.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_stat e.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status FROM mc_state INNER JOIN (SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip) AS anon_1 ON mc_state.state_id = anon_1.state_id INNER JOIN mc_machine ON mc_state.ip = mc_machine.ip WHERE mc_machine.pool IN ('hadoop-repair-quick-repair') LIMIT 0, 999999999999; # Query_time: 6.826629  Lock_time: 0.000125 Rows_sent: 98  Rows_examined: 8394700
SELECT mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_stat e.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_stat e.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status FROM mc_state INNER JOIN (SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip) AS anon_1 ON mc_state.state_id = anon_1.state_id INNER JOIN mc_machine ON mc_state.ip = mc_machine.ip WHERE mc_machine.pool IN ('kuorong_beehive') LIMIT 0, 999999999999; # Query_time: 7.824977  Lock_time: 0.000139 Rows_sent: 148  Rows_examined: 8394700
SELECT mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_stat e.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_stat e.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status FROM mc_state INNER JOIN (SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip) AS anon_1 ON mc_state.state_id = anon_1.state_id INNER JOIN mc_machine ON mc_state.ip = mc_machine.ip LIMIT 0, 999999999999; # Query_time: 7.899820  Lock_time: 0.000095 Rows_sent: 98  Rows_examined: 8394700
SELECT mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_stat e.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_stat e.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status FROM mc_state INNER JOIN (SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip) AS anon_1 ON mc_state.state_id = anon_1.state_id INNER JOIN mc_machine ON mc_state.ip = mc_machine.ip WHERE mc_machine.pool IN ('kuorong_beehive') LIMIT 0, 999999999999;

 

业务的逻辑是什么?获取每一个池中,每台机器最新的状态数据.
其实最原始的业务需求是天天得到每台机器最新的状态数据,但一个SQL执行时间太长了,常常超时报错,因此最后修改成这样,按池获取.
但其实这样,每次执行都要执行SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip
大量这这个pool不相关的数据也要获取一遍,其实存在着明显的资源浪费的.

其实业务逻辑能够下面这样实现:

$last_ip = 0; $result1 = $dbconn->prepare("select ip from mc_machine where pool='ps_diaoyan' and ip>? order by ip limit 1000"); $result2 = $dbconn->prepare("select mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_state.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_state.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status from mc_state where ip=? order by state_id desc limit 1"); $v_file = fopen("matrix_mc1.result","w+"); $result1->bindParam(1,$last_ip,PDO::PARAM_INT); $result2->bindParam(1,$this_ip,PDO::PARAM_INT); while (true) { $result1->execute(); $iplist = $result1->fetchAll(PDO::FETCH_ASSOC); foreach ( $iplist as $row ) { $this_ip = intval($row["ip"]); $result2->execute(); foreach ( $result2->fetchAll(PDO::FETCH_NUM) as $row2 ) { $v_str = implode(",",$row2).PHP_EOL; fwrite($v_file,$v_str); } } if ( count($iplist) < 1000) { break; } $last_ip = intval($row["ip"]); } $result1 = null; $result2 = null; $dbconn = NULL; fclose($v_file);

 

原始SQL的执行计划不在这里展现了.
  
优化后的方案只涉及到2类SQL,都是很简单的SQL,均可以经过高效的索引快速的返回结果:
mysql> explain select ip from mc_machine where ip>169524751 order by ip limit 1000; +----+-------------+------------+-------+---------------+---------+---------+------+--------+--------------------------+
| id | select_type | table      | type  | possible_keys | key     | key_len | ref  | rows   | Extra                    |
+----+-------------+------------+-------+---------------+---------+---------+------+--------+--------------------------+
|  1 | SIMPLE      | mc_machine | range | PRIMARY       | PRIMARY | 4       | NULL | 103043 | Using where; Using index |
+----+-------------+------------+-------+---------------+---------+---------+------+--------+--------------------------+
1 row in set (0.00 sec) mysql> explain select mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, -> mc_state.ip AS mc_state_ip,mc_state.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, -> mc_state.update_time AS mc_state_update_time, mc_state.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, -> mc_state.error_status AS mc_state_error_status -> from mc_state where ip=169524751 order by state_id desc limit 1; +----+-------------+----------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table    | type | possible_keys | key    | key_len | ref   | rows | Extra       |
+----+-------------+----------+------+---------------+--------+---------+-------+------+-------------+
|  1 | SIMPLE      | mc_state | ref  | idx_ip        | idx_ip | 4       | const |    1 | Using where |
+----+-------------+----------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec) 

 

这样经过SQL的拆分,经过循环的方式,将原来的一个复杂的自关联查询,变成2类简单的SQL循环执行,从而达到了优化的目的.
针对由此带来的应用端和DB端网络交互太多带来的时间成本,能够考虑使用multiquery一次发送执行多条SQL来减小频繁网络交互带来的影响(具体一次发送执行多少个SQL合适,须要业务层面进行测试肯定).

固然,业务最终没有使用这里的方案,而是根据业务逻辑,变为简单的读取两个表的记录,然后代码层进行关联,也成功的消除了业务的读取压力问题. 
这也说明了业务层面的优化是很重要的.

优化案例3

手百的夏逗活动, 是手百为了提高用户黏度推出的一个活动,鼓励用户经过手百搜索一些奇葩的问题,并奖励给用户必定的豆币,最终排名前1500名的用户,能够瓜分一笔现金大奖.
表结构以下:

mysql> show create table xiadou_user\G *************************** 1. row ***************************
       Table: xiadou_user Create Table: CREATE TABLE `xiadou_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户 id', `uid` bigint(20) NOT NULL COMMENT '百度帐号 uid', `uname` varchar(128) NOT NULL DEFAULT '' COMMENT '百度帐号名称', `displayname` varchar(128) NOT NULL DEFAULT '' COMMENT '百度帐号显示名称', `securemobil` varchar(50) NOT NULL DEFAULT '' COMMENT '绑定的手机号', `score` int(10) NOT NULL DEFAULT '0' COMMENT '用户当前的豆子数', `money` float NOT NULL DEFAULT '0' COMMENT '累积抽奖赚取的金额', `last_sign_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最近一次签到时间', `sign_continue_days` tinyint(3) NOT NULL DEFAULT '0' COMMENT '签到连续天数', `create_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '建立时间', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `last_add_score_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最近一次加豆子的时间', `cuid` varchar(255) NOT NULL DEFAULT '' COMMENT '用户的 cuid 信息,可能包含多个,逗号分隔,最多存3个', `win` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是不是最后大奖中奖用户', `awarded` tinyint(1) NOT NULL DEFAULT '0' COMMENT '表示是否领取过最后大奖', `issafe` tinyint(2) NOT NULL DEFAULT '1' COMMENT '安全状态,1 正常,0高危', `appos` varchar(100) NOT NULL DEFAULT '' COMMENT 'appos', PRIMARY KEY (`id`), UNIQUE KEY `uid_UNIQUE` (`uid`), KEY `SCORE_TIME_INDEX` (`score`,`last_add_score_time`), KEY `WIN_INDEX` (`win`), KEY `idx_appos` (`appos`) ) ENGINE=InnoDB AUTO_INCREMENT=9891815 DEFAULT CHARSET=utf8 COMMENT='用户信息表'
1 row in set (0.00 sec)

 

具体的排名规则是: ORDER BY score DESC, last_add_score_time ASC 优先按照分数降序排名, 分数相同的, 早得到这个分数的用户排名靠前.
用户参与活动,搜索了奇葩问题后,极可能想查看本身目前的积分,距离瓜分大奖的资格还差多少分.
因此业务提供了这样一个功能:
SELECT score FROM xiadou_user ORDER BY score DESC, last_add_score_time ASC LIMIT 1500, 1;  
查询目前第1500名(其实应该是limit 1499,1 的) 的分数,而后显示目前用户的分数距离它还差多少分数.
这个查询应用端是有CACHE的,但每次只要用户积分有变化,排名就可能发生变化,因此业务会del相关的CACHE,因此对于这个查询而言,CACHE是没有用的,白天时段,读取基本上还都是要实时的走DB的.
可是很快DB端CPU就打满了,DB端都是上面这个查询,为何呢?

mysql> explain SELECT score FROM xiadou_user ORDER BY score DESC, last_add_score_time ASC LIMIT  1500, 1; +----+-------------+-------------+-------+---------------+------------------+---------+------+---------+-----------------------------+
| id | select_type | table       | type  | possible_keys | key              | key_len | ref  | rows    | Extra                       |
+----+-------------+-------------+-------+---------------+------------------+---------+------+---------+-----------------------------+
|  1 | SIMPLE      | xiadou_user | index | NULL          | SCORE_TIME_INDEX | 8       | NULL | 9255660 | Using index; Using filesort |
+----+-------------+-------------+-------+---------------+------------------+---------+------+---------+-----------------------------+
1 row in set (0.00 sec)

 

存在KEY  (,) 可是它只能优化这2个字段的同向排序,都升序或者都降序均可以经过这个索引避免物理排序,快速的返回TOPN记录.SCORE_TIME_INDEXscorelast_add_score_time

由于MYSQL自己只有升序索引,没有降序索引,但索引叶节点是经过双向链表来保证逻辑有序的,因此SQL层面两个排序字段都升序或者都降序,都是能够经过索引来优化的,就是正向扫描和逆向扫描索引而已.
但对于2个排序字段排序方向不一样的状况,是没法经过索引优化的,只能进行物理排序了,因此执行计划中出现了Using filesort ,也就是说读取出几百W的记录,然后物理排序,最后输出第1500个记录,因此SQL性能不好的.
大并发的状况,状况进一步恶化,从而致使DB主机CPU打满的状况(随着数据的持续增长,状况只会是进一步的恶化).
优化方案:
由于排序时优先按分数排序,分数相同的,再按照时间排序,这里并非要得到确切的第1500名的用户信息,而只是要得到第1500名的分数而已,因此上面的SQL在业务逻辑层其实等价于下面的SQL:

SELECT score FROM xiadou_user ORDER BY score DESC LIMIT 1500, 1; mysql> explain SELECT score FROM xiadou_user ORDER BY score DESC LIMIT  1500, 1; +----+-------------+-------------+-------+---------------+------------------+---------+------+------+-------------+
| id | select_type | table       | type  | possible_keys | key              | key_len | ref  | rows | Extra       |
+----+-------------+-------------+-------+---------------+------------------+---------+------+------+-------------+
|  1 | SIMPLE      | xiadou_user | index | NULL          | SCORE_TIME_INDEX | 8       | NULL | 1501 | Using index |
+----+-------------+-------------+-------+---------------+------------------+---------+------+------+-------------+
1 row in set (0.00 sec)

 

 

 

这个SQL是可使用KEY SCORE_TIME_INDEX (score,last_add_score_time) 来优化的,是不须要物理排序的.
业务改写为这个SQL,上线后,DB主机CPU恢复正常,并且业务响应时间大幅提高.
固然,最终的用户排名仍是要调用上面的2个字段的排序SQL的.
不过业务21点结束活动,22点公布排名,这中间彻底能够执行SQL,把结果插入到一个结果表去,然后只是读取这个结果表就能够了.并且这样也方便业务干预最终的排名结果.
固然,应用端使用cache缓存上面2个字段的排序SQL的执行结果,也是彻底可行的,由于数据再也不变更,彻底能够经过cache挡住所有的读取流量.
总结:
本来并不等价的2个SQL,但在业务层面是彻底等价的,经过SQL的改写,达到了优化的目的.

转自:

mysql索引设计的注意事项(大量示例,收藏再看)






原文出处:https://www.cnblogs.com/wangtcc/p/mysql-suo-yin-she-ji-de-zhu-yi-shi-xiang-da-liang-.html

相关文章
相关标签/搜索