---
author: 江峰
title: "《高性能MySQL》读书笔记"
date: 2018-11-10 18:55
comments: true
categories: MySQL
summary: 读了《高性能MySQL》一书,收获很多。在这里,将一些重要的知识点记录下来,以便自己回顾知识、或是供他人学习。
tags:
- MySQL
---
不求甚解的读书和不读有何区别 —— 江峰
下载sakila数据库结构文件和数据文件:https://dev.mysql.com/doc/index-other.html
对于select语句,在解析查询之前,服务器会先检查缓存,如果能够在其中找到对应的查询,服务器就不必再执行查询解析、优化和执行的整个过程,而是直接返回查询缓存中的结果集。
1.读写锁(又称共享锁和排他锁):都是行级锁。 读锁是共享的,相互不阻塞。多个客户在同一时刻可以读取同一个资源,而互补干扰。读的时候不能修改数据。 写锁是排他的,也就是说一个写锁会阻塞其他的写锁和读锁。在同一时刻,只有一个用户能进行写入,并防止其他用户读取正在写入的统一资源。
2.锁粒度:尽量只锁定需要修改的部分数据,而不是所有的资源。锁定的数据量越小,则系统的并发程度越高。加锁本身也需要消耗资源。 锁策略:就是在锁的开销和数据的安全性之间寻求平衡,这种平衡也会影响到性能。锁策略有表锁和行级锁等。
3.表锁:是最基本的锁策略,也是开销最小的策略,它会锁定整张表,对表进行写操作(插入,删除,修改),都需要先获取写锁,并阻塞其他用户 对该表的读写操作。只有没有写锁时,其他用户才能获取读锁,读锁之间是不会相互阻塞的。 注意:写锁具有比读锁更高的优先级(写锁可以插入到锁队列中读锁的前面,反之读锁则不能插入到写锁的前面)。
4.行级锁:行级锁可以最大程度的支持并发处理(同时也带来了最大的锁开销),行级锁只在存储引擎层实现,而MySQL服务器层没有实现。服务器从完全不了解存储引擎中的锁实现。
事务就是一组原子性的SQL查询,或者说一个独立的工作单元。事务内的语句,要不全部执行成功,要么全部执行失败。一个实现了ACID的数据库,通常需要更高的性能。所以用户需要根据是否需要事务处理,来选择合适的存储引擎。
原子性**:事务是不可分割的最小的执行单元,一个事务中的所有操作要么全部执行成功,要么全部执行失败。 一致性:数据库总是从一个一致性的状态转换到另外一个一致性的状态。 隔离性:通常来说,一个事务所做的修改在最终提交以前,对其他事务来说是不可见的。 持久性:一旦事务提交,则其所做的修改就会永久的被保存到数据库中。
1.隔离级别:
读未提交:事务中修改,即使没有提交,对其他事务也都是可见的。 读已提交:一个事务从开始到提交之前,对其他事务是不可见的。它是一个事务,要等另一个事务提交后才能读取数据。它可能出现不可重复读。(大多数数据库的隔离级别,如SqlServer,Oracle等) 可重复读:解决了脏读的问题,并保证了在同一个事务中中多次读取同样的记录的结果是一致的。(MySQL默认的隔离级别) 串行化:强制事务串行执行,避免了幻读的问题。SERIALIZABLE会在读取的每一行数据上都加锁。(性能消耗很高)
2.死锁:
指两个或多个事务对同一资源相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。
解决办法:死锁检测和死锁超时机制 InnoDB处理死锁的办法是:将持有最少行级排它锁的事务进行回滚。 锁的行为和顺序是和执行引擎有关系的,死锁发生以后,只有部分或完全回滚其中一个事务,才能打破事务。
3.事务日志
事务日志可以帮助提高使用事务的效率。使用事务日志,存储引擎在修改表数据的时候只需要修改其内存拷贝,再将该修改行为持久化到硬盘中的事务日志中,而不是每次都将数据本身持久化到磁盘。事务持久以后,内存中被修改的数据在后台可以慢慢的刷会磁盘。所以修改数据需要写两次磁盘。
4.MySQL中的事务:
- 自动提交:MySQL默认采用自动提交(AUTOCOMMIT)模式,也就是说,如果不是显式的开启一个事务,则每个sql语句都被当做一个事务执行提交动作。 当set autocommit = 0时,所有的sql语句都是在一个事务当中,直到执行commit或rollback。该事务结束,同时又开启了另一个事务。
- 在事务中混合使用存储引擎:MySQL服务器层是不管理事务的,事务是由下层的存储引擎实现的。所以在同一个事务中,使用多种存储引擎是不可靠的。
- 隐式和显式锁定:InnoDB采用的是两阶段锁定协议。
select ... lock in share mode; 加共享锁 select ... for update; 加排它锁 - 多版本并发控制(MVCC): 1. MVCC的实现,是通过保存数据在某个给时间点的快照来实现的。
show table status查看表的状态信息;
-
InnoDB存储引擎:InnoDB采用MVCC来支持高并发,并且实现了四个标准的隔离级别。 InnoDB表是基于聚族索引建立的,聚族索引对主键查询有很高的性能。
-
MyISAM存储引擎:不支持事务,不支持行级锁,且崩溃后无法安全恢复
MyISAM特性:
- 加锁与并发:MyISAM对整张表加锁,而不是针对行。读取时对需要读取的所有表加共享锁,写入时则对表加排它锁。并在读取的时候也支持插入(称为并发插入)
- 修复:MySQL可以手工或者自动执行检查和修复操作,执行表的修复可能会导致部分数据丢失,并且修复操作是非常慢的。
- 索引特性:对于MyISAM表,即使是BLOB和TEXT等长字段,也可以基于前500个字符创建索引,MyISAM也支持全文索引,支持复杂查询。
- 延迟更新索引键:如果开启了DELY_KEY_WRITE选项,在每次修改的时候,不会立即将修改的索引数据更新到磁盘,而是会写到内存中的键缓冲区。清理缓冲区或关闭表的时候才会写入到磁盘,提高了写入的性能。
- MyISAM压缩表:支持索引,压缩表不能修改(除非先。解压缩,在修改,再压缩)。可以减少磁盘IO,提升查询性能。
- MyISAM性能:最典型的性能问题是表锁的问题,如果发现所有的查询都长期处于 “locked” 的状态,那么毫无疑问表锁就是罪魁祸首。
-
MySQL内建的其他存储引擎
-
第三方存储引擎
-
选择合适的存储引擎:
除非用到某些InnoDB不具备的特性,并且没有其他办法替代,否则都应该优先考虑使用InnoDB引擎。
事务:如果需要事务,选择InnoDB,不需要事务,并且主要是INSERT 和 SELECT操作,那么MyISAM是不错的选择。 备份:如果可以定期的关闭服务器进行备份,那么备份的因素可以忽略。反之,如果需要热备份,那么选择InnoDB引擎。 崩溃恢复:MyISAM崩溃恢复后发生损坏的概率比InnoDB高的多,而且恢复速度也很慢,所以即时不需要支持事务,很多人也选择InnoDB,这是一个很重要的因素。 日志型应用:对插入速度有很高的要求,可以考虑使用MyISAM,开销低,插入快。只读或者大部分情况下只读的表:读多写少的业务,如果不介意MyISAM的崩溃恢复,选用MyISAM是合适的。不要低估崩溃后恢复问题的重要性(MySIAM引擎是只将数据写到内存中,然后操作系统定期将数据刷到磁盘中)。 订单处理:涉及到订单处理,那么支持事务就是必须选项。InnoDB是支持订单处理的最佳选择。 电子公告牌和主题讨论论坛:如select count(*) from table;对MyISAM是比较快的,但对于其他的存储引擎可能都不行。 大数据量:几个TB的数据量,需要合理的选择硬件,做好物理涉及,并对服务器的I/O瓶颈做好规划。在这样的数据量下,如果选用MyISAM,如果崩溃了,那么进行数据恢复基本就是凉凉。
-
转换表的存储引擎:
- ALTER TABLE:比如将myTable表的存储引擎换成InnoDB, CREATE TABLE innodb_table like myTable; ALTER TABLE innodb_table ENGINE = INNODB; INSERT INTO innodb_table SELECT * FROM myTable; 该操作是按行将数据从这张表复制到另一张表中,需要执行很长的时间,在复制期间可能会消耗系统所有的I/O能力,同时会在原表加上读锁。所以在繁忙的表上执行此操作需要小心。
- 导出与导入:使用mysqldump工具将数据导出到文件等操作。
- 创建与查询(CREATE 和 SELECT):创建一个新的存储引擎的表,然后将数据导入到新表。数据量大的话就需要分批处理。 这样操作以后,新表就是原表的一个复制。如果有必要,可以在执行的过程中对原表加锁。
MySQL拥有分层的架构。上层是服务器层的服务和查询执行引擎,下层则是执行引擎。等等。。。(对于InnoDB来说,所有的提交都是事务)
对系统的性能做出一个大概的评估, TPS(每秒事务数)
主要有两种,一种是对整个系统进行测试(集成式基准测试),二是单独测试MySQL(单组件式基准测试)。
测试何种指标:
- 吞吐量:指的是单位时间内的事务处理数。
- 响应时间或延迟:这个指标用于测试任务所需的整体时间。计算出平均响应时间、最小响应时间、最大响应时间和所占百分比等。
- 并发性:Web服务器的并发性也不等同于数据库的并发性,而仅仅表示回话存储机制能够处理多少数据的能力。Web服务器的并发性更正确的度量指标,应该是在任意时间有多少同时发生的并发请求。Web服务器的高并发,一般也会导致数据库的高并发。
- 可拓展性:
归根结底,应该测试那些对用户来说最重要的指标。
测试中一些常见的错误,见书82页
性能即响应时间。数据库服务器的目的是执行SQL语句,所以它关注的应该是查询或者语句,如SELECT,UPDATE ,DELETE等。 数据库的性能用查询的响应时间来度量,单位是每个查询花费的时间。
- 剖析服务器负载
- 剖析单条查询 set profiling = 1; 打开测量服务器中运行的所有的语句的运行时间,默认关闭。 show profiles; 显示SQL语句的运行时间。(会发现每条语句都会创建一个id) show profile for query [id]; 详细显示这条语句(id)的运行时间
show status本身也会创建一条临时表
- 使用性能剖析
比如系统偶尔停顿或者慢查询。 1.使用 SHOW GLOBAL STATUS
-
更小的通常更好:一般情况下,应该选择正确存储数据的最小数据类型。(占用更少的磁盘、内存、CPU缓存)
-
简单就好:简单数据类型的操作通常需要更少的CPU周期,整形比字符型操作代价更低(因为字符集和校对规则使字符比较比整型比较要复杂)
例子1:应该使用MySQL内建的类型(timeStamp,dateTime),而不是字符串来存储日期和时间。 例子2:应该使用整型来存储IP地址。
-
尽量避免NULL:因为可为NULL的列使得索引,索引统计,和值都比较复杂。导致更难优化,把可为NULL的列改为非NULL能带来性能上的提升。
-
数据类型
在为列选择数据类型时:
第一步,选择合适的大类型。数字、字符串、时间等。 第二步,选择具体类型。很多MySQL的数据类型可以存储相同的数据,只是存储的范围,精度或占用磁盘和内存空间不同。
-
整型类型:TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT。分别占用8位、16、24、32、64位存储空间。
整数类型有可选的UNSIGNED属性,表示不允许负数。
这大致可以使整数的上限大概提升一倍。有符号类型与无符号类型占用相同的存储空间,并具有相同的性能,因此根据实际情况选择合适的数据类型。 MySQL可以为整数类型指定宽度:例如int(11),对大多数应用来说,是没有意义的:它不会限制值得合法范围,只是规定了MySQL的一些(例如MySQ命令行客户端)用来显示字符的个数。对于存储和计算来说,INT(1)和INT(20)来说是一样的。
-
实数类型:实数是带有小数部分的数字。FLOAT,DOUBLE是浮点类型,DECIMAL类型(需要额外的空间和计算开销,要慎用)。
-
字符串类型:
VARCHAR:存储可变长字符串,是最常见的字符串类型。它比定长类型更节省空间,因为它使用必要的空间(例如,越短的字符串使用越少的空间)。VARCHAR需要1或2个额外字节记录字符串的长度 CHAR:定长的,MySQL根据定义的字符串长度分配足够的空间。CHAR适合存储很短的字符串或者所有值都接近同一个长度。
通常情况下使用varchar(20)和varchar(255)保持'hello'占用的空间都是一样的,但使用长度较短的列却有巨大的优势。
较大的列使用更多的内存,因为MySQL通常会分配固定大小的内存块来保存值,这对排序或使用基于内存的临时表尤其不好。
同样的事情也会发生在使用文件排序或者基于磁盘的临时表的时候。
BLOB和TEXT类型:都是为了存储很大的数据而设计的字符串数据类型,分别采用二进制和字符方式存储。
如果查询使用了BLOB或TEXT列并且需要使用隐士临时表,将会使用磁盘临时表。这会导致严重的性能开销。使用枚举(ENUM)代替字符串类型:
-
日期和时间类型
DATETIME:精度为秒,默认情况下,MySQL以一种可排序的,无歧义的格式显示DATETIME值,与时区无关,使用8个字节的存储空间。例如:" 2018-11-14 10:49:00 "。
TIMESTAMP:占4个字节的存储空间, 通常情况下,应该尽量使用TIMESTAMP而不是DATETIME,因为它比DATETIME占用空间更小。
-
位数据类型:从技术来说都是字符串类型。
- BIT:最好不要使用
- SET:缺点是改变列的代价太高。
- 在整数列上进行按位操作:
-
6.选择标识符:
数据库中schema指的是数据库的组织和结构。如ORM自动生成的schema可能不会存储任意的数据类型,所以要检查清楚。避免性能问题。
ip地址使用无符号整型:unsigned int(10)来存储就行了。原因:数值型比字符串效率好。查询快,占用更小的内存空间等。
7.特殊类型数据:某些数据的类型并不直接与内置类型一致。
太多的列:API进行工作时,会在服务器层和存储引擎层之间进行行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列。字段太多的话,转换代价就会很高。 太多的关联:一个粗略的经验法则,如果希望查询执行的快速且并发性好,单个查询最好在12个表以内做关联。但阿里巴巴开发手册,严禁超过三张表以上做关联。
1.范式的优点很多,缺点是通常需要关联,这不但代价昂贵,且有可能使一些索引策略失效。 2.反范式的schema因为所有的数据都在一张表里面,可以很好的避免关联。
ALTER TABLE操作对于大表性能上是个问题。因为ALTER TABLE 是用新的结构创建一个新表,从旧表汇总查出所有数据插入到新表中,然后删除旧表。
(很重要,得再看一遍)
索引是存储引擎快速找到记录的一种数据结构,在MySQL中,首先在索引中找到对应值,然后在根据匹配到的索引记录找到对应的数据行。
B-Tree索引是对索引列是顺序存储的。搜索时从根节点开始判断,不断比较直到找到叶子节点的索引值。只有叶子节点的索引才对应的数据行。
可以使用B-Tree索引查询的类型: 全值匹配 匹配最左前缀 匹配列前缀 匹配范围值 精确匹配某一列并范围匹配另外一列
B-Tree索引的限制
-
如果不是按照索引的最左列开始查找,则无法使用索引。
-
不能跳过索引中的列
-
如果查询中有某个列的范围查询,则其右边所有的列都无法使用索引优化查询。
根据索引查询时只能从第一个索引列开始依次次往后开始查询,所以建立索引是索引的列的顺序是非常重要的。
基于哈希表实现,只有精确匹配索引所有列的查询才有效。 对于每一行数据,存储引擎都会对所有的索引列计算一个哈希吗,哈希吗是一个较小的值,并且不同键值的行计算出来的哈希吗是不一样的。哈希索引将所有的哈希吗存储在索引中,同时在哈希表中保存指向每个数据行的指针。 因为索引只需存储对应的哈希值,所以索引的结构十分紧凑,这也让哈希索引查找的速度非常快。哈希索引的限制很多,比如只能进行等值比较查询。
是一种特殊类型的索引,它查找的是文本中的关键字词,而不是直接比较索引中的值。全文索引更类似于搜索引擎做的事情,而不是简单的WHERE条件匹配。
索引可以让服务器快速定位到表的指定位置。B-Tree索引是按照顺序存储数据。
索引总结下来有如下三个优点:
- 索引大大减少服务器需要扫描的数据量
- 索引可以帮助服务器避免排序和临时表
- 索引可以将随机I/O变成顺序I/O
注意:对于小表:不用建索引,对于中表:索引很高效。对于大表:建立索引的成本太高
-
独立的列是指索引列不能是表达式的一部分,也不能是函数的参数。始终将索引列放在符号的一侧。
-
将数据行和索引放在一起。
InnodDB通过主键聚集数据。二级索引需要两次索引查找,而不是一次。因为二级索引的叶子节点保存的是行的主键值。
顺序的主键并不一定都是好的,对于高并发工作负载,在InnoDB中按主键顺序插入可能会造成明显的争用。
- 主键上界成为“热点”,因为所有的插入都发生在这里,并发插入可能造成间隙锁竞争
- AUTO_increment锁机制,遇到这个问题考虑重新设计表或者应用,或者更改innodb_autoinc_lock_mode配置。
-
索引中包含所有需要查询的字段的值,也就是说可以通过索引直接获取列的数据,不需要再读取数据行。 能够极大的提高性能。查询只需要扫描索引而无需回表。
-
只有当索引的列方向顺序和ORDER BY子句的顺序完全一致,并且列的排序方向(倒序或正序)都一样时, MySQL才能够使用索引来对结果做排序。多表关联时,只有order by子句引用的字段全部为第一个表时,才会使用索引做排序。
-
MySQL的唯一限制和主键限制都是通过索引来实现的。 如果创建了索引(A,B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引。 冗余索引通常发生在为表添加新索引的时候。 大多数情况下都不需要冗余索引,应该尽量拓展已有的索引不是创建新索引。
-
建议删除。使用Percona Server或Percona Toolkit中的pt-index-usage进行定位未使用的索引。
-
索引会对数据行进行排序,如果你的查询从不访问那些不需要的行,那么使用索引进行查询可以锁定更少的行。
-
支持多种过滤条件
-
避免多个范围条件
in 和 where 的查询类型type都是“range”类型,但其实in的查询就是多个等值查询。对于范围条件查询,MySQL无法再使用范围列后面的索引列了, 但是对于“多个等值条件查询”则没有这个限制。
-
优化排序
比如查询分页时比较靠后的数据,有一个比较好的策略就是延迟关联,通过使用覆盖索引查询需要返回的主键,在根据这些主键关联原表
维护表有三个主要的目的:找到并修复损坏的表,维护准确的索引统计信息,减少碎片。
-
找到并修复损坏的表
对于MyISAM存储引擎,表损坏通常是系统崩溃造成的。 使用CHECK TABLE <表名> 来检查表是否损坏,使用REPAIR TABLE <表名>
-
更新索引统计信息
可以使用ANALYZE TABLE命令获取统计信息
-
减少索引和数据的碎片
B-Tree索引可能会碎片化,这会降低查询的效率。 表的数据也可能会碎片化,有三种类型的数据碎片:
-
行碎片
-
行间碎片
-
剩余空间碎片
可以通过OPTIMIZE TABLE或者导入再导出的方式来重新整理数据。
-
在选择索引和编写利用这些索引的查询时,有如下三个原则始终需要记住:
-
单行访问时很慢的。
-
按顺序访问数据时很快的。
-
索引覆盖查询时很快的。
如果一个索引包含了查询中的所有列,那么存储引擎就不需要再回表查询。这就避免了大量的单行访问,单行访问时很慢的。
发现sql查询比较慢时,使用explain来剖析该sql。
库表结构优化,索引优化,查询优化才能实现高性能。减少查询的响应时间。
查询的生命周期大致可以按照顺序来看:从客户端,到服务器,然后咋服务器上进行解析,生成执行计划,执行,并返回结果给客户端。其中“执行”被认为是最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序,分组等。
查询性能最基本的原因就是向数据库放为的数据太多。 对于低效的查询,通过以下两个步骤来分析总是很有效。
- 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,当有时候也有可能是访问了太多的列。
- 确认MySQL服务器层是否放在分析大量超过需要的数据行。
具体优化
-
是否向数据库请求了不需要的数据
请求了不需要的数据的话会给MySQL带来额外的负担,并增加网络开销,另外也会消耗应用服务器的CPU和内存资源。
典型案例:查询不需要的记录
- 多表关联时返回全部列
- 总是取出全部列:最好不要使用SELECT * 。
- 重复查询相同的数据:第一次查询的建议缓存起来,以后查询就直接从缓存中读取。
-
MySQL是否在扫描额外的记录
在确定查询值返回需要的数据以后, 接下来应该看看查询为了返回结果是否 扫描了过多的数据。 衡量查询开销的三个指标:
- 响应时间
- 查询的行数
- 返回的行数
这三个指标都会记录到MySQL的慢日志中
-
响应时间
响应时间是两个部分之和:服务时间和排队时间。
服务时间:是指数据库处理这个查询真正花了多少时间。
排队时间:是指服务器因为等待某些资源而没有真正执行查询的时间——可能是等I/O操作完成,也可能是等待行锁等等。
-
扫描的行数和返回的行数
-
扫描的行数和访问类型
EXPLAIN语句中的type列表示访问类型。
访问类型有全盘扫描,索引扫描,范围扫描,唯一索引扫描,常数引用等。 type -- 连接类型。这里只记录和理解最重要且经常遇见的六种类型,它们分别是all,index,range,ref,ref_eq,const。从左到右,它们的效率依次是增强的。
-
all:全表扫描
-
index
这种连接类型只是另外一种形式的全表扫描,只不过它的扫描顺序是按照索引的顺序,然后根据索引回表取数据。
-
range
range指的是有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。关于range比较容易理解,需要记住的是出现了range,则一定是基于索引的。同时除了显而易见的between,and以及'>','<'外,in和or也是索引范围扫描。
-
ref
查找条件列使用了索引而且不为主键和unique。其实,意思就是虽然使用了索引,但该索引列的值并不唯一,有重复。这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描。但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
-
ref_eq
使用了主键或者唯一性索引进行查找,这种连接类型每次都进行着精确查询,无需过多的扫描,因此查找效率更高,当然列的唯一性是需要根据实际情况决定的。
-
const
通常情况下,如果将一个主键放置到where后面作为条件查询,mysql优化器就能把这次查询优化转化为一个常量。至于如何转化以及何时转化,这个取决于优化器。好的索引可以让查询使用合适的访问类型,尽可能的只扫描需要的行。
理解一个查询需要扫描多少行和实际需要使用的行数需要先去理解这个查询背后的逻辑和思想。 如果发现查询需要扫描大量的数据但只返回少量的行数,那么通常可以尝试下面的技巧去优化它。
- 使用索引覆盖扫描,把所有需要用到的列都放到索引中,这样存储引擎无须回表获取对应行就可以返回结果了。
- 改变库表结构。例如使用单独的汇总表。
- 重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询。
-
-
一个复杂查询还是多个简单查询
在其他条件都相同的时候,使用尽可能少的查询自然是更好的。但是有时候,将一个大查询分解为多个小查询是很有必要的。
-
切分查询
对于一个大查询有时候需要“分而治之”,将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只完成一小部分查询结果。
比如删除旧的数据,可以分多次删除。
-
分解关联查询
可以对每一个表进行一次简单的查询,然后将结果在应用程序中进行关联。
用分解关联查询重构查询有如下优势:
-
让缓存的效率更高。
-
将查询分解后,执行单个查询可以减少锁的竞争。
-
在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和高拓展。
-
查询本身效率也可能会有所提升。
-
可以减少冗余记录的查询。
在很多场景下,通过重构查询将关联放到应用程序中将会更加高效,这样的场景有很多。
比如:当应用能够方便的缓存单个查询结果的时候,当可以将数据分布到不同的MySQL服务器的时候,当能够使用IN()的方式代替关联查询的时候,当查询中使用同一个数据表的时候。
-
MySQL执行一个查询的过程(流程图见248页)
- 客户端发送一条查询给服务器。
- 服务器会先查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则,进入下一阶段。
- 服务器进行SQL解析,预处理,再由优化器生成对应的执行计划。
- MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。
- 将结果返回给客户端。
具体流程分析
MySQL客户端和服务器之间的通信协议是半双工的。
查询状态:有多种方式可以查看当前的状态,最简单的就是使用SHOW FULL PROCESSLIST命令。
Sleep:线程正在等待客户端发送新的请求。
Query:线程正在执行查询或者正在将结果返回给客户端。
Locked:在MySQL服务层,该线程正在等待表锁。
Analyzing and statistics:线程正在收集存储引擎的统计信息,并生成查询的执行计划。
Copying to tmp table [on disk]:线程正在执行查询,并且将其结果集都复制到一个临时表中,这种状态一般要么是在做GROUP BY 操作,要么是文件排序操作,或者是UNION操作。如果这个状态后面还有“on disk”标记,那表示MySQL正在将一个内存临时表放到磁盘上。
Sorting Result:线程正在对结果集进行排序。
Sending data:这表示多种情况,线程可能在多个状态之间传送数据,或者在生成结果集,或者在向客户端返回数据。
在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存中的数据(这个查找通过大小写敏感的哈希查找实现)。
如果命中了查询缓存,那么在返回查询结果之前会检查一次用户权限。这种情况下,不会被解析,不用生成执行计划,不会被执行。
查询的生命周期的下一步是将一个SQL转换成一个执行计划,MySQL在依照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析SQL,预处理,优化SQL执行计划。
-
语法解析器和预处理
首先,MySQL通过关键字将SQL语句进行解析,并生成一颗对应的"解析树"。MySQL解析器将使用MySQL语法规则验证和解析查询。预处理器则会根据一些MySQL语法规则进一步检查解析树是否合法,例如,这里讲检查数据表和数据里是否存在,还会解析名字和别名,看看它们是否有歧义。下一步预处理器会验证权限。这通常很快,除非服务器上有非常多的权限配置。
-
查询优化器
现在语法树被认为是合法的了。并且由优化器转化成执行计划。一条查询可能有多种执行方式,最终都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。
优化器在评估成本的时候并不考虑任何层面的缓存,它假设读取任何一行数据都需要一次I/O;
优化策略简单的分为两种
静态优化和动态优化。
- 静态优化:可以直接对解析数进行解析,并完成优化。静态优化在第一次完成后就一直有效,即使使用不同的参数执行相同的查询也不会发生变化。
- 动态优化:则和查询的上下文有关,也可能与很多其他因素有关,例如WHERE条件中的取值,索引中条目对应的数据行数等。这需要再每次查询时都需要评估。
MySQL能够处理的优化类型
-
重新定义关联表的顺序:数据表的关联并不总是按照在查询中指定的顺序进行。决定关联的顺序是优化器很 要的一部分功能。
-
将外连接转化成内连接
-
使用等价变换规则:MySQL可以使用一些等价变换来简化并规范表达式。它可以合并和减少一些比较,还可以移除一些恒成立和恒不成立的判断。
-
优化COUNT(),MIN()和MAX()
索引可列是否可为空通常可以帮助MySQL优化这类表达式。例如,要找到某一列的最小值,只需要查询B-Tree索引最左端的记录,MySQL可以直接获取索引的第一行记录。类似的,如果要查找一个最大值,也只需要读取B-Tree索引的最后一条记录。使用了这种优化的话,可以从explain语句中看到“Select tables optimized away”,它表示优化器已经从执行计划中移除了这张表,并以一个常数取而代之。类似的,没有任何WHERE条件的COUNT(*)查询通常也可以使用存储引擎提供的一些优化(如MyISAM维护了一个变量来存放数据表的行数)
-
预估并转化为常数表达式:当MySQL检测到一个表达式可以转换为常数的时候,就会一直把该表达式作为常数进行优化处理。
-
覆盖索引扫描:当索引中的列包含所有查询中所有需要使用的列的时候,MySQL就可以直接通过索引返回需要的数据,而无需回表查询对应的数据行。
-
子查询优化:MySQL在某些请况下可以将子查询转换一种效率更高的形式,从而减少多个查询多次对数据的访问。
-
提前终止查询:在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。比如 LIMIT
-
等值传播:如果两个列的值通过等式关联,那么MySQL能够将其中一个列的WHERE条件等式传播到另一个列上。
-
列表IN()的比较:在很多数据库系统中,IN()完全等同于多个OR条件的子句,因为这两者是完全等价的。在MySQL中这点事不成立的,MySQL先将IN()列表中的数据进行排序,然后通过二分查找的方式来确定列表中的值是否慢则条件。这是一个O(log n)复杂度的操作,等价的转换成OR查询的复杂度为O(n)对于IN()列表中有大量取值的时候,MySQL的处理速度将会更快。
-
在服务器层有查询优化器,却没有保存数据和索引的统计信息。统计信息由存储引擎实现。MySQL查询优化器在生成执行计划前,需要存储引擎获取相应的统计信息。包括:每个表或者索引有多少个页面,每个表的每个索引的基数是多少,数据行和索引长度,索引的分布信息等。优化器根据这些信息来选择一个最优的执行计划。
-
MySQL如何执行关联查询
对于UNION查询,MySQL先将一系列的单个查询的结果放到一个临时表中,然后在重新读出临时表数据来完成UNION查询。
-
执行计划
MySQL生成查询(多表查询)的一颗指令树,然后通过执行引擎完成这颗指令树并返回结果。最终的执行计划包含了重构查询的全部信息。
MySQL的执行计划是一颗左侧深度优先的树。
-
关联查询优化器
MySQL优化器最重要的一部分就是关联查询优化器,它决定了多个表关联时的顺序。通常多表关联时,可以有多种不同的顺序来获取相同的结果。
关联查询优化器通过评估不同顺序时的成本来选择一个代价小的关联查询。可以使用staraight_join关键字,使查询语句按照原本的顺序来执行,不执行查询优化处理。
-
排序优化
无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能对大量数据进行排序。数据量小,内存中排序。数据量大,将数据分块。在内存中排序,结果放在磁盘上,最最终将结果合并,返回排序结果。
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的执行引擎则根据这个执行计划来完成整个查询。在执行执行计划的过程中,需要调用存储引擎实现的接口,这些接口也就是我们称为“handler API”的接口。实际上,MySQL在优化段就已经为每个表创建了一个hander实例,优化器根据这些实例的接口可以获取表的相关信息,包括表的所有列名,索引统计信息,等等。
即使查询不需要返回结果给客户端,MySQL仍然会返回这个查询的一些信息,如影响的行数。如果查询可以被缓存,那么MySQL在这个阶段也会将结果存放到缓存中。MySQL将结果返回给客户端是一个增量,逐步返回的过程。
这样处理有两个好处
服务器端无需存储太多结果,也就不会因为要返回太多结果而消耗太多内存。 另外,这样的处理也能让MySQL客户端第一时间获得返回的结果。
-
关联子查询
-
UNION的限制
MySQL无法将限制条件“下推”到内层,这使得原本能够限制部分返回结果的条件无法应用到内层的条件上。
MySQL UNION 操作符用于连接两个以上的 SELECT 语句的结果组合到一个结果集合中。多个 SELECT 语句会删除重复的数据。
-
索引合并优化
当WHERE子句中包含多个复杂条件的时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位查找需要的行。
-
等值传递:
这个问题实际上很少会碰到。
-
并行执行
MySQL无法利用多核特性来并行执行查询。
-
哈希关联
在本书写作的时候,MySQL并不支持哈希关联——MySQL所有的关联都是嵌套循环关联。
-
松散索引扫描
-
最大值和最小值优化
-
在同一张表上进行查询和更新
MySQL不允许对同一张表进行查询和更新。
-
优化COUNT()查询
COUNT的作用:COUNT()是一个特殊的函数,有两种非常不同的作用:它可以统计某个列值的数量,也可以统计行数。
如果在COUNT()的括号内指定了列或者列的表达式,则统计的就是这个表达式有值的结果数(不包含NULL)。
COUNT()的另一个作用就是统计结果集的行数。当MySQL确认了括号内的表达式不可能为空时,实际上就是在统计行数。
COUNT(*)的通配符 * 并不会像我们猜想的那样拓展成所有的列,实际上,它会忽略所有的列而直接统计所有的行数。
有一个最常见的错误就是,在括号内指定了列值却希望统计结果集的行数,如果希望知道查询的是结果集的行数,就直接使用COUNT(),这样写意义清晰,性能上也会更好。
count(*):忽略所有列,直接统计该表的所有行数
count(列值):统计该列的不为null的行数。
关于MyISAM的神话MyISAM的COUNT()函数在没有WHERE子句时执行的非常快,因为无需实际的计算表的行数。MySQL可以利用MySIAM存储引擎的特性直接获得这个值。如果MySQL知道某列col不可能为NULL值,那么MySQL会将COUNT(col)优化为COUNT().
当统计带WHERE子句的结果集行数,可以是统计某个列值的数量时,MyISAM的COUNT()和其他存储引擎没有任何不同。
简单的优化
有时候可以利用MyISAM的COUNT()非常快的特性,来加速一些特定COUNT() 的查询。利用表的行数COUNT()这个常量来减去查询到相反的数量, 这样一来可以大大减少扫描的行数。
更复杂的优化
COUNT()一般来说都要扫描大量的数据,才能获取进去的结果,因此很难优化,除了索引覆盖扫描之外,如果这还不够,就需要考虑修改应用的架构,可以增加汇总表。
-
优化关联查询:
- 确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序,当表A和表B用列c关联的时候,如果优化器的关联顺序是B、A, 那么就不需要在B表的对应列上建索引。没有用到的索引只会带来额外的负担。一般来说,除非有特别其他理由,否则只需要在关联顺序中的第二个表的相应列上建索引。
- 确保任何的GROUP BY 和 ORDER BY 中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程。
-
优化子查询
最好的办法就是尽量不要使用子查询,尽可能的使用关联查询来代替。
-
优化GROUP BY 和 DISTINCT
在MySQL中,当无法使用索引的时候,GROUP BY 使用两种策略来完成:使用临时表或文件排序来做分组。 如果是使用关联查询做分组(GROUP BY),并且是按照查找表中的某个列进行分组,那么通常采用查找表的标志列分组的效率回避其他列高。
-
优化LIMIT分页:一个最简单的方法就是使用覆盖索引进行扫描,(否则MySQL需要做大量的文件排序操作)通过写一个包含所有索引的子查询进行关联。这种延迟关联将大大提升查询效率。
-
优化SQL_CALC_FOUND_ROWS
-
优化UNION查询
MySQL总是通过创建并填充临时表的方式来执行UNION查询。因此很多优化策略在UNION中会失效。 经常需要手工的将WHERE,LIMIT,ORDER BY等子句“下推”到UNION的各个子查询中,以便优化器能够充分利用这些条件进行优化。除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,这一点很重要。如果没有ALL关键字,MySQL会在临时表加上DISTINCT关键字,这会导致对整个临时表的数据做唯一性检查。这样做的代价很高。即使有ALL关键字,MySQL仍然会使用临时表存储结果。
-
静态查询分析
-
使用用户自定义变量
省略。。。
EXPLAIN命令是查看查询优化器如何决定执行查询的主要方法。
调用EXPLAIN:在SELECT前加上EXPLAIN,
-
type列
-
ALL
全表扫描
-
index
按照索引顺序进行全表扫描,如果在Extra列看到了“use index” ,说明使用的是覆盖索引。
-
range
范围扫描就是一个有限制的索引扫描,它开始于索引的某一点,返回匹配这个值域的行。不用遍历全部索引。
-
ref
这是一种索引访问(有时也叫索引查找),它返回所有匹配某个单个值的行。
-
eq_ref
使用这种索引进行查找,MySQL知道最多只能返回一条符合条件的记录。
-
const,system
当MySQL能对查询的某部分进行优化并将其转换为一个常量时,它就会使用这些访问类型。如根据主键查询
-
NULL
这种访问方式意味着MySQL能够在优化阶段分解查询语句,在执行阶段甚至用不着再访问表或者索引。例如,从索引列中获取最小值,不需要再执行时访问表。
-
-
possible_key列 这一列显示了查询可以使用哪些索引,这是基于访问你的列和使用的比较操作符来判定的。 这个列表示在优化过程的早期创建的,因此有些罗列出来的索引可能对于后续优化过程是没用的。
-
key列
这一列显示了MysQL决定采用哪个索引来优化对该表的访问。如果该索引在possible_key列中没有,那么MySQL选用它时处于另外的原因。例如:可能选择了一个覆盖索引。
-
ref列
这一列显示了之前的表在key列记录的索引中查找值所用的列或常量。
-
rows列
这一列是为了估计为了找到所需的行而读取的行数。这个数字是内嵌循环关联计划里的循环数目。 通常把rows列的值相乘,可以粗略的计算真个查询会检查的行数。
-
Extra列
-
Using index
表示MySQL将使用覆盖索引,以避免访问表。不要把覆盖索引和index访问类型弄混了
-
Using where
如果查询未能使用索引,Using where的作用只是提醒我们MySQL将用where子句来过滤结果集
-
Using filesort
无法利用索引完成的排序,需要额外的排序操作,可能在内存也可能在磁盘完成,常见于group by和order by 操作中
-
Using temporary
表示使用了临时表存储中间结果,临时表可以是内存临时表和磁盘临时表。主要常见于group by、order by和多表join等操作中。
-
Distinct
优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作
-
Not exists
优化left join操作,一旦它找到了匹配left join标准的行,就不再搜索了
-
sql语句: 开启事务:START TRANSACTION; 查看事务是否自动提交:show variables like 'autocommit'; 修改事务是否自动提交:set autocommit = 1; 1或者on代表启用,0或者off代表禁用。 查看当前会话隔离级别:SELECT @@tx_isolation; 设置当前会话隔离级别:set session transaction isolation level repeatable read;
一个人有两块手表就永远不知道时间。