一文让你理解mysql内部原理

本文主要基于《高性能MySQL》

文章目录

  • 一、mysql总体结构
  • 二、执行查询SQL流程
    • 1、客户端/服务器通讯协议
    • 2、查询缓存
    • 3、查询优化
      • (1)优化器
      • (2)关联查询优化
        • UNION查询
        • 多表关联查询
      • (3)排序优化
      • (4)优化器局限性
    • 4、查询执行引擎
    • 5、返回结果给客户端
  • 三、常用命令
  • 四、SQL优化建议
    • 1、分析SQL执行慢的方法
    • 2、优化特定类型的查询
      • (1)COUNT()
      • (2)优化关联查询
      • (3)优化子查询
      • (4)UNION优化
    • 3、mysql表字段优化建议
      • (1)NOT NULL
      • (2)整型
      • (3)浮点型
      • (4)字符串
      • (5)BLOB和TEXT
      • (6)日期
    • 4、大字段列
  • 五、mysql配置简述

一、mysql总体结构

一文让你理解mysql内部原理_第1张图片
上图是mysql的总体结构,本文使用的存储引擎是InnoDB,下文也是基于InnoDB介绍的。这张图涉及了一些重要组件,下文会分别介绍这些组件的作用。
mysql是单进程多线程的运行模式。一个mysql服务器就是一个进程。

二、执行查询SQL流程

mysql的客户端和服务端必须通过通讯协议进行交互,服务端收到SQL语句后,首先检查查询缓存,如果查询缓存中已经缓存了结果,那么直接返回,如果没有,需要解析SQL,通过优化器优化执行计划,之后交给执行器执行,最后向客户端返回结果。这是一个完整的SQL语句执行流程,下面详细介绍流程中的各个环节,还会介绍一些SQL语句的执行原理。

1、客户端/服务器通讯协议

客户端和服务器之间的通讯是半双工的,也就是同时只有一方向另一方发送数据,不能双方同时都发送。一方要发送数据时必须等到另一方完全发送结束才可以。
mysql服务器执行完查询后会向客户端发送结果,当服务器将所有的结果都发送到客户端后才会释放服务器资源,除非中间强行断开连接。mysql客户端一般会缓存服务器返回的查询结果,当所有数据都缓存到本地内存后,服务端便可以释放资源了,所以客户端使用缓存可以加快资源释放,但是当数据量非常大时,缓存可能会撑爆内存。当然也可以设置客户端不缓存查询结果。
这里需要了解一下参数max_allowed_packet 。该参数会限制服务器接受的数据包大小。当数据包大于max_allowed_packet时,触发错误EN_NET_PACKET_TOO_LARGE,并且关闭Connection。如果SQL语句过大或者返回的单个数据行很大,比如数据行里面有blob字段,可以适当调大该参数。

2、查询缓存

mysql服务器收到查询语句后,首选检查查询缓存是否命中的数据,如果命中则检查一下用户权限,如果权限没有问题,那么直接将缓存结果返回,如果没有命中,再解析SQL等后续处理。命中缓存的检查是根据一个对大小写敏感的哈希查找实现的。

3、查询优化

查询优化由多个子阶段组成:解析SQL、预处理、优化执行计划。前两个阶段很简单,就是判断SQL语句是否合法,比如SQL语句是否符合语法要求,查询的字段是否存在等。下面重点介绍第三个阶段。

(1)优化器

执行计划是由优化器生成的,一个SQL语句可以有多种执行方式,每个方式都可以返回正确结果,优化器的作用就是找出最好的执行计划。
MySQL使用基于成本的的优化器。成本的计算公式非常复杂,可以简单的认为成本是随机读取数据页的个数。可以通过last_query_cost的值得到当前SQL查询的成本。如下图:
一文让你理解mysql内部原理_第2张图片
上图的last_query_cost值表示优化器认为大概需要做3个数据页的随机查找才能完成上面的查询。它是根据一系列统计信息计算出来的。优化器在评估成本时并不考虑任何层面的缓存,它假设读取任何数据都需要一次磁盘IO。
有很多原因会导致优化器选择错误的执行计划,如下:

  1. 统计信息不准确,如上面介绍的InnoDB提供的索引统计信息是估算值;
  2. 执行计划中的成本估算不等同于实际执行的成本,比如有些页面可能缓存在内存中,而执行计划会忽略这一点;
  3. 优化器只是基于当前的SQL语句制定执行计划,并不考虑其他并发执行的查询,也就是说优化器是基于局部优化,并不是整体优化;
  4. 优化器并不是任何时候都是基于成本的优化,有时候会基于一些固定的规则;
  5. 优化器有时候无法估算所有可能的执行计划,可能会错过最好的执行计划。

优化器的优化策略分为两种:静态优化和动态优化。静态优化基于一些固定规则,例如使用代数变换将where条件转换为另一种等价形式,它不依赖于特别的数值。静态优化在第一次完成后就一直有效,即使使用不同的参数重复执行也不会发生变化。动态优化和查询上下文有关,例如它会考虑where条件中的数值,每次查询都需要重新评估,甚至在执行过程中也会重新优化。
下面是优化器可以处理的优化类型:

  1. 重新定义关联表的顺序;
  2. 将外连接转化为内连接;
  3. 使用等价变换规则,简化where条件;
  4. 优化count(),min(),max(),例如可以借助索引快速找到最大或最小值;
  5. 预估查询条件并转化为常数表达式,比如在索引上查询min();
  6. 使用索引覆盖扫描;
  7. 子查询优化;
  8. 提前终止查询,比如limit;
  9. 优化IN(),有一些资料上介绍尽量少用in,in不走索引,但是在mysql中不一样,in可以使用索引,而且mysql还优化了in的处理方式,加快了处理速度。

优化器还有其他很多优化类型,这里不再介绍。优化器在绝大部分时候都可以正常工作,个别情况下可能不理想,我们可以在查询中添加hint提示,或者重新SQL等方式。

(2)关联查询优化

UNION查询

mysql执行UNION查询时,先执行一系列单个查询,将查询结果放到一个临时表中,该临时表就是UNION查询结果,最后将临时表数据返回客户端。

多表关联查询

简单的说,mysql对任何关联都执行嵌套循环关联操作,即先在一个表中循环取出单条数据,然后嵌套循环到下一个表寻找匹配的行,依次下去,直到找到所有表中的匹配记录。下面以两个表关联作为例子:select * from A,B where A.id=B.id and A.name='allen',这个sql语句执行逻辑类似于下面:

Result r;
List listA=找到A表中所有name为'allen'的记录组成一个链表;
for(A a:listA){
	List listB=找到B表中所有id为a.id的记录组成一个链表;
	for(B b:listB){
		r.add(a+b);//将a和b组合起来,放到结果集中
	}
}

内连接、外连接和子查询的执行逻辑与上面类似,或者说mysql的所有类型查询本质上都是以相同的方式运行。子查询是先执行子查询,将结果放到临时表,然后将临时表当做普通版,按照上面多表关联的模式执行。mysql语法不支持全外连接,不过可以通过UNION一个做外链接和一个右外连接得到结果。
优化器可以调整多表关联查询的表关联顺序,选择一个代价最小的关联顺序。理想情况下,mysql会尝试所有的关联顺序,然后选择代价最小的,但是当表较多时,尝试所有情况就不现实了,mysql会采用一种“贪婪”搜索来查找,这种情况下偶尔就会错过最优的关联顺序。参数optimizer_search_depth可以设置超过多少表关联时,mysql启用“贪婪”搜索,默认是62,因为mysql最多支持61张表关联,所以默认情况下不会启用“贪婪”搜索。

(3)排序优化

这里的排序不是单指order by操作,group by、distinct也要先排序。
排序有两种算法:

  • 两次传输排序:读取行指针和需要排序的字段,对其进行排序,然后根据排序结果读取对应数据行,因为需要两次读取数据,所以叫做两次传输排序,第二次读取是根据排序记录读取数据行,这会产生大量的随机IO,优点是因为只需要行指针和排序字段,所以排序时占用的空间比较少;
  • 单次传输排序:先读取查询所需要的所有列,然后根据排序字段排序,最后直接返回排序结果。这个算法在mysql4.1版本引入。相比两次传输排序,该算法少量一次随机IO,缺点是因为读取了所有列再排序,这会造成排序时占用大量的空间,甚至需要使用磁盘。

两个算法没有哪个最好或者哪个最差,mysql提供了参数max_length_for_sort_data来确定使用哪种算法,该参数默认值是1024字节,如果查询需要返回的列和order by的列的总长度不超过max_length_for_sort_data,使用第二种算法,否则使用第一种。这里注意一点,对于VARCHAR列,mysql是按照定义的最大长度参与比较,而不是实际存储的长度。
mysql有一个排序缓存区,如果需要排序的数据量小于缓存区,则在内存中完成排序,否则将数据分块,对每个数据块排好序然后存储到磁盘上,最后将各个排好序的数据块合并。参数sort_buffer_size用于设置排序缓存区的大小。尽管内存排序快,但是排序缓存也不能设置的太大。
排序时mysql使用的临时存储空间可能非常大,原因是排序时,对每个排序记录都会分配一个足够长的定长空间来存放,这个定长空间必须足够长以容纳其中最长的字符串。
关联查询中如果需要排序,mysql会分两种情况进行处理,一种是order by子句中的所有列都来自关联表的第一个表,那么mysql在处理第一个表的时候就对记录进行排序。另一种是首先将关联的结果存放到临时表中,然后在所有的关联都结束后,在进行排序。

临时表有两种:内存临时表和磁盘临时表。内存临时表的存储引擎是Memory,磁盘临时表的存储引擎是myisam。参数tmp_table_size用于设置内部创建的内存临时表可以使用的最大内存(不过,实际起作用的是tmp_table_size和max_heap_table_size的最小值),如果超过了,将会被转换为磁盘临时表。运行时,每个线程分配自己的内存存储内存临时表。
max_heap_table_size:定义了用户可以创建的内存表的大小,这个值用来计算内存表的最大行数值。
mysql不会在内部创建的临时表上添加索引。

(4)优化器局限性

  1. mysql对子查询优化实现的非常糟糕,最糟糕的是where条件中包含IN()的子查询,例如:
select * from file where file_id in(select file_id from film_actor where actor_id=1);

mysql会将IN()加子查询改写为EXISTS()加子查询,使得mysql无法利用IN()的专门优化策略。
但也不是说禁止使用子查询或者子查询的性能一定差,有的子查询也可以很快的查询出结果,而且mysql5.6版本及以后版本,子查询已经做了优化,性能已经有了很大的提升,具体是否使用子查询,我们可以先进行测试,如果发现性能不好,再将子查询改写为多表关联或者使用EXISTS()等效的方式。
《高性能MySQL》介绍在mysql5.6版本及以后版本可以不考虑使用关联查询等方式替换子查询的优化建议。

  1. UNION无法将外层的LIMIT限制内推到各个子句。比如:
(select first_name from actor  order by first_name )
UNION ALL
(select first_name from customer order by first_name )
limit 20;

对于上面的语句,其实各个子句最多查询20条即可获得正确的结果,但是优化器处理的时候,外层的limit 20无法内推到内层,mysql实际执行的时候会将各个子句的所有结果查询出来,然后在选出20条数据,其实不只是limit无法内推,包括where、order by等也无法内推;

  1. 尽量减少索引合并的使用;
  2. mysql无法利用多个线程并行执行查询,也就是说一个查询语句只能使用一个线程;
  3. 不支持哈希关联,就像在多表关联一节中介绍,mysql都是使用嵌套循环关联;
  4. mysql不支持松散索引扫描,松散索引扫描是指扫描一段索引后跳过一段索引然后扫描下一段,也就是可以分段扫描;

4、查询执行引擎

mysql优化器将SQL语句转化为执行计划,mysql的执行引擎根据执行计划完成整个查询。

5、返回结果给客户端

查询的最后一步是将结果返回给客户端。如果查询可以被缓存,mysql在这一阶段也会将结果存放到查询缓存中。
mysql不是等到所有结果都准备好之后才返回,而是开始生成第一条结果时就开始向客户端发送结果集了,它是一个增量、逐步返回的过程,这么做有两个好处:1、客户端可以第一时间得到返回结果;2、服务器无需存储太多的结果,可以减少内存消耗。

三、常用命令

这里只是介绍命令的作用,大家可以从网上查询每个命令输出结果的介绍。
SHOW VARIABLES:查询系统变量
SHOW STATUS:查询系统状态
show processlist:显示用户正在运行的线程。除了root用户能看到所有正在运行的线程外,其他用户都只能看到自己正在运行的线程,看不到其它用户正在运行的线程。除非单独个这个用户赋予了PROCESS 权限。
optimize table:该命令会重建表并且更新索引统计信息,如果进行了大量的插入和删除操作,导致表空间出现碎片,执行该命令可以减少对存储空间使用和提升访问表的IO效率。
show full processlist:可以显示当前所有的线程的运行状态,root用于可以查看所有用户的线程,否则只能查看当前用户的线程。
SHOW TABLE STATUS LIKE ‘表名’:可以查看表信息,里面包含了记录数、索引占用空间大小(单位是字节)、表占用空间大小。

四、SQL优化建议

1、分析SQL执行慢的方法

  1. SHOW PROFILE:默认是关闭的,需要执行set profiling=1打开,set global profiling=1用于打开全局,该工具将结果记录到一个临时表中,它会给出语句执行过程中每个阶段花费的时间,非常详细。还有参数profiling_history_size表示会话保留最近多少条查询的统计结果,默认是15条,除了执行show profiles/show profile for query 数字外,还可以查询INFORMATION_SCHEMA库的PROFILING表,后者提供了比前者更多的信息。无论是SHOW PROFILE命令还是PROFILING表都是基于会话的,在一个会话中执行查询后的统计结果只能在本会话查询,在另一个会话是不可见的。
  2. SHOW STATUS/SHOW GLOBAL STATUS:该命令展示的是一些计数器值,对于会话级别的计数器,可以通过FLUSH STATUS清零。该命令的执行结果也会创建一个临时表,不过对该临时表的操作也会记录到计数器中。
  3. 慢查询日志:使用慢查询日志,需要将慢查询日志开关打开,默认是关闭的,可以通过参数slow_query_log开启。long_query_time表示超过该参数设置的执行时间才会记录到慢查询日志中,long_query_time=0可以将所有的查询信息写入慢查询日志,该参数既有全局设置也有会话级别的设置,如果设置的是全局,新建立的连接才会生效。有很多工具可以辅助分析慢查询日志文件。慢查询日志提供了比上面两个命令更多的细节信息。

下面介绍几个优化查询语句的方法,这些方法并不是一开始设计的时候就采用,可以到出现性能问题优化的时候使用。

  1. 尽量避免使用select * from,查询尽量只返回需要的字段,但这个并不是必须的,只有当确实遇到性能问题了,再采用这个方法;
  2. 如果一个复杂查询性能出现性能问题,可以尝试修改为多个简单查询;
  3. 如果一个查询返回的数据量很多,可以对查询结果分段,每次查询一部分数据,这个对数据更新或者删除的时候,非常有用,每次更新和删除一部分数据;
  4. 分解关联查询,将关联查询分解为多个单表查询,然后在应用程序中做关联。

2、优化特定类型的查询

(1)COUNT()

count函数用于统计行数或者列值的个数,统计列值个数时,如果列值为null,则不再统计范围内。列值也可以放在表达式中,将整个表达式作为count()的参数,这样count()统计的是表达式结果不为null的数量。
统计行数最好使用count(*)。
InnoDB没有存储表的行数大小,所以每次执行count都是实时查询,如果表非常大,统计很慢,可以采用近似值,或者创建汇总表。

(2)优化关联查询

确保ON或者USING子句中的列上有索引;确保group by和order by的表达式只涉及到一个表中的列,这样mysql才有可能使用索引优化这个过程。

(3)优化子查询

mysql5.6之前的版本尽量将子查询修改为关联查询。
mysql5.6版本或者更新的版本,可以不考虑上述建议。

(4)UNION优化

mysql总是将UNION中各个子句的查询结果放到临时表中,最后从临时表中读出数据返回客户端。因此很多优化策略在UNION查询中无法很好的发挥作用。
默认mysql会对临时表执行distinct,最终临时表中没有重复数据,但是这样做的代价非常高。
除非确实需要消除重复行,否则就一定要使用UNION ALL。

3、mysql表字段优化建议

字段类型尽量选择可以正确存储数据的最小数据类型,因为数据越小,占用的磁盘、内存空间越少,相应的加载数据速度就会越快。而且类型尽量使用简单的,能使用数字的不使用字符串。
表的主键最好使用整型,尽量不要使用MD5、UUID这种随机或者近似有序的字符串做主键。表中字段尽量不要太多,mysql限制最多61个表关联,尽管如此也要注意表关联不要太多。

(1)NOT NULL

还要尽量将列定义为NOT NULL,因为1、无论是在索引还是在表中,存储NULL值是要占空间的,这一点和空值是不同的,有些资料介绍NULL值需要占一个字节,2、在使用聚合函数时,有些函数会忽略NULL值,造成统计结果不准确,3、在使用null作为条件判断时,应该使用is null/is not null,4、当查询条件中包含索引列,且索引列条件是IS NULL或者IS NOT NULL,那么在一些场景下mysql不使用索引,这样造成索引失效。
尽管使用可以为NULL的列有这些缺点,但是并不是说我们不能使用,我觉得这需要结合我们的需求、数据量综合考虑,如果使用可以为NULL的列,对系统性能没有任何影响,而且还会带来开发便利,我们为什么不使用?而且将列修改为NOT NULL,对性能带来的提升很有限。

(2)整型

mysql提供了TINYINT/SMALLINT/MEDIUMINT/INT/BIGINT等多种的整形类型,mysql也支持在类型后面加括号,比如INT(8),这里的8是没有意义的,它不限制该字段的取值范围,也不限制数据库在磁盘上占用的空间,它只是用于标示这个字段的长度应该不超过8。

(3)浮点型

实数类型提供了FLOAT(4字节)/DOUBLE(8字节)/DECIMAL三种类型,其中FLOAT/DOUBLE不支持精确计算,DECIMAL支持精确计算。如果字段定义的是float类型,mysql内部计算的时候是将其转换为double进行计算的。
下面重点说一下decimal类型,MySQL分别为整数和小数部分分配存储空间。 MySQL使用二进制格式存储DECIMAL值。它将9位数字包装成4个字节。例如DECIMAL(P,D),P是表示有效数字数的精度,P范围为最大65,D是表示小数点后的位数,D的范围是0~30。MySQL要求D小于或等于(<=)P。整数部分的长度为P-D,这里的位数都是按10进制。存储的decimal的时候,小数点也要占一个字节空间。一般当进行精确计算时才使用decimal,比如计算金额。

(4)字符串

字符串类型有VARCHAR和CHAR两种,VARCHAR是变长类型,字符串越短使用的空间越少,另外还需要额外的1或者2个字节存储长度。CHAR是定长类型,当字符串末尾有空格时,mysql会自动删除末尾的空格而VARCHAR不会。因为CHAR是定长的,所以相对于VARCHAR,不易产生碎片,对于非常短的列,CAHR也比VARCHAR节省空间,因为CAHR不需要存储字符串的长度,总的来说CHAR适合存储定长或者非常短的字符串。VARCHAR在磁盘存储时是变长的,但是在内存中是定长的,所以在内存中,当存储存储相同的字符串,VARCHAR(20)占用的空间要大于VARCHAR(5)。VARCHAR和CHAR定义的长度都是字符长度。

(5)BLOB和TEXT

BLOB和TEXT用于存储大数据量的二进制和字符串。mysql在存储这两种类型时,会使用专门的外部存储区域来存储真实数据,每个值在行内需要1到4个字节存储一个指针。当对BLOB和TEXT类型排序时,只针对每个列最前的max_sort_length字节(可以通过set命令设置,默认是1024)而不是整个字符串排序,mysql也不会将BLOB和TEXT列的全部长度的字符串进行索引。实际使用中要尽量避免使用BLOB和TEXT类型。

(6)日期

DATETIME和TIMESTAMP是mysql提供的日期类型,DATETIME占8个字节,与时区无关,表示范围从1001年到9999年。TIMESTAMP占4个字节,表示范围是1970年到2038年,显示值依赖时区。另外如果插入时没有设置TIMESTAMP的值,默认情况下数据库会自动设置为当前时间。除非有特殊要求,尽量使用TIMESTAMP类型,因为其存储空间少。
为了加快查询速度,我们可以反范式化,在表里面冗余一些数据,这样便可以创建索引,由表关联变为单表使用索引查询,另外也可以创建数据汇总表、计数器表以方便查询。

4、大字段列

对于变长列,比如BLOB、CLOB、很长的VARCHAR,InnoDB存储一个768字节的前缀在行内,其余的内容在行外分配扩展空间来存储。BLOB、CLOB、很长的VARCHAR这样的列是无法使用内存临时表的,如果查询涉及这样的字段而且需要临时表,那么InnoDB只能在磁盘上创建临时表,这样的话会造成处理性能的下降。一个解决办法是使用substring()函数截取一部分放到临时表中。
一般情况下,最好不要在数据库中存储BLOB、CLOB、很长的VARCHAR这样的大字段,如果必须要存储,尽量将他们放到一张表里面。
使用大字段也会导致排序时只能使用两次传输排序算法,即使order by子句不涉及大字段。

五、mysql配置简述

本文不对mysql的各个配置项含义以及如何配置做介绍,因为如果对mysql的内部运行原理不是很了解,修改默认配置可能会带来灾难性的后果,最好的方式是采用默认值。其实从调优的角度来说,修改配置带来的收益比较小。而且当我们发现一些查询出现性能问题时,首先应该从索引、表结构等方面入手分析。
mysql的配置文件名为my.ini(Win系统)或者my.cnf(Linux系统)。配置文件里面分成了几个部分,每个部分开头是一个用方括号括起来的分段名称,mysql服务器通常读取mysqld这部分的配置。
配置项都是小写的,单词之间用横线或者下划线分隔。配置的作用范围是不一样的,有些配置修改会影响全局,有些配置只会在当前会话中生效,修改时要注意这一点。
mysql内部使用的系统表、内部创建的磁盘临时表存储引擎使用的都是myisam(内存临时表存储引擎是Memory),所以需要分配少量的系统资源给myisam。

mysql预编译
MySQL运行状态show status详解
MySQL 参数- Innodb_File_Per_Table(独立表空间)
[MySQL]浅谈InnoDB存储引擎(四)插入缓冲
MySQL插入缓冲

你可能感兴趣的:(mysql,mysql,查询优化,数据库,innodb,sql)