Bootstrap

深入理解MySQL

MySQL逻辑框架

最上层服务并不是MySQL所独有,大多基于网络的客户端/服务器的工具或服务都有类似的架构。如连接处理、授权认证、安全等

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

第三层包含存储引擎 ,负责MySQL中数据的存储和提取。服务器通过API与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。存储引擎不会去解析SQL,不同存储引擎之间也不会相互通信,只是响应上层服务器的请求

连接管理与安全性

每个客户端连接都会在服务器进程中拥有一个线程,连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或CPU中运行。服务器会缓存线程,不需为每一个新建的连接创建或销毁线程

当客户端(应用)连接到MySQL服务器时,需要对其认证,认证基于用户名、原始主机信息和密码。如果使用安全套接字的方式连接,还可以使用X.509证书认证。若客户端连接成功,服务器会继续验证客户端是否具有执行某个特定查询的权限(如,是否允许客户端对world数据库的Country表执行SELECT语句)

优化与执行

MySQL会解析查询,并创建内部数据结构(解析数),然后对其进行各种优化,包括重写查询、决定表的读取顺序,以及选择合适的索引等。用户可以通过特殊的关键字提示优化器,影响其决策过程,也可请求优化器解析优化过程的各个因素,使用户可以知道服务器是如何优化决策的,并提供一个参考基准,便于重构查询和schema-修改

相关配置,使应用尽可能高效运行

优化器会请求存储引擎提供容量或某个具体操作的开销信息,以及表数据的统计信息等。例如,某些存储引擎的某种索引,对一些特定的查询有优化

对于SELECT语句,在解析查询之前,服务器会先检查查询缓存,如果能在其中找到对应的查询,服务器不必再执行查询解析、优化和执行的某个过程,直接返回查询缓存中的结果集

并发控制

MySQL在两个层面的并发控制:服务器层和存储引擎层

读写锁

在处理并发读或写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题,这两种锁通常被称为共享锁和排他锁,也叫读锁和写锁

读锁使共享的(相互不阻塞),多个客户在同一时间可以同时读取同一资源,而互不干扰。写锁是排他的,即一个写锁会阻塞其他的写锁和读锁,能确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源

锁粒度

一种提高共享资源并发性的方式是让锁定对象具有选择性,尽管只锁定需要修改的部分数据,而不是所有资源。更理想的方式是只对会修改的数据片进行精确的锁定,锁定的数据量越少,系统的并发程度越高,只要相互之间不发生冲突即可

锁策略就是在锁的开锁和数据的安全性之间寻求平衡,平衡会影响性能。每种MySQL存储引擎都可以实现自己的锁策略和锁粒度,将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,同时会失去对另外一些应用场景的良好支持

表锁

MySQL中最基本的锁策略,并且是开销最小的策略。一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,会阻塞其他用户对该表的所有读写操作。没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的

READ LOCAL表锁支持某些类型的并发写操作。写锁比读锁由更高的优先级,因此一个写锁请求可能会被插入到读锁队列前面

行级锁

可以最大程度支持并发处理,同时也带来最大的锁开销。行级锁存储引擎层实现,MySQL服务器层没有实现

事务

事务是一组原子性的SQL查询,或者说一个独立的工作单位,如果数据库引擎能够成功对数据库应用该组查询的全部语句,就执行该组查询。事务内的语句要么全部执行成功,要么全部执行失败

隔离级别(4种)

1、READ UNCOMMITTED(未提交读)

事务种的修改,即使没有提交,对其他事务也是可见的。事务可以提交未提交的数据,也被称为脏读,在实际应用中一般很少使用

2、READ COMITTED(提交读)

大多数数据库系统的默认隔离级别都是READ COMMITTED(MySQL不是)。一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的,也叫不可重复度

3、REPEATABLE READ(可重复度)

解决了脏读的问题,保证在同一个事务中多次读取同样记录的结果是一致的,但还是无法解决另一个幻读的问题。幻读是指当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,可重复读是MySQL的默认事务隔离级别

4、SERIALIZABLE(可串行化)

最高隔离级别,会在读取的每一行数据都加锁,可能导致大量的延时和锁争用的问题,实际应用中也很少用这个隔离级别

死锁

指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁

锁的行为和顺序是和存储引擎相关的,以同样的顺序执行语句,有些存储引擎会产生死锁,有些则不会。死锁产生有双重原因:有些是因为真正的数据冲突,这种情况一般难以避免,有些则是由于存储引擎的实现方式导致的

对于事务型的系统,死锁是无法避免的,所以应用程序在设计时必须考虑如何处理死锁。大多数情况下只需要重写执行因死锁回滚的事务即可

事务日志

可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,不用每次都将修改的数据本身持久到磁盘。事务采用追加的方式,写日志的操作是磁盘上一小块区域内的顺序I/O。事务日志持久以后,内存中被修改的数据在后台可以刷回到磁盘。大多存储引擎是如此实现的,通常称之为预写式日志,修改数据需要写两次磁盘

若数据的修改已经记录到事务日志并持久化,但数据本身没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据

MySQL中的事务

MySQL提供了两种事务型的存储引擎:InnoDB和NDB Cluster,另外还有一些第三方存储引擎也支持事务,如XtraDB和PBXT

自动提交(AUTOCOMMIT)

默认采用自动提交模式,如果不是显式地开始一个事务,则每个查询都被当作一个事务执行提交操作。通过设置AUTOCOMMIT变量来启动和禁用自动提交模式

1或ON表示启用,0或OFF表示禁用

MySQL可通过执行SET TRANSACTION ISOLATION LEVEL 命名来设置隔离级别。新的隔离级别会在下一个事务开始时生效。可以在配置文件中设置整个数据库的隔离级别,也可以只改变当前会话的隔离级别:

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED

在事务中混合使用存储引擎

MySQL服务器层不管理事务,事务由下层存储引擎实现

若在事务中混合使用了事务型和非事务型的表(如InnoDB和MYISAM表),在正常情况下提交不会有什么问题,但如果该事务需回滚,非事务型的表上的变更无法撤销,会导致数据库处于不一致状态,这种情况难以修复,导致事务最终结果无法确定

隐式和显式锁定

InnoDB采用的是两阶段锁定协议。在事务执行过程中,随时可以执行锁定,锁只有执行COMMIT或ROLLBACK时才会释放,并且所有锁会在同一时刻被释放。前描述的锁定都是隐式锁定,InnoDB会根据隔离级别在需要时自动加锁

多版本并发控制

MVCC是行级锁的一个变种,在大多数情况下避免了加锁操作,因此开销更低,大都实现了非阻塞的读操作,写操作也会锁定必要的行

MVCC的实现是通过保存数据在某个时间点的快照来实现的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的

通过InnoDB的简化版行为说明MVCC是如何工作的

InnoDB的MVCC通过在每行记录后面保存两个隐式的列实现,一个保存了行的创建时间,一个保存行的过期时间,存储的是系统版本号,没开始一个新的事务,系统版本号都会自动递增

SELECT

InnoDB会根据以下的两个条件检查每行记录

1、InnoDB只查找版本早于当前事务版本的数据行,可以确保事务读取的行,要么在事务开始前已经存在的,要么是事务自身插入或修改果

2、行的删除版本要么未定义,要么大于当前事务版本号。确保事务读取到的行在事务开始之前未被删除

INSERT:InnoDB为新插入的每一行保存当前系统版本号作为行版本号

DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识

UPDATE:InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识

MySQL的存储引擎

在文件系统中,MySQL将每个数据库保存为数据目录下的一个子目录。创建表时,MySQL会在数据库子目录下创建一个和表同名的.frm文件保存表的定义。如创建一个名为MyTable的表,MySQL会在MyTable.frm文件中保存该表的定义

注:Windows大小写不敏感 Unix敏感

可以使用SHOW TABLE STATUS命令显示表的相关信息,例如对于mysql数据库中的user表

Name:表名   

Engine:表的存储引擎类型   

Row_format:行的格式   

Rows:表中的行数   

Avg_row_length:表数据的大小(以字节为单位)   

Max_data_length:表数据的最大容量,该值和存储引擎有关    

Index_length:索引的大小(以字节为单位)

Data_free:对于MyISAM表,表示已分配但目前没有使用的空间。这部分空间包括之前删除的行,以及后续可以被INSERT利用到的空间

Auto_increment:下一个AUTO-INCREMENT的表   

Create_time:表的创建时间   

Update_time:表数据的最后修改时间

Check_time:使用CHECK TABLE命令或myisamchk工具最后一次检查表的时间

Collation:表的默认字符集和字符排序规则

Cheoksum:如果启用,保存的是整个表的实际校验和

Create_options:创建表时指定的其他选项

Comment:包含一些其他的额外信息

InnoDB存储引擎

MySQL默认事务引擎,用来处理大量的短期事务

概览:InnoDB的数据存储在表空间中,表空间是由InnoDB管理的一个黑盒子,由一系列的数据文件组成

InnoDB采用MVCC来支持高并发,并且实现了四个标准的隔离级别。默认级别是REPEATABLE READ(可重复读),并且通过间隙锁策略防止幻读的出现。间隙锁使得InnoDB不仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入

InnoDB表是基于聚簇索引建立的

MyISAM存储引擎

提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且崩溃后无法安全恢复

存储:将表存储在两个文件;数据文件和索引文件,分别以.MYD和.MYI为扩展名。MyISAM表可以包含动态或静态行。MySQL根据表的定义决定采用何种行格式

MyISAM特性

加锁和并发:MyISAM对整张表加锁,不是针对行。读取时会对需要读到的所有表加共享锁,写入时对表加排他锁

修复:对于MyISAM表,MySQL可以手工或自动执行检查和修复操作。若MySQL服务器已经关闭,也可以通过myisamchk命令行工具进行检查和修复操作

索引特性:对于MyISAM表,即使BLOB和TEXT等长字段,也可以基于其前500个字符创建索引。MyISAM也支持全文索引

创建MyISAM表的时候,如果指定了DELAY_KEY_WRITE选项,每次修改执行完成时,不会立刻将修改的索引数据写入磁盘,会写到内存中的键缓冲区,只有在清除缓冲区或关闭表时才将对应的索引块写入到磁盘,极大提升了写入性能

MyISAM压缩表

可使用myisampack对MyISAM表进行压缩。压缩表不能进行修改,可以极大减少磁盘空间占用,也可以减少磁盘I/O,从而提升查询性能

MyISAM性能

MyISAM引擎设计简单,数据从紧密格式存储,有一些服务器级别的性能扩展限制,如对索引键缓冲区的Mutex锁,MariaDB基于段的索引键缓冲区机制来避免该问题

MySQL内建的其他存储引擎

Archive引擎:只支持INSERT和SELECT操作,会缓存所有写并利用zlib对插入的行进行压缩,所以比MyISAM表的磁盘I/O更少。每次SELECT查询都需执行全表扫描,所以Archive表适合日志和数据采集类应用

Archive引擎支持行级锁和专业的缓冲区,可以实现高并发的插入,在一个查询开始直到返回表中存在的所有行数之前,Archive引擎会阻止其他的SELECT执行,以实现一致性读

Blackhole引擎

没有任何的存储机制,会丢弃所有插入的数据,不做任何保存。服务器会记录Blackhole表的日志,所以可以用复制数据到备库,或简单地记录到日志

CSV引擎

可以将普通的CSV文件作为MySQL的表来处理,这种表不支持索引,该引擎可以在数据库运行时拷入或拷出文件。还可以将Excel等电子表格软件中的数据存储为CSV文件,然后复制到MySQL数据目录下,就能在MySQL中打开使用

Federated引擎

访问其他MySQL服务器的一个代理,它会创建一个到远程MySQL服务器的客户端连接,并将查询传输到远程服务器执行,然后提取或发送需要的数据

Memory引擎

若需要快速地访问数据,并且这些数据不会被修改,重启以后丢失也没有关系,则使用Memory表,比MyISAM表快一个数量级。Memory表的结构在重启以后还会保留,但数据会丢失

Memory表在如下场景可以发挥好的作用:

1、用于查找或映射表,例如将邮编和州映射的表

2、用于缓存周期性聚合数据的结果

3、用于保存数据分析中产生的中间数据

Memory表支持Hash索引,因此查找操作非常快。Memory表是表级锁,因此并发写入的性能较低,不支持BLOB或TEXT类型的列,并且每行的长度是固定的,即使指定了VARCHAR列,实际存储时也会转换成CHAR,导致部分内存浪费

Merge引擎

由多个MyISAM表合并而来的虚拟表。如果将MySQL用于日志或数据仓库类应用,该引擎可以发挥作用,引入分区功能后,该引擎已经被放弃

NDB集群引擎

作为SQL和NDB原生协议之间的接口,MySQL服务器、NDB集存储引擎,NDB数据库的组合称为MySQL集群

选择合适的引擎

考虑因素

事务:如果应用需要事务支持,选择InnoDB;不需要事务,并且主要是SELECT和INSERT操作,选择MyISAM

备份:若可以定期地关闭服务器来执行备份,那备份的因素可以忽略。如果需要在线热备份,选择InnoDB

崩溃恢复:数据量较大时选择InnoDB引擎

日志型应用

利用MySQL内置的复制方案将数据复制一份到备库,然后在备库上执行比较消耗时间和CPU的查询。主库只用于高效的插入工作,备库上执行的查询也无须担心影响到日志的插入性能

只读或大部分情况下只读的表

有些表的数据用于编制类目或分列清单,这种应用场景是典型的读多写少的业务,建议采用InnoDB

订单选择:支持事务是必要选项,还要考虑存储引擎对外键的支持情况

CD—ROM应用:可以考虑使用MyISAM表或MyISAM压缩表,这样表之间可以隔离并且可以在不同介质上相互拷贝

大数据量:很多InnoDB数据库数据量在3~5TB之间,或更大。这些系统运行需要合理选择硬件,做好物理设计,并会服务器I/O瓶颈做好规划,不能采用MyISAM

注:数据量增长至10TB以上,可能需建立数据仓库

转换表的引擎

ALTER TABLE

将表从一个引擎改为另一个引擎的办法是使用ALTER TABLE:

mysql> ALTER TABLE mytable ENGINE = InnoDB

该语法适用任何存储引擎,但需要执行很长时间。MySQL会按行将数据从原表复制到一张新的表中,在复制期间可能会消耗系统所有的I/O能力,同时原表会加上读锁。若转换表的存储引擎,将会失去和原引擎相关的所有特性

导出与导入

适用mysqldump工具将数据导出到文件,然后修改文件中CREATE TABLE语句的存储引擎选项,注意同时修改表名,因为同一个数据库中不能存在相同的表名,即使它们使用的是不同的存储引擎

创建和查询

综合了第一种和第二种,不需要导出整个表的数据,先创建一个新的存储引擎的表,然后利用INSERT...SELECT语法来导数据

若数据量很大,考虑做分批处理

Schema与数据类型优化

选择优化的数据类型

更小的通常更好:一般情况下,尽量使用可以正确存储数据的最小数据类型,因为它们占用更少的磁盘、内存和CPU,并且处理时需要的CPU周期更少,但也要确保没有低估需要存储的值的范围,因为在schema中的多个地方增加数据类型的范围是一个耗时的操作

简单就好:操作通常需要更少的CPU周期,例如整型比字符操作代价更低,因为字符集的校对规则使字符比整型更加复杂

尽量避免NULL:如果查询中包含可为NULL的列,对MySQL来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊处理

整数类型

若存储整数,使用:TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,分别使用8,16,32,64位存储空间,存储的值的范围:

-2^(N-1)~2^(N-1)-1,N是存储空间的位数

整数类型有可选的UNSIGNED属性,表示不允许负值,可以使正数的上限提高一倍,如TINYINT.UNSIGNED可以存储的范围是0~255,而TINYINT的存储范围是-128~127

有符号和无符号类型使用相同的存储空间,并具有相同的性能,可以根据实际情况选择合适的类型

实数类型

实数是带有小数部分的数字,不只是为了存储小数部分,也可以使用DECIMAL存储比BIGINT还大的整数,MySQL既支持精确类型,也支持不精确类型

FLOAT和DOUBLE支持使用标准的符浮运算进行近似计算    DECIMAL类型用于存储精确的小数

浮点和DECIMAL可以指定精度。对于DECIMAL列,可以指定小数点前后所允许的最大位数,会影响列的空间消耗,例如,DECIMAL(18,9)小数点两边将各存储9个数字,一共使用9个字节,小数点前后各用4个字节,小数点占一个字节

浮点类型在存储同样范围的值时,通常比DECIMAL使用更少的空间。FLOAT使用4个字节存储,DOUBLE占用8个字节,和整数类型一样,能选择的只是存储类型,MySQL使用DOUBLE作为内部浮点计算的类型

因为需额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用DECIMAL,如存储财务数据

字符串类型

VARCHAR:用于存储可变长字符串,比定长类型更节省空间,需要使用1或2个额外字节记录字符串长度,如果列的最大长度小于或等于255字节,则只使用1个字节,其他情况使用2个字节。假设采用latinl字符集,一个VARCHAR(10)的列需要11个字节的存储空间,VARCHAR(1000)的列需要1002个字节,因为需要2个字节存储长度信息

VARCHAR节省存储空间,对性能有所帮助,由于行是变长的,在UPDATE时可能使行变得比原来更长,导致需要做额外的工作

如下情况适用VARCHAR:1、字符串的最大长度比平均长度大很多    2、列的更新很少,所以碎片不是问题   3、使用了像UTF-8这样复杂的字符集,每个字符都使用不同的字节数进行存储

CHAR

是定长的,MySQL根据定义的字符串长度分配足够的空间。当存储CHAR值时,MySQL会删除所有的末尾空格。CHAR值会根据需要采用空格进行填充以方便比较。CHAR适合短的字符串,如存储密码的MD5值,因为这是一个定长的值

先创建一张只有CHAR(10)字段的表并且往里面插入一些值

当检查这些值时,发现string3末尾的空格被截断

如果用VARCHAR(10)字段存储相同的值,可以得到如下结果

更长的列会消耗更多的内存,因为MySQL会分配固定大小的内存卡来保存内布置,尤其是使用内存表进行排序或操作时会很糟糕,所以最好的策略是只分配真正需要的空间

BLOB和TEXT类型

为存储很大数据而设计的字符串数据类型,分别采用二进制和字符方式存储

MySQL把每个BLOB和TEXT值作一个独立的对象处理。当BLOB和TEXT太大时,InnoDB会使用专门的“外部”存储区域来进行存储,此时每个值在行内需要1~4个字节存储一个指针,然后在外部存储区域存储实际的值

BLOB和TEXT之间仅有的不同是BLOB类型存储的是二进制数据,没有字符集或排序规则,TEXT类型有字符集和排序规则

MySQL对BLOB和TEXT列进行排序与其他类型不同:只对每个列的最前max_sort_length字节而不是整个字符串做排序。如果只需排序前面一小部分字符,可以减小max_sort_length的配置或使用ORDER BY SUSTRING

使用枚举(ENUM)代替字符串类型

枚举列可以把一些不重复的字符串存储成一个预定义的集合。MySQL在存储枚举时非常紧凑,会根据列表值的数量压缩到一个或两个字节中

这三行数据实际存储为整数,不是字符串,可以通过数字上下文环境检查看到这个双重属性

枚举不好的地方是字符串列表是固定的,添加或删除字符串必须使用ALTER TABLE,MySQL把每个枚举值保存为整个,并且必须进行查找才能转换为字符串,所以枚举有开销

转换列为枚举的好处:根据SHOW TABLE STATUS命令输出结果中Data_length列的值,这两列转换为ENUM可以让表的大小缩小1/3

日期和时间类型

MySQL能存储的最小时间粒度为秒,MySQL也可以使用微秒级的粒度进行临时运算,MySQL提供两种相似的日期类型:DATETIME和TIMESTAMP

DATETIME

保存大范围的值,从1001年到9999年,精度为秒,把日期和时间封装到格式为YYYYMMDDHHMMSS的整数中,与时区无关,使用8个字符的存储空间

默认情况下,MySQL以一种可排序的、无歧义的格式显示DATETIME的值,例如“2008-01-16  22:33:08”,这是ANSI标准定义的日期和时间表示方法

TIMESTAMP

保存了从1970年1月1日午夜以来的秒数,它和UNIX时间戳相同。TIMESTAMP只使用4个字节的存储空间,范围比DATETIME小得多,只能表示从1970年到2038年,MySQL提供了FROM_TIMESTAMP()函数把日期转换为Unix时间戳

如果在多个时区存储或访问数据,TIMESTAMP和DATETIME的行为将很不一样,前者提供的值与时区有关系,后者则保留文本表示的日期和时间

如果需要存储比秒更小粒度的日期和时间值时,可以使用自己的存储格式,使用BIGINT类型存储微妙级别的时间戳,或使用DOUBLE存储秒之后的小数部分

位数据类型

BIT

可以使用列在一列中存储一个或多个true/false值。BIT(1)定义了一个包含单个位的字段,BIT(2)存储2个位,依此类推。BIT列的最大长度是64个位。BIT行为因存储引擎而已,MyISAM会打包所有BIT列,所以17个单独的BIT列只需17个位的存储,MyISAM只使用3个字节就能存储这17个BIT列

MySQL把BIT当作字符串类型而不是数字类型。当检索BIT(1)的值时,结果是一个包含二进制0或1的字符串,在数字上下文的场景中检索时,结果将是位字符串转换成的数字。例:如果存储一个值b‘00111001’到BIT(8)的列并且检索它,得到的内容是字符串码为57的字符串

如果想在一个bit的存储空间中存储一个true/false值,另一个方法是创建一个可以为空的CHAR(0列,该列可以保存空值(NULL)或长度为0的字符串

SET

如果需要保存很多true/false值,可以考虑合并这些列到一个SET数据类型,在MySQL内部是以一系列打包的位的集合来表示的,有效的利用了存储空间,并且MySQL有FIND_IN_SET()和FIELD()这样的函数,方便在查询中使用

缺点是改变列的定义的代价较高,需要ALTER TABLE,也无法在SET列上通过索引查找

选择标识符

当选择标识列的类型时,不仅要考虑存储类型,还要考虑MySQL对这种类型怎么执行计算和比较。例如,MySQL在内部使用整数存储ENUM和SET类型。然后再做比较操作时转换为字符串

确定了一种类型,要确保在所有关联表中都使用同样的类型。类型之间需要精确匹配,包括像UNSIGNED这样的属性。在可以满足值的范围的需求,并且预留未来增长空间的前提下,应该选择最小的数据类型

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

ENUM和SET类型:通常是一个糟糕的选择,对某些只包含固定状态或类型的静态“定义表”来说可能没问题,ENUM和SET列适合存储固定信息,例如有序的状态、产品类型、人的性别

字符串类型:应避免使用字符串类型作为标识列,它们消耗空间很大,通常比数字类型慢

MySQL schema设计中的陷阱

太多的列

MySQL的存储引擎API工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列。行缓冲将编码过的列转换成行数据结构的操作代价是高昂的,MyISAM定长行结构实际与服务器层的行结构正好匹配,不需要转换

太多的关联

MySQl限制了每个关联操作最多只能有61张表,但EVA数据库需要许多的关联,如果希望查询执行得快速且并发性好,单个查询最好在12个表以内做关联

全能的枚举

注意防止过度使用枚举,例如

应该用整数作为外键关联到字典表或查找表来查找具体值,在MySQL中,当需要在枚举表中增加一个新的国家时就要做一次ALTER TABLE操作

变相的枚举

枚举列允许在列中存储一组定义值中的单个值,集合列允许在列中存储一组定义值中的一个或多个值,例如

如果真和假两种情况不会同时出现,应该使用枚举列代替集合列

范式和反范式

在范式化的数据库中,每个事实数据会出现并且只出现一次,相反,在反范式化的数据库中,信息的冗余的,可能会存储在多个地方

范式的优点:

1、范式化的更新操作通常比反范式化要快

2、当数据较好地范式化时,就只有很快或没有重复数据,所以只需要修改更少的数据

3、范式化的表通常更小,可以更好地放在内存里,执行操作会更快

4、少有多余的数据意味着检索列表数据时更少需要DISTINCT或GROUP BY语句

范式的缺点:

1设计的schema的缺点是通常需要关联。稍复杂一些的查询语句在符合范式的schema上都可能需要至少一次关联,也许更多

反范式的优点和缺点

反范式的schema因为所有数据都在一张表中,可以很好避免关联。当数据比内存大时可能比关联要快得多,避免了随机I/O

缓存表和汇总表

缓存表表示存储那些可以比较简单地从schema其他表获取(速度慢)数据的表。例如逻辑上冗余的数据

汇总表保存的是使用GROUP BY语句聚合数据的表,数据不是逻辑上冗余的

以网站为例,假设需要计算之前24小时内发送的消息数,以每小时汇总表为基础,把前23个完整的小时的统计表中的计数全部加起来,最后再加上开始阶段和结束阶段不完整的小时内的计数,假设统计表叫作msg_per_hr并且这样定义:

可以通过把下面三个语句的结果加起来,得到过去24小时发送消息的总数,使用LEFT(NOW(),14)来获得当前的日期和时间最接近的小时

在使用缓存表和汇总表时,必须决定是实时维护数据还是定期重建,哪个更好依赖于应用程序,但是定期重建并不是节省资源,也可以保持表不会有很多碎片,以及有完全顺序组织的索引

当重建汇总表和缓存表时,需要保证数据在操作时依然可用,通过使用“影子表”来实现,“影子表”指的是一张在真实表“背后”创建的表。当完成了建表操作后,通过一个原子的重命名操作切换影子表和原表。例如,如果需要重建my_summary则可以先创建my_summary_new,然后填充好数据,最后和真实表做切换

物化视图

预先计算并且存储在磁盘上的表,可以通过各种策略刷新和更新。MySQL并不原生支持物化视图,使用开始工具Flexviews可以自己实现物化视图,提供了很多功能使得可以更简单地创建和维护物化视图,由以下部分组成:

1、变更数据抓取功能,可以读取服务器的二进制日志并且解析相关行的变更

2、一系列可以帮助创建和管理试图的定义的存储过程

3、一些可以应用变更到数据库中的物化视图的工具

Flexviews通过提取对源表的更改,可以增量地重新计算物化视图的内容,不需要通过查询原始数据来更新视图。例如,如果创建了一张汇总表用于计算每个分组的行数,此后增加了一行数据到源表中,Flexviews给相应的组的行数加一

计数器表

可以缓存一个用户的朋友数,文件下载次数等

假设有一个计数器表,只有一行数据,记录网站的点击次数

网站每次点击都会导致对计数器进行更新

获得更高的并发更新性能,可以将计数器保存在多行中,每次随机选择一行进行更新,需要对计数器表进行如下修改

然后预先在这张表增加100行数据,选择一个随机的槽进行更新

要获得统计结构,需要使用下面这样的聚合查询

加快ALTER TABLE的操作速度

有两种方式可以改变或删除一个列的默认值,假如要修改电影的默认租赁期间,从三天改到五天,如下是慢方式

SHOW STATUS显示这个语句做了1000次读和1000次插入操作,拷贝整张表到新表

理论上,MySQL可以跳过创建新表的步骤,列的默认值实际上存在表的.frm文件中可以修改文件不改动表本身

另一种是通过ALTER COLUMN操作来改变列的默认值

此语句直接修改.frm文件

只修改.frm文件

下面这些操作可能不需要重建表

1、移除一个列的AUTO_INCREMENT属性

2、增加、移除、更改ENUM和SET常量,如果移除的是已有行数据用到其值的常量,查询将会返回一个空字串值

基本的技术是为想要的表结构创建一个新的.frm文件,然后用它替换掉已存在的那张表的.frm文件,如

1、创建一张有相同结构的空表,并进行所需要的修改(例如增加ENUM常量)

2、执行FLUSH TABLE WITH READ LOCK,将会关闭所有正在使用的表,并且禁止任何表被打开

3、交换.frm文件

4、执行UNLOCK TABLES来释放第2步的读锁

例:给sakila film表的rating列增加一个常量,当前列如下:

增加一个PG—14的电影分级

接下来用操作系统的命名交换.frm文件

再回到MySQL命令行,可以解锁表并且看到变更后的效果

最后需要删除为完成这个操作而创建的辅助表

快速创建MyISAM索引

为了高效地载入数据到MyISAM表中,有一个常用技巧是先禁用索引,载入数据,然后重新启用索引

当已经知道所有数据都是有效的并且没有必要做唯一性检查时可以如下操作:

1、用需要的表结构创建一张表,不包括索引

2、载入数据到表中以构建.MYD文件

3、按照需要的结构创建另外一张空表,要包含索引。创建需要的.frm个.MYI文件

4、获取读锁并刷新表

5、重命名第二张表的.frm和.MYI文件,让MySQL认为是第一张表的文件

6、释放读锁

7、使用REPAIR TABLE来重建表的索引。该操作会通过排序来构建所有索引,包括唯一索引

需要使用的数据库保持简单原则

1、尽量避免过度设计,例如会导致极其复杂查询的shcema设计,或有很多列的表设计

2、使用小而简单的合适数据类型,应该尽可能避免使用NULL值

3、尽量使用相同的数据类型存储相似或相关的值,尤其是要在关联条件中使用的列

4、注意可变长字符串,其在临时表和排序时可能导致悲观的按最大长度分配内存

5、尽量使用整型定义标识列

6、避免使用MySQL已经遗弃的特性,例如指定浮点数的精度,或整数的显示宽度

7、小心使用ENUM和SET

创建高性能的索引

索引(在MySQL也叫“键”)是存储引擎用于快速找到记录的一种数据结构,索引能够轻易将查询性能提高几个数量级

索引基础

在MySQL中,存储引擎用类似的方法使用索引,先在索引中找到对应值,然后根据匹配的索引记录找到对应的数据行,例如运行如下查询

mysql> SELECT first _name FROM sakila.actor WHERE actor_id = 5;

如果在actor_id列上建有索引,则MySQL将使用该索引找到actor_id为5的行,MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行

索引可以包含一个列或多个列的值,如果索引包含多个列,那列的顺序也十分重要,因为MySQL只能高效地使用索引的最左前缀列

索引的类型

B—Tree索引

使用B—Tree数据结构来存储数据,大多数MySQL引擎都支持这种索引。

存储引擎以不同的方式使用B—Tree索引,性能不同,各有优势,如MyISAM使用前缀压缩技术使得索引更小,但InnoDB按照原数据格式进行存储

B—Tree意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同

B—Tree索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,从索引的根节点开始搜索

B—Tree对索引是顺序存储的,适合查找范围数据,例如,在一个基于文本域的索引树上,按字母顺序传递连续的值进行查找是合适的

假设有如下数据表

对表中的每一行数据,索引中包含了last_name、first_name和dob列的值

索引对多个值进行排序的依据是CREATE TABLE语句中定义索引时列的顺序。B—Tree索引适合全键值、键值范围或键前缀查找,其中键前缀查找只适用于根据最左前缀的查找,前面所述索引对如下类型的查询有效

全值匹配:指的是和所有列进行匹配,例如前面提到的索引可用于查找姓名为Cuba Allen 出生于1960-01-01的人

匹配最左前缀:索引可用于查找所有姓为Allen的人,即只使用索引的第一列

匹配列前缀:可以只匹配某一列的值的开头部分,例如前面提到的索引可用于查找所有以j开头的姓的人

匹配范围值:例如前面提到的索引可用于查找姓在Allen和Barrymore之间的人

精确匹配某一列并范围匹配到另外一列:前面提到的索引可用于查找所有姓为Allen,并且名字是字母K开头的人

B-Tree通常可以支持“只访问索引的查询”,即查询只需要访问索引,无须访问数据行

关于B—tree索引的限制

1、如果不是按照索引的最左列开始查找,则无法使用索引

2、不能跳过索引中的列

3、如果查询中有个列的范围查询,其右边所有列都无法使用索引优化查找

哈希索引

基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个Hash Code,Hash Code是一个较小的值,并且不同键值的行计算出来的Hash Code也不一样。Hash索引将所有的Hash Code存储在索引中,同时在Hash表中保存指向每个数据行的指针

在MySQl中,只有Memory引擎显式支持Hash索引

例如,假设有如下表

表中包含如下数据

假设索引使用假象的Hash函数f(),它返回下面的值

则Hash索引的数据结构如下

每个槽的编号是顺序的,但数据行不是,如下查询

mysql> SELECT lname FROM testhash WHERE fname = "Peter";

MySQL先计算‘Peter’的Hash值,并使用该值寻找对应的记录指针,因为f('Peter')=8784,所以先在索引中查找8784,可以找到指向第3行的指针,最后一步比较第三行的值是否为‘Peter’,以确保就是要查找的行

Hash索引的限制:

1、Hash索引只包含Hash值和行指针,不存储字段值,不能使用索引中的值来避免读取行

2、Hash索引数据并不按照索引值顺序存储,无法用于排序

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

4、Hash索引只支持等值比较查询,包括=、IN()、<=>,不支持任何范围查询,例如WHERE price > 100

 5、当出现Hash冲突时,存储引擎必须遍历表中所有的行指针,逐行进行比较,直到找到所有符合条件的行

创建自定义Hash索引。如果存储引擎不支持Hash索引,可以模拟InnoDB一样创建Hash索引

思路:需要存储大量URL,需要根据URL进行搜索查找,如下查询

mysql> SELECT id FROM url WHERE url = "http:// www.mysql.com";

若删除原来URL列上的索引,新增一个被索引的url-crc列,如下查询

触发器如何在插入和更新时维护url-crc列,首先创建如下表

然后创建触发器,先临时修改一下语句分隔符,可以在触发器定义中使用分号:

验证触发器如何维护Hash索引

空间数据索引(R-Tree)

MyISAM表支持空间索引,可以用作地理数据存储。和B-Tree索引不同,无须前缀查询,空间索引会以所有维度来索引数据,查询时,可以有效地使用任何维度来组合查询。必须使用MySQL的GIS相关函数如MBRCONTAINS()来维护数据。MySQL的GIS支持并不完善,开源关系数据库系统对GIS解决方案做得好的是PostgreSQl和PostGIS

全文索引

一种特殊类型的索引,查找的是文本中的关键词,不直接比较索引中的值,需注意的细节如停用词、词干和复数、布尔搜索等

索引的优点

1、大大减少了服务器需要扫描的数据量    2、可以帮助服务器避免排序和临时表

3、可以将随机I/O变为顺序I/O

高性能的索引策略

独立的列

如果查询中的列不是独立的,MySQL不会使用索引。“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数

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

mysql> SELECT actor_id FROM sakila.actor WHERE actor_id +1 = 5;

可以看出WHERE中的表达式等价于actor_id = 4 ,但MySQL无法自动解析这个方程式

前缀索引的和索引选择性

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

对于BLOB、TEXT或很长的VARCHAR类型的列。必须使用前缀索引,因为MySQL不允许索引这些列的完整长度

为决定前缀的合适长度,需要找到最常见的列表,然后和最常见的前缀列表进行比较

从表中生成一个实例表,如图

找到最常见的城市列表

查找最频繁出现的城市前缀,从3个前缀字母开始

每个前缀都比原来的城市出现的次数更多,唯一前缀比唯一城市更少得多。增加前缀长度,直到这个前缀的选择性接近完整列的选择性

计算合适的前缀长度的另一个办法是计算完整列的选择性,并使前缀的选择性接近完整列的选择性

前缀索引是一种能使索引更小、更快的有效方法,但MySQL无法使用前缀索引做ORDER BY和GROUP BY,也无法使用前缀索引做覆盖扫描

多列索引

在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的查询性能。MySQL5.0和更新版本引入了一种叫“索引合并”的策略,一定程度上可以使用表上的多个单列索引来定位指定的行,查询能够同时使用这两个单列索引进行扫描,并将结果进行合并。这种算法有三个变种:OR条件的联合、AND条件的相交、以及组合前两种情况的联合及相交

如下的查询是使用了两个索引扫描的联合,通过EXPLAIN中的Extra列可看出

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

1、出现服务器对多个索引做相交操作时,意味着需要一个包含所有相关列的多列索引,不是多个独立的单列索引

2、当服务器需要对多个索引做联合操作时,需要消耗大量CPU和内存资源在算的缓存、排序和合并操作上

3、优化器不会把这些计算到“查询成本”中,只关心随即页面读取

如果在EXPLAIN中看到有索引合并,应该检查以下查询和表结构,看是否已经是最优的,也可以通过参数optimizer-switch来关闭索引合并功能,也可以使用IGNORE INDEX提示让优化器忽略掉某些索引

选择合适的索引列顺序

经验法则:将选择性最高的列放到索引最前端,但通常不如避免随机IO和排序重要。当不需要考虑排序和分组时,将选择性最高的列放在最前面,这时索引的作用是优化WHERE条件的查找

性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,可能需要根据运行效率最高的查询来调整索引列的顺序,让这种情况下的索引选择性更高

以下面的查询为例:

SELECT *FROM payment WHERE staff-id = 2 AND customer-id = 584;

可以跑一些查询来确定在这个表中值的分布情况,并确定那个列的选择性更高

将索引列customer_id放到前面,因为对应条件值的customer_id数量更小

案例:在一个用户分享购买商品和购买经验的论坛上,这个特殊表上的查询运行得非常慢

查询看似没有建立合适的索引,所以客户端是否可以优化,EXPLAIN结果如下

这个案例的解决办法是修改应该程序代码,区分这类特殊用户和组,禁止这类用户和组执行这个查询

聚簇索引

不是一种单独的索引类型,是一种数据存储方式,具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了

B-Tree索引和数据行

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

存储引擎负责实现索引,因此一个表只能有一个聚簇索引;存储引擎负责实现索引,因此不是所有的存储引擎都支持聚簇索引

如图展示了聚簇索引中的记录是如何存放的

聚集数据的优点:

1、可以把相关数据保存在一起,例如实现电子邮箱时,可以根据用户ID聚集数据,只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件

2、数据访问更快,聚簇索引将索引和数据保存在同一个B-Tree中,因此聚簇索引中获取数据通常比在非聚簇索引中查找要快

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

聚簇索引的缺点

1、最大限度提高了I/O密集型应用的性能

2、插入速度严重依赖于插入顺序,按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式

3、更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置

4、基于聚簇索引的表再插入新行,或主键被更新导致需行移动行的时候,可能“页分裂”的问题。页分裂是指当行的主键值要求必须将这一行插入到某个已满的页时,存储引擎会将该页分裂成两个页面来容纳该行,会导致占用更多磁盘空间

5、聚簇索引可能会导致全表扫描变慢

覆盖索引

如果查询只需要扫描索引而无须回表,会带来如下好处:

1、索引条目通常会远小于数据行大小,所以如果只需读取索引,那么MySQL会极大地减少数据访问量

2、因为索引是按照列值顺序存储的,对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少得多

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

4、InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询

MySQL只能使用B-Tree索引做覆盖索引

例如:表sakila.inventory有一个多列索引(store_id、film_id),MySQL如果只需访问这两列,可以使用这个索引做覆盖索引,如下

索引覆盖查询还有很多陷阱可能会导致无法实现优化。MySQL查询优化会在执行查询前判断是否有一个索引能进行覆盖

这里索引无法覆盖查询,有两个原因:

1、没有任何索引能够覆盖这个查询,因为查询从表中选择了所有的列,没有任何索引覆盖了所有的列。理论上MySQL还有一个捷径可利用:WHERE条件中的列是有索引可以覆盖的,因此MySQL可以使用该索引找到对应的actor并检查title是否匹配,过滤之后再读取需要的数据行

2、MySQL不能在索引中执行LIKE操作,能在索引中做最左前缀的LIKE比较,因为该操作可以转换为简单的比较操作,但如果是通配符开头的LIKE查询,存储引擎无法做比较匹配

有办法解决以上两种问题,需要重新查询并巧妙地设计索引,先将索引扩展至覆盖三个数据到(artist、title、prod_id),按如下方式重写查询

这种方式叫做延迟关联,延迟了对列的访问。在查询第一阶段MySQL可以使用覆盖索引,在FROM子句的子查询中找到匹配的prod_id,根据其值在外层查询匹配获取需要的所有列值

例:sakila.actor使用InnoDB存储引擎,并在last_name字段有二级索引,该索引的列不包括主键actor_id,但也能够用于对actor_id做覆盖查询

使用索引扫描来做排序

MySQL有两种方式可以生成有序的结果:1、通过排序操作   2、按索引顺序扫描

如果EXPLAIN出的type列的值为“index”,说明MySQL使用索引扫描来做排序

MySQL可以使用同一个索引既满足排序,又用于查找行。只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向都一样时,MySQL才能使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序

例:sakila示例数据库的表rental在列(rental_date、inventory_id、customer_id)上有名为rental_date的索引

MySQL可以使用rental_date索引为下面的查询做排序,从EXPLAIN中可看到没有出现文件排序操作

如下是不能使用索引做排序的查询

使用了两种不同的排序方向,但索引列都是正序排序的

...WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC ,customer_id ASC;

ORDER BY引用了一个不在索引中的列:

...WHERE rental_date = '2005-05-25' RDER BY inventory_id,staff_id;

WHERE和ORDER BY中的列无法组合成索引的最左前缀

...WHERE rental_id = '2005=05-25'  ORDER BY customer_id;

在索引列的第一列上是范围条件,所以MySQL无法使用索引的其余列,在inventory——id列上有多个等于条件,对于排序来说,这也是一种范围查询

...WHERE rental_date = '2005-05-25' AND inventory_id IN (1,2) ORDER BY customer_id;

压缩(前缀压缩)索引

MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引放入内存中,一些情况下可以大幅度提升吸能。默认只压缩字符串,通过参数设置也可以对整数做压缩

MyISAM压缩每个索引块的方法:先完全保存索引块中的第一个值,然后将其他值和第一个值进行比较得到相同前缀的字节和剩余的不同后缀部分。如第一个值是“perform”,第二个值是“performance”,那么第二个值的前缀压缩后存储的是类似“7,ance”这样的形式

冗余和重复索引

重复索引是指在相同列上按相同的顺序创建的相同类型的索引,应当避免创建重复索引

有时会不经意间创建重复索引,如下

MySQL唯一限制和主键限制都是通过索引实现的,因此上面的写法实际上在相同的列上创建了三个重复的索引

冗余索引和重复索引有所不同,如果创建了索引(A,B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引

冗余索引通常发生在为表添加新索引时。如,增加一个新的索引(A,B)而不是扩展已有的索引(A)

解决冗余索引和重复索引的方法很简答,删除这些索引即可,先找出这样的索引,写一些复杂访问INFORMATION——SCHEMA是一系列可以安装到服务器上的常用的存储和视图,还可以使用Percona Toolkit中的pt_duplicate_key_checker,该工具通过分析表结构来找出冗余和重复索引

未使用的索引

这样的索引完成是累赘,建议考虑删除。最简单有效的办法是在Percona Server或Maria DB中先打开userstates服务器变量(默认是关闭的),然后让服务器正常运行一段时间,再通过查询INFORMATION——SCHEMA.INDEX_STATISTICS能查询到每个索引的使用频率

索引和锁

索引可以让查询锁定更少的行,如果查询从不访问那些不需要的行,那么会锁定更少的行,从两个方面来说对性能都有好处。首先,虽然InnoDB的行锁效率很高,内存使用也少,但锁定行的时候仍会带来额外的开销,其次,锁定超过需要的行会增加锁争用并减少并发性

InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从而减少锁的数量,但只有要InnoDB在存储引擎层能够过滤掉所有不需要的行时才有效

索引案例学习

支持多种过滤条件

country列的选择性通常不高,但可能很多查询都会用到。sex列的选择性肯定很低,但也会在很多查询中用到,考虑到使用的频率,建议在创建不同组合索引时将(sex,country)列作为前缀

这么做的理由有两个:1、几乎所有的查询都会用到sex列    2、索引中加上这一列并无坏处,即使查询没有使用sex列也可通过“诀窍”绕过

诀窍是指如果某个查询不限制性别,可以通过在查询条件中新增AND SEX IN(‘m’,‘f’)让MySQL选择该索引

案例显示了一个基本原则:考虑表上所有的选项,当设计索引时,不要只为现有的查询考虑需要哪些索引,还要考虑对查询进行优化

避免多个范围条件

假设有一个last_online列并希望通过下面的查询显示在过去几周上线过的用户

这个查询有一个问题:它有两个范围条件,last_online和age列,MySQL可以使用last_online列索引或age列索引,但无法同时使用它们

优化排序

对于选择性非常低的列,可以增加一些特殊的索引来做排序,例如,可以创建(sex,rating)索引用于下面的查询

mysql> SELECT <cols> FROM profiles WHERE sex = 'M' ORDER  BY rating LIMIT 10;

同时使用了ORDER BY和LIMIT,没有索引的话会很慢,即使有索引,如果用户界面上需要翻页,并且翻页到比较靠后时查询也可能非常慢

因为随着偏移量的增加,MySQL需要花费大量的时间来扫描需要丢弃的数据,办法之一是限制用户能够翻页的数量,实际对用户体验影响不大

维护索引和表

维护表的三个主要目的:1、找到并修复损坏的表  2、维护准确的索引统计信息  3、减少碎片

找到并修复损坏的表

损坏的索引会导致查询返回错误的结果和莫须有的主键冲突等问题,严重时还会导致数据库的崩溃。可以尝试运行CHECK TABLE来检查是否发生了表损坏。CHECK TABLE通常能找出大多数的表和索引的错误

可以使用REPAIR TABLE命令来修复损坏的表,但同样不是所有的存储引擎都支持该命令,如果不支持,也可通过一个不做任何操作的ALTER操作来重建表,例如修改表的存储引擎为当前的引擎,如下是一个针对InnoDB表的例子

mysql ALTER TABLE innodb_tbl ENGINE = INNODB;

更新索引统计信息

MySQL的查询优化器会通过两个API来了解存储引擎值的分布信息,以决定如何使用索引。第一个API是records_in_range(),通过向存储引擎传入两个边界值获取在这个范围内大概有多少条记录。对于某些存储引擎,该接口返回精确值,例如MyISAM,对于另一些存储引擎则是一个估算值,例如InnoDB。第二个API是info(),该接口返回各种类型的数据,包括索引的基数(每个键值有多少条记录)

如果存储引擎向优化器提供的扫描信息是不准确的数据,或执行计划本身太复杂以致无法准确地获取各个阶段匹配的行数,那么优化器会使用索引统计信息来估算扫描行数

每种存储引擎实现索引统计信息的方式不同,所以需要进行ANALYZE TABLE的频率也因不同的引擎而不同,每次运行的成本也不同

1、Memory引擎根本不存储索引统计信息

2、2、MyISAM将索引统计信息存储在磁盘中,ANALY TABLE需要进行一次全索引扫描来计算索引基数。在整个过程中需要锁表

3、InnoDB不在磁盘存储索引统计信息,通过随机的索引访问进行评估并将其存储在内存中

可以使用SHOW INDEX FROM命令来查看索引的基数,如

减少索引和数据的碎片

碎片化的索引可能会以很差或无序的方式存储在磁盘上

有三种类型的数据碎片

1、行碎片,指的是数据行被存储为多个地方的多个片段中。即使查询只以索引中访问一行记录,行碎片也会导致性能下降

2、行间碎片:逻辑上顺序的页,或行在磁盘上不是顺序存储的。行间碎片对诸如全表扫描和聚簇索引扫描之类的操作有很大影响,因为这些操作原来能够以磁盘上顺序存储的数据中效益

3、剩余空间碎片:指数据页有大量的空余空间,会导致服务读取大量不需要的数据,从而造成浪费

查询性能优化

为什么查询速度会慢?

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

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

慢查询基础:优化数据访问

大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化,对于低效的查询,通过下面两个步骤分析有效

1、确认应用程序是否在检索大量超过需要的数据

2、确认MySQL服务器层是否在分析大量超过需要的数据行

是否向数据库请求了不需要的数据

有些查询会请求超过实际需要的数据,多余的数据会被应用程序丢弃,给MySQl服务器带来额外的负担,并增加网络开销,也会消耗应用服务器的CPU和内存资源

MySQL是否在扫描额外的记录

对于MySQL,最简单的衡量查询开销的三个指标如下:

1、响应时间  2、扫描的行数   3、返回的行数

响应时间:服务时间和排队时间之和,服务时间是指数据库出列这个查询真正花了多长时间,排队时间指的是因为等待某些资源而没有真正执行查询的时间,可能是等I/O操作完成,可能是等待行锁等

扫描的行数和返回的行数

扫描的行数在一定程度上能够说明该查询找到需要的数据的效率高不高,理想情况下扫描的行数和返回的行数应该是相同的,但实际上相同的情况并不多,例如在做一个关联查询时,服务器需要扫描多行才能生成结果集中的一行。扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间

扫描的行数和访问类型

访问类型速度从慢到快,扫描的行数从小到大依次是:从全表扫描到索引扫描、范围扫描、唯一索引查询、常熟引用

数据库sakila中的一个查询案例

mysql> SELECT * FROM sakila.film_actor WHERE film_id = 1;

这个查询将返回10行数据,从EXPLAIN的结果可以看到,MySQL在索引idx_fk_film_id上使用了ref访问类型来执行查询

EXPLAIN的结果也显示MySQL预估需要访问10行数据

一般MySQL能够使用如下三种应用WHERE条件,从好到坏依次是

1、在索引使用WHERE条件来过滤不匹配的记录,在存储引擎层完成

2、使用索引覆盖扫描来返回记录,直接从索引中过滤不需要的记录并返回命令中的结果,在MySQL服务器上完成,无须再回表查询记录

3、从数据表中返回数据,然后过滤不满足条件的记录,在MySQL服务器层完成,MySQL需要先从数据表读出记录然后过滤

如果发现查询需要扫描大量的数据但只返回少数的行,以下方法可优化:

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

2、改变库表结构,例如使用单独的汇总表

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

重构查询的方式

切分查询

定期地清除大量数据时,如果用一个大的语句一次性完成的话,则可能需要一次锁住很多数据、沾满整个事务日志、耗尽系统资源,阻塞很多小但重要的查询。将一个大的DELETE语句切分成多个较小的查询可以尽可能小的影响MySQL性能,同时还可以减少MySQL复制的延迟。例如,需要每个月运行一次下面的查询

那么可以用类似下面的办法来完成同样的工作

一次删除一万行数据一般来说是一个比较高效而且对服务器影响也最小的做法,如果每次删除数据后,都暂停一会再做下一次删除,也可以将服务器上原本一次性的压力分散到一个很长的时间中,可以大大降低对服务器的影响,还可以大大减少删除时锁的持有时间

分解关联查询

可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,例如:

可以分解成以下查询代替

分解关联查询有如下优势

1、让缓存的效率更高,应用程序可以方便地缓存单笔查询对应的结果对象

2、将查询分解后,执行单个查询可以减少锁的竞争

3、在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩招

4、查询本身效率也可能会有所提升

5、可以减少冗余记录的查询

6、相当于在应用中实现了Hash关联,而不是使用MySQL的嵌套循环,提高效率

查询执行的基础

1、客户端发送一条查询给服务器

2、服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果,否则进入下一阶段

3、服务器端进行SQL解析,预处理,再由优化器生成对应的执行计划

4、MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询

5、将结果返回给客户端

MySQL客户端/服务器通信协议

在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,两个动作不能同时发生,无法也无须将一个消息切成小块独立来发送

协议让MySQL通信简单快速,但无法进行流量控制

查询状态:有很多种方式能查看当前的状态,最简单的是使用SHOW FULL PROCESSLIST命令,在一个查询的生命周期中,状态会变化很多次

sleep:线程正在等待客户端发送新的请求

Query:线程正在执行查询或正在将结果发送给客户端

Locked:在MySQL服务器层,该线程正在等待表锁。在存储引擎实现的锁,例如InnoDB的行锁,并不会体现在线程状态中

Analyzing and statistics:线程正在收集存储引擎的统计信息,并生成查询的执行计划

Copying to tmp table [on disk]:线程正在执行查询,并且将其结果集都复制到一个临时表中,这种状态一般要么是再做GROUP BY操作,要么是文件排序操作,或是UNION操作。如果这个状态后有“on disk”标记,表示MySQL正在将一个内存临时表放到磁盘上

Sorting result:线程正在对结果集进行排序

Sending data:表示多种情况:线程可能在多个状态之间发送数据,或在生成结果集,或在向客户端返回数据

查询缓存

如果当前的查询恰好命中了查询缓存,在返回查询结果之前MySQl会检查一次用户权限,无须解析查询SQL语句,因为在查询缓存中已存放了当前查询需要访问的表信息

查询优化处理

查询的生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互,包括多个子阶段:解析SQL、预处理、优化SQL执行计划

语法解析器和预处理

首先,MySQL通过关键字将SQL语句进行解析,并声称一棵对应的“解析树”。MySQL解析器将使用MySQL语句规则验证和解析查询,预处理器则根据一些MySQL规则进一步检查解析树是否合法

查询优化器

优化器的作用是找到最好 的执行计划

可以通过查询当前会话的Last_query_cost的值来得知MySQL计算的当前查询的成本

这个结果表示MySQL的优化器认为大概需要做1040个数据页的随机查找才能完成上面的查询

有以下原因会导致MySQL优化器选择错误的执行计划:

1、统计信息不准确,MySQL依赖存储引擎提供的统计信息来评估成本,但有的存储引擎提供的信息是准确的,有的偏差可能非常大。例如,InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息

2、执行计划中的成本估算不等同于实际执行的成本

3、MySQL从不考虑其他并发执行的查询,可能会影响到当前查询的速度

4、MySQL并不是任何时候都基于成本的优化。有时也会基于一些固定的规则,例如,如果保存再全文搜索的MATCH()子句,则在存在全文索引的时候就是用全文索引

5、MySQL不会考虑不受其控制的操作的成本,例如执行存储过程或用户自定义函数的成本

6、优化器有时无法去估算所有可能的执行计划,所以它可能错过实际上最优的执行计划

优化策略可以简单地分为两种:静态优化和动态优化

静态优化可以直接对解析树进行分析,并完成优化。例如,优化器可以通过一些简单的代数变换将WHERE条件转换成另一种等价形式。静态优化在第一次完成后就一直有效,即使使用不同的参数重复执行也不会发生变化

动态优化则和查询的上下文有关,也可能和很多其他因素有关,例如,WHERE条件中的取值、索引中条目对应的数据行数等

数据和索引的统计信息

因为服务器层没有任何统计信息。MySQl查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息,包括:每个表或索引有多少个页面,每个表的每个索引的基数是多少、数据行和索引长度、索引的分布信息等

MySQl如何执行关联查询

MySQL先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止,然后根据各个表匹配的行,返回查询中需要的各个列

假设MySQL按查询中表顺序进行关联操作,可以用下面的伪代码表示MySQL将如何完成

执行计划:

MySQL生成查询的一棵指令数,然后通过存储引擎执行完成这棵指令树并返回结果,最终的执行计划包含了重构查询的全部信息。如果对某个查询执行EXPLAIN EXTENDED后,再执行SHOW WARNINGS,可以看到重构出的查询

任何多表查询都可以使用一棵树表示,可以按如图执行一个四表的关联操作

关联查询优化器

决定了多个表关联时的顺序。通常多表关联的时候,可以有多种不同的关联顺序获得相同的执行结果

下面的查询通过不同顺序的关联最后都获得相同的结果

排序优化

当不能使用索引生成排序结果的时候,MySQL需要自己进行排序,如果数量小,则在内存中进行,如果数据大,则需要使用磁盘,MySQL将这个过程统一称为文件排序

单次传输排序

先读取查询所需要的所有列,然后根据给定列进行排序,最后直接返回排序结果。优点是只需要依次顺序I/O读取所有的数据,无须任何的随机I/O;缺点是如果需要返回的列非常多、非常大,会额外占用大量的空间,这些列对排序操作本身是没有任何作用的

查询执行引擎

MySQL的查询执行引擎则根据这个执行计划来完成整个查询,执行计划是一个数据结构

MySQL只是简单地根据执行计划给出的指令逐步执行,在根据执行计划逐步执行的过程中,有大量的操作需要调用需要调用存储引擎实现的接口来完成

返回结果给客户端

即使查询不需要返回结果集给客户端,MySQL仍会返回这个查询的一些信息,如该查询影响到的行数。如果查询可以被缓存,MySQL在这个阶段也会将结果存放到查询缓存中

MySQL查询优化器的局限性

关联子查询

例如:希望找到sakila数据库中,演员Penelope Guiness (actor_id为11)参数过的所有影片信息,按如下方式查询实现:

MySQL会将相关的外层表压到子查询中,认为这样可以更高效率地查找到数据行,如下图

子查询根据film_id来关联外部表film,因为需要film_id字段,所以MySQL认为无法先执行这个子查询,通过EXPLAIN可以看到子查询是一个相关子查询

根据EXPLAIN的输出可以看到,MySQL先选择对file表进行全表扫描,然后根据返回的film_id逐个执行子查询

如何使用好关联子查询

例:

一般会建议使用在外连接重写该查询

例如:

执行计划基本上一样,如下是一些微小的区别:

1、表film_actor的访问类型是DEPENDENT SUBQUERY,另一个是SIMPLE

2、对film表,第二个查询的Extra中没有Using where

3、在第二个表film_actor的执行计划的Extra列有Not exist

UNION的限制

若希望UNION的各个子句能根据LIMIT只取部分结果集,或希望能够先排好序再合并结果集的话,需要在UNION的各个子句中分别使用这些子句。例如,将两个子查询结果联合起来,然后再取前20条记录,MySQL会将两个表都存放到同一个临时表中,然后再取出前20行记录

索引合并优化

当WHERE子句中包含多个复杂条件时,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行

等值传递

例:有一个非常大的IN()列表,而MySQL优化器发现存在WHERE、ON或USING的子句,将这个列表的值和另一个表的某个列相关列,优化器会将IN()列表都复制应用到关联的各个表中

并行执行

MySQL无法利用多核特性来并行执行查询,很多其他的关系型数据库能够提供这一特性,MySQL不能

Hash关联

MySQL所有的关联都是嵌套循环,可以通过建立一个Hash索引来曲线地实现Hash关系

松散索引扫描

MySQL的索引扫描需要先定义一个起点和终点,即使需要的数据只是这段索引中很少数的记几个,MySQL仍需扫描这段索引中的每一个条目

最大值和最小值优化

对于MIN()和MAX()查询,MySQL的优化并不好,例如:

因为在first_name字段上并没有索引,所有MySQL将会进行一次全表扫描。如果MySQL能进行主键扫描,当MySQL读到第一次满足条件的记录时,就是需要的最小值,因为主键是严格按照actor_id字段的大小顺序排列的

在同一个表上查询和更新

MySQL不允许对同一张表同时查询和更新,如下是一个无法运行的SQL

可以通过使用生成表的形式绕过上面的限制,因为MySQL只会把这个表当作一个临时表来处理,实际上,这执行了两个查询:一个是子查询中的SELECT语句,另一个是多表关联UPDATE,只是关联的表是一个临时表,子查询会在UPDATE语句打开表之前完成

下面的查询将会正常执行

优化特定类型的查询

优化COUNT()查询

COUNT()是一个特殊的函数,两个作用:1、可以统计某个列值的数量,也可以统计行数。在统计列值时要求列值是非空的(不统计NULL)如果在COUNT()的括号中制定了列或列的表达式,则统计的是这个表达式有值的结果树

2、统计结果集的行数,当MySQL确认括号内的表达式不可能为空时,实际上在统计行数

简单的优化

可以使用MyISAM在COUNT(*)全表非常快的特性,来加速一些特定条件的COUNT()的查询,如下,使用标准数据库world来快速查找所有ID大于5的城市

mysql> SELECT COUNT(*) FROM world.City WHERE ID > 5;

通过SHOW STATUS的结果可以看到该查询需要扫描4097行数据。如果将条件反转,先查找ID小于5的城市,然后用总城市数一减就能得到同样的结果,可以将扫描的行数减少到5行以内

mysql> SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*)

         -> FROM world.City WHERE ID<=5;

这一可以大大减少需要扫描的行数,因为在查询优化阶段会将其中的子查询直接当作一个常熟来处理,通过EXPLAIN验证

使用近似值

有时某些业务场景并不要求完全精确的COUNT值,可以用近似值来代替。EXPLAIN出的优化器估算的行数是一个近似值,执行EXPLAIN不需要真正地去执行查询,成本较低

优化关联查询

1、确保ON或USING子句中的列上有索引,在创建索引时要考虑到关联的顺序。当表A和表B用列C关联时,如果优化器的关联顺序是B、A,那就不需要在B表的对应列上建立索引

2、确保任何的GROUP BY  和ORDER BY中的表达式只涉及到一个表中的列,MySQL才有可能使用索引来优化这个过程

3、升级SQL时需要注意:关联语法、运算符优先级等

优化子查询

建议尽可能使用关联查询代替

优化GROUP BY 和DISTINCT

这两种类型都可以使用索引来优化。在MySQL中,当无法使用索引时,GROUP BY使用两种策略来完成:使用临时表或文件排序来做分组

例如

使用actor.actor_id列分组的效率比使用film_actor.actor_id更好

优化GROUP BY WITH ROLLUP

通过EXPLAIN来观察其执行计划,注意分组是否通过文件排序或临时表实现,然后再去掉WITH ROLLUP子句看执行计划是否相同

优化LIMIT分页

尽可能地使用索引覆盖扫描,而不是查询所有的列,然后根据需要做一次关联操作再返回所需的列。对于偏移量很大时,效率提升很大,如下查询

mysql> SELECT film_id ,description FROM sakila-film ORDER BY title LIMIT 50,5;

若这个表非常大,查询最好改成下面这样子

这里的“延迟关联”将大大提升查询效率,让MySQL扫描尽可能少的页面,获取需要访问的记录后再根据关联列回到原表查询需要的所有列

优化UNION查询

除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,会导致对整个临时表的数据做唯一性检查,代价高昂,即使有ALL关键字,MySQL仍会使用临时表存储结果

静态查询分析

Percona Toolkit 中的pt_query_advisor能够解析查询日志、分析查询模式,然后给出所有可能存在潜在问题的查询,并给出足够详细的建议

使用用户自定义变量

一个用来存储内容的临时容,在连接MySQL的整个过程都存在。可以使用下面的SET和SELECT语句定义

然后可以在任何可以使用表达式的地方使用这些自定义变量:

mysql> SELECT  _.WHERE col <= @last_week;

以下场景不能使用自动逸变量

1、使用自定义变量的查询,无法使用查询缓存

2、不能在使用常量或标识符的地方使用自定义变量,例如表名、列名和LIMIT子句中

3、用户自定义变量的生命期在一个连接中有效,所以不能用其做连接间的通信

4、如果使用连接池或持久化连接,自定义变量可能让看起来毫无关系的代码发生交互

5、在5.0之前的版本,是大小写敏感的,所以要注意代码在不同MySQL版本间的兼容问题

6、不能显式地声明自定义变量的类型

优化排名语句

可以给一个变量赋值的同时使用这个变量,例如

避免重复查询刚刚更新的数据

例如:一个客户希望能够更高效的更新一条记录的时间戳,同时希望查询当前记录中存放的时间戳是什么,可以用以下代码实现

使用变量,可以按如下方式重写查询

统计更新和插入数量

当每次由于冲突导致更新时对变量@X自增一次,然后通过对表达式乘以0来让其不影响要更新的内容

确定取值的顺序

如下查询

因为WHERE和SELECT是在查询执行的不同阶段被执行的,如果在查询中再加入ORDER BY的话,结果可能会不同

OEDER BY引入了文件排序,WHERE条件是在文件排序操作之前取值的,所以这条查询会返回表中的全部记录。解决问题的办法是让变量的赋值和取值发生执行查询的同一阶段

用户自定义变量的其他用处

1、查询运行时计算总数和平均值

2、模拟GROUP语句中的函数FIRST()和LAST()

3、对大量数据做一些数据计算

4、计算一个大表的MD5散列值

5、编写一个样本处理的函数,当样本中数值超过某个边界值时将其变成0

6、模拟读/写游标

7、在SHOW语句的WHERE子句中加入变量值

;