若是能在头脑中构建一幅MySQL各组件之间如何协同工做的架构图,有助于深刻理解MySQL服务器。下图展现了MySQL的逻辑架构图。mysql
MySQL逻辑架构总体分为三层,最上层为客户端层,并不是MySQL所独有,诸如:链接处理、受权认证、安全等功能均在这一层处理。程序员
MySQL大多数核心服务均在中间这一层,包括查询解析、分析、优化、缓存、内置函数(好比:时间、数学、加密等函数)。全部的跨存储引擎的功能也在这一层实现:存储过程、触发器、视图等。redis
最下层为存储引擎,其负责MySQL中的数据存储和提取。和Linux下的文件系统相似,每种存储引擎都有其优点和劣势。中间的服务层经过API与存储引擎通讯,这些API接口屏蔽了不一样存储引擎间的差别。算法
咱们老是但愿MySQL可以得到更高的查询性能,最好的办法是弄清楚MySQL是如何优化和执行查询的。一旦理解了这一点,就会发现:不少的查询优化工做实际上就是遵循一些原则让MySQL的优化器可以按照预想的合理方式运行而已。sql
MySQL客户端/服务端通讯协议是“半双工”
的:在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动做不能同时发生。一旦一端开始发送消息,另外一端要接收完整个消息才能响应它,因此咱们没法也无须将一个消息切成小块独立发送,也没有办法进行流量控制。数据库
客户端用一个单独的数据包将查询请求发送给服务器,因此当查询语句很长的时候,须要设置max_allowed_packet
参数。可是须要注意的是,若是查询实在是太大,服务端会拒绝接收更多数据并抛出异常。缓存
与之相反的是,服务器响应给用户的数据一般会不少,由多个数据包组成。可是当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,而后让服务器中止发送。于是在实际开发中,尽可能保持查询简单且只返回必需的数据,减少通讯间数据包的大小和数量是一个很是好的习惯,这也是查询中尽可能避免使用SELECT *
以及加上LIMIT
限制的缘由之一。安全
在解析一个查询语句前,若是查询缓存是打开的,那么MySQL会检查这个查询语句是否命中查询缓存中的数据。若是当前查询刚好命中查询缓存,在检查一次用户权限后直接返回缓存中的结果。这种状况下,查询不会被解析,也不会生成执行计划,更不会执行。性能优化
MySQL将缓存存放在一个引用表(不要理解成table,能够认为是相似于HashMap的数据结构),经过一个哈希值索引,这个哈希值经过查询自己、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息计算得来。因此两个查询在任何字符上的不一样(例如:空格、注释),都会致使缓存不会命中。服务器
若是查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,其查询结果
都不会被缓存。好比函数NOW()
或者CURRENT_DATE()
会由于不一样的查询时间,返回不一样的查询结果,再好比包含CURRENT_USER
或者CONNECION_ID()
的查询语句会由于不一样的用户而返回不一样的结果,将这样的查询结果缓存起来没有任何的意义。
既然是缓存,就会失效,那查询缓存什么时候失效呢?MySQL的查询缓存系统会跟踪查询中涉及的每一个表,若是这些表(数据或结构)发生变化,那么和这张表相关的全部缓存数据都将失效。正由于如此,在任何的写操做时,MySQL必须将对应表的全部缓存都设置为失效。若是查询缓存很是大或者碎片不少,这个操做就可能带来很大的系统消耗,甚至致使系统僵死一下子。并且查询缓存对系统的额外消耗也不只仅在写操做,读操做也不例外:
基于此,咱们要知道并非什么状况下查询缓存都会提升系统性能,缓存和失效都会带来额外消耗,只有当缓存带来的资源节约大于其自己消耗的资源时,才会给系统带来性能提高。但要如何评估打开缓存是否可以带来性能提高是一件很是困难的事情,也不在本文讨论的范畴内。若是系统确实存在一些性能问题,能够尝试打开查询缓存,并在数据库设计上作一些优化,好比:
SQL_CACHE
和SQL_NO_CACHE
来控制某个查询语句是否须要进行缓存最后的忠告是不要轻易打开查询缓存,特别是写密集型应用。若是你实在是忍不住,能够将query_cache_type
设置为DEMAND
,这时只有加入SQL_CACHE
的查询才会走缓存,其余查询则不会,这样能够很是自由地控制哪些查询须要被缓存。
固然查询缓存系统自己是很是复杂的,这里讨论的也只是很小的一部分,其余更深刻的话题,好比:缓存是如何使用内存的?如何控制内存的碎片化?事务对查询缓存有何影响等等,读者能够自行阅读相关资料,这里权当抛砖引玉吧。
MySQL经过关键字将SQL语句进行解析,并生成一颗对应的解析树。这个过程解析器主要经过语法规则来验证和解析。好比SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据MySQL规则进一步检查解析树是否合法。好比检查要查询的数据表和数据列是否存在等等。
通过前面的步骤生成的语法树被认为是合法的了,而且由优化器将其转化成查询计划。多数状况下,一条查询能够有不少种执行方式,最后都返回相应的结果。优化器的做用就是找到这其中最好的执行计划。
MySQL使用基于成本的优化器,它尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。在MySQL能够经过查询当前会话的last_query_cost
的值来获得其计算当前查询的成本。
mysql> select * from t_message limit 10; ...省略结果集 mysql> show status like 'last_query_cost'; +-----------------+-------------+ | Variable_name | Value | +-----------------+-------------+ | Last_query_cost | 6391.799000 | +-----------------+-------------+
示例中的结果表示优化器认为大概须要作6391个数据页的随机查找才能完成上面的查询。这个结果是根据一些列的统计信息计算得来的,这些统计信息包括:每张表或者索引的页面个数、索引的基数、索引和数据行的长度、索引的分布状况等等。
有很是多的缘由会致使MySQL选择错误的执行计划,好比统计信息不许确、不会考虑不受其控制的操做成本(用户自定义函数、存储过程)、MySQL认为的最优跟咱们想的不同(咱们但愿执行时间尽量短,但MySQL值选择它认为成本小的,但成本小并不意味着执行时间短)等等。
MySQL的查询优化器是一个很是复杂的部件,它使用了很是多的优化策略来生成一个最优的执行计划:
MIN()
和MAX()
函数(找某列的最小值,若是该列有索引,只须要查找B+Tree索引最左端,反之则能够找到最大值,具体原理见下文)I/O
密集型应用,效率会高不少)随着MySQL的不断发展,优化器使用的优化策略也在不断的进化,这里仅仅介绍几个很是经常使用且容易理解的优化策略,其余的优化策略,你们自行查阅吧。
在完成解析和优化阶段之后,MySQL会生成对应的执行计划,查询执行引擎根据执行计划给出的指令逐步执行得出结果。整个执行过程的大部分操做均是经过调用存储引擎实现的接口来完成,这些接口被称为handler API
。查询过程当中的每一张表由一个handler
实例表示。实际上,MySQL在查询优化阶段就为每一张表建立了一个handler
实例,优化器能够根据这些实例的接口来获取表的相关信息,包括表的全部列名、索引统计信息等。存储引擎接口提供了很是丰富的功能,但其底层仅有几十个接口,这些接口像搭积木同样完成了一次查询的大部分操做。
查询执行的最后一个阶段就是将结果返回给客户端。即便查询不到数据,MySQL仍然会返回这个查询的相关信息,好比该查询影响到的行数以及执行时间等等。
若是查询缓存被打开且这个查询能够被缓存,MySQL也会将结果存放到缓存中。
结果集返回客户端是一个增量且逐步返回的过程。有可能MySQL在生成第一条结果时,就开始向客户端逐步返回结果集了。这样服务端就无须存储太多结果而消耗过多内存,也可让客户端第一时间得到返回结果。须要注意的是,结果集中的每一行都会以一个知足①中所描述的通讯协议的数据包发送,再经过TCP协议进行传输,在传输过程当中,可能对MySQL的数据包进行缓存而后批量发送。
回头总结一下MySQL整个查询执行过程,总的来讲分为6个步骤:
看了这么多,你可能会期待给出一些优化手段,是的,下面会从3个不一样方面给出一些优化建议。但请等等,还有一句忠告要先送给你:不要听信你看到的关于优化的“绝对真理”,包括本文所讨论的内容,而应该是在实际的业务场景下经过测试来验证你关于执行计划以及响应时间的假设。
选择数据类型只要遵循小而简单的原则就好,越小的数据类型一般会更快,占用更少的磁盘、内存,处理时须要的CPU周期也更少。越简单的数据类型在计算时须要更少的CPU周期,好比,整型就比字符操做代价低,于是会使用整型来存储ip地址,使用DATETIME
来存储时间,而不是使用字符串。
这里总结几个可能容易理解错误的技巧:
NULL
的列改成NOT NULL
不会对性能提高有多少帮助,只是若是计划在列上建立索引,就应该将该列设置为NOT NULL
。INT(11)
,没有任何卵用。INT
使用32位(4个字节)存储空间,那么它的表示范围已经肯定,因此INT(1)和INT(20)对于存储和计算是相同的。UNSIGNED
表示不容许负值,大体可使正数的上限提升一倍。好比TINYINT
存储范围是-128 ~ 127,而UNSIGNED TINYINT
存储的范围倒是0 - 255。DECIMAL
数据类型。即便是在须要存储财务数据时,仍然可使用BIGINT
。好比须要精确到万分之一,那么能够将数据乘以一百万而后使用BIGINT
存储。这样能够避免浮点数计算不许确和DECIMAL
精确计算代价高的问题。TIMESTAMP
使用4个字节存储空间,DATETIME
使用8个字节存储空间。于是,TIMESTAMP
只能表示1970 - 2038年,比DATETIME
表示的范围小得多,并且TIMESTAMP
的值因时区不一样而不一样。ALTER TABLE
(若是只是在列表末尾追加元素,不须要重建表)。schema
的列不要太多。缘由是存储引擎的API工做时须要在服务器层和存储引擎层之间经过行缓冲格式拷贝数据,而后在服务器层将缓冲内容解码成各个列,这个转换过程的代价是很是高的。若是列太多而实际使用的列又不多的话,有可能会致使CPU占用太高。ALTER TABLE
很是耗时,MySQL执行大部分修改表结果操做的方法是用新的结构建立一个张空表,从旧表中查出全部的数据插入新表,而后再删除旧表。尤为当内存不足而表又很大,并且还有很大索引的状况下,耗时更久。固然有一些奇技淫巧能够解决这个问题,有兴趣可自行查阅。索引是提升MySQL查询性能的一个重要途径,但过多的索引可能会致使太高的磁盘使用率以及太高的内存占用,从而影响应用程序的总体性能。应当尽可能避免过后才想起添加索引,由于过后可能须要监控大量的SQL才能定位到问题所在,并且添加索引的时间确定是远大于初始添加索引所须要的时间,可见索引的添加也是很是有技术含量的。
接下来将向你展现一系列建立高性能索引的策略,以及每条策略其背后的工做原理。但在此以前,先了解与索引相关的一些算法和数据结构,将有助于更好的理解后文的内容。
一般咱们所说的索引是指B-Tree
索引,它是目前关系型数据库中查找数据最为经常使用和有效的索引,大多数存储引擎都支持这种索引。使用B-Tree
这个术语,是由于MySQL在CREATE TABLE
或其它语句中使用了这个关键字,但实际上不一样的存储引擎可能使用不一样的数据结构,好比InnoDB
就是使用的B+Tree
。
B+Tree
中的B
是指balance
,意为平衡。须要注意的是,B+
树索引并不能找到一个给定键值的具体行,它找到的只是被查找数据行所在的页,接着数据库会把页读入到内存,再在内存中进行查找,最后获得要查找的数据。
在介绍B+Tree
前,先了解一下二叉查找树
,它是一种经典的数据结构,其左子树的值老是小于根的值,右子树的值老是大于根的值,以下图①。若是要在这课树中查找值为5的记录,其大体流程:先找到根,其值为6,大于5,因此查找左子树,找到3,而5大于3,接着找3的右子树,总共找了3次。一样的方法,若是查找值为8的记录,也须要查找3次。因此二叉查找树的平均查找次数为(3 + 3 + 3 + 2 + 2 + 1) / 6 = 2.3次,而顺序查找的话,查找值为2的记录,仅须要1次,但查找值为8的记录则须要6次,因此顺序查找的平均查找次数为:(1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.3次,所以大多数状况下二叉查找树的平均查找速度比顺序查找要快。
因为二叉查找树能够任意构造,一样的值,能够构造出如图②的二叉查找树,显然这棵二叉树的查询效率和顺序查找差很少。若想二叉查找数的查询性能最高,须要这棵二叉查找树是平衡的,也即平衡二叉树
(AVL树
)。
平衡二叉树首先须要符合二叉查找树的定义,其次必须知足任何节点的两个子树的高度差不能大于1。显然图②不知足平衡二叉树的定义,而图①是一课平衡二叉树。平衡二叉树的查找性能是比较高的(性能最好的是最优二叉树),查询性能越好,维护的成本就越大。好比图①的平衡二叉树,当用户须要插入一个新的值9的节点时,就须要作出以下变更。
经过一次左旋操做就将插入后的树从新变为平衡二叉树是最简单的状况了,实际应用场景中可能须要旋转屡次。至此咱们能够考虑一个问题,平衡二叉树的查找效率还不错,实现也很是简单,相应的维护成本还能接受,为何MySQL索引不直接使用平衡二叉树?
随着数据库中数据的增长,索引自己大小随之增长,不可能所有存储在内存中,所以索引每每以索引文件的形式存储的磁盘上。这样的话,索引查找过程当中就要产生磁盘I/O
消耗,相对于内存存取,I/O
存取的消耗要高几个数量级。能够想象一下一棵几百万节点的二叉树的深度是多少?若是将这么大深度的一颗二叉树放磁盘上,每读取一个节点,须要一次磁盘的I/O读取,整个查找的耗时显然是不可以接受的。那么如何减小查找过程当中的I/O
存取次数?
一种行之有效的解决方法是减小树的深度,将二叉树变为m叉树
(多路搜索树),而B+Tree
就是一种多路搜索树。理解B+Tree
时,只须要理解其最重要的两个特征便可:第一,全部的关键字(能够理解为数据)都存储在叶子节点
(Leaf Page
),非叶子节点(Index Page
)并不存储真正的数据,全部记录节点都是按键值大小顺序存放在同一层叶子节点上。其次,全部的叶子节点由指针链接。以下图为高度为2的简化了的B+Tree
。
怎么理解这两个特征?MySQL将每一个节点的大小设置为一个页的整数倍(缘由下文会介绍),也就是在节点空间大小必定的状况下,每一个节点能够存储更多的内结点,这样每一个结点能索引的范围更大更精确。全部的叶子节点使用指针连接的好处是能够进行区间访问,好比上图中,若是查找大于20而小于30的记录,只须要找到节点20,就能够遍历指针依次找到2五、30。若是没有连接指针的话,就没法进行区间查找。这也是MySQL使用B+Tree做为索引存储结构的重要缘由。
MySQL为什么将节点大小设置为页的整数倍,这就须要理解磁盘的存储原理。磁盘自己存取就比主存慢不少,在加上机械运动损耗(特别是普通的机械硬盘),磁盘的存取速度每每是主存的几百万分之一,为了尽可能减小磁盘I/O,磁盘每每不是严格按需读取,而是每次都会预读,即便只须要一个字节,磁盘也会从这个位置开始,顺序向后读取必定长度的数据放入内存,预读的长度通常为页的整数倍。
页是计算机管理存储器的逻辑块,硬件及OS每每将主存和磁盘存储区分割为连续的大小相等的块,每一个存储块称为一页(许多OS中,页的大小一般为4K)。主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,而后一块儿返回,程序继续运行。
MySQL巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每一个节点只须要一次I/O就能够彻底载入。为了达到这个目的,每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了读取一个节点只需一次I/O。假设B+Tree的高度为h,一次检索最多须要h-1次I/O(根节点常驻内存),复杂度O(h) = O(logmN)。实际应用场景中,M一般较大,经常超过100,所以树的高度通常都比较小,一般不超过3。
最后简单了解下B+Tree节点的操做,在总体上对索引的维护有一个大概的了解,虽然索引能够大大提升查询效率,但维护索引仍要花费很大的代价,所以合理的建立索引也就尤其重要。
仍以上面的树为例,咱们假设每一个节点只能存储4个内节点。首先要插入第一个节点28,以下图所示。
接着插入下一个节点70,在Index Page中查询后得知应该插入到50 - 70之间的叶子节点,但叶子节点已满,这时候就须要进行也分裂的操做,当前的叶子节点起点为50,因此根据中间值来拆分叶子节点,以下图所示。
最后插入一个节点95,这时候Index Page和Leaf Page都满了,就须要作两次拆分,以下图所示。
拆分后最终造成了这样一颗树。
B+Tree为了保持平衡,对于新插入的值须要作大量的拆分页操做,而页的拆分须要I/O操做,为了尽量的减小页的拆分操做,B+Tree也提供了相似于平衡二叉树的旋转功能。当Leaf Page已满但其左右兄弟节点没有满的状况下,B+Tree并不急于去作拆分操做,而是将记录移到当前所在页的兄弟节点上。一般状况下,左兄弟会被先检查用来作旋转操做。就好比上面第二个示例,当插入70的时候,并不会去作页拆分,而是左旋操做。
经过旋转操做能够最大限度的减小页分裂,从而减小索引维护过程当中的磁盘的I/O操做,也提升索引维护效率。须要注意的是,删除节点跟插入节点相似,仍然须要旋转和拆分操做,这里就再也不说明。
经过上文,相信你对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
列的值,下图展现了索引是如何组织数据存储的。
“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。好比:
select * from table_name where id + 1 = 5
咱们很容易看出其等价于 id = 4,可是MySQL没法自动解析这个表达式,使用函数是一样的道理。
若是列很长,一般能够索引开始的部分字符,这样能够有效节约索引空间,从而提升索引效率。
在多数状况下,在多个列上创建独立的索引并不能提升查询性能。理由很是简单,MySQL不知道选择哪一个索引的查询效率更好,因此在老版本,好比MySQL5.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
所以explain
时若是发现有索引合并(Extra
字段出现Using union
),应该好好检查一下查询和表结构是否是已是最优的,若是查询和表都没有问题,那只能说明索引建的很是糟糕,应当慎重考虑索引是否合适,有可能一个包含全部相关列的多列索引更适合。
前面咱们提到过索引如何组织数据存储的,从图中能够看到多列索引时,索引的顺序对于查询是相当重要的,很明显应该把选择性更高的字段放到索引的前面,这样经过第一个字段就能够过滤掉大多数不符合条件的数据。
索引选择性是指不重复的索引值和数据表的总记录数的比值,选择性越高查询效率越高,由于选择性越高的索引可让MySQL在查询时过滤掉更多的行。惟一索引的选择性是1,这是最好的索引选择性,性能也是最好的。
理解索引选择性的概念后,就不难肯定哪一个字段的选择性较高了,查一下就知道了,好比:
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
多数状况下使用这个原则没有任何问题,但仍然注意你的数据中是否存在一些特殊状况。举个简单的例子,好比要查询某个用户组下有过交易的用户信息:
select user_id from trade where user_group_id = 1 and trade_amount > 0
MySQL为这个查询选择了索引(user_group_id
,trade_amount
),若是不考虑特殊状况,这看起来没有任何问题,但实际状况是这张表的大多数数据都是从老系统中迁移过来的,因为新老系统的数据不兼容,因此就给老系统迁移过来的数据赋予了一个默认的用户组。这种状况下,经过索引扫描的行数跟全表扫描基本没什么区别,索引也就起不到任何做用。
推广开来讲,经验法则和推论在多数状况下是有用的,能够指导咱们开发和设计,但实际状况每每会更复杂,实际业务场景下的某些特殊状况可能会摧毁你的整个设计。
实际开发中,咱们会常用多个范围条件,好比想查询某个时间段内登陆过的用户:
select user.* from user where login_time > '2017-04-01' and age between 18 and 30;
这个查询有一个问题:它有两个范围条件,login_time
列和age
列,MySQL可使用login_time
列的索引或者age
列的索引,但没法同时使用它们。
若是一个索引包含或者说覆盖全部须要查询的字段的值,那么就没有必要再回表查询,这就称为覆盖索引。覆盖索引是很是有用的工具,能够极大的提升性能,由于查询只须要扫描索引会带来许多好处:
MySQL有两种方式能够生产有序的结果集,其一是对结果集进行排序的操做,其二是按照索引顺序扫描得出的结果天然是有序的。若是explain
的结果中type
列的值为index
表示使用了索引扫描来作排序。
扫描索引自己很快,由于只须要从一条索引记录移动到相邻的下一条记录。但若是索引自己不能覆盖全部须要查询的列,那么就不得不每扫描一条索引记录就回表查询一次对应的行。这个读取操做基本上是随机I/O
,所以按照索引顺序读取数据的速度一般要比顺序地全表扫描要慢。
在设计索引时,若是一个索引既可以知足排序,又知足查询,是最好的。
只有当索引的列顺序和ORDER BY
子句的顺序彻底一致,而且全部列的排序方向也同样时,才可以使用索引来对结果作排序。若是查询须要关联多张表,则只有ORDER BY
子句引用的字段所有为第一张表时,才能使用索引作排序。ORDER BY
子句和查询的限制是同样的,都要知足最左前缀的要求(有一种状况例外,就是最左的列被指定为常数,下面是一个简单的示例),其余状况下都须要执行排序操做,而没法利用索引排序。
-- 最左列为常数,索引:(date,staff_id,customer_id) select staff_id,customer_id from demo where date = '2015-06-01' order by staff_id,customer_id
冗余索引是指在相同的列上按照相同的顺序建立的相同类型的索引,应当尽可能避免这种索引,发现后当即删除。好比有一个索引(A,B),再建立索引(A)就是冗余索引。冗余索引常常发生在为表添加新索引时,好比有人新建了索引(A,B),但这个索引不是扩展已有的索引(A)。
大多数状况下都应该尽可能扩展已有的索引而不是建立新索引。但有极少状况下出现性能方面的考虑须要冗余索引,好比扩展已有索引而致使其变得过大,从而影响到其余使用该索引的查询。
按期删除一些长时间未使用过的索引是一个很是好的习惯。
关于索引这个话题打算就此打住,最后要说一句,索引并不老是最好的工具,只有当索引帮助提升查询速度带来的好处大于其带来的额外工做时,索引才是有效的。对于很是小的表,简单的全表扫描更高效。对于中到大型的表,索引就很是有效。对于超大型的表,创建和维护索引的代价随之增加,这时候其余技术也许更有效,好比分区表。最后的最后,explain后再提测是一种美德。
COUNT()
多是被你们误解最多的函数了,它有两种不一样的做用,其一是统计某个列值的数量,其二是统计行数。统计列值时,要求列值是非空的,它不会统计NULL
。若是确认括号中的表达式不可能为空时,实际上就是在统计行数。最简单的就是当使用COUNT(*)
时,并非咱们所想象的那样扩展成全部的列,实际上,它会忽略全部的列而直接统计行数。
咱们最多见的误解也就在这儿,在括号内指定了一列却但愿统计结果是行数,并且还经常误觉得前者的性能会更好。但实际并不是这样,若是要统计行数,直接使用COUNT(*)
,意义清晰,且性能更好。
有时候某些业务场景并不须要彻底精确的COUNT
值,能够用近似值来代替,EXPLAIN
出来的行数就是一个不错的近似值,并且执行EXPLAIN
并不须要真正地去执行查询,因此成本很是低。一般来讲,执行COUNT()
都须要扫描大量的行才能获取到精确的数据,所以很难优化,MySQL层面还能作得也就只有覆盖索引了。若是不还能解决问题,只有从架构层面解决了,好比添加汇总表,或者使用redis这样的外部缓存系统。
在大数据场景下,表与表之间经过一个冗余字段来关联,要比直接使用JOIN有更好的性能。若是确实须要使用关联查询的状况下,须要特别注意的是:
ON
和USING
字句中的列上有索引。在建立索引的时候就要考虑到关联的顺序。当表A和表B用列c关联的时候,若是优化器关联的顺序是A、B,那么就不须要在A表的对应列上建立索引。没有用到的索引会带来额外的负担,通常来讲,除非有其余理由,只须要在关联顺序中的第二张表的相应列上建立索引(具体缘由下文分析)。GROUP BY
和ORDER BY
中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化。要理解优化关联查询的第一个技巧,就须要理解MySQL是如何执行关联查询的。当前MySQL关联执行的策略很是简单,它对任何的关联都执行嵌套循环关联操做,即先在一个表中循环取出单条数据,而后在嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到全部表中匹配的行为为止。而后根据各个表匹配的行,返回查询中须要的各个列。
太抽象了?以上面的示例来讲明,好比有这样的一个查询:
SELECT A.xx,B.yy FROM A INNER JOIN B USING(c) WHERE A.xx IN (5,6)
假设MySQL按照查询中的关联顺序A、B来进行关联操做,那么能够用下面的伪代码表示MySQL如何完成这个查询:
outer_iterator = SELECT A.xx,A.c FROM A WHERE A.xx IN (5,6); outer_row = outer_iterator.next; while(outer_row) { inner_iterator = SELECT B.yy FROM B WHERE B.c = outer_row.c; inner_row = inner_iterator.next; while(inner_row) { output[inner_row.yy,outer_row.xx]; inner_row = inner_iterator.next; } outer_row = outer_iterator.next; }
能够看到,最外层的查询是根据A.xx列来查询的,A.c上若是有索引的话,整个关联查询也不会使用。再看内层的查询,很明显B.c上若是有索引的话,可以加速查询,所以只须要在关联顺序中的第二张表的相应列上建立索引便可。
当须要分页操做时,一般会使用LIMIT
加上偏移量的办法实现,同时加上合适的ORDER BY
字句。若是有对应的索引,一般效率会不错,不然,MySQL须要作大量的文件排序操做。
一个常见的问题是当偏移量很是大的时候,好比:LIMIT 10000 20
这样的查询,MySQL须要查询10020条记录而后只返回20条记录,前面的10000条都将被抛弃,这样的代价很是高。
优化这种查询一个最简单的办法就是尽量的使用覆盖索引扫描,而不是查询全部的列。而后根据须要作一次关联查询再返回全部的列。对于偏移量很大时,这样作的效率会提高很是大。考虑下面的查询:
SELECT film_id,description FROM film ORDER BY title LIMIT 50,5;
若是这张表很是大,那么这个查询最好改为下面的样子:
SELECT film.film_id,film.description FROM film INNER JOIN ( SELECT film_id FROM film ORDER BY title LIMIT 50,5 ) AS tmp USING(film_id);
这里的延迟关联将大大提高查询效率,让MySQL扫描尽量少的页面,获取须要访问的记录后在根据关联列回原表查询所须要的列。
有时候若是可使用书签记录上次取数据的位置,那么下次就能够直接从该书签记录的位置开始扫描,这样就能够避免使用OFFSET,好比下面的查询:
SELECT id FROM t LIMIT 10000, 10; -- 改成: SELECT id FROM t WHERE id > 10000 LIMIT 10;
其余优化的办法还包括使用预先计算的汇总表,或者关联到一个冗余表,冗余表中只包含主键列和须要作排序的列。
MySQL处理UNION
的策略是先建立临时表,而后再把各个查询结果插入到临时表中,最后再来作查询。所以不少优化策略在UNION
查询中都没有办法很好的时候。常常须要手动将WHERE
、LIMIT
、ORDER BY
等字句“下推”到各个子查询中,以便优化器能够充分利用这些条件先优化。
除非确实须要服务器去重,不然就必定要使用UNION ALL
,若是没有ALL
关键字,MySQL会给临时表加上DISTINCT
选项,这会致使整个临时表的数据作惟一性检查,这样作的代价很是高。固然即便使用ALL
关键字,MySQL老是将结果放入临时表,而后再读出,再返回给客户端。虽然不少时候没有这个必要,好比有时候能够直接把每一个子查询的结果返回给客户端。
理解查询是如何执行以及时间都消耗在哪些地方,再加上一些优化过程的知识,能够帮助你们更好的理解MySQL,理解常见优化技巧背后的原理。但愿本文中的原理、示例可以帮助你们更好的将理论和实践联系起来,更多的将理论知识运用到实践中。
其余也没啥说的了,给你们留两个思考题吧,能够在脑壳里想一想答案,这也是你们常常挂在嘴边的,但不多有人会思考为何?
有很是多的程序员在分享时都会抛出这样一个观点:尽量不要使用存储过程,存储过程很是不容易维护,也会增长使用成本,应该把业务逻辑放到客户端。既然客户端都能干这些事,那为何还要存储过程?
JOIN自己也挺方便的,直接查询就行了,为何还须要视图呢?