一起学 MySQL 性能优化

用户头像
xcbeyond
关注
发布于: 2020 年 08 月 25 日
一起学MySQL性能优化

作为程序员的你,数据库作为一门必修课,而MySQL数据库毫无疑问已经是最常用的数据库了。系统的稳定、高效、高并发等指标,很大程度上取决于数据库性能是否够优,可见性能优化的重要性,这也就不难理解各位在任何一场面试中都会被问及到数据库调优相关的问题。



因此,这就是我为何考虑写本篇文章的主要原因,希望该文章(MySQL性能优化)能够给你带来收获,让你更系统、更全面的掌握MySQL性能优化的技能、技巧。如果觉得现在或者将来可能对你有用,不妨关注、收藏。



在MySQL性能优化之前,你有必要重新再认识下MySQL,便于逐步由浅入深的了解MySQL,以及更容易理解MySQL性能优化中涉及到的知识点和优化技巧。



本文将从以下几个方面开始MySQL性能优化的学习:



  • MySQL的架构与核心问题

  • 如何选择优化的数据类型

  • 带你深入了解索引这些事

  • 如何高效正确的使用索引

  • 为什么你的SQL查询这么慢

  • 常见优化SQL技巧

  • 带你搞懂MySQL执行计划



一、MySQL的架构与核心问题



为了充分发挥MySQL的性能,并能够正确、高效的使用它,就必须理解MySQL的设计、存储引擎等问题,做到知己知彼,百战百胜。本节将从MySQL架构、一些核心问题(高并发、存储引擎等)两大方面展开讨论,这些内容将有助于你更清楚、更明白MySQL优化的原理。



1.1 MySQL逻辑架构



想深入探究MySQL之前,有必要了解一下MySQL的逻辑架构,逻辑架构图如下:

MySQL的逻辑架构中,分为三层,如上图红色虚线框的三部分。



最上层架构并不是MySQL所独有的,大多数基于客户端/服务器形态的系统或者服务,都有类似的架构,其中包含MySQL的连接处理、授权认证、安全控制等等。



第二层架构是MySQL中最为核心的部分,其中包括查询解析、分析、优化、缓存以及所有的内置函数(如:日期、时间、函数等),所有跨存储引擎的功能都在这一层实现,例如:存储过程、触发器、视图等。



第三层架构是存储引擎。存储引擎负责MySQL中数据的存储和提取,类似与Linux系统下的各种文件系统一样,不同存储引擎都有各自的优势和劣势,不同场景可选择不同的引擎。不同存储引擎之间是不会相互通信的,只是简单地响应上层的请求。



1.2 核心问题



1.2.1 SQL执行流程



为了便于分析SQL执行的性能瓶颈,我们有必要认清楚一条SQL执行过程中,究竟经历了什么,都有谁和它亲密的接触过,下面我们就来看看SQL执行过程中经历了些什么吧。



(1)连接



1)客户端发起一次SQL请求,监听客户端的【连接管理模块】接收到请求。



2)将请求转发到【连接进/线程模块】。



3)调用【用户模块】来进行权限检查。



4)通过检查后,【连接进/线程模块】从【线程连接池】中取出空闲的被缓存的连接线程和客户端请求对接,如果失败,则创建一个新的连接请求。



(2)处理



1)先查询缓存,检查SQL语句是否完全匹配,接着再检查是否具有权限,都成功则直接取数据返回。(即:缓存命中)



2)上一步失败,则转交给【命令解析器】,经过词法分析、语法分析后生成解析树。



3)接下来是预处理阶段,处理解析器无法解决的语义,检查权限等,生成新的解析树。



4)再转交给对应的模块处理。



5)如果是SELECT查询,还会经由【查询优化器】做大量的优化,生成执行计划。



6)模块收到请求后,通过【访问控制模块】检查所连接的用户是否有访问目标表和目标字段的权限。



7)有则调用【表管理模块】,先是查看表cache中是否存在,有则直接对应的表和获取锁,否则重新打开表文件。



8)根据表的meta数据,获取表的存储引擎类型等信息,通过接口调用对应的存储引擎处理。



9)上述过程中产生数据变化的时候,若打开日志功能,则会记录到相应二进制日志文件中。



(3)结果



1)SQL请求完成后,将结果集返回给【连接进/线程模块】。



2)返回的也可以是相应的状态标识,如成功或失败等。



3)【连接进/线程模块】进行后续的清理工作,并继续等待请求或断开与客户端的连接。



1.2.2 如何控制高并发的读写?



无论何时,对于数据库而言,高并发的读写操作是很常见的,针对同一条记录在同一时刻进行修改、查询操作,都会产生并发控制的问题,处理不当将会出现大量的脏数据。那么,如何控制高并发的读写操作呢?



1.2.2.1 读写锁



在我们学习任何一门语言时,针对处理并发问题都会选择锁机制来解决并加以控制,这也是解决并发控制的经典方法,MySQL也不例外。在MySQL处理高并发的读或者写时,可以通过实现两种类型的锁来解决,这两种类型的锁通常被称为共享锁(Shared lock)和*排他锁(exclusive lock)*,其实就是大家叫的读锁(read lock)和*写锁(write lock)*。



读锁,是共享的,也就是说是互相不阻塞的。多个请求在同一时刻可以同时读取同一条记录,而互不干扰。



写锁,是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,避免在写的过程中进行读、再写的操作,这更是出于安全的考虑,只有这样才能确保数据的准确、干净。在数据库中,每时每刻都在发生锁定,当某次请求修改数据时,MySQL都会通过锁来防止其他请求读取同一数据。



1.2.2.2 锁策略



有了锁的机制,就能更好的控制高并发的读写操作,我们都知道锁也是有范围的,锁定对象范围的选择,更具有挑战性。尽量只锁定需要修改的部分数据,而不是所有数据,这也是选择锁定对象范围最想满足的。锁定范围越精确,锁定的数据量就越小,则系统的并发程度越高,加锁本身消耗的资源也就越小。



上述提到的无非就是设定锁的粒度,而MySQL则提供了多种选择,每种MySQL存储引擎都可以实现自己的锁策略和锁粒度。下面将介绍两种最常用的锁策略。



  • 表级锁(table lock)



表级锁是MySQL最基本的锁策略,并且是开销最小的策略,它会锁定整张表。一个请求在对表进行写操作(插入、修改、删除等)前,需要先获得写锁,此时会阻塞其他请求对该表的所有读写操作。只有没有写锁时,其他请求才能读取并获得读锁,<u>读锁之间是不相互阻塞的</u>。



尽管存储引擎可以管理自己的锁,而且MySQL本身还会使用各种有效的表级锁来实现不同的目的。例如,诸如ALTER TABLE之类的语句就使用了表级锁,而忽略存储引擎的锁机制。



开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。



表级锁,更适用于以查询为主,只有少量按索引条件更新数据的应用。



  • 行级锁(row lock)



行级锁可以最大程度地支持并发处理(同时也带来了最大的锁开销)。



开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。



行级锁,更适合于有大量按索引发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理系统。



1.2.3 MySQL存储引擎是怎样的?



在文件系统中,MySQL将每个数据库(即:schema)保存为数据目录data下的一个子目录。创建表时,MySQL会在数据库data目录下创建一个和表同名的.frm文件来保存表的定义。



不同的存储引擎保存数据和索引的方式是不同的,但表的定义则是在MySQL服务层统一处理的。



可以使用show table status like '表名' \G命令来查看表的存储引擎以及表的其他相关信息,例如,查看mysql数据库中的user表:



mysql> use mysql;
No connection. Trying to reconnect...
Connection id: 20587
Current database: *** NONE ***

Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show table status like 'user' \G;
*************************** 1. row ***************************
Name: user
Engine: MyISAM
Version: 10
Row_format: Dynamic
Rows: 3
Avg_row_length: 125
Data_length: 512
Max_data_length: 281474976710655
Index_length: 4096
Data_free: 136
Auto_increment: NULL
Create_time: 2019-07-12 14:45:17
Update_time: 2019-12-20 15:55:44
Check_time: NULL
Collation: utf8_bin
Checksum: NULL
Create_options:
Comment: Users and global privileges
1 row in set (0.00 sec)

ERROR:
No query specified



从查询结果的Engine字段可以表明,user表的存储引擎类型为MyISAM,其他字段在此就不一一说明,如想详细了解可查阅相关文档。



在了解MySQL存储引擎前,可以先看看你的MySQL数据库支持哪些存储引擎,可通过show engines命令查看。我使用的MySQL版本为5.7.25,查看结果如下图所示:

从上述结果可以看出,支持的存储引擎有: InnoDBMrg_MyisamMemoryBlackholeMyISAMCSVArchivePerformance_SchemaFederated ,其中也做了简单的解释说明。



本文只针对InnoDBMyISAM两种最常见的存储引擎进行着重说明,其它存储引擎只做简单说明,详细可查阅官方文档。



1.2.3.1 InnoDB存储引擎



InnoDB是MySQL的默认事务型引擎,也是最重要、使用最广泛的存储引擎,并且有行级锁定和*外键约束*。



它被设计用来处理大量的短期(short-lived)事务,短期事务大部分情况是正常提交,很少会被回滚。InnoDB的性能和自动崩溃恢复特性,使得它在非事务型存储的需求中也很流行。除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎。



InnoDB的适用场景/特性,有以下几种:



  • 经常更新的表,适合处理多重并发的更新请求。

  • 支持事务。

  • 可以从灾难中恢复(通过bin-log日志等)。

  • 外键约束。只有他支持外键。

  • 支持自动增加列属性auto_increment。



1.2.3.2 MyISAM存储引擎



MyISAM提供了大量的特性,包括全文检索、压缩等,但不支持事务和行级锁,支持表级锁

对于只读的数据,或者表较小、可以忍受修复操作的场景,依然可以使用MyISAM。



MyISAM的适用场景/特性,有以下几种:



  • 不支持事务的设计,但是并不代表着有事务操作的项目不能用MyISAM存储引擎,完全可以在程序层进行根据自己的业务需求进行相应的控制。

  • 不支持外键的表设计。

  • 查询速度很快,如果数据库insertupdate的操作比较多的话比较适用。

  • 整天 对表进行加锁的场景。

  • MyISAM极度强调快速读取操作。

  • MyIASM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyIASM也是很好的选择。



1.2.3.3 MySQL内建的其他存储引擎



MySQL还有一些特殊用途的存储引擎,在一些特殊场景下用起来会很爽的。在MySQL新版本中,有些可能因为一些原因已经不再支持了,还有一些会继续支持,但是需要明确地启用后才能使用。



1.2.3.3.1 Archive存储引擎



Archive引擎**只支持insertselect操作**,并且在MySQL 5.1之前连索引都不支持。



Archive引擎会缓存所有的写并利用zlib对插入的行进行压缩,所以比MyISAM引擎的磁盘I/O更少。但是每次select查询都需要进行全表扫描,所以Archive更适合日志和数据采集类应用,况且这类应用在做数据分析时往往需要全表扫描。



Archive引擎支持行级锁和专用的缓冲区,所以可以实现高并发的插入。在一个查询开始直到返回表中存在的所有行之前,Archive引擎会阻止其他的select执行,以实现一致性读。另外,这也实现了批量插入在完成之前对读操作是不看见的。



1.2.3.3.2 Blackhole存储引擎



Blackhole引擎没有实现任何的存储机制,它会丢失所有插入的数据,不做任何保存。怪哉,岂不是一无用处?



但是服务器会记录Blackhole的日志,所以可以用于复制数据到备库,或者只是简单地记录到日志。这种特殊的存储引擎可以在一些特殊的复制架构和日志审核时发挥作用。



但这种存储引擎的存在,至今还是有些难以理解。



1.2.3.3.3 CSV存储引擎



CSV引擎可以将普通的CSV文件作为MySQL的表来处理,但这种表不支持索引。



CSV引擎可以在数据库运行时拷入或者拷出文件,可以将Excel等电子表格软件中的数据存储为CSV文件,然后复制到MySQL数据目录下,就能在MySQL中打开使用。同样,如果将数据写入到一个CSV引擎表中,其他的外部程序也能立即从表的数据文件中读取CSV格式的数据。



因此,CSV引擎可以作为一种数据交换的机制,是非常有用的。



1.2.3.3.4 Memory存储引擎



如果需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没有关系,那么使用Memory引擎是非常有用的。Memory引擎至少比MyISAM引擎要快一个数量级,因为所有的数据都保存在内存中,不需要进行磁盘I/O。Memory引擎的表结构在重启以后还会保留,但数据会丢失。



Memory引擎在很多场景下可以发挥很好的作用:



  • 用于查找或者映射表,例如将邮箱和州名映射的表。

  • 用于缓存周期性聚合数据的结果。

  • 用于保存数据分析中产生的中间数据。



Memory引擎支持Hash索引,因此查找非常快。虽然Memory的速度非常快,但还是无法取代传统的基于磁盘的表。Memory引擎是表级锁,因此并发吸入的性能较低。



如果MySQL在执行查询的过程中,需要使用临时表来保存中间结果,内部使用的临时表就是Memory引擎。如果中间结果太大超出了Memory的限制,或者含有BLOBTEXT字段,则临时表会转换成MyISAM的引擎。



看了上面的说明,大家就会经常混淆Memory和临时表了。临时表是指使用CREATE TEMPORARY TABLE语句创建的表,它可以使用任何存储引擎,因此和Memory不是一回事。临时表只在单个连接中可见,当连接断开时,临时表也将不复存在。



关于临时表和Memory引擎的那些事,可参考MySQL · 引擎特性 · 临时表那些事儿



MySQL的存储引擎及第三方存储引擎,还有很多,在此就不一一介绍了,后续如有需要,再进一步来谈谈。



1.2.4 如何选择合适的存储引擎呢



这么多存储引擎,真是眼花缭乱,我们该如何选择呢?



大部分情况下,都会选择默认的存储引擎——InnoDB,并且这也是最正确的选择,所以Oracle在MySQL 5.5版本时终于将InnoDB作为默认的存储引擎了。



<u>对于如何选择合适的存储引擎,可以简单地归纳为一句话:”除非需要用到某些InnoDB不具备的特性,并且没有其他可以替代,否则都应该优先选择InnoDB引擎”。</u>



例如,如果要用到全文检索,建议优先考虑InnoDB加上Sphinx的组合,而不是使用支持全文检索的MyISAM。当然,如果不需要用到InnoDB的特性,同时其他引擎的特性能够更好地满足需求,就可以考虑一下其他存储引擎。



除非万不得已,建议不要混合使用多种存储引擎,否则可能带来一系列复杂的问题,以及一些潜在的bug和边界问题。



如果需要使用不同的存储引擎,建议考虑从以下几个因素进行衡量考虑。



  • 事务

  • 备份

  • 恢复

  • 特有的特性



二、如何选择优化的数据类型



良好的设计是高性能的基石,应该根据系统的实际业务需求、使用场景进行设计、优化、再调整,在这其中往往需要权衡各种因素,例如,数据库表究竟如何划分、字段如何选择合适的数据类型等等问题。



MySQL支持的数据类型非常之多,对于选择恐惧症的小伙伴而言,苦不可言。大部分人在创建数据库表时,基本一股脑的使用INTVARCHAR这两种类型最多,至于长度,则会选择足够大即可,避免日后不够用咋办。只顾当时一时爽,之后坑谁谁知道。



如果你是一个追求极致、高效的开发者,对于上面的情况肯定是不愿让其发生的。在众多的数据类型面前,如何选择正确的数据类型,对于高性能是至关重要的。本节将介绍如何选择优化的数据类型,来提高MySQL的性能,将会选取最为常用的类型进行说明,便于在实际开发中创建表、优化表字段类型时提供帮助。



2.1 选择原则



不管存储哪种类型的数据,下面几个简单的原则将有助于你做出更好的选择。



2.1.1 更小的通常更好



一般情况下,应该尽可能选择正确存储数据的最小数据类型。更小的数据类型通常更快,因为它们占用更少的磁盘空间、内存,并且处理时需要的CPU周期更少。



但是,在选择更小数据类型时,一定不要低估存储值的范围,因为后期修改数据类型及长度是一件非常痛苦、耗时的操作。如果无法确定哪个数据类型是最好的,就选择你认为不会超过范围的最小类型。



2.1.2 简单就好



简单的数据类型操作通常需要更少的CPU周期。例如,整型比字符操作代价更低,因为字符集和校队规则(如:排序规则)使得字符比较比整型比较更复杂。



2.1.3 尽量避免用NULL



NULL是在常见不过的值了,通常都习惯对某些字段设置默认值为NULL,这其实是一种非常不好的习惯。如果查询中的字段值恰巧是设置的NULL值,对MySQl来说更难优化,因为可为NULL的字段使得索引、值比较都更复杂。



NULL值不能进行索引,影响索引的统计信息,影响优化器的判断。复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。



2.2 字符串类型



字符串类型是数据库中使用频率最高的数据类型,VARCHARCHAR是两种最主要的字符串类型,都可以用来存储字符串,但它们保存和检索的方式不同。VARCHAR属于可变长度的字符类型,而CHAR属于固定长度的字符类型。下面是关于这两种类型的说明、比较。



### 2.2.1 VARCHAR



VARCHAR类型用于存储可变长字符串,它比定长类型更节省空间,因为它仅使用必要的空间(例如,越短的字符串使用最少的空间)。



VARCHAR需要使用1或2个额外的字节来记录字符串的长度(如果字段的最大长度小于或等于255字节,则只使用1个字节表示长度,否则使用2个字节来表示长度)。例如,一个VARCHAR(10)的字段需要11个字节的存储空间,VARCHAR(1000)则需要1002个字节的存储空间,其中需要2个字节来存储长度。



2.2.2 CHAR



CHAR类型是定长的。当数据类型为CHAR时,MySQL会删除所有的末尾空格。



CHAR类型适合存储很短的字符串,或者所有值都接近同一个长度。例如,CHAR类型非常适合存储密码的MD5值,因为这是一个定长的值。对于经常变更的数据,CHAR类型也比VARCHAR类型更好,因为定长的CHAR类型不容易产生碎片。对于存储非常短的列,CHAR类型比VARCHAR在存储空间上更有效率。例如,用CHAR(1)来存储只有Y和N的值,如果采用VARCHAR(1)却需要2个字节,因为还会有一个记录长度的额外字节。



通过下面具体例子来对CHAR进行说明,有助于更好的理解。这里创建一张只有一个CHAR(10)字段的表char_test,并往里面插入三个字符串xcbeyond,注意前后有空格的区别:



mysql> create table char_test(ch char(10));
Query OK, 0 rows affected
mysql> insert into char_test(ch) values('xcbeyond'),(' xcbeyond'),('xcbeyond ');
Query OK, 3 rows affected
Records: 3 Duplicates: 0 Warnings: 0



奇怪的事情发生了,当我们查询时,会发现第三个字符串末尾的空格被自动截断了。为了更好的显示出是否有空格,对ch字段前后拼接'字符便于查看对比。



mysql> select concat("'",ch,"'") from char_test;
+--------------------+
| concat("'",ch,"'") |
+--------------------+
| 'xcbeyond' |
| ' xcbeyond' |
| 'xcbeyond' |
+--------------------+
3 rows in set



如果用VARCHAR(10)字段存储相同的值,则字符串末尾的空格是不会被截断的。



2.3 日期类型



MySQL提供了两种相似的日期类型:DATETIMETIMESTAMP,使用起来傻傻分不清,看完本节后不要再说不知道如何选择了。



对于应用程序而言,他们都能很好的表示日期,但是在某些场景下,各有不同。接下来让我们一起看看吧。



2.3.1 DATETIME



DATETIME类型能够保持很大范围的日期,从1001年到9999年,精度为秒。它把日期和时间封装到格式为YYYYMMDDHHMMSS的整数中,与时区无关,使用8个字节的存储空间。



默认情况下,MySQL是以一种可排序、无歧义的格式显示DATETIME值,例如2020-03-05 22:38:40



2.3.2 TIMESTAMP



TIMESTAMP,从它的名字不难看出,它和UNIX时间戳相同,保存了从1970年1月1日0时0分0秒以来的秒数TIMESTAMP只使用4个字节的存储空间,因此它的范围比DATETIME小得多,只能表示从1970年到2038年



TIMESTAMP显示的值依赖于时区,MySQL服务器、操作系统,以及客户端连接都有时区设置。因此,存储值为0的TIMESTAMP在美国东部时区显示为1969-12-31 19:00:00,与格林尼治时差5个小时。



(如果需要存储比秒更小粒度的日期和时间值该怎么办?MySQL目前没有提供合适的数据类型,但可以采用其他变通的方式,如可以使用自己的存储格式:可以使用BIGINT类型存储微妙级别的时间戳,或者使用DOUBLE存储秒之后的小数部分。或者也可以使用MariaDB数据库替代MySQL。)



**通常应该尽量使用TIMESTAMP,因为它比DATETIME更节省存储空间,而且对于跨时区的业务,TIMESTAMP更为合适。**



2.4 TEXT和BLOB类型



一般在保存少了字符串的时候,我们会选择CHARVARCHAR类型,而在保存较大文本等数据时,通常会选择使用TEXTBLOB



TEXTBLOB类型都是存储很大的数据而设计的字符串数据类型,分别采用字符串和二进制方式存储。例如,TEXT通常用来保存文章内容、日志等字符串内容,而BLOB通常用来保存图片、视频等二进制数据内容。有如下特点:



  • TEXT类型有字符集和排序规则。



  • BLOB类型存储的是二进制数据,没有排序规则或字符集。

  • MySQL中不能将TEXT和BLOB类型的列进行索引,也不能使用这些索引消除排序。



与其他数据类型不同,MySQL把每个TEXTBLOB类型的值当作一个独立的对象处理。存储引擎在存储时通常会做特殊处理,当它们的值太大时,InnoDB会使用专门的“外部”存储区域来进行存储,此时每个值在行内需要1~4个字节来存储一个指针,然后在外部存储区域存储实际的值。



在面对TEXT、BLOB之间的选择时,应该根据实际情况选择能够满足需求的最小存储类型,接下来主要针对TEXT、BLOB类型存在的一些常见问题进行介绍。



**1.在执行了大量的删除操作时,TEXTBLOB会引起一些性能问题**



删除操作会在数据库表中留下很大的“空洞”,以后填入这些“空洞”的记录在插入的性能上会有影响。为了提高性能,建议定期使用OPTIMZE TABLE功能对这类表进行碎片整理,避免因为“空洞”导致性能问题。



实战演示验证说明如下:



1)创建测试表text_test,字段idcontext的类型分别为int(11)text:



mysql> create table text_test(id int(11),context text);
Query OK, 0 rows affected



2)往表text_test中插入大量的数据,这里使用repeat函数插入字符串:



repeat函数用于字符串的复制



mysql> insert into text_test(id,context) values(1,repeat('xcbeyond',1000));
Query OK, 1 row affected
mysql> insert into text_test(id,context) values(2,repeat('xcbeyond',1000));
Query OK, 1 row affected
mysql> insert into text_test(id,context) values(3,repeat('xcbeyond',1000));
Query OK, 1 row affected
mysql> insert into text_test(id,context) values(4,repeat('xcbeyond',1000));
Query OK, 1 row affected
mysql> insert into text_test(id,context) values(5,repeat('xcbeyond',1000));
Query OK, 1 row affected
mysql> insert into text_test(id,context) values(6,repeat('xcbeyond',1000));
Query OK, 1 row affected
……



3)此时看看表text_test的物理文件大小:



2020/03/07 周六 15:58 540,672 text_test.ibd



这里显示数据文件大小为540Kb



4)从表text_test中删除一大部分数据,这些数据占总数据量的2/3:



mysql> delete from text_test where id < 10;
Query OK, 9 rows affected



5)在此查看text_test的物理文件大小:



2020/03/07 周六 16:05 573,440 text_test.ibd



奇怪的是,数据文件大小并没有因为删除数据而减少,反而还增加了一点。



6)接下来对表text_test进行OPTIMIZE优化操作:



mysql> optimize table text_test;
+----------------+----------+----------+-------------------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+----------------+----------+----------+-------------------------------------------------------------------+
| test.text_test | optimize | note | Table does not support optimize, doing recreate + analyze instead |
| test.text_test | optimize | status | OK |
+----------------+----------+----------+-------------------------------------------------------------------+
2 rows in set



7)再次查看表text_test的物理文件大小:



2020/03/07 周六 16:08 458,752 text_test.ibd



可以发现,表的数据文件大小减少了,则说明“空洞”空间已经被回收了。



**2.使用合成索引来提高大文本字段(TEXTBLOB类型)的查询性能**



合成索引,就是根据大文本字段的内容建立一个散列值,并把这个值存储在单独的数据列中,接下来就可以通过检索散列值找到数据行了。



但是,要注意这种技术只能用于精确匹配的查询(散列值对于类似<>=等范围搜索操作符是没有用处的)。可以使用MD5()函数生成散列值,也可以使用 SHA1()CRC32(),或者使用自己的应用程序逻辑来计算散列值。请记住数值型散列值可以很高效率地存储。同样,如果散列算法生成的字符串带有尾部空格,就不要把它们存储在CHARVARCHAR 列中,它们会受到尾部空格去除的影响。合成的散列索引对于那些 BLOB TEXT 数据列特别有用。用散列标识符值查找的速度比搜索BLOB 列本身的速度快很多。



实战演示验证说明如下:



1)创建测试表text_test2,字段idcontexthashValue字段类型分别为int(11)textvarchar(40):



mysql> create table text_test2(id int(11),context text,hashValue varchar(40));
Query OK, 0 rows affected



2)往表text_test2中插入数据,其中hashValue用来存入context列内容的MD5值:



mysql> insert into text_test2 values(1,repeat('xcbeyond',10),md5(context));
Query OK, 1 row affected
mysql> insert into text_test2 values(2,repeat('xcbeyond',10),md5(context));
Query OK, 1 row affected
mysql> select * from text_test2;
+----+----------------------------------------------------------------------------------+----------------------------------+
| id | context | hashValue |
+----+----------------------------------------------------------------------------------+----------------------------------+
| 1 | xcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyond | 537f6020f5b2b59456a61271a2b3f285 |
| 2 | xcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyond | 537f6020f5b2b59456a61271a2b3f285 |
+----+----------------------------------------------------------------------------------+----------------------------------+
2 rows in set



3)如果需要查询context列的值,则通过散列值hashValue来查询:



mysql> select * from text_test2 where hashValue = md5(repeat('xcbeyond',10));
+----+----------------------------------------------------------------------------------+----------------------------------+
| id | context | hashValue |
+----+----------------------------------------------------------------------------------+----------------------------------+
| 1 | xcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyond | 537f6020f5b2b59456a61271a2b3f285 |
| 2 | xcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyondxcbeyond | 537f6020f5b2b59456a61271a2b3f285 |
+----+----------------------------------------------------------------------------------+----------------------------------+
2 rows in set



上面的例子则是展示了合成索引的用法,由于这种技术只能用于精确匹配,在一定程度上减少 I/O,从而提高查询效率。



**3.在不必要的情况下避免检索TEXTBLOB类型的值**



例如,SELECT * 查询就不是很好的操作,除非能够确定作为约束条件的 WHERE 子句只会找到所需要的数据行。否则,很可能毫无目的地在网络上传输大量的值。这也是 BLOB 或 TEXT标识符信息存储在合成的索引列中对用户有所帮助的例子。用户可以搜索索引列,决定需要的哪些数据行,然后从符合条件的数据行中检索 BLOB 或 TEXT 值。



4.把 BLOB 或 TEXT 列分离到单独的表中



在某些环境中,如果把这些数据列移动到第二张数据表中,可以把原数据表中的数据列转换为固定长度的数据行格式,那么它就是有意义的。这会减少主表中的碎片,可以得到固定长度数据行的性能优势。它还可以使主数据表在运行 SELECT * 查询的时候不会通过网络传输大量的 BLOB 或 TEXT 值。



2.5 选择唯一标识符



唯一标识符,也就是我们常常所说的主键,用于充当表记录的唯一判断依据。唯一标识符,选择合适的数据类型是非常重要的。



通常唯一标识符更多的是用来与其它值或者其它表的值进行比较(如,关联查询中),标识列也可能在其它表中作为外键使用,所以为标识列选择数据类型时,应该选择根关联表中对应列一样的类型。



当选择唯一标识符的类型时,不仅仅需要考虑存储类型,还需要考虑MySQL对这种类型怎么执行计算和比较的,因为比较在SQL查询中使用最多,而且也是制约性能的最大因素。



一旦选定了一种类型,就一定要确保所有关联表中都使用相同的类型。因为类型直接往往都是需要精确匹配,混用不同数据类型可能导致性能问题,即使没有性能影响,在比较操作时隐式类型转换也可能导致很难发现的错误问题。



在可以满足值的范围的需求,并且预留未来增长空间的前提下,应该选择最小的数据类型。



下面是一些小技巧:



1.整数类型



整数通常是标识列最好的选择,因为它很快,并且可以使用AUTO_INCREMENT



2.字符串类型



如果可以避免,尽可能的避免使用字符串类型作为标识列的类型,因为它很消耗空间,并且通常比数字类型慢。尤其是在MyISAM存储引擎的表里使用字符串作为标识列时,要特别的小心,MyISAM默认对字符串使用压缩索引,这会导致查询慢很多。



对于完全“随机”的字符串也需多加注意,例如MD5()SHA1()或者UUID()产生的字符串。这些函数生成的新值会任意分布在很大的空间内,会导致insert以及一些select操作变得很慢:



  • 因为插入值会随机地写到索引的不同位置,所以使得insert语句更慢。这会导致页分裂、磁盘随机访问。

  • select语句会变得更慢,是因为逻辑上不相邻的数据会分布在磁盘和内存的不同地方。

  • 随机值会导致缓存对所有类型的查询语句效果很差,因为会使得缓存赖以工作的访问局部性原理失效。



2.6 总结



在实际开发中,有很多工具会自动生成建表脚本等等,自动生成前期给开发带来了很大的便利,但与此同时却导致严重的性能问题。有些工具生成的东西,在存储任何数据都会使用很大的VARCHAR类型,这往往是不正确的。如果是自动生成的,一定要反复检查确认是否合理。



例如,一些ORM框架(如,MyBatisHibernate),会存储任意类型的数据列到任意类型的后端数据,这通常意味着没有设计使用更优的数据类型来存储,后期安全隐患很大,出现问题也很难排查。总之,一定要反复检查确认是否合理。这也是我个人不太喜欢用这类类似的工具,来生成代码的原因,检查真的很浪费我的时间。



在这里已经介绍了大部分常用的数据类型,各自都有哪些特点,哪些地方会严重影响性能等等。在选择数据类型时,把握好“选择原则”,你就成功了一半,其余细节在日常开发接触中慢慢琢磨、留意,选择类型时不要随意、盲目选择就好。



简单归纳如下:



  • 对于字符串类型,最好的策略是只分配真正需要的空间。



  • 日期类型,要根据实际需要选择能够满足应用的最小存储的日期类型。



  • 对含有 TEXTBLOB字段的表,如果经常做删除和修改记录的操作要定时执行OPTIMIZE TABLE功能对表进行碎片整理。



三、带你深入了解索引这些事



索引,对于良好的数据库性能非常关键。只要提及到数据库性能优化,都会首先想到“索引”,看看表中是否添加索引。尤其是当表中的数据量越来越大时,索引对性能的影响尤为突出。在数据量较小且负载较低时,没有索引或者不恰当索引对性能的影响可能还不明显,但当数据量逐渐增大时,性能则会急剧下降。



不过,索引却经常被忽略,有时候甚至被误解、误用,在实际使用中经常会遇到糟糕索引而导致的性能问题。本文就索引的概念、类型、优点等方面聊聊,一起深入理解索引的这点事,更有助于你清楚的理解索引,能够正确的使用它,便于利用它来进行数据库的优化。



3.1 什么是索引



索引(Index),是帮助MySQL高效获取数据的数据结构,是存储引擎用于快速找到记录的一种数据结构。



要理解MySQL中索引是如何工作的,最简单的例子就是去看看一本书的目录“索引”部分。如果想在一本书中找到某个章节,一般我们会先看书的目录“索引”,就会立即找到对应的页码。



在MySQL中,存储引擎也是用类似的方法使用索引,首先在索引中找到对应的值,然后根据匹配的索引记录找到对应的数据行。



查询是数据库中最常用的操作,我们都希望查询的速度尽可能快,因此数据库系统的设计者会从查询算法角度去进行优化。最基本的查询算法当然就是顺序查找,但是这种算法的复杂度为O(n),在数据量很大时就会显得非常糟糕。例如,在一张用户表t_user中有如下数据,想要查询到年龄为89岁的人,如果按照顺序查找,则得逐行扫描,可见查询效率有多低下(数量量越大,数据分布越不均匀,则花费的时间就更长)。



mysql> select * from t_user;
+----+----------+-----+
| id | name | age |
+----+----------+-----+
| 1 | xcbeyond | 22 |
| 2 | jack | 34 |
| 3 | tom | 77 |
| 4 | kitty | 5 |
| 5 | make | 91 |
| 6 | Mickey | 23 |
| 7 | Andy | 89 |
+----+----------+-----+
7 rows in set



好在,数据库系统的设计者早都意识到了这一点,参考了更优秀的查找算法,如二分查找、二叉树查找等等,但是分析之后发现,每种查找算法都只能应用于特定数据结构之上,如二分查找要求被查询的数据有序,而二叉树查找只能应用于二叉查找树。鉴于此,在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,即:这就是数据库中的索引



为了更好的理解索引,下图就以表t_user中的数据,展示了<u>一种可能的索引方式</u>。

左边是表中的数据,一共7条记录,为加快age列的查找,维护了一个右边所示的二叉查找树,每个节点包含索引键值及一个指向对应数据记录的指针,这样运用二叉查找就能很快的查找对应的数据了,时间复杂度为O(log2 N)



然而,在实际数据库中,几乎没有使用这样的二叉查找树来实现(因为二叉查找树对数据是有要求的),但其原理和这类似。



3.2 索引操作



在正式介绍索引之前,先一起来看看MySQL是如何创建索引、重建索引、查询索引、删除索引等操作的,以备后续使用。(建议单独保存收藏)



3.2.1 创建索引



索引的创建可以在CREATE TABLE语句中进行,也可以单独用CREATE INDEXALTER TABLE来给表增加索引。



语法:



CREATE [UNIQUE/FULLTEXT] INDEX <索引名> ON <表名>(<列名>)



ALTER TABLE <表名> ADD INDEX|UNIQUE|PRIMARY KEY|FULLTEXT <索引名>(<列名>)



其中,创建索引时,可以指定索引类型:主键索引(PRIMARY KEY)、 唯一索引(UNIQUE)、 全文索引(FULLTEXT)、 普通索引(INDEX)。



例如:



1)以表index_test为例说明,先创建一个普通的表index_test



(创建表时,也可以直接创建索引,此处为了说明索引的创建,则单独创建索引)



mysql> create table index_test(id int,ch varchar(32));
Query OK, 0 rows affected



2)为表index_test单独创建索引:



mysql> create index idx on index_test(id);
Query OK, 0 rows affected
Records: 0 Duplicates: 0 Warnings: 0



或者



mysql> alter table index_test add index idx(id);
Query OK, 0 rows affected
Records: 0 Duplicates: 0 Warnings: 0



3.2.2 重建索引



重建索引,在常规的数据库维护操作中经常使用。在数据库运行了较长时间后,索引都有损坏的可能,这时就需要重建。对数据重建索引可以起到提高检索效率。



重建索引,实质上的对表的修复。



例如:



mysql> repair table index_test quick;
+-----------------+--------+----------+---------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+-----------------+--------+----------+---------------------------------------------------------+
| test.index_test | repair | note | The storage engine for the table doesn't support repair |
+-----------------+--------+----------+---------------------------------------------------------+
1 row in set



3.2.3 查询索引



有时,为了查看某张表是否有索引,索引情况如何,就需要通过命令show index from|in table_name来查看索引。



语法:



SHOW INDEX FROM|IN <表名>



例如:



mysql> show index from index_test;
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| index_test | 1 | idx | 1 | id | A | 0 | NULL | NULL | YES | BTREE | | |
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
1 row in set



细心的你,或许看到查询结果中的字段index_type的值BTREE,这也就是接下来会讲到的B Tree索引,从另一方面也可以知道InnoDB默认的索引类型为B Tree



3.2.4 删除索引



删除索引可以使用DROP INDEXALTER TABLE语句来实现。



语法:



DROP INDEX <索引名> ON <表名>



ALTER TABLE <表名> DROP INDEX <索引名>



例如:



mysql> drop index idx on index_test;
Query OK, 0 rows affected
Records: 0 Duplicates: 0 Warnings: 0



或者



mysql> alter table index_test drop index idx;
Query OK, 0 rows affected
Records: 0 Duplicates: 0 Warnings: 0



3.3 索引类型



索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层实现的,所以,并没有统一的索引标准:不同存储引擎的索引的工作方式也是不一样的,也不是所有的存储引擎都支持所有类型的索引。



从存储结构上来划分:



  • BTree索引(B-Tree或B+Tree索引)

  • 哈希索引

  • 全文索引(full-index)



从应用层次来分:



  • 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引。

  • 唯一索引:索引列的值必须唯一,但允许有空值。

  • 复合索引:即一个索引包含多个列。



下面我们从索引的存储结构上,来看看MySQL支持的索引类型,底层是如何实现的,以及它们的优缺点。



MySQL默认存储引擎是Innodb,只显式支持B-Tree索引,对于频繁访问的表,Innodb会透明建立自适应哈希索引,即在B树索引基础上建立哈希索引,可以显著提高查找效率,对于客户端是透明的,不可控制的,隐式的。



3.3.1 B-Tree索引



当大家在谈论索引的时候,如果没有特别指明类型,多半说的是B-Tree索引,它使用B-Tree数据结构来存储数据,可以让系统高效的找到数据所在的磁盘块。



B代表平衡(balance),而不是二叉(binary),因为B Tree是从最早的平衡二叉树演化而来的。



B-Tree是为磁盘等外存储设备设计的一种平衡查找树。因此在讲B-Tree之前先了解下磁盘的相关知识。



系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。



InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每个页的大小为16KB,可通过参数innodbpagesize将页的大小设置为4K、8K、16K,在MySQL中可通过如下命令查看页的大小:



mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set



而系统一个磁盘块的存储空间往往没有这么大,因此InnoDB每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小16KBInnoDB在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O次数,提高查询效率。



B-Tree定义数据记录为一个二元组[key、data]:



  • key为记录的主键,即表中的主键值,用于记录唯一的数据行,key值是唯一且互不相同的。



  • data为一行记录中除主键外的数据。



一棵m阶的B-Tree有如下特性:



  • 每个节点最多有m个孩子。

  • 除了根节点和叶子节点外,其它每个节点至少有ceil(m/2)个孩子。

  • 若根节点不是叶子节点,则至少有2个孩子。

  • 所有叶子节点都在同一层,且不包含其它关键字信息。

  • 每个非终端节点包含n个关键字信息(p0,p1,...pn,k1,...kn

  • 关键字key的个数n满足:ceil(m/2)-1 <= n <= m-1

  • ki(i=1,…n)为关键字,且关键字升序排序。

  • pi(i=1,…n)为指向子节点的指针。p(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)



*注:ceil()为取整函数。*



B-Tree中的每个节点根据实际情况,可以包含大量的键值key、数据data、和指针p。如下图所示为一个3阶的B-Tree索引结构:

每个节点占用一个磁盘块空间,一个节点上有两个升序排序的关键字key和三个指向子节点的指针p,指针存储的是子节点所在磁盘块的地址。两个关键词key划分成为三个范围域对应的三个指针p,并指向的子节点的数据的范围域。以根节点为例,关键字为1735p1指针指向的子节点的数据范围为小于17p2指针指向的子节点的数据范围为17~35p3指针指向的子节点的数据范围为大于35



模拟查找关键字为29数据行的过程:



1) 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】



2) 比较关键字29在区间(17,35),找到磁盘块1的指针p2



3) 根据p2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】



4) 比较关键字29在区间(26,30),找到磁盘块3的指针p2



5) 根据`p2'指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】



6) 在磁盘块8中的关键字列表中找到关键字29



分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字key是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree(高度平衡的二叉树)缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。



3.3.2 B+Tree索引



B+Tree是在B-Tree基础上的一种优化,使其更适合实现存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。



从上一节中的B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。



B+Tree相对于B-Tree有几点不同:



  • 非叶子节点只存储键值信息。



  • 所有叶子节点之间都有一个链指针。



  • 数据记录都存放在叶子节点中。



将上一小节中的B-Tree进行优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:

通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。



可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:



InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为10^3)。也就是说一个深度为3的B+Tree索引可以维护10^3 10^3 10^3 = 10亿 条记录。



实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2~4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。



3.3.3 哈希索引



哈希索引(hash index),是基于哈希表实现的。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希值(hash value),不同键值的行计算出来的哈希值也不一样。<u>哈希索引将所有的哈希值存储在索引中,同时在哈希表中保存指向每个数据行的指针。</u>



**在MySQL中,只有Memory引擎显示支持哈希索引,同时哈希索引也是Memory存储引擎的默认索引类型**,并且Memory存储引擎也是支持B-Tree索引。



如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希值。



继续以表t_user中的数据举例说明,并对字段name设置哈希索引。假设索引使用的哈希函数是f(),则计算出来的哈希值(都是举例数据,并非真实数据)为:



f('xcbeyond')=2390

f('jack')=4010

f('tom')=5178

f('kitty')=1067

f('make')=7901

f('Mickey')=3079

f('Andy')=8301



计算出来的哈希值,会指向对应数据行的数据,指向关系如下图:

执行如下查询,并能够查询到对应的数据。



mysql> select * from t_user where name = 'xcbeyond';
+----+----------+-----+
| id | name | age |
+----+----------+-----+
| 1 | xcbeyond | 22 |
+----+----------+-----+
1 row in set



先计算出xcbeyond的哈希值,根据该哈希值寻找到对应指向的数据行。f('xcbeyond')=2390,所以MySQL在索引中查找2390,并找到指向第1行的数据行,然后比较第1行的值是否等于xcbeyond,以确保查找到数据的准确性。



<u>因为索引自身只需存储对应的哈希值,所有索引的结构十分紧凑,这也让哈希索引查找的速度非常快。</u>然而,哈希索引也有它的限制,即:索引失效。



  • 哈希索引数据并不是按照索引值顺序存储的,所以无法用于排序。

  • 哈希索引不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如,在数据列(A,B)上都建立哈希索引,如果查询时只有数据列A,则无法使用该索引。

  • 哈希索引只支持等比较查询,包括=in(),**不支持任何范围、模糊查找**,例如,where age > 20where name like '%xc%'

  • 如果哈希冲突很多的话,存储引擎必须进行链表来维护,维护这些链表的操作代价会很大,则查询的性能会很低。



3.3.4 全文索引



全文索引是一种特殊类型的索引,它查找的是文本中的关键字,而不是比较索引中的值



全文索引和其他类型索引的匹配方式完全不一样,它有许多需要注意的细节。更类似于搜索引擎做的事情,而不是简单的where条件匹配。



在相同的列上同时创建全文索引和基于值的B-Tree索引是不会有冲突的,全文索引适用于全文模糊搜索(MATCH AGAINST)操作,而不是普通的where条件操作



3.4 索引优点



索引可以让MySQL服务器快速地定位到表的指定位置,但这并不是索引的唯一作用,到目前为止可以看到,根据创建索引的数据结构不同,索引也有一些其他的附加作用。



最常见的B-Tree索引,按照顺序存储数据,所以MySQL可以用来做ORDER BY和GROUP BY操作。因为数据是有序的,所以B-Tree也就会将相关的列值都存储在一起。最后,因为索引中存储了实际的列值,所以某些查询只使用索引就能够完成全部查询。据此特性,总结下来索引有如下优点:



  • 索引大大减少了MySQL服务器需要扫描的数据量。(全表扫描)

  • 索引可以帮助MySQL服务器避免排序和临时表。

  • 索引可以将随机I/O变为顺序I/O。



索引是最好的解决方案吗?



索引并不总是最好的方案。总的来说,只有当索引帮助存储引擎快速查找到记录带来的好处大于其带来的额外工作时,索引才是有效的。对于非常小的表,大部分情况下简单的全表扫描更高效。对于中到大型的表,索引就非常有效。但对于特大型的表,建立和使用索引的代价将随之增长,这种情况下,则需要一种技术可以直接区分出查询需要的一组数据,而不是一条记录一条记录的匹配,如可以采用表分区的方式。



如果表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录“哪个用户的信息存放在哪个表中”的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于大型系统,这是一个常用的技巧。



四、如何高效正确的使用索引



正确地创建和使用索引是实现高性能查询的基础。前面已经介绍了各种类型的索引及其特点,而在实际使用索引中,如何真正有效地发挥索引的价值,还需要进一步持续不断地学习、磨练。接下来本篇文章将分享如何高效、正确的使用索引。



实践是检验真理的唯一途径,本篇只是站在索引使用的全局来定位的,你只需要通读全篇并结合具体的例子,或回忆以往使用过的地方,对整体有个全面认识,并理解索引是如何工作的,就可以了。在后续使用索引,或者优化索引时,可以从这些方面出发,进一步来加深对索引正确高效的使用。



4.1 索引失效



索引失效,是一个老生常谈的话题了。只要提到数据库优化、使用索引,都能一口气说出一大堆索引失效的场景,什么不能用、什么不该用这类的话,在此,我就不再一一罗列啰嗦了。



索引失效,是指表中有字段创建了索引,由于sql语句书写不当导致索引失效的情况。



在sql语句中,将索引列作为表达式的一部分、参与函数/数学等运算,将会导致索引失效。



例如,下面这个查询无法使用age列的索引:



select id,name,age from t_user where age + 1 = 7;



很容易看出where中的表达式其实等价于age=8,但是MySQL无法自动解析这个表达式,这完全是用户行为。



(在上一篇文章中,我们知道MySQL先在索引上按值进行查找,然后返回索引值对应的数据行,一旦对索引列进行运算,则将无法正确的找到对应的数据行,从而改为全表逐行扫描查询对比)



4.2 前缀索引和索引选择性



有时候将内容很长的列作为索引列,这将会让索引变得很大而且很慢。如果非要在该列添加索引,解决策略就是上一篇文章提到过的模拟哈希索引。



<u>通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率,但这样也会降低索引的选择性。</u>



索引的选择性是指,不重复的索引值(也称为基数)和表数据的记录总数T的比值,范围从1/T到1之间。索引的选择性越高,则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。



唯一索引的选择性为1,这是最好的索引选择性,性能也是最好的。



对于BLOB、TEXT或很大的VARCHAR类型的列,作为查询条件时(原则上是要避免这样的操作,但有时总是情非得已),该列必须使用前缀索引,这样来提高查询性能。因为MySQL是不允许索引这些列的完整长度的。



4.3 多列索引



多列索引,是指为每个列创立独立的索引。



在SQL优化时,有人会采取“把where条件里面的列都建上索引”,希望能够对查询性能有所优化。但实际上这样的优化是非常错误的,这样一来最好的情况下也只能是“一星”索引,其性能比起真正最优的索引可能差几个数据级。有时如果无法设计一个“三星”索引,那么不如忽略掉where子句,集中精力优化索引列的顺序,或者创建一个全覆盖索引。



三星索引:在Lahdenmaki和Leach编写的Relational Database Index Design and the Optimizers一书中,提到如何评价一个索引是否适合某个查询的“三星系统”:索引将相关的记录放到一起则获得“一星”;如果索引中的数据顺序和查找中的排序顺序一致则获得“二星”;如果索引中的列包含了查询中需要的全部列则获得“三星”。



在多个列上建立独立的单列索引,大部分情况下并不能提高MySQL的查询性能。这也是将其错误的做法。



MySQL5.0及之后版本引入了索引合并策略,一定程度上可以使用表上的多个单列索引来定位指定的行。更早的MySQL只能使用其中某一个单列索引,然而这个情况下没有哪一个独立的单列索引是非常有效的。



索引合并策略有时候是一种优化的结果,但实际上更多时候说明了表上的索引建的很糟糕:



1)当出现对多个索引做相交操作时(通常由多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。



2)当需要对多个索引做联合操作室(通常有多个OR条件),通常需要耗费大量的CPU和内存资源在算法的缓存、排序和合并操作上。特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候。



3)优化器不会把这些计算到“查询成本”中,优化器只关心随机页面读取。这会使得查询的成本被“低估”,导致该执行计划还不如直接走全表扫描。这样做不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性,但如果是单独运行这样的查询,则往往会忽略对并发性的影响。



如果在执行计划EXPLAIN中看到索引合并,应该好好检查一下查询和表的结构,看是不是已经是最优的。也可以通过参数optimizer_switch来关闭索引合并功能,也可以使用IGNORE INDEX提示让优化器忽略掉某些索引。



对于多列索引,只要查询的条件中用到了最左边的列,索引一般就不会失效。



举例说明如下:



表t_user创建了(id,name)的多列索引,具体如下:



mysql> show create table t_user;
+--------+---------------+
| Table | Create Table |
+--------+---------------+
| t_user | CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
KEY `idx` (`id`,`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 |
+--------+-----------------------------------------+
1 row in set



根据id进行查询,具体如下:



mysql> explain select * from t_user where id = 1;
+----+-------------+--------+------------+------+---------------+-----+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+-----+---------+-------+------+----------+-------+
| 1 | SIMPLE | t_user | NULL | ref | idx | idx | 4 | const | 1 | 100 | NULL |
+----+-------------+--------+------------+------+---------------+-----+---------+-------+------+----------+-------+
1 row in set



从执行计划中的type可以看出,索引是有效的。但如果根据name进行查询,则索引将会失效(全表扫描),如下:



mysql> explain select * from t_user where name = 'xcbeyond';
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | t_user | NULL | ALL | NULL | NULL | NULL | NULL | 7 | 14.29 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set



4.4 选择合适的索引列顺序



索引列顺序实在是非常重要的。正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好的满足排序和分组的需要(只用于B-Tree索引,哈希或者其他索引存储数据并不是顺序存储)。



在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排列。所以索引可以按照升序或者降序进行扫描,以满足符合列顺序的order by,group by和distinct等子句的查询需求。



所以多列索引列的顺序至关重要。对于如何选择索引的列顺序有一个经验法则:将选择性最高的索引放在索引的最前列。在某些场景这个经验时非常有用,但是通常不如避免随机IO和排序那么重要,考虑问题需要更全面。



当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化where条件的查找。这种情况下,这样设计的索引确实能够最快的过滤出需要的行,对于在where的子句中只是用了索引部分前缀列的查询来说选择性也更高。然而性能不只是依赖于所有索引列的选择性,也和查询条件的具体值有关,也就是和值的分布有关(需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下的索引列的选择性最高)。



4.5 聚簇索引



聚簇索引并不是一种单独的索引类型,而是一种数据存储方式,将数据存储与索引放到了一块,找到索引页就找到了数据。具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。



非聚簇索引:将数据存储与索引分开存储,索引结构的叶子节点指向了数据的对应行。当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因。



当表有聚簇索引时,它的数据行实际上存放在索引的叶子页中。“聚簇”表示数据行和相邻的键值紧凑的存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。



聚簇索引的设定:



默认为主键。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoD会隐式定义一个主键来作为聚簇索引。InnoDB只聚集在同一个页面中的记录,包括相邻键值的页面可能会相距甚远。



(看到这里,如果你对B-Tree索引结构熟悉的话,就知道为啥[key、data]作为一个二元组存放在一个节点了)



聚簇主键可能对性能有帮助,但也可能导致严重的性能问题。所以需要仔细的考虑聚簇索引,尤其是将表的存储引擎从InnoDB改成其他引擎的时候(反过来也一样)。



聚簇索引的优点:



  • 可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户ID来聚集数据,这样子只需要从磁盘中读取少数的数据也技能获取某个用户的全部邮件。

  • 数据访问更快。聚簇索引把索引和数据都放在同一个B-Tree中,因此从聚簇索引中获取数据比从非聚簇索引中要快。

  • 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。



聚簇索引的缺点:



  • 最大限度的提高了I/O密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势了。

  • 插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。但如果不是按照逐渐顺序加载数据,那么在加载完成后最好使用OPTIMIZE TABLE重新组织一下表。

  • 更新聚簇索引列的代价很高。因为要强制InnoDB将每个被更新的行移动到新的位置。

  • 基于聚簇索引的表在插入新行,或者主键被更新导致移动行的时候,可能面临“页分裂”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页来容纳该行,这就是一次页分裂操作,这也意味着这样导致表占用更多的磁盘空间。

  • 聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏时,或者由于页分裂导致数据存储不连续的时候。

  • 二级索引(非聚簇索引)可能比想想的要更大。因为二级索引的叶子结点包含了引用行的主键列。

  • 二级索引访问需要两次索引查找,而不是一次。



4.6 覆盖索引



通常大家都会根据查询的where条件来创建合适的索引,不过这也只是索引优化的一个方面。设计优秀的索引应该考虑到整个查询,而不单单是where条件部分。索引确实是一种查找数据的高效方式,但是MySQL也可以使用索引来直接获取列的数据,这样就不再需要读取数据行。如果一个索引包含所有需要查询的字段值,我们就称其为“覆盖索引”,即:一个索引覆盖where条件的所有列。



覆盖索引的好处如下:



  • 索引条目通常远小于数据行的大小,所以如果只需要读取索引,那么MySQL就会极大的减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝下。覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放进去内存。

  • 因为索引是按照列值顺序存储的,对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少的多。对于某些存储引擎,例如MyISAMPercona XtraDB,甚至可以通过POTIMIZE命令使得索引完全顺序排列,这样就可以让简单的范围查询能使用完全排序的索引访问。

  • 一些存储引擎,如MyISAM在内存中只缓存索引。数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用。这可能会导致严重的性能问题,尤其是那些系统调用占了数据访问中的最大开销的场景。

  • 由于InnoDB的聚簇索引,覆盖索引对于InnoDB表特别有用。InnoDB的二级索引在叶子节点保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询



不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree所以来做覆盖索引,另外不同的存储引擎实现覆盖索引的方式也不同,而且不是所有的引擎都支持覆盖索引。



4.7 使用索引扫描来排序



MySQL有两种方式可以生成有序的结果集:通过排序操作,或者按索引顺序扫描。如果EXPLAIN出来的type列的值为index,则说明MySQL使用了索引扫描来做排序。



扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录。但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序的全表扫描慢,尤其是在I/O密集型的工作负载时。



MySQL可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能的同时满足这两种情况,即:索引列作为排序列。



  • 只有当索引的列顺序和order by子句的顺序完全一致,并且所有列的排序方向都一样时,MySQL才能够使用索引来对结果做排序。



  • 如果查询需要关联多张表,则只有当order by子句引用的字段全部为第一个表时,才能使用索引做排序。order by子句和查找性查询的限制是一样的:需要满足索引的最左前缀的要求;否则,MySQL都需要执行的顺序操作,而无法使用索引排序。



4.8 冗余、重复索引



重复索引,是指在相同列上按照相同的顺序创建的相同类型的索引。应该避免这样的创建重复索引,发现以后也应该立即移除。



比如:   



create table test{
id int not null primary key,
a int not null,
b int not null,
unique(id)
index(id)
}engine=InnoDB;



一个经验不足的人可能是想创建一个主键,先加上唯一限制(unique(id)),然后再加上索引(index(id))以供查询使用。然而唯一限制和主键限制都是通过索引使用,因此,上面的写法实际上在相同的列上创建了三个重复的索引。通常并没有理由要这样做,除非是在同一列上创建不同类型的索引来满足不同的查询需求。



冗余索引和*重复索引*有一些不同,比如:如果创建了索引(A,B),再创建(A)那就是冗余索引,因为A就是前一个索引的前缀索引。索引(A,B)完全就可以当做A来使用。但是如果创建了索引(B,A)那就不是冗余索引了,索引B也不是。因为B不是索引(A,B)的最左前缀索引。另外,其他不同类型的索引,例如哈希,全文索引也不会是B-Tree的冗余索引。



冗余索引通常发生在为表添加新索引的时候。例如,有人可能会增加一个新的索引(A,B)而不是拓展已有的索引(A),还有一种情况是将一个索引扩展为(A,ID),其中的ID是主键,对于InnoDB来说主键列已经包含在二级索引当中了, 所以这也是冗余的。



大多数情况下不需要冗余索引,应该尽量扩展已有的索引而不是创建新的索引。但也有时候处于性能方面的考虑需要冗余索引,因为扩展已有的索引会导致其变得太大 ,从而影响其他使用该索引的查询的性能。例如,在一个整数列索引上添加一个很长的varchar列,那性能可能会急剧下降。特别是有索引把这个索引当中覆盖索引时,或者这是MyISAM表并且有很多范围查询的时候。



解决冗余索引和重复索引的方法非常简单,删除这些索引就可以。但是首先要做的事找出这样的索引。可以通过写一些复杂的访问information_schema表的查询来找,不过还有两个更简单的方法就是使用Shlomi Noachcommon_schema中的一些视图来定位(common_schema是一系列可以安装在服务器上的常用的存储和视图)。另外一个方法就是使用Percona Toolkit中的pt_duplicate-key-checker,该工具通过分析表结构来找出冗余和重复索引。



4.9 未使用的索引



除了冗余索引和重复索引,可能还会有一些服务器永远不用的索引。这样的索引完全是累赘,建议直接删除。



可以使用Performance_schema中的table_io_waits_summary_by_index_usage表进行查找:



SELECT object_schema, object_name, index_name FROM performance_schema.table_io_waits_summary_by_index_usage WHERE index_name IS NOT NULL AND count_star = 0 ORDER BY object_schema, object_name, index_name;



4.10 索引和锁



索引可以让查询锁定更少的行。如果你的查询从不访问那些不需要的行,那么就会锁定更少的行,从两个方面来看这对性能都有什么好处。



首先,虽然InnoDB的行锁效率很高,内存使用也很少,但是锁定行的时候仍然会带来额外的开销,其次,锁定超过需要的行会增加锁争用并减少并发性。



4.11 总结



通过上面大篇文字的讲解,都是用来说明如何高效的使用索引,避免错误使用。索引是一个看似简单,但实际用起来却是非常复杂的东西,要想真正用好它,需要不断的实践。实践是检验真理的唯一途径,本篇只是站在索引使用的全局来定位的,你只需要通读全篇并结合具体的例子,或回忆以往使用过的地方,对整体有个全面认识,并理解索引是如何工作的,就可以了。在后续使用索引,或者优化索引时,可以从这些方面出发,进一步来加深对索引正确高效的使用。



在平时使用索引中,有以下几点总结及建议:



  1. 在区分度高的字段上面建立索引可以有效的使用索引,区分度太低,无法有效的利用索引,可能需要扫描所有数据页,此时和不使用索引区别不大。

  2. 联合索引,注意最左匹配原则:必须按照从左到右的顺序匹配,MySQL会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

  3. 查询记录的时候,少使用*,尽量去利用索引覆盖,可以减少回表操作,提升效率。

  4. 有些查询可以采用联合索引,进而使用到索引下推,也可以减少回表操作,提升效率。

  5. 禁止对索引字段使用函数、运算符操作,这样将会使索引失效。

  6. 字符串字段和数字比较的时候会使索引无效。

  7. 模糊查询'%值%'会使索引无效,变为全表扫描,但是'值%'这种可以有效利用索引。

  8. 排序中尽量使用到索引字段,这样可以减少排序,提升查询效率。



五、为什么你的 SQL 查询速度这么慢



前面章节我们介绍了如何选择优化的数据类型、如何高效的使用索引,这些对于高性能的MySQL来说是必不可少的。但这些还完全不够,还需要合理的设计查询。如果查询写的很糟糕,即使表结构再合理、索引再合适,也是无法实现高性能的。



谈到MySQL性能优化,查询优化作为优化的源头,它也是最能体现一个系统是否更快。本章以及接下来的几章将会着重讲解关于查询性能优化的内容,从中会介绍一些查询优化的技巧,帮助大家更深刻地理解MySQL如何真正地执行查询、究竟慢在哪里、如何让其快起来,并明白高效和低效的原因何在,这样更有助于你更好的来优化查询SQL语句。



本章从“为什么查询速度这么慢”开始谈起,让你能够清楚的知道查询可能会慢在哪些环节,这样将有助于你更好的优化查询,做到心中有数,高人一筹



5.1 慢在哪



真正衡量查询速度的是响应时间。如果把查询看作是一个任务,那么它是由一系列子任务组成的,每个任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,那么消除其中一些子任务,那么减少子任务的执行次数,要么让子任务运行的更快。



MySQL在执行查询的时候,有哪些子任务,哪些子任务花费的时间最多?这就需要借助一些工具,或者一些方法(如:执行计划)对查询进行剖析,来定位发现究竟慢在哪。



通常来说,查询的生命周期大致大致可以按照顺序来看:从客户端到服务器,然后在服务器上进行解析,生成执行计划,执行,并返回结果给客户端。其中,“执行”可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。



在完成这些任务的时候,查询需要在不同阶段的不同地方花费时间,包括网络、CPU计算,生成统计信息和执行计划、锁等待等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作,还可能会产生大量的上下文切换以及系统调用。



在上述这些操作中,都会消耗大量的时间,其中会存在一些不必要的额外操作,其中有些操作可能被额外地重复执行了很多次、某些操作执行的很慢等等。这也就是查询真正可能慢的地方,优化查询的目的就是减少和消除这些操作所花费的时间。



通过上面的分析,我们对查询的过程有了整体的了解,能够清楚的知道查询可能在哪些地方会存在问题,最终导致整个查询很慢,为实际查询优化提供方向。



换言之,查询优化可以从以下两个角度来出发:



  • 减少子查询次数

  • 减少额外、重复的操作



查询性能低下常见的原因是访问的数据太多。在数据量小的时候,查询速度还不错,一旦数据量上来,查询速度将会发生巨变,让人抓狂、体验极差。针对查询优化方面,可以从以下方面进行排查:



  • 是否查询了不需要的数据

  • 是否扫描了额外的记录



5.2 是否查询了不需要的数据



在实际查询中很多时候,会查询了实际需要的数据,然后这些多余的数据会被应用程序丢弃。这对MySQL来说是额外的开销,同时也会消耗应用服务器的CPU和内存资源。



一些典型案例如下:



5.2.1 查询不需要的记录



这是一个常见的错误,常常会误以为MySQL只会返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。



开发者习惯性的先使用SELECT语句查询大量的结果,然后由应用查询或者前端展示层再获取前面的N行数据,例如,在新闻网站中查询100条记录,但是只是在页面上显示前10条。



**最有效的解决方法是需要多少记录就查询多少记录,通常会在查询后面加上LIMIT,即:分页查询。**



5.2.2 多表关联时返回全部列



如果你想查询所有在电影Academy Dinosaur中出现的演员,千万不要按下面的方式来进行查询:



select * fromt actor a
inner join film_actor fa.actorId = a.actorId
inner join film f f.filmId = fa.filmId
where fa.title = 'Academy Dinosaur';



这样将会返回三张表的全部数据列,而实际需求是要查询演员信息,正确的写法应该是:



select a.* fromt actor a
inner join film_actor fa.actorId = a.actorId
inner join film f f.filmId = fa.filmId
where fa.title = 'Academy Dinosaur';



5.2.3 总是查询出全部列



**每次看到select *的时候一定要用异样的目光来审视它,是不是真的需要返回全部数据列?**



在大部分情况下,是不需要的。select *会导致进行全表扫描,会让优化器无法完成索引扫描这类优化,过多的列还会为服务器带来额外的I/O、内存和CPU的消耗。即使真的需要查询出全部列,应该逐个罗列出全部列而不是*



5.2.4 重复查询相同的数据



如果你不太留意,很容易出现这样的错误:不断地重复执行相同的查询,然后每次都返回完全相同的数据。



例如,在用户评论的地方需要查询用户头像的URL,那么用户多次评论的时候,可能就会反复来查询这个数据。比较好处理方法是,在初次查询的时候将这个数据缓存起来,后续使用时直接从缓存中取出。



5.3 是否扫描了额外的记录



确定查询只查询了需要的数据以后,接下来应该看看查询过程中是否扫描了过多的数据。对于MySQL,最简单衡量查询开销的三个指标如下:



  • 响应时间

  • 扫描的行数

  • 返回的行数



没有哪个指标能够完全来衡量查询的开销,但它们能够大致反映MySQL内部执行查询时需要访问多少数据,并可以大概推算出查询运行的实际。这三个指标都会记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描行数过多查询的办法。



慢查询:用于记录在MySQL中响应时间超过阈值(long_query_time,默认10s)的语句,并会将慢查询记录到慢日志中。可通过变量slow_query_long来开启慢查询,默认是关闭状态,可以将慢日志记录到表slow_log或文件中,以供检查分析。



5.3.1 响应时间



响应时间是两个部分之和:服务时间和排队时间。服务时间是指数据库处理这个查询真正花费了多长时间。排队时间是指服务器因为等待某些资源而没有真正执行查询的时间,可能是等待I/O操作,也可能是等待行锁等等。



在不同类型的应用压力下,响应时间并没有什么一致的规律或者公式。诸如存储引擎的锁(表锁,行锁),高并发资源竞争,硬件响应等诸多因素都会影响响应时间,所以,响应时间既可能是一个问题的结果也可能是一个问题的原因,不同案例情况不同。



当你看到一个查询的响应时间的时候,首先需要问问自己,这个响应时间是否是一个合理的值。



5.3.2 扫描的行数和返回的行数



在分析查询时,查看该查询扫描的行数是非常有帮助的,在此之上也能够分析是否扫描了额外的记录。



对于找出那些糟糕查询,这个指标可能还不够完美,因为并不是所有行的访问代价都是相同的。较短的行的访问速度相当快,内存中的行也比磁盘中的行的访问速度要快的多。



理想的情况下,扫描的行数和返回的行数应该是相同的。但实际上这种美事并不多,例如在做一个关联查询的时候,扫描的行数和对返回的行数的比率通常都很小,一般在1:110:1之间,不过有时候这个值也可能非常大。



5.3.3 扫描的行数和访问类型



在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种访问方式可以查找并返回一行结果。这些访问方式可能需要访问很多行才能返回一条结果,也有些访问方式可能无需扫描就能返回结果。



**在执行计划EXPLAIN语句中的type列反映了访问类型。访问类型有很多种,从全表扫描到索引扫描,范围扫描,唯一索引,常数索引等。这里列的这些,速度是从慢到快,扫描的行数也是从多到少。**



如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引,这也是我们之前讨论索引的问题。现在应该明白为什么索引对于查询优化如此重要了。索引让MySQL以最高效,扫描行数最少的方式找到需要的记录。



如果发现查询扫描了大量的数据但只返回少数的行,通常可以尝试下面的技巧去优化它:



  • 使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无需回表获取对应的行就可以返回结果了。

  • 优化表结构。例如使用单独的汇总表来完成查询。

  • 重写复杂查询,让MySQL优化器能够以更优化的方式执行这个查询。



六、常见优化 SQL 技巧



在面对不够优化、或者性能极差的SQL语句时,我们通常的想法是将重构这个SQL语句,让其查询的结果集和原来保持一样,并且希望SQL性能得以提升。而在重构SQL时,一般都有一定方法技巧可供参考,本文将介绍如何通过这些技巧方法来重构SQL。



6.1 分解SQL



有时候对于一个复杂SQL,我们首先想到的是是否需要将一个复杂SQL分解成多个简单SQL,来完成相同业务处理结果。



在以前,大家总是强调需要数据库层来完成尽可能的工作,这也就不难理解在一些老的产品、项目中时常会看见很多超级复杂、超级长的SQL语句,这样做的逻辑在以前认为多次交互,在网络带宽、程序与数据库间网络通信等方面是一件代价很高的事情。然后在现在,无论是带宽还是延迟,网络速度比以前要快的很多,多次交互也没有太大的问题。即使在一个通用服务器上,也能够运行每秒超过10万的查询,所以运行多个小查询现在已经不是大问题了。



复杂SQL的分解,在面对超级复杂SQL语句时,性能提升尤为明显。所以,在面对超级复杂SQL语句,并且存在性能问题时,推荐分解为小查询来进行优化。



不过,在应用设计的时候,如果一个查询能够胜任并且不会产生性能问题,这时完全可以用一个稍微复杂的SQL来完成的,倘若再死板的强制拆分成多个小查询是不明智的。



<u>在当今很多高性能的应用系统中,都是极力推荐使用单表操作,然后将单表查询结果在应用程序中进行关联,以满足复杂业务的查询需求。</u> 一个SQL可以搞定事情,为何要分开来写,而且还得在应用程序中多次执行SQL查询,再进行结果集的关联,这到底为什么要这么做呢?



乍一看,这样做复杂不说而且没有什么好处,原本一条查询,这样却变成了多条查询。事实上,这样分解有如下的优势:



  • 让缓存更高效。在应用程序中,可以很方便地缓存单表查询结果对应的结果对象,便于后续任何时候可以直接从结果对象中获取数据。

  • 分解查询后,执行单个查询可以减少表锁的竞争。

  • 在程序应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。

  • 单表查询效率高于多表复杂查询。

  • 减少冗余记录的查询。在程序应用层关联,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据记录。从这点来看,这样的重构还可能减少网络和内存的消耗。



6.2 查询切分



有时候对于一个大查询,即:结果集很大的查询,我们需要采用“分而治之”的思想,将大查询切分为小查询,每个查询功能完全一样,只是完成一小部分,每次只返回一小部分查询结果。通俗来讲,就是对where条件的过滤范围进行切分,每次只查询其中一部分数据,即:类似于分页查询。



这样做,不管对于SQL查询本身,还是对于上层业务来说,都是很小的开销。最典型的的案例就是分页查询,目前各类框架都有了很好的支持,如:MyBatis等,只需在实际使用时稍加留意就可避免。



6.3 执行计划



使用执行计划EXPLAIN关键字,可以使我们知道MySQL是如何执行SQL语句的,这样可以帮助我们分析我们的查询语句或是表结构的性能瓶颈。EXPLAIN的查询结果还会告诉我们索引主键是如何被利用的,数据表是如何被搜索或排序的....等等。



语法格式是:



EXPLAIN SELECT语句;



通过执行计划结果,将会指导我们进一步来重构SQL语句,如:增加索引、调整索引顺序、避免使用某些函数等等。



关于执行计划,后续章节将会单独详细讲解。



6.4 遵守原则



在平时写SQL时,养成好的习惯,多加留意,很大程度上就会避免一些SQL性能问题。汇总如下:



  • 永远为每张表设置一个ID主键。



  • 避免使用SELECT *



  • 为搜索字段建立索引。



  • 在Join表的时候使用对应类型的列,并将其索引。



  • 尽可能的使用NOT NULL。



  • 越小的列会越快。



  • 当只要一行数据时使用LIMIT 1。



  • 操作符的优化,尽量不采用不利于索引的操作符,目的就是为了避免全表扫描。



1)in not in慎用,尽量用 between代替in,用 not exists 代替 not in



2)is nullis not null慎用



3)!=<>操作符能不用就不用,否则将使引擎放弃使用索引而进行全表扫描。



  • ……



6.5 使用查询缓存



当有很多相同的查询被执行了多次的时候,这些查询结果会被放入一个缓存中,这样后续的相同查询就不用操作而直接访问缓存结果了。



MySQL查询缓存保存查询返回的完整结果。当查询命中该缓存,MySQL会like返回结果,跳过了解析、优化和执行截断。



这是提高查询性能最有效的方法之一,而且这是被MySQL引擎处理的,通常MySQL默认是不开启查询缓存的,需要手动开启。



查询缓存对应用程序是完全透明的。应用程序无需关心MySQL是通过查询返回的还是实际执行返回的结果。事实上,这两种方式执行的结果是完全相同的。换句话说,查询缓存无需使用任何语法。



随着现在的通用服务器越来越强大,查询缓存被发现是一个影响服务器扩展性的因素。它可能成为整个服务器的资源竞争单点,在多核服务器上还可能导致服务器僵死。所以大部分时候应该默认关闭查询缓存,如果查询缓存作用很大的话,可以配置个几十兆的小缓存空间。(在选择时,需要进行权衡)



关于查询缓存有如下参数可供配置:



  • query_cache_type



是否打开查询缓存。可以设置OFFONDEMANDDEMAND表示只有在查询语句中明确写入sql_cache的语句才放入查询缓存。



  • query_cache_size



查询缓存使用的总内存空间,单位是字节。这个值必须是1024的整倍数,否则实际分配的数据会和指定的大小有区别。



  • query_cache_min_res_unit



在查询缓存中分配内存块时的最小单位。



  • query_cache_limit



缓存的最大查询结果。如果查询结果大于这个值,则不会被缓存。因为查询缓存在数据生成的时候就开始尝试缓存数据,所以只有当结果全部返回后,MySQL才知道查询结果是否超出限制。



七、带你搞懂 MySQL 执行计划



一条SQL被一个懵懂的少年,一阵蹂躏,扔向了MySQL服务器的尽头,少年苦苦等待,却迟迟等不来那满载而归的硕果。于是少年气愤,费尽苦心想从度娘那边寻求帮助,面对执行计划EXPLAIN,却等来的是无尽的折磨与抓狂。



通过explain命令,根据执行计划找到存在性能问题的SQL语句,以帮助我们优化SQL提供方向和依据。



如果面对执行计划,你也是一脸疑惑,甚至抓狂,那么你真的需要认真的来了解它了。在数据库性能优化中,执行计划,真的很重要,通过执行计划能够帮助我们更加明确的来进行SQL优化。本文将从执行计划开始说起,讲解执行计划该如何用,其中各个列的含义究竟是什么。



7.1 执行计划



执行计划,就是一条SQL语句,在数据库中实际执行的时候,一步步的分别都做了什么。也就是我们用EXPLAIN分析一条SQL语句时展示出来的那些信息。



EXPLAIN命令是查看查询优化器是如何决定执行查询的主要方法,从它的查询结果中可以知道一个SQL语句每一步是如何执行的,都经历了些什么,分为哪几步,有没有用到索引,哪些字段用到了什么样的索引,是否有一些可优化的地方等,这些信息都是我们SQL优化的依据。



要使用·EXPLAIN,只需在查询中的SELECT关键字之前增加EXPLAIN。语法如下:



EXPLAIN + SELECT查询语句;



当执行执行计划时,只会返回执行计划中每一步的信息,它会返回一行或多行信息,显示出执行计划中的每一部分和执行的次序。



如:

如果查询的是多个关联表,执行计划结果可能是多行。



在接下来涉及到的示例表,均来自于MySQL官方的示例数据库sakila,脚本下载:https://downloads.mysql.com/docs/sakila-db.zip



7.2 执行计划中的列



EXPLAIN的结果总是有相同的列,每一列代表着不同的含义,可变的只是行数和内容。从上面的例子中,我们看到返回的有很多列,为了更加清楚的了解每一列的含义,便于我们更好的完成优化SQL。



涉及到的列有:



| 列名 | 含义 |

| ------------- | ------------------------------------------------------------ |

| id | id列,表示查询中执行select子句或操作表的顺序。 |

| select_type | 查询类型,主要是用于区分普通查询、联合查询、子查询等复杂的查询。 |

| table | 表明对应行正在访问的是哪个表。 |

| partitions | 查询涉及到的分区。 |

| type | 访问类型,决定如何查找表中的行。 |

| possible_keys | 查询可以使用哪些索引。 |

| key | 实际使用的索引,如果为NULL,则没有使用索引。 |

| key_len | 索引中使用的字节数,查询中使用的索引的长度(最大可能长度),并非实际使用长度,理论上长度越短越好。 |

| ref | 显示索引的那一列被使用。 |

| rows | 估算出找到所需行而要读取的行数。 |

| filtered | 返回结果的行数占读取行数的百分比,值越大越好。 |

| Extra | 额外信息,但又十分重要。 |



7.2.1 id列



id列是一个编号,用于标识SELECT查询的序列号,表示执行SQL查询过程中SELECT子句或操作表的顺序。



如果在SQL中没有子查询或关联查询,那么id列都将显示一个1。否则,内层的SELECT语句一般会顺序编号。



id列分为三种情况:



1)id相同



如下普通查询,没有子查询。



explain select f.* from film f,film_actor fa,actor a where f.film_id = fa.film_id and fa.actor_id = a.actor_id and a.first_name = 'NICK';





2)id不同



如果存在子查询,id的序号会递增,id值越大优先级越高,越先被执行



explain select * from film where film_id = (select film_id from film_actor where actor_id = 2 limit 1);





3)id相同又不同



1)、2)两种情况同时存在。id如果相同,认为是一组,从从上往下执行。在所有组中,id值越大,优先级越高,越先执行。



7.2.2 select_type列



select_type列表示对应行的查询类型,是简单查询还是复杂查询,主要用于区分普通查询、联合查询、子查询等复杂的查询。



select_type列有如下值:



| select_type值 | 说明 |

| ------------- | ------------------------------------------------------------ |

| SIMPLE | 简单查询,意味着不包括子查询或UNION。 |

| PRIMARY | 查询中包含任何复杂的子部分,最外层查询则被标记为PRIMARY |

| SUBQUERY | 在select where列表中包含了子查询 |

| DERIVED | 表示包含在from子句的子查询中的select,MySQL会递归执行并将结果放到一个临时表中,称其为“派生表”,因为该临时表是从子查询中派生而来的。 |

| UNION | 第二个select出现在UNION之后,则被标记为UNION。 |

| UNION RESULT | 从UNION表获取结果的select。 |



7.2.3 table列



table列表示对应行正在执行的哪张表,指代对应表名,或者该表的别名(如果SQL中定义了别名)。



7.2.4 partitions列



查询涉及到的分区。



7.2.5 type列



type列指代访问类型,是MySQL决定如何查找表中的行。



是SQL查询优化中一个很重要的指标,拥有很多值,依次从最差到最优:



ALL < index < range < index_subquery < unique_subquery < index_merge < ref_or_null < fulltext < ref < eq_ref < const < system



1)ALL



众所周知的全表扫描,表示通过扫描整张表来找到匹配的行,很显然这样的方式查询速度很慢。



这种情况,性能最差,在写SQL时尽量避免此种情况的出现。



举例如下:



explain select * from film;



在平时写SQL时,避免使用select *,就不难理解了。换言之,是为了避免全表扫描,因为全面扫描是性能最差的。



2)index



全索引扫描,和全表扫描ALL类似,扫描表时按索引次序进行,而不是按行扫描,即:只遍历索引树。



indexALL虽然都是读全表,但index是从索引中读取,而ALL是从硬盘读取。显然,index性能上优于ALL合理的添加索引将有助于性能的提升



举例如下:



explain select title from film;
explain select description from film;



通过explain结果来看,只查询表film中字段title时,是按照索引扫描的(type列为index),倘若查询字段description,却是按照全表扫描的(type列为ALL)。这是为何呢?



接下来,我们不妨看看表film的结构:

desc film结果来看,字段title创建的有索引,而字段description没有,所以select title from film是按索引扫描,而select description from film按全表扫描。



从上面的举例对比中,也充分印证了索引的重要性。



3)range



只检索给定范围的行,使用一个索引来选择行。key列显示使用了那个索引。一般就是在where语句中出现了bettween、<、>、in等的查询。这种索引列上的范围扫描比全索引扫描index要好。



举例如下:



explain select * from film where film_id between 1 and 10;





4)ref



非唯一性索引扫描,返回匹配某个单独值的所有行。本质是也是一种索引访问,它返回所有匹配某个单独值的行,然而它可能会找到多个符合条件的行,所以它属于查找和扫描的混合体。



此类型只有当使用非唯一索引或者唯一索引的非唯一性前缀时,才会发生。



举例如下:



show index from film;
explain select * from film where title = 'ACADEMY DINOSAUR';





5)eq_ref



唯一索引扫描。常见于主键或唯一索引扫描。



6)const



通过索引一次就能找到,const用于比较primary key 或者 unique索引。因为只需匹配一行数据,所有很快。如果将主键置于where列表中,mysql就能将该查询转换为一个const



举例如下:



show index from film;
explain select * from film where film_id = 1;





7)system



表只有一行记录,这是const类型的特例,比较少见,如:系统表。



7.2.6 possible_keys列



显示在查询中使用了哪些索引。



7.2.7 key列



实际使用的索引,如果为NULL,则没有使用索引。查询中如果使用了覆盖索引,则该索引仅出现在key列中。



possible_keys列表明哪一个索引有助于更高效的查询,而key列表明实际优化采用了哪一个索引可以更加高效。



举例如下:



show index from film_actor;
explain select actor_id,film_id from film_actor;



7.2.8 key_len列



表示索引中使用的字节数,查询中使用的索的长度(最大可能长度),并非实际使用长度,理论上长度越短越好。key_len是根据表定义计算而得的,不是通过表内检索出的。



7.2.9 ref列



表示在key列记录的索引中查找值,所用的列或常量const



7.2.10 rows列



估算出找到所需行而要读取的行数。



这个数字是内嵌循环关联计划里的循环数,它并不是最终从表中读取出来的行数,而是MySQL为了找到符合查询的那些行而必须读取行的平均数,只能作为一个相对数来进行衡量。



7.2.11 filtered列



返回结果的行数占读取行数的百分比,值越大越好。



举例如下:

表filmactor中actorid为1的记录有19条,而SQL查询时扫描了19行(rows:19),19条符合条件(filtered: 100 19/19)



7.2.12 Extra列



额外信息,但又十分重要。



常见的值如下:



1)Using index



表示SQL中使用了覆盖索引。



举例如下:

2)Using where



许多where条件里是涉及索引中的列,当它读取索引时,就能被存储引擎检验,因此不是所有带·where子句的查询都会显示“Using where”



3)Using temporary



对查询结果排序时,使用了一个临时表,常见于order bygroup by



4)Using filesort



对数据使用了一个外部的索引排序,而不是按照表内的索引进行排序读取。也就是说MySQL无法利用索引完成的排序操作成为“文件排序”。



7.3 总结



通过上述对执行计划的了解,我们能够从中得到什么?



  • SQL如何使用索引

  • 复杂SQL的执行顺序

  • 查询扫描的数据函数

  • ……



当面临不够优的SQL时,我们首先要查看其执行计划,根据执行计划结果来分析可能存在哪些问题,从而帮助、指导我们是否添加索引、是否调整SQL顺序、是否避免不应该的书写方式等等。



以上就是这篇文章的全部内容,希望本文的内容对大家在SQL性能优化、SQL书写时,有一定的帮助。



执行计划,真的很重要,尤其是SQL调优时,很香!



发布于: 2020 年 08 月 25 日 阅读数: 65
用户头像

xcbeyond

关注

不为别的,只为技术沉淀、分享。 2019.06.20 加入

公众号:程序猿技术大咖 知识星球:技术那些事

评论

发布
暂无评论
一起学MySQL性能优化