1. 使用EXPLAIN

使用EXPLAIN关键字可以帮助我们分析select语句,让我们知道查询效率低下的原因,从而改进我们查询,让查询优化器能够更好的工作。

基本思路

  • 一定要注意看执行计划里的 possible_keys、key和rows这三个值
  • 让影响行数尽量少
  • 保证使用到正确的索引
  • 减少不必要的Using temporary/Using filesort;

字段解释

列名

说明

id

执行编号,标识select所属的行。如果在语句中没子查询或关联查询,只有唯一的select,每行都将显示1。否则,内层的select语句一般会顺序编号,对应于其在原始语句中的位置

select_type

显示本行是简单或复杂select。如果查询有任何复杂的子查询,则最外层标记为PRIMARY(DERIVED、UNION、UNION RESUlT)

table

访问引用哪个表(引用某个查询,如“derived3”)

type

数据访问/读取操作类型(ALL、index、range、ref、eq_ref、const/system、NULL)

possible_keys

揭示哪一些索引可能有利于高效的查找

key

显示mysql决定采用哪个索引来优化查询

key_len

显示mysql在索引里使用的字节数

ref

显示了之前的表在key列记录的索引中查找值所用的列或常量

rows

为了找到所需的行而需要读取的行数,估算值,不精确。通过把所有rows列值相乘,可粗略估算整个查询会检查的行数

Extra

额外信息,如using index、filesort等

select_type列

select_type

说明

SUBQUERY

在select列表中的子查询,如SELECT *,(SELECT id FROM product_info) AS id FROM product_info

DERIVED

在from子语句中子查询,如SELECT * FROM product_info p1 ,(SELECT * FROM product_info) p2.Mysql会递归执行,并把结果放到临时表中

UNION

在UNION中第二个和随后的SELECT被标记为UNION

UNION RESULT

用来从UNION的匿名临时表检索结果的SELECT被标记为UNION RESULT

DEPENDENT SUBQUERY

子查询中的第一个SELECT,取决于外面的查询。(需要优化)

type列(依次从最差到最优):

type

说明

All

最坏的情况,从头到尾全表扫描

index

和全表扫描一样。只是扫描表的时候按照索引次序进行而不是行。主要优点就是避免了排序, 但是开销仍然非常大。如在Extra列看到Using index,说明正在使用覆盖索引,只扫描索引的数据,它比按索引次序全表扫描的开销要小很多

range

范围扫描,一个有限制的索引扫描。key 列显示使用了哪个索引。当使用=、 <>、>、>=、<、<=、IS NULL、<=>、BETWEEN 或者 IN 操作符,用常量比较关键字列时,可以使用 range

ref

一种索引访问,它返回所有匹配某个单个值的行。此类索引访问只有当使用非唯一性索引或唯一性索引非唯一性前缀时才会发生

eq_ref

最多只返回一条符合条件的记录。使用唯一性索引或主键查找时会发生 (高效)

const/system

当主键放入where子句时,mysql把这个查询转为一个常量(高效)

Null

意味说mysql能在优化阶段分解查询语句,在执行阶段甚至用不到访问表或索引(高效)

Extra列常见情况(需要优化):

Extra

说明

Using temporary

表示 MySQL 在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by

Using filesort

表示 MySQL 会对结果使用一个外部索引排序,而不是从表里按索引次序读到相关内容。可能在内存或者磁盘上进行排序。MySQL 中无法利用索引完成的排序操作称为“文件排序”

2. 建立索引

2.1 基本原则

  • 不要在选择性非常差的字段上建索引
  • 查询条件里出现范围查询(如A>7,A in (2,3))时,要警惕,不要建了组合索引却完全用不上
  • 在Join表的时候使用相同类型的列,并将其索引
  • select 和order by和 group by字段要建立合适的索引
  • 单个索引字段数不超过5个,单表索引数量不超过5个,索引设计遵循B+Tree 索引最左前缀匹配原则
  • 建立的索引能覆盖80%主要的查询,不求全,解决问题的主要矛盾
  • 避免索引的隐式转换
  • 谨慎合理使用索引,改善查询、减慢更新,索引一定不是越多越好
  • innodb主键推荐使用自增列、主键建立聚簇索引、主键不应该被修改、字符串不应该做主键(除非采用分布式id),如果不指定主键,innodb会使用唯一且非空值索引代替
  • 不用外键,请由程序保证约束

优化策略A:字段选择性

  • 选择性较低索引 可能带来的性能问题
  • 索引选择性=索引列唯一值/表记录数;(可执行show index from ads命令看字段的Cardinality(散列程度))
  • 选择性越高索引检索价值越高,消耗系统资源越少;选择性越低索引检索价值越低,消耗系统资源越多;
  • 查询条件含有多个字段时,不要在选择性很低字段上创建索引
  • 可通过创建组合索引来增强低字段选择性和避免选择性很低字段创建索引带来副作用;
  • 尽量减少possible_keys,正确索引会提高sql查询速度,过多索引会增加优化器选择索引的代价,不要滥用索引;

优化策略B:组合索引字段顺序

由于 mysql 索引是基于 B-Tree 的,所以组合索引有“字段顺序”概念。

所以,查询条件中有 ac.city_id IN (0, 8005),而组合索引是 (ads_id,city_id),则该查询无法使用到这个组合索引。

组合索引查询的各种场景

兹有 Index (A,B,C) ——组合索引多字段是有序的,并且是个完整的BTree索引。
下面条件可以用上该组合索引查询:

A>5
A=5 AND B>6
A=5 AND B=6 AND C=7
A=5 AND B IN (2,3) AND C>5

下面条件将不能用上组合索引查询:

B>5 ——查询条件不包含组合索引首列字段
B=6 AND C=7 ——查询条件不包含组合索引首列字段

下面条件将能用上部分组合索引查询:

A>5 AND B=2 ——当范围查询使用第一列,查询条件仅仅能使用第一列
A=5 AND B>6 AND C=2 ——范围查询使用第二列,查询条件仅仅能使用前二列
组合索引排序的各种场景

兹有组合索引 Index(A,B)。
下面条件可以用上组合索引排序:

ORDER BY A——首列排序
A=5 ORDER BY B——第一列过滤后第二列排序
ORDER BY A DESC, B DESC——注意,此时两列以相同顺序排序
A>5 ORDER BY A——数据检索和排序都在第一列

下面条件不能用上组合索引排序:

ORDER BY B ——排序在索引的第二列
A>5 ORDER BY B ——范围查询在第一列,排序在第二列
A IN(1,2) ORDER BY B ——理由同上
ORDER BY A ASC, B DESC ——注意,此时两列以不同顺序排序
索引合并

顺着组合索引怎么建继续往下延伸,请各位注意“索引合并”概念:
- MySQL 5,0以下版本,SQL查询时,一张表只能用一个索引(use at most only one index for each referenced table),
- 从 MySQL 5.0开始,引入了 index merge 概念,包括 Index Merge Union Access Algorithm(多个索引并集访问),包括Index Merge Intersection Access Algorithm(多个索引交集访问),可以在一个SQL查询里用到一张表里的多个索引。
- MySQL 在5.6.7之前,使用 index merge 有一个重要的前提条件:没有 range 可以使用。

索引合并的简单说明:
1. SELECT * FROM TB WHERE A=5 AND B=6
- 能分别使用索引(A) 和 (B) 或 索引合并;
- 创建组合索引(A,B) 更好;
2. SELECT * FROM TB WHERE A=5 OR B=6
- 能分别使用索引(A) 和 (B) 或 索引合并;
- 组合索引(A,B)不能用于此查询,分别创建索引(A) 和 (B)会更好;

2.2 索引类型的选择

B-tree索引

  • B-Tree 索引是 MySQL 数据库中使用最为频繁的索引类型。
  • B-tree索引适用于全键值,键值范围或键前缀查找。其中键前缀查找只适用于根据最左前缀查找。

HASH索引

  • hash索引只支持等值比较:=,in(),<=>(<>不同于<=>),也不能用于范围查找,比如:WHERE price>100;
  • Hash 索引无法被用来避免数据的排序操作;
  • Hash 索引不能利用部分索引键查询(通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用);
  • Hash 索引在任何时候都不能避免表扫描;Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。

查询中的索引原理区别

  • 在 Innodb 中如果通过主键来访问数据效率是非常高的,而如果是通过 Secondary Index 来访问数据的话, Innodb 首先通过 Secondary Index 的相关信息,通过相应的索引键检索到 Leaf Node之后,需要再通过 Leaf Node 中存放的主键值再通过主键索引来获取相应的数据行。
  • MyISAM 存储引擎的主键索引和非主键索引差别很小,只不过是主键索引的索引键是一个唯一且非空 的键而已。而且 MyISAM 存储引擎的索引和 Innodb 的 Secondary Index 的存储结构也基本相同,主要的区别只是 MyISAM 存储引擎在 Leaf Nodes 上面出了存放索引键信息之外,再存放能直接定位到 MyISAM 数据文件中相应的数据行的信息(如 Row Number ),但并不会存放主键的键值信息。

3. 表设计

  • 尽可能的使用NOT NULL
  • 使用紧凑的数据类型
  • 对于大多数的数据库引擎来说,硬盘操作可能是最重大的瓶颈。所以,把你的数据变得紧凑会对这种情况非常有帮助,因为这减少了对硬盘的访问。
  • 如果一个表只会有几列(比如说字典表,配置表),那么我们不需要使用INT来做主键,使用MEDIUMINT,SMALLINT或是更小的TINYINT会更经济一些。
  • 如果你不需要记录时间,使用DATE要比DATETIME好得多;
  • 使用 TIMESTAMP 存储日期时间。DATETIME和TIMESTAMP都是精确到秒,优先选择TIMESTAMP,因为TIMESTAMP只有4个字节,而DATETIME8个字节。同时TIMESTAMP具有自动赋值以及自动更新的特性。
  • ENUM类型是非常快和紧凑的。在实际上,其保存的是TINYINT,但其外表上显示为字符串。适用于选项列表,比如“性别”,“国家”,“民族”,“状态”或“部门”,这些字段取值有限而且固定,则应该使用ENUM而不是VARCHAR。
  • IP地址使用UNSIGNED INT。如果你用整形来存放,只需要4个字节,并且你可以有定长的字段。而且,这会为你带来查询上的优势,尤其是当你需要使用这样的WHERE条件:IP between ip1 and ip2
  • 存储精确浮点数必须使用DECIMAL替代FLOAT和DOUBLE。
  • 永远为每张表设置一个ID
  • 我们应该为数据库里的每张表都设置一个ID做为其主键,而且最好的是一个INT型的(推荐使用UNSIGNED),并设置上自动增加的AUTO_INCREMENT标志。
  • 就算是你users表有一个主键叫“email”的字段,你也别让它成为主键。使用VARCHAR类型来当主键会使用得性能下降。另外,在你的程序中,你应该使用表的ID来构造你的数据结构。
  • 而且,在MySQL数据引擎下,还有一些操作需要使用主键,在这些情况下,主键的性能和设置变得非常重要,比如,集群,分区……
  • 在这里,只有一个情况是例外,那就是“关联表”的“外键”,也就是说,这个表的主键,通过若干个别的表的主键构成。我们把这个情况叫做“外键”。比如:有一个“学生表”有学生的ID,有一个“课程表”有课程ID,那么,“成绩表”就是“关联表”了,其关联了学生表和课程表,在成绩表中,学生ID和课程ID叫“外键”其共同组成主键。
  • 选择合适的存储引擎
  • MyISAM适合于一些需要大量查询的应用,但其对于有大量写操作并不是很好。甚至你只是需要update一个字段,整个表都会被锁起来,而别的进程,就算是读进程都无法操作直到读操作完成。另外,MyISAM对于 SELECT COUNT(*) 这类的计算是超快无比的。
  • InnoDB是一个非常复杂的存储引擎,对于一些小的应用,它会比 MyISAM还慢。支持“行锁” ,于是在写操作比较多的时候,会更优秀。并且,他还支持更多的高级应用,比如:事务。
  • 禁止在数据库中使用VARBINARY、BLOB存储图片、文件等。
  • 单表记录控制在2000w行
  • 控制字段数在20以内
  • 单库数据容量控制在30G,超过需要做分库处理
  • 少用 blob或者 text,varchar的性能会比text 高很多,实在避免不了blob,请拆表
  • 固定长度的表会更快
  • 如果表中的所有字段都是“固定长度”的,整个表会被认为是 “static” 或 “fixed-length”。 例如,表中没有如下类型的字段: VARCHAR,TEXT,BLOB。只要你包括了其中一个这些字段,那么这个表就不是“固定长度静态表”了,这样,MySQL 引擎会用另一种方法来处理。
  • 固定长度的表会提高性能,因为MySQL搜寻得会更快一些,因为这些固定的长度是很容易计算下一个数据的偏移量的,所以读取的自然也会很快。而如果字段不是定长的,那么,每一次要找下一条的话,需要程序找到主键。
  • 并且,固定长度的表也更容易被缓存和重建。不过,唯一的副作用是,固定长度的字段会浪费一些空间,因为定长的字段无论你用不用,他都是要分配那么多的空间。
  • 使用“垂直分割”技术,你可以分割你的表成为两个一个是定长的,一个则是不定长的。
  • 从PROCEDURE ANALYSE()取得建议
  • PROCEDURE ANALYSE()用来分析你的字段和其实际的数据,并会给你一些有用的建议。
  • 只有表中有实际的数据,这些建议才会变得有用,因为要做一些大的决定是需要有数据作为基础的。
SELECT * FROM TABLE_NAME PROCEDURE ANALYSE();
  • 垂直分割
  • “垂直分割”是一种把数据库中的表按列变成几张表的方法,这样可以降低表的复杂度和字段的数目,从而达到优化的目的。
  • 示例一:在Users表中有一个字段是家庭地址,这个字段是可选字段,相比起,而且你在数据库操作的时候除了个人信息外,你并不需要经常读取或是改写这个字段。那么,为什么不把他放到另外一张表中呢?这样会让你的表有更好的性能,因为对于用户表来说,只有用户ID,用户名,口令,用户角色等会被经常使用。小一点的表总是会有好的性能。
  • 示例二:你有一个叫“last_login”的字段,它会在每次用户登录时被更新。但是,每次更新时会导致该表的查询缓存被清空。所以,你可以把这个字段放到另一个表中,这样就不会影响你对用户ID,用户名,用户角色的不停地读取了,因为查询缓存会帮你增加很多性能。
  • 另外,你需要注意的是,这些被分出去的字段所形成的表,你不会经常性地去Join他们,不然的话,这样的性能会比不分割时还要差。

4. SQL语句

  • 避免 SELECT *,消耗cpu、io、内存、带宽,这种写法不具有扩展性
  • 当只要一行数据时使用LIMIT 1
  • 为查询缓存优化你的查询
  • 大多数的MySQL服务器都开启了查询缓存。这是提高性能最有效的方法之一,而且这是被MySQL的数据库引擎处理的。
  • 当有很多相同的查询被执行了多次的时候,这些查询结果会被放到一个缓存中,这样,后续的相同的查询就不用操作表而直接访问缓存结果了。
  • CURDATE(),NOW()和RAND()会让MySQL的查询缓存不起作用,因为这些函数的返回是会不定的易变的。用一个变量来代替MySQL的函数,从而开启缓存。
  • 不要使用ORDER BY RAND()
  • 如果你真的想把返回的数据行打乱了,你有N种方法可以达到这个目的。这样使用只让你的数据库的性能呈指数级的下降。这里的问题是:MySQL会不得不去执行RAND()函数(很耗CPU时间),而且这是为了每一行记录去记行,然后再对其排序。就算是你用了Limit 1也无济于事(因为要排序)
  • Prepared Statements
  • 可以写动态参数化的查询
  • 更佳的性能优势。SQL语句会预编译在数据库系统中,执行计划同样会被缓存起来,它允许数据库做参数化查询。
  • 可以防止SQL注入式攻击
  • like时避免负向%
  • 避免在数据库中进行数学运算和其他大量计算任务
  • 使用load data导数据,load data比insert快约20倍。
  • 合理的分页,尤其大分页。limit 越大,效率越低。
select id from t limit 10000,10; 
=> 
select id from t where id > 10000 limit 10;
  • 尽量不使用存储过程、触发器、函数等
  • sql语句尽可能简单、一条 sql只能在一个 cpu运算、大语句拆小语句,减少锁时间、一条大sql
    可以堵死整个库
  • 多用性能分析工具:
  • show profile:用来分析当前会话中语句执行的资源消耗情况,可以用于SQL的调优的测量。
  • mysqlsla:daniel-nichter用perl写的一个脚本,专门用于处理分析Mysql的日志而存在。
  • mysqldumpslow:mysql自带的用来分析慢查询的工具。
  • show processlist:显示哪些线程正在运行。

参考资料

  • MySQL开发规范和原则大全
  • 建索引时注意字段选择性 & 范围查询注意组合索引的字段顺序