utf8字符集表示一个字符需要14字节,但通常常用的字符是13字节且一个字符所用的最大字节长度可能会影响系统的存储和性能。MySQL设计者定义了两个概念:
MySQL中,utf8mb3是utf8的别名。
上图中,可以看出MySQL中支持41中字符集,其中的default_collation列表示这种字符集中一种默认的比较规则。
后缀表示该比较规则是否区分语言中的重音、大小写。具体如下表:
后缀 | 英文释义 | 描述 |
---|---|---|
_ai | accent insensitive | 不区分重音 |
_as | accent sensitive | 区分重音 |
_ci | case insensitive | 不区分大小写 |
_cs | case sensitive | 区分大小写 |
_bin | binary | 以二进制方式比较 |
字符集名称 | Maxlen |
---|---|
ascii | 1 |
latin1 | 1 |
gbk2312 | 2 |
gbk | 2 |
utf8(别称utf8mb3) | 3 |
utf8mb4 | 4 |
/etc/my.cnf
中的这两个系统变量的值;# 可以通过修改配置文件/etc/my.cnf
[server]
character_set_server=gbk # 默认字符集
collation_server=gbk_chinese_ci # 对应的字符集的比较规则
# 查看服务器的字符集和比较规则
show variables like '*_server';
# 创建库时,指定字符集和比较规则
create database 库名 [[default]character set 字符集名称][collate 比较规则];
# 修改库的字符集和比较规则
alter database 库名 [[default]character set 字符集名称][collate 比较规则];
# 比如character set gbk2312 collate gbk2312_chinese_ci
# 查看数据库的字符集和比较规则
show variables like '*_database';
# 查看具体的数据库的字符集和比较规则
show create database 库名;
# 创建表时,指定字符集和比较规则
create table 表名(
列信息
)[[default] character set 字符集名称][collate 比较规则];
# 修改表的字符集和比较规则
create table 表名 [[default] character set 字符集名称][collate 比较规则];
# 比如character set utf8 collate utf8_general_ci
# 查看具体的表的字符集和比较规则
show create table 表名;
# 创建表时,指定字符集和比较规则
create table 表名(
列名 类型 [character set 字符集名称][collate 比较规则]
);
# 修改表的字符集和比较规则
alter table 表名 modify 列名 类型 [character set 字符集名称][collate 比较规则];
# 比如character set utf8 collate utf8_general_ci
注意:
show variables like '%character%'
语句:set names 字符集名;
# 等价于下列三条:
set character_set_client = 字符集名;
set character_set_connection = 字符集名;
set character_set_results = 字符集名;
# 或者直接在配置文件/etc/my.cnf中设置,即在启动客户端时就将三个系统变量设置为一样的
[client]
default-character-set=utf8 # 启动选项
/var/lib/mysql/
MySQL服务器程序在启动时会到文件系统的某个目录下加载一些文件,之后在运行过程中产生的数据也会存储在该目录下的某个文件中,该目录称为数据目录datadir=/var/lib/mysql/
。
相关命令目录:/usr/bin
和 /usr/sbin
。bin目录中存放着许多关于控制客户端程序和服务端程序的命令,即许多可执行文件。
配置文件目录:/usr/share/mysql
(命令及配置文件) 和 /etc/my.cnf
。
像InnoDB、MyISAM这样的存储引擎,将表存储在磁盘上的,而操作系统则通过文件系统来管理磁盘结构。
msql
:MySQL系统自带的核心数据库,存储MySQL的用户账户和权限信息,一些存储过程、事件的定义信息,一些运行过程中产生的日志信息,一些帮助信息以及时区信息等。information_schema
:保存着MySQL服务器维护的所有其他数据库的信息,比如哪些表、视图、触发器、列、索引等。这些信息只是一些描述性的信息,有时称为元数据。其中提供的一些以innodb_sys
开头的表,用于表示内部系统表。performance_schema
:主要用来表示MySQL运行过程中的一些状态信息,用来监控MySQL服务的各类性能指标,包括统计最近执行了哪些语句、执行过程中每个阶段花费了多长时间、内存使用情况等信息。sys
:该数据库,主要用来将information_schema
和performance_schema
结合起来,帮助系统管理员和开发人员监控MySQL的技术性能。MySQL8.0
之后,将不再有***.opt
文件,***.frm
文件会被整合到***.ibd
文件中。
1、数据是以记录的形式插入表中的,每个表含有的信息包括:
***.frm
文件:有该表的名称、列(多少、类型)、约束条件和索引、使用的字符集和比较规则等各种信息;***.ibd
文件;MySQL8.0
之后,***.frm
会被整合到***.ibd
文件中。2、表中的数据和索引:
16KB
。3、为了更好的管理这些页,InnoDB提出了一个表空间或者文件空间的概念。表空间是一个抽象的概念,可以对应文件系统中的一个/多个真实文件(不同表空间对应的文件数可能不同)。每个表空间可以被划分为很多页。表数据就存放在某个表空间下的某些页里。
表空间的类型:
system tablespace
:默认情况下,InnoDB会在数据目录datadir=/var/lib/mysql
下,创建一个名为ibdata1
的大小为12M(该文件是子扩展文件,当大小不够用时,自动扩展文件大小)的文件,即系统表空间在文件系统上的表示。# 如果想让系统表空间在文件系统中对应多个实际文件,则需要修改配置文件my.cnf
[server]
innodb_data_file_path=data1:512M;data2:128M;autoextend
# 这样MySQL启动后会创建两个大小为512M的data1和data2文件;autoextend表示这两个文件不够用时,系统会自动扩展data2文件的大小
注意:一个MySQL服务器中,系统表空间只有一份。mysql5.5~5.6
的各个版本中,表中的数据都会被默认存储在这个系统表空间。
file-per-table tablespace
:mysql5.6.6
之后,InnoDB并不会默认将各个表的数据存储在系统表空间中,而会为每个表建一个独立表空间,其在文件系统中对应的实际文件是***.ibd
。# 如果想指定村使用系统表空间还是独立表空间,可以修改配置文件/etc/my.cnf
[server]
innodb_file_per_table=0
# 0:表示使用系统表空间、1:表示使用独立表空间
# 修改后,只能对新建的表有效
general tablespace
temporary tablespace
MyISAM存储引擎中,数据和索引分别保存在.MYD
和.MYI
文件中,表结构存放在.frm
文件中。
MySQL8.0
之后,***.frm、***.MYD、***.MYI
文件被整合到了.sdi
文件中。
视图本质是虚拟表,故数据目录下的数据库文件中只会含有其结构文件即***.frm
,并不会有其数据文件。
数据目录datadir=/var/lib/mysql
下,还包含了为了更好运行程序的一些额外的文件:
SSL
和RSA
正数和密钥文件:主要是为了客户端与服务器安全通信而创建的一些文件。MySQL用户可以分为普通用户(只拥有被授予的各种权限)和root用户(超级管理员,拥有所有权限,包括创建用户、删除用户、修改用户的密码等管理权限)。
MySQL提供许多语句用来管理用户账号,包括登录、退出MySQL服务器、创建用户、删除用户、密码管理和权限管理等内容。
MySQL数据库的安全性,需要通过账户管理来保证。
MySQL数据库中,推荐使用create user
语句创建新用户,使用此语句时,必须拥有creat user
的权限,每添加一个用户,MySQL.user
表中会添加一条新纪录,但新创建的用户没有任何权限。
create user 用户名 [identified by '密码']; # 用户名:host,user,默认host是%
# 添加的用户会体现在MySQL.user表中,表中的host和user字段是联合主键
#,即host和user任一个不同,即可添加成功
update mysql.user set user='new_name' where user='old_name';
flush privileges;
drop user 'user_name'@'host_name';
# 使用drop命令会删除用户以及对应的权限,执行命令后mysql.user和mysql.db表中的相应的数据都会删除
# 不推荐使用,删除后会有部分记录保留
delete from mysql.user where host='host_name' and user='user_name';
flush privileges;
适用于root用户修改自己的密码,以及普通用户登陆后修改密码。
alter user user() identified by 'new_password';
set password = 'new_password';
alter user 'user_name'@'host_name' identified by 'new_password';
set password for 'user_name'@'host_name' = 'new_password';
show privileges;
// 可以用来查看数据库的权限
权限分布 | 可能设置的权限 |
---|---|
表权限 | select、insert、update、delete、create、drop、grant、references、index、alter |
列权限 | select、insert、update、references |
过程权限 | execute、alter routine、grant |
1、通过把角色授予用户给用户权限
2、直接给用户权限
grant 权限1,...,权限n on 数据库名称.表名称 to 用户名@用户地址 [identified by '密码'];
// 只有root用户才能给其他用户赋予不同的权限:(因其含有"with grant option")
// 给该用户的所有数据库和所有表赋予全部权限,但“不包括grant权限”
grant all privileges on *.* to 用户名@用户地址 [identified by '密码'];
注意:如果需要赋予包括grant权限,则需要添加参数"with grant option"这个选项即可,表示该用户可以将自己拥有的权限授予别人。grant重复给用户添加权限,“权限会叠加”,如先后分别赋予insert和select权限则最后该用户就同时拥有select和insert权限。
show grants; // 查看当前用户的权限
show grants for 'user'@'主机地址'; // 查看某个用户的权限
收回用户不必要的权限,可以在一定程度上保证系统的安全性。
revoke 权限1,...,权限n on 数据库名称.表名称 from 用户名@用户地址;
// 收回所有权限:
revoke all privileges on *.* from 用户名@用户地址;
使用revoke收回权限之后,用户账户的记录将从db、host、tables_priv、columns_priv表中删除,但用户账户记录仍然保存在user表中,删除user表中的账户记录需要使用drop user语句
。
注:在将用户账户从user表删除之前,应该收回相应用户的所有权限。
建议尽量使用数据库自己的角色和用户机制(即使用具体的用户名来登录和访问)来控制访问权限,不要轻易使用root账号(权限太大)。
MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库中,如下图:
,MySQL数据库会根据这些权限表中的内容来给每个用户赋予权限相应的权限。
权限表(mysql数据库):user表、db表、table_priv表、columns_priv表、proc_priv表等,最重要的是前两个表。在MySQL启动时,服务器将这些数据库表中的权限信息的内容读入内存中。
MySQL的访问控制阶段分为,连接核实阶段、请求核实阶段。
**连接核实阶段:**客户端在连接请求时提供的用户名、主机地址、密码,MySQL服务器接收到用户请求后,会使用user表中的host、user、authentication_string这三个字段匹配客户端提供的信息。
请求核实阶段:
角色是MySQL8.0引入的新功能,是权限的集合,并且可以为角色添加和移除权限。用户可以被授予角色,也可以被授予角色包含的权限。引入角色的目的是,方便管理拥有相同权限的用户。
对角色的操作需要较高的权限,并像用户账户一样,角色可以拥有授予和撤销的权限。
create role 'role_name'@'hostname'; # 创建角色
drop role 'role_name'@'hostname'; # 删除角色
grant privileges_ on db.table_name to 'role_name'@'hostname'; # 赋予角色权限
grant all privileges on *.* to 'boss'@'%'; # 赋予所有权限给boss角色
revoke privileges_ on db.table_name from 'role_name'@'hostname'; # 回收角色权限
show grants for 'role_name'@'hostname'; # 看看角色的权限
角色创建并授权后,要赋给用户并处于激活状态才能发挥作用。
grant role to 'role_name'@'hostname'; # 给用户赋予角色
# 使用'role_name'@'hostname'用户登录,并查询当前的角色
select current_role(); # 如果显示为none,则表示角色未激活
# MySQL中创建角色后,默认是没有激活的状态,即不能使用,必须手动激活且重新登陆后,该用户才能拥有角色对应的权限
# 激活角色
set default role all to 'user_name'@'hostname'; # 方式一
# 方式二:将active_all_roles_on_login设置为on
show variables like 'active_all_roles_on_login'; # 查看该全局变量是on/off
set global active_all_roles_on_login=on; # 即对所有角色永久激活
# 撤销用户的角色
revoke role from 'user_name'@'hostname';
MySQL是典型的C/S架构,即Client/Server架构,服务器端程序使用mysqld。
MySQL server结构,可分为三层:连接层、服务层、引擎层。
无论客户端进程和服务端进程如何通信,最后实现的效果是:客户端进程向服务器进程发送一段文本(SQL语句),服务器进程处理后再向客户端进程发送一段文本(处理结果)。
具体设计的组件:
MySQL首先是一个网络程序,即需要在TCP之上定义自己的应用层协议。 编写代码与MySQL server建立TCP连接,之后按照其定义好的协议进行交互。
通过SDK来访问MySQL,本质上就是在TCP连接上通过MySQL协议跟MySQL进行交互。
系统(客户端)访问MySQL服务器前,第一件事就是建立TCP连接。
经过三次握手建立连接成功后,MySQL服务器会对TCP传输过来的账号、密码进行身份验证。一旦用户名、密码验证通过,会从权限表中查出账号拥有的权限与连接关联,之后的权限判断逻辑都将依赖于此时读到的权限。
MySQL可以与多个系统建立连接,且每个系统建立的连接不止一个。为解决TCP无限创建与频繁创建销毁带来的资源损耗、性能下降等问题。MySQL服务器中专门的TCP连接池限制连接数,采用长连接模式复用TCP连接。
TCP连接收到请求后,必须要分配给一个线程专门与这个客户端交互。每个连接都只会从线程池中获取线程,从而省去了创建和销毁线程的开销。
服务层提供核心的功能,如SQL接口、缓存查询、SQL分析和优化、内置函数的执行等。所有跨存储引擎的功能也在这一层实现,如过程、函数等。
该层会解析查询并建立相应的内部解析树,并完成相应的优化,如确定查询表的顺序、是否利用索引、最后生成一个执行计划。执行计划,表明应该使用哪些索引进行查询(全表检索、索引检索),表之间的连接顺序如何,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户。
使用 “选取-投影-连接” 策略进行查询:
select id,name from student where gender = '女';
# 通过where语句选取,之后通过属性投影,最终将两个查询条件连接起来生成最终查询结果
MySQL 内部维持了一些 Cache 和 Buffer,如查询缓存 Query Cache 用来缓存一条 select 语句的执行结果,如果能在其中找到查询结果则不用再执行分析、优化、执行等操作,直接将结果反馈给客户端。这种查询机制,是通过一系列小缓存组成的,如表缓存、记录缓存、key缓存、权限缓存等,还可以在不同客户端之间共享。如果缓存空间足够大,这样在大量读操作的环境中能够很好的提升系统的性能。但这种查询缓存的命中率很低。
插件式的存储引擎 Storage Engines,真正负责了MySQL中数据的存储和提取,对物理服务器级别维护的底层数据执行操作。
服务器通过API与存储引擎进行通信,不同的存储引擎具有的功能不同。
所有数据、数据表、数据库的定义,表的每一行内容、索引等都存储在文件系统上,以文件的形式存在MySQL的数据目录中,并完成于存储引擎的交互。
查询缓存:之前执行过的语句及其结果可能会以key-value对的形式,被直接缓存在内存中。Server如果在查询缓存中,发现了这条SQL语句,就会直接将结果返回给客户端;没有则进入解析器阶段。
需要说明,查询缓存命中率低,MySQL 8.0以弃用该功能。
① 只有相同的查询操作才能命中查询缓存,MySQL中查询缓存命中率不高。
② 如果查询请求中含有某些系统函数、用户自定义变量和函数、一些系统表(如mysql、information_schema、performance_schema中的表),那么该请求就不会被缓存。
③ 缓存失效:当MySQL的缓存系统检测出到涉及到的表的结构或者数据被修改时,如果缓存中对该表使用了 insert、delete、update、truncate table、alter table、drop table、drop database 等语句,则该表的所有高速缓存都会被删除。
一般情况下,建议在静态表(即极少更新的表)中使用查询缓存。MySQL提供了这种 “按需使用” 的方式,可以将my.cnf中参数query_cache_type=demand
,代表当SQL语句中有 SQL_CACHE关键词时才缓存。
# query_cache_type有三个值,0:关闭查询缓存、1:表示开启、2:表示demand
query_cache_type=2;
show global variables like "%query_cache_type%";
# 将查询和查询结果,以键值对key-value的形式,存放在查询缓存中
select SQL_CACHE * from student where ID=5;
select SQL_no_CACHE * from student where ID=5;
# 监控查询缓存的命中率
show status like "%Qcache%";
词法分析过程:
优化器:确定SQL语句的执行路径,比如是全表检索,还是索引检索。优化器的作用,就是找到其中最好的执行计划。
比如,表中有多个索引时,决定使用哪个索引;一个与语句有多表关联join时,决定各个表的连接顺序;表达式简化、子查询转为连接、外连接转为内连接等。
查询优化中,可分为逻辑查询优化和物理查询优化。
执行器:产生的执行计划后,会进入执行器阶段。
执行前需要判断用户是否有权限:
总结:SQL语句在MySQL中的执行流程就是:SQL语句->查询缓存->解析器->优化器->执行器。
mysql5.7中,相同两条语句的执行过程分析:
存储引擎之前被称为表处理器,后改名为存储引擎,功能:接受上层传下来的指令后对表中的数据进行提取或写入操作。存储引擎就是指表的类型。
# 查看默认的存储引擎
select @@default_storage_engine;
# 查看表使用的存储引擎
show create tables tableName;
# 修改表的存储引擎
alter table tableName engine=InnoDB;
# 直接修改存储引擎
set default_storage_engine = InnoDB;
# 修改my.cnf文件
default-storage-engine=InnoDB;
# 重启服务
systemctl restart mysqld.service
对比项 | InnoDB | MyISAM |
---|---|---|
外键 | √ | × |
事务 | √ | × |
锁机制 | 行锁,操作时只锁某一行,适合高并发的操作 | 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 |
缓存 | 缓存索引和真实数据(表名.ibd文件中),对内存要求较高,而且内存大小对性能有决定性影响 | 只缓存索引(表名.myi文件中),不缓存数据(表名.myd文件中) |
关注点 | 事务:并发写、崩溃修复能力、更大资源 | 性能:节省资源、消耗少、简单业务 |
自带系统表使用 | × | √ |
所有数据保存在内存中(不需要进行磁盘I/O操作),响应速快,但当mysqld
守护进程崩溃时数据会丢失。但memory表的结构(.frm
表结构文件存储在磁盘中)重启后还会保留,只是数据会丢失。另外,要求存储的数据是数据长度不变的格式。
memory同时支持哈希hash索引和B+树索引。默认使用哈希hash索引,其速度要比使用B型树索引快。
memory表至少比myISAM表快一个数量级
数据文件和索引文件是分开存储的:
每个基于memory存储引擎的表实际上对应一个磁盘文件,该文件的文件名和表名相同为frm类型,只存储表的结构。数据文件存储在内存中。
memory表的大小是受限制的。主要由max_rows和max_heap_table_size控制。max_rows是在创建表时指定,max_heap_table_size大小是默认的16MB,需要时可进行扩大。
创建CSV表时,服务器会创建一个纯文本数据文件,其名称为表名.csv
。当你将数据存储到表中时,存储引擎将其以逗号分隔的格式保存到数据文件中。
create table test_csv(i int not null, c char(10) not null) engine=csv;
# 其中,CSV存储引擎使用时,所有字段必须在创建表时加上“非空约束”
InnoDB存储引擎以页为单位来管理存储空间的,我们进行的增删改查操作本质上就是在访问页面(包括读页面、写页面、创建新页面等操作)。DBMS会申请占用内存来作为数据缓冲池,在真正访问页面之前,会将磁盘上的页缓存到内存中的buffer pool之后才可访问。这样做可以极大的减少磁盘I/O操作,从而极大地提高mysql的性能。
本质上是InnoDB向操作系统申请的一块连续内存空间。
对于InnoDB作为存储引擎的表来说,无论是用于存储用户数据的索引(聚集索引和二级索引),还是各种系统信息,都是以页的形式存放在表空间中。而表空间是InnoDB对文件系统中的一个/几个实际文件的抽象,故数据最终还是存储在磁盘上的。
InnoDB存储引擎在处理客户端请求时,当需要访问某个页的数据时,就会把完整的页全部加载到内存中(即使只访问页中的一条数据),之后对其进行读写访问,且读写访问后并不会立即释放其占用的内存空间,而是将其缓存起来,当再次访问该页面时,就减少了磁盘I/O的开销。
“位置 * 频次” 这个原则,可以帮我们对I/O访问效率进行优化。
读取数据时存在一个 “局部性原理”:当使用一些数据时,大概率还会使用其周围的一些数据,因此采用 “预读” 的机制提前加载,可减少未来可能的磁盘I/O开销。
查询缓存是提前将查询结果进行缓存,这样下次不需要执行即可直接拿到结果。
但命中条件极其苛刻,只要数据表发生变化,查询缓存就会失效。
缓冲池服务于数据库整体的I/O操作,查询缓存是提前将查询结果进行缓存,但共同点是通过缓存的机制提升效率。
缓冲池管理器,会尽量将经常使用的数据保存起来,在数据库进行页面的读操作时,首先会判断页面是否在缓冲池中,存在则直接读取,不存在则会通过内存或磁盘将页面存放到缓冲池中再进行读取。
缓冲池在数据库中的结构和作用:
如果执行sql语句时,更新了缓冲池中的数据,那么会马上同步到磁盘上吗?
当对数据库中的记录进行修改时,首先会修改缓冲池中页里面的记录信息,然后数据库会以一定的频率刷新到磁盘上(每次发生更新完后,并不会立即进行磁盘回写)。
存在的问题:当buffer pool中修改了数据,但没来得及回写到磁盘,mysql服务挂掉了,即更新的数据只存在在buffer pool
中。这就需要Redo Log & Undo Log。
缓冲池采用一种**checkpoint
的机制**,将数据回写到磁盘,这样能提升数据库的整体性能。
当缓冲池不够用时,需要释放掉不常用的页时,就可以强行采用checkpoint
的方式,将不常用的脏页回写到磁盘上,然后再从缓冲池中将这些页释放掉。
注意:这里的脏页,指缓冲池中 被修改过的页,与磁盘上的数据页不一致。
MyISAM存储引擎,只缓存索引,并不缓存数据,对应的键缓存参数为key_buffer_size
。
InnoDB存储引擎,命令如下:
# 查看缓冲池大小:
show variables like 'innodb_buffer_pool_size';
#设置缓冲池的大小:
set global innodb_buffer_pool_size=新的大小(单位B字节);
缓冲池buffer pool
本质上是InnoDB
向操作系统申请的一块连续内存空间。
多线程环境下,访问buffer pool
数据需要进行加锁处理。
在缓冲池特别大且多线程并发访问特别高时,单一的buffer pool会影响请求的处理速度,故需要将其拆分为若干个小的buffer pool
(每个buffer pool
被称为一个实例,相互之间是独立、独立的管理各种链表)。这样在多线程并发访问时,多个buffer pool
并不会相互影响,从而提高并发处理能力。
# 服务器启动时,通过设置innodb_buffer_pool_instances的值来修改buffer pool实例的个数
[server]
innodb_buffer_pool_instances=实例个数
# 查询当前mysql的buffer pool的实例数:
show variables like 'innodb_buffer_pool_instances';
注意:buffer pool的实例个数并不是越多越好,分别管理各个buffer pool实例也是需要性能开销的。建议当buffer pool大于等于1G时,设置多个buffer pool实例。
一次查询同一张表,只能使用一条索引。当存在多个索引时,选择查询数据少的索引。
索引是存储引擎用于快速找到数据记录的一种数据结构。在进行数据查找时,首先查看查询条件是否命中某条索引,命中则通过索引查找相关数据,否则则需要全表扫描,即一条条查找记录。
在没有索引的情况下,CPU必须不断从磁盘逐行的找到记录,加载到内存,再进行处理判断。该过程,涉及多次的磁盘I/O非常耗时(要考虑磁盘的旋转时间(速度较快)、磁头的寻道时间(速度快且耗时))。
给上图中的col2加了索引,即相当于在硬盘上为col2维护了一个索引的数据结构,即二叉搜索树,其每个结点存储的是(key,Value)键值对(key是col2,Value是Key所在行的文件指针(即地址))。通过索引能快速的定位到记录的地址,即提高查询速度。
建索引的目的:减少磁盘I/O的次数,加快查询效率。
官方的定义:索引Index是帮助MySQL高效获取数据的数据结构。
索引的本质是数据结构。可以简单看作 “排好序的快速查找数据结构” ,这些数据结构以某种方式指向数据,这样就可以在这些数据结构的基础上实现高级查找算法。
索引在存储引擎中实现,每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。存储引擎可以定义每个表的最大索引数和最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256字节。
优点:
缺点:
提示:索引可以提高查询速度,但会影响增/删/改的速度(这种情况下,最好的办法:删除索引 --> 更新表 --> 插入索引)。
InnoDB的索引按照物理实现方式,分为:聚簇索引、非聚簇索引(又称二级索引或辅助索引)。
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式(所有用户记录都存储在叶子节点),也就是索引即数据,数据即索引。
注:这里 “聚簇” 表示数据行和相邻的键值聚簇的存储在一起。
特点:
使用记录主键值的大小进行记录和页的排序,即:
1)页内记录按照主键的大小组成单链表;
2)不同的用户记录页,按照主键的大小组成双向链表;
3)存放目录项记录的页,在同一层中按照目录页中主键的大小组成双向链表。
B+树的叶子节点存储的是完整的用户记录(即每条记录中,存储了所有列的值)。
注:聚簇索引并不需要我们在MySQL语句中显式的使用INDEX语句创建,InnoDB存储引擎会自动为我们创建聚簇索引。
优点:
缺点:
限制:
聚簇索引只能在搜索条件是主键值时,才能发挥作用,且B+树中的数据都是按照主键进行排序的。
如果要以别的列作为搜索条件怎么办?可多建几棵B+树,不同的B+树中的数据采用不同的排序规则。
上图的二级索引与之前介绍的聚簇索引的不同点:
根据以c2列大小排序的B+树只能确定需要查找的记录的主键值,如果需要查找完整的记录,仍然需要回到聚簇索引中再查一遍,该过程称为回表。总的来说,根据c2列的值查询一条完整的用户记录,需要使用2棵B+树。
直接把完整地用户记录放在以c2列构建的B+树的叶子节点中是否可行?不行,这样每建立一棵B+树就需要存储完整地用户记录,太浪费空间了。
这种按照非主键列建立的B+树,需要一次回表操作才能定位完整地用户记录,这种B+树称为二级索引/辅助索引/非聚簇索引。
非聚簇索引的存在并不影响数据在聚簇索引中的组织,故一张表可有多个非聚集索引。
由于非聚簇索引的节点只包含了 “key+主键”,即包含的信息更少,故每页能容纳的信息就越多,树的高度会极大地降低,进而查找时有更少的磁盘I/O。
索引下推:
索引下推是MySQL 5.6及之后版本推出的,用来优化查询。
索引下推,是将本应在server层进行筛选的条件,下推到存储引擎层进行筛选判断,这样能有效减少回表的次数。
select * from t where c1 > 12 and c2 = 2;
# 如果没有索引下推,则只会走c1的索引,筛选出数据(满足c1 > 12条件的数据),之后在server层对c2字段进行过滤。
# 有了索引下推之后,会将 c1>12和C2=2 全部下推到存储引擎,进行数据筛选,极大的减少了回表的次数。
聚簇索引和非聚簇索引的原理不同,在使用上也有区别:
本质上将联合索引,是一种特殊的非聚簇索引。
以c2、c3列的大小为排序规则而建立的B+树称为联合索引。
根页面位置一直不动
非叶子节点(内节点)中目录项记录的唯一性
如果当多条记录的某个字段不唯一,导致目录项不唯一,则需要用索引列和主键列来构建B+树,保证目录项的唯一性。
一个页面最少存储2条记录
InnoDB和MyISAM默认的索引是BTree索引,而Memory默认的索引是Hash索引。
MyISAM引擎使用B+Tree作为索引结构,叶子节点的data域存放的是数据记录的地址,即索引和数据是分开存储的。
MyISAM索引的原理:
将表中的数据按照记录的插入顺序单独存储在一个文件中且不用划分为若干的数据页,称之为数据文件。
(由于插入时,并没有刻意按照主键大小排序,所以并不能用二分法查找。)
使用MyISAM存储引擎的表中,将索引信息单独存储在索引文件中,且索引的叶子节点中存储的是主键值+数据记录的地址的组合。
MyISAM的索引方式都是 “非聚簇” 的,与InnoDB包含一个聚簇索引是不同的。
InnoDB存储引擎中,需要根据主键值对聚簇索引进行一次查找就能找到对应的完整用户记录。
MyISAM中,则需要进行一次回表操作,故MyISAM中建立的索引相当于全部是二级索引。
InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是分离的(索引文件仅保存数据记录的地址)。
InnoDB的非聚簇索引data域存储相应记录主键的值(InnoDB的所有非聚簇索引都引用主键作为data域),而MyISAM索引记录的是数据的地址。
InnoDB要求表必须有主键,但MyISAM没有要求。
InnoDB如果没有显示指定主键,则mysql系统会选择一个非空且唯一的数据记录列作为主键,也没有则生成一个隐含字段作为主键(类型为长整型,长度为6字节)。
为什么MyISAM比InnoDB快?
MyISAM的回表操作十分迅速,地址偏移量直接到文件中取数据。
InnoDB的回表,通过获取的主键再去聚簇索引中找到记录,虽说也不慢,但比不上直接用地址去访问。
MyISAM只缓存了索引块,减少了缓存换入换出的频率。
InnoDB还需要维护MVCC一致,而MyISAM表锁牺牲了写性能,提高了读性能。
空间上的代价:
每建立一个索引都要为它建立一颗B+树,其中树的每个节点都是一个数据页(默认大小为16KB
),故一颗B+树由许多数据页组成,需要占用很大的存储空间。
时间上的代价:
每次对表中的数据进行增/删/改操作时,都需要去修改各个B+树的索引。
增/删/改操作,可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收等操作来维护好节点和记录的排序。如果索引太多,维护操作也就会相应的增多,导致性能下降。
总结:一个表上索引建的索引越多,占用存储空间就越多,在增/删/改操作时性能就越差,故并非索引越多越好。
查找都是索引操作,一般来说索引非常大(尤其关系型数据库),可能几个G甚至更多。为了减少索引在内存中的占用,数据库索引是存储在外部磁盘上的,当利用索引查询时不可能把整个索引全部加载到内存,只能逐一加载,故mysql衡量查询效率的标准就是磁盘I/O次数。
注意:磁盘I/O操作次数对索引的使用效率至关重要。
Hash本身是一个函数,又称散列函数,可帮助我们大幅提升检索数据的效率。
注意:哈希函数可能将两个不同的关键字映射到相同的位置,称为碰撞。数据库中,一般采用链表法解决,即将散列到同一槽位的元素放在一个链表中。
hash索引,仅能满足=
和in
的查询的O(1)
级别,范围查询会退化为O(n)
级别。
树型结构,“有序” 特性,保证了查询效率稳定在O(log2N)
。
hash索引,数据存储是无序的,order by
时还需对数据重新排序。
联合索引中,hash值是将联合索引键合并后一起计算的,故无法单独对某一个键或几个索引进行查询。
“等值查询”中,通常hash索引的效率更高,但对索引列的重复值来太多,效率会降低(hash冲突时,一个桶中要用行指针进行比较,找到查询的关键字,非常耗时)。hash索引通常不会用在重复值太多的列上。
hash索引在键值类型(key-value)数据库中,redis存储的核心是hash表。
mysql中的memory存储引擎支持hash索引,如需查询临时表时,可选择memory存储引擎,将某个字段设置为hash索引。当**“字段的重复率低”且频繁进行“等值查询”时,hash索引**是不错的选择。
InnoDB本身不支持hash索引,但提供自适应hash索引(Adaptive Hash Index
)。如果InnoDB存储引擎监测到同样的二级索引不断被使用,则会在内存上根据二级索引数上的索引值创建一个哈希索引(将该数据页的地址存放在hash表中),下次查询时可直接找到该页面所在的位置。这样B+树也具备了Hash索引的优点。
采用自适应hash索引,可方便根据sql的查询条件加速定位叶子节点,特别是当B+树比较深时,通过自适应hash索引可明显提高数据的检索效率。在高并发场景下,自适应hash索引的不同的分区(共8个分区)持有不同的锁,但同一个分区只有一把锁,意味着如果一个RW_latch
的等待线程数过多,则会导致性能的下降。
# 默认情况下,innodb_adaptive_hash_index是开启状态
show variables like '%innodb_adaptive_hash_index%';
# 查看mysql的自适应哈希索引中桶的个数
show variables like 'innodb_adaptive_hash_index_parts';
show engine innodb status
# 能看到两个较为重要的信息:
# 1. RW-latch等待的线程数量(自适应hash索引的不同的分区(共8个分区)持有不同的锁),即同一个分区等待的线程数量;
# 2. 走“自适应hash索引”和“二级索引树”的搜索频率
存在的问题:可能会让树型结构退化为链表,查询的时间复杂度会从O(log2N)提升至O(n)。
为了提高查询效率,就需要减少磁盘I/O次数,故需要尽可能降低树的高度,每层的分叉越多越好。
为解决二叉搜索树退化为链表的问题,提出了平衡二叉树/AVL树。
每访问一次节点就需要进行一次磁盘I/O操作,故由于平衡二叉树的深度较高,会导致磁盘I/O次数过大,影响整体数据查询的效率。
B树英文Balanced Tree,即多路平衡查找树。它的高度远小于AVL树。
B-Tree相比AVL树来说磁盘的I/O操作要少,故查询效率更高。只要树的高度足够低,I/O次数足够少,就可以提高查询性能。
B+树也是一种多路搜索树,基于B-Tree进行的改进。B+Tree更适合文件索引系统。
B+Tree中,孩子节点数 = 关键字数B-Tree中,孩子节点数 = 关键字数 + 1。
B+Tree中,非叶子节点的关键字也同时存在在子节点中,且是子节点中所有关键字的最大/最小。
B+Tree中,非叶子节点仅用于索引,不保存数据,所有用户记录都存放在叶子节点中。
B-Tree中,非叶子节点既保存索引,也保存数据记录。
B+树所有关键字都在叶子节点出现,叶子节点构成一个有序链表,且叶子节点本身按照关键字大小从小到大顺序链接。
B+Tree非叶子节点不直接存放数据,好处在于?
B+Tree的叶子节点通过双链表进行递增有序连接,在范围查询上(可直接通过指针查找),效率更高。
B-Tree则需要通过中序遍历,才能完成范围查询,故效率更低。
B+Tree的磁盘读写代价更低
B+Tree的内部节点相对B-Tree较小,同等磁盘页大小,可存储更多的关键字,故极大的减少了I/O次数。
B+树的查询效率更加稳定
B+Tree所有关键字的查找必须走一条从根节点到叶子节点的完整路径,即关键字查询的路径长度相同,这样每个数据的查询效率是相当的。
可实现大多数页在申请时是连续的,会更好的加快查询寻址速度、范围检索速度,减少随机I/O。
B+Tree中每一层的页都会形成一个双链表,如果以页为单位分配存储空间,双向链表相邻的两个页之间的物理位置可能相距很远。范围查询则只能进行随机I/O,非常慢。如果能让链表中相邻的页物理位置也相邻,这样在范围查询时就可使用顺序I/O。
引入区的概念,一个区就是物理位置上连续64个页。这样就能够实现大多数页在申请时都是连续的,会加快范围查询速度,减少随机I/O。
是逻辑上的概念,是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。InnoDB对段的管理由引擎自身实现。
当范围查询(即对叶子节点进行顺序检索)时,需要查找所有叶子节点,故通过隔离索引段和数据段,可实现直接顺序检索。
综上,段不能仅仅定义为某些区的集合,准确定义是某些零散的页面以及一些完整的区的集合。
是一个逻辑容器,存储对象是段。一个表空间可有多个段,一个段只属于一个表。
数据库是由一个或多个表空间组成。
表空间从管理上分为:系统表空间、独立表空间、撤销表空间、临时表空间等。
独立表空间:每张表有一个独立的表空间,且独立表空间可在不同的数据库之间迁移。
结构:段、区、页组成
真实表空间的大小:InnoDB中.ibd文件只占用96KB=16KB*6,即6个页面大小(mysql5.7)。
系统表空间:
系统表空间和独立表空间基本类似,(由于整个mysql进程只有一个系统表空间)在系统表空间中会额外记录一些有关整个系统信息的页面。
为了考虑以完整的区为单位分配给某个段对于数据量较小的表太浪费空间的情况。InnoDB提出碎片fragment区的概念。
在一个碎片区内的页,可用于不同的目的,即有些页用于段A、有些页用于段B、有些页不属于任何段。
碎片区直属于表空间,不属于任何一个段。
InnoDB将数据划分为若干页,默认页的大小为16KB。
#查看InnoDB默认页的大小:
show variables like '%innodb_page_size%';
以页作为磁盘和内存之间交互的基本单位,即磁盘 --[最少读取16KB]–> 内存、内存 --[最少刷新16KB]–> 磁盘。
数据库中,不论读取一行、多行,都是将这些行所在的页(页中有多个行记录)加载到内存。数据库管理存储空间的基本单位是Page页,数据库I/O操作的最小单位是页。
注:记录以行存储,但读取是按页读取。
InnoDB从磁盘中读取数据的最小单位为数据页。
MySQL存放的数据逻辑上称之为表,磁盘等物理层面上是按数据页形式进行存放的,当其加载到MySQL中我们称之为缓存页。
如果缓冲池中没有该页数据,那么缓冲池有三种读取数据的方式,每种方式的读取效率都不同:
如果该数据存在内存中,基本上执行时间在1ms左右,效率很高。
如果数据没有内存中,就需要在磁盘上对该页查找,整体时间预估在10ms左右。
顺序读取本质是一种批量读取的方式,因为我们请求的数据往往是相邻存储的,这样一次性加载到缓冲池中就不需要再对其他页面单独进行磁盘I/O操作。
批量读取的方式,即使从磁盘进行读取,效率也比内存中只单独读取一个页的效率更高。
这些页不在物理结构上相连,通过双向链表相关联。
数据页中的记录会按照主键值从大到小的顺序组成一个单向链表。每个数据页会为存储在其中的记录生成一个页目录(数组结构),通过主键查找某条记录时可在页目录用二分法快速定位槽,然后再遍历该槽对应分组的记录即可快速定位指定的记录。
按类型划分,常见的页有:数据页(保存B+树节点,包括目录页)、系统页、Undo页和事务数据页。
数据页16KB大小的存储空间被划分为7部分,如下图:
File Header & FileTailer:
File Header:记录各种页的一些通用信息,如页的编号、上一页、下一页(双向链表就依靠此完成)等。
File Tailer:8字节中,前4字节存放校验和,后4字节存该页刷盘时对应得redo log中得LSN。
为了加快速度,页都会被加载到buffer pool中修改,之后按照一定频率回写到磁盘中,但回写中途掉电如何处理?
答:页头部和页尾部,存放当前页的校验和,如果不一致则说明刷盘中途出现了问题。
Page Header & Page Directory:
Page Directory:为了提高页内查找的速度,采用了一个槽对记录行进行分组。
总结:在查询一条记录时,先通过二分查找到记录所在的槽,之后从该槽的最小记录开始,通过**单向链表遍历(不超过8行记录)**即可找到对应的记录。
Infimum+supremum & User Records & Free Space:
User Records & Free Space:
一开始生成的页中,并没有User Records这部分。每当添加一条记录,都会从Free Space中申请一个记录大小的空间划分到User Records中,直到Freee Space的空间全部被User Record替换,(表明该页已满)需要重新申请新的页。
User Records是按照指定的行格式,一条条摆放在User Records中,相互之间形成单链表(根据行格式的的记录头信息所得)。
Infimum+supremum(最大最小记录):非自定义的记录,主要是用来辅助定位的。
B+Tree分为两部分:叶子节点(存储行记录)和非叶子节点(存储索引和页面指针,并不存储行记录本身)。
B+Trees是如何进行记录检索的?如果通过B+Tree的索引查询行记录,
变长字段长度列表:存放所有变长字段的真实数据占用的字节长度。
注意:这里存储的变长长度和字段顺序是反过来的。
NULL值列表:将可为NULL的列统一管理。如果表中没有允许为NULL的列,则NULL值列表就不存在了。
通过逆序表示NULL值列,1表示为空,0表示不为空。
记录头信息:
被删除的记录为什么还在页中存储?
答:移除被删除的记录,需要对B+树进行排列,导致性能下降。被删除的记录会组成一个所谓的垃圾链表,这个链表中占用的空间为可重用的空间,之后有新的记录插入到表中,可能会将这些被删除的记录占用的空间覆盖掉。
为什么添加的数据的heap_no值直接从2开始?
答:mysql会自动为每个页添加两个最大/最小记录,又被称为虚拟记录/伪记录。最小记录的 heap_no=0,最大记录的 heap_no=1。
一个页的大小默认为16KB(16384B),但一个varchar(M)类型的列最多可存储65533B。这样可能出现一个页都无法存储一条记录,这种现象称为行溢出。
在Compact格式下,针对行溢出,只会存储该列前768B的数据,和一个指向其他页的地址(将剩余数据存放在该页)。
dynamic采用完全溢出的方式,在数据页只存储20B的指针(溢出页地址),实际数据全部存放在off Page(溢出页)中。
MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、所列索引、空间索引等。
unique+not null
索引,且一张表中只能有一个主键索引InnoDB / MyISAM
:支持B-Tree
、Full-text
等索引,不支持Hash索引;Memory
:支持B-Tree
、Hash
等索引,不支持Full-text
索引;create table if not exists student (
id int,
age int,
phone varchar(15),
address varchar(25),
# 创建普通索引
index idx_id(id)
);
# 通过命令查看索引
show create table student; # 方式二
show index from student; # 方式二,更直观
# 性能分析工具:explain
声明有唯一性索引的字段,添加数据时要保证唯一性(可添加null值)
create table if not exists student (
id int,
age int,
phone varchar(15),
address varchar(25),
# 创建唯一性索引
unique index un_idx_id(id)
);
通过定义主键约束的方式定义主键索引
create table if not exists student (
# 隐式创建主键索引
id int primary key,
age int,
phone varchar(15),
address varchar(25),
);
# 通过删除主键约束的方式删除主键索引
alter table student drop primary key;
联合索引查询时,遵循 “最左前缀原则”。
create table if not exists student (
id int,
age int,
phone varchar(15),
address varchar(25),
# 创建联合索引
index mulIdx_id_age(id, age)
);
create table table_name (
col data_type
[fulltext | spatial] index idx_name(col)
);
# 全文索引可用于全文搜索,只能在char、varchar、text列创建索引。索引总是对整列进行,不支持局部(前缀)索引。
# 创建空间索引时,要求控件类型字段必须非空
# 方式一:
alter table student add index idx_id(id);
alter table student add unique index unIdx_id(id);
alter table student add index mulIdx_id_age(id,age);
# 方式二:
create index idx_id on student(id);
create index unIdx_id on student(id);
create index mulIdx_id_age on student(id,age);
添加auto_increment
约束字段的唯一性约束/主键约束不能被删除。
删除表中列时,则该列也会从索引中删除。
如果组成索引的所有列都被删除,则整个索引将被删除。
# 方式一:
alter table student drop index idx_id(id);
alter table student drop unique index unIdx_id(id);
alter table student drop index mulIdx_id_age(id,age);
# 方式二:
drop index idx_id on student(id);
drop index unIdx_id on student(id);
drop index mulIdx_id_age on student(id,age);
key_len
过长,否则索引会失效;字段的数值有唯一性约束
索引本身可以起到约束的作用,如唯一性索引、主键索引都可起到唯一性约束的作用。
唯一性限制的字段,创建唯一性索引/主键索引(非空),可极大的提高通过索引定位某条记录的速度。
频繁作为where查询条件的字段
尤其是在数据量较大时,创建普通索引,就可以大幅提升数据查询效率。
经常group by和order by的列
使用group by对数据进行分组查询、order by对数据进行排序时,就需要对分组或排序的字段进行索引。
如果待排序的列有多个,则可在这些列上建立联合索引。
注:如果同时group by和order by时,最优的索引创建方式是给group by和order by后的字段构建联合索引。(受sql语句的执行顺序影响,联合索引中group by后的字段在前,order by后的字段在后)
update、delete的where条件列
根据where先筛选出记录,然后再更新或删除。如果where字段创建了索引,可大幅提升效率。
如果进行update时,更新字段是非索引字段,提升效率则更明显(非索引字段的更新,不需要对索引进行维护)。
distinct字段需要创建索引
索引会对字段进行排序,这样去重就快很多。
多表join连接操作时,创建索引注意事项
连接表的数量尽量不要超过3个,每增加一张表就相当于增加了一次嵌套循环,这严重影响效率。
对where条件创建索引,因where才是对数据条件的过滤,在数据量大时影响巨大。
join时,用于连接的字段创建索引 ,且该字段在多张表中的类型必须一致。
使用列类型小的创建索引
数据类型小,查询时比较操作较快
数据类型小,索引占用的存储空间少,同一个数据页就能存储更多的记录,从而减少磁盘I/O带来的性能消耗。
这个建议对主键更适用。主键不仅在聚簇索引中出现,在二级索引中也会存储主键,如果主键使用更小的数据类型,即可节省更多的存储空间和更高效的I/O。
使用字符串前缀创建索引
通过截取字段的前缀作为索引,称为前缀索引。这样虽然不能精确定位记录的位置,但能定位到相应前缀所在的位置,并根据前缀相同的记录的主键值回表查询完整的字符串。既节省了空间,又减少了字符串的比较时间。
前缀长度的选择问题:多了,达不到节省索引存储空间的目的;少了,重复内容太多,字段的散列度(选择性)会降低。
使用索引前缀的方式,无法支持使用索引排序。
区分度高(散列性高)的列适合作为索引
在记录行数一定的情况下,列的基数(指某列中不重复数据的个数)越大,该列中的值越分散,创建索引的效果越好。
# 计算表中列的散列性:
select count(distinct 列)/count(*) from 表名
注:联合索引应把区分度高(散列性高)的列放在前面。
使用频繁的列放在联合索引的最左边
由于 “最左前缀原则” ,可以增加联合索引的使用率。
在多个字段都需要索引时,联合索引优于单值索引
实际中,单张表索引的数量不超过6个。
where
条件(包括group by
和order by
)中使用不到的字段,不要设置索引
数据量小的表,创建索引纯属浪费空间
有大量重复数据(散列度低)时,不要创建索引
避免对经常更新的表创建过多的索引
不建议用无序的值作为索引
插入时可能造成页分裂。
删除不再使用或者很少使用的索引
不要定义冗余或重复的索引
主键本身就会生成聚簇索引,就没必要再定义唯一索引或普通索引,否则会重复。
索引是一把双刃剑,可提高查询效率,但也会降低增/删/改的效率并占用磁盘空间。
commit
)事务,把结果写回磁盘上。rollback
)到最初的状态。MySQL最重要的就是日志!!!,用来保证事务操作的原子性(redo日志)、一致性、隔离性(锁/MVCC)、持久性(redo日志)。
每一个事务必须满足下面的4个特性:
事务的原子性(Atomic):事务是一个不可分割的整体,事务必须具有原子特性,即不允许事务部分的完成。undo
日志,保证了事务执行出错时,能回滚到事务执行前的数据库的状态。
事务的一致性(Consistency):一个事务执行之前和执行之后,数据库数据必须保持一致性状态。数据库的一致性状态必须由用户来负责,由并发控制机制实现,需要通过原子性、隔离性和持久性来保证。
事务的隔离性(Isolation):当两个或者多个事务并发执行时,为了保证数据的安全性,将一个事物内部的操作与其它事务的操作隔离起来(不被其它正在执行的事务所看到),使得并发执行的各个事务之间不能互相影响。由mysql的事务的锁机制或MVCC来保证的。
事务的持久性(Durability):事务完成(commit
)以后,DBMS
保证它对数据库中的数据的修改是永久性的,即使数据库因为故障出错,也应该能够恢复数据。redo
日志,保证了数据库的持久性,即使突然宕机也能在重启后恢复之前的状态,即恢复已提交的数据。
事务处理不经隔离,并发执行事务时通常会发生以下的问题:
脏读(Dirty Read):一个事务读取了另一个事务未提交的数据。
例如:当事务A和事务B并发执行时,当事务A更新后,事务B查询读取到A尚未提交的数据,此时事务A回滚,则事务B读到的数据就是无效的脏数据。(事务B读取了事务A尚未提交的数据)
不可重复读(NonRepeatable Read):一个事务的操作导致另一个事务前后两次读取到不同的数据。
例如:当事务A和事务B并发执行时,当事务B查询读取数据后,事务A更新操作更改事务B查询到的数据,此时事务B再次去读该数据,发现前后两次读的数据不一样。(事务B读取了事务A已提交的数据)
幻读(Phantom Read)幻读:一个事务的操作导致另一个事务前后两次查询的结果数据量不同。
例如:当事务A和事务B并发执行时,当事务B查询读取数据后,事务A新增或者删除了一条满足事务B查询条件的记录,此时事务B再去查询,发现查询到前一次不存在的记录,或者前一次查询的一些记录不见了。(事务B读取了事务A新增加的数据或者读不到事务A删除的数据)
MySQL支持的四种隔离级别是:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发效率 | 数据安全性 |
---|---|---|---|---|---|
未提交读 | √ | √ | √ | 最高 | 最低 |
已提交读(oracle默认) | × | √ | √ | 兼顾并发效率和数据安全性 | |
可重复读(mysql默认) | × | × | √ | 兼顾并发效率和数据安全性 | |
串行化 | × | × | × | 最低 | 最高 |
注意:
举例:
Orcale默认采用“已提交读”的隔离级别(避免了脏读),MySQL中InnoDB默认采用“可重复读”的隔离级别(避免了不可重复读)。
begin # 开启一个事务
commit # 提交一个事务
rollback # 事务回滚到最初的位置
savepoint point1 # 设置一个名为point1的保存点
rollback to point2 # 事务回滚到point1保存点
select @@autocommit;
# 0:手动提交事务
# 1:自动提交事务
set autocommit = 0; # 设置为手动提交事务
select @@tx_isolation;
set tx_isolation='read-committed';
# 设置事务的隔离级别为“读未提交”
set tx_isolation='read-uncommitted';
# 设置事务的隔离级别为“读已提交”
set tx_isolation='repeatable-read';
# 设置事务的隔离级别为“可重复读”
set tx_isolation='serializable';
# 设置事务的隔离级别为“串行化”
表级锁:对整张表加锁。开销小,加锁快,不会出现死锁;锁粒度大,发生锁冲突的概率高,并发度低。
行级锁:对某行记录加锁。开销大,加锁慢,会出现死锁;锁粒度最小,发生锁冲突的概率最低,并发度高。
注意:MyISAM不支持事务,故只支持表级锁;MySQL支持事务,故需要行级锁。
X
锁,写锁。S
锁,读锁。X
和S
锁之间有以下的关系:SS
可以兼容的,XS、SX、XX
之间是互斥的
显示加锁:
select ... lock in share mode # 强制获取共享锁
select ... for update # 获取排它锁
InnoDB存储引擎支持事务处理,表支持行级锁定,并发能力更好。
当我们用范围条件而不是相等条件检索数据,并请求共享或排它锁时,
举例来说, 假如 user 表中只有 101 条记录, 其 userid 的值分别是 1,2,…,100,101, 下面的 SQL:
select * from user where userid > 100 for update; # 范围条件的检索
InnoDB 不仅会对符合条件的 userid 值为 101 的记录加锁,也会对 userid 大于 101(但是这些记录并不存在)的"间隙"加锁,防止其它事务在表的末尾增加数据,那么本事务如果再次执行上述语句,就会发生幻读。
注意:InnoDB使用间隙锁的目的,为了防止幻读,以满足串行化隔离级别的要求。
对辅助索引加间隙锁,因辅助索引可能存在相同的值(但主键值不相同且升序排列),则
注意:等值查询和范围查询、主键索引和辅助索引,对应加锁的不同情况分析。
意向共享锁(IS
锁):事务计划给记录加行共享锁,事务在给一行记录加共享锁前,必须先取得该表的 IS
锁。
意向排他锁(IX
锁):事务计划给记录加行排他锁,事务在给一行记录加排他锁前,必须先取得该表的 IX
锁。
X
和S
指的是表锁,不是行锁!!!);死锁一般是应用自身造成的问题,即在对数据库的多个表做更新时,不同的代码段应对这些表按相同的顺序进行更新操作,以防止锁冲突导致的死锁问题。
一般一旦检测到死锁,会对其中的一个事务进行回滚,造成的代价较大!!!
多版本并发控制(Multi-Version Concurrency Control,简称MVCC),是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现已提交读和可重复读隔离级别。
MVCC机制会生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本(系统版本号和事务版本号)。
snapshot read
):读的是记录的**可见版本**,不用加锁(非锁定读)。如select
。
current read
):读取的是记录的**最新版本**,并且当前读返回的记录。如insert,delete,update,select...lock in share mode/for update
。
MVCC
:每一行记录实际上有多个版本,每个版本的记录除了数据本身之外,增加了其它字段:
DB_TRX_ID
:记录当前事务ID;DB_ROLL_PTR
:指向undo log
日志上数据的指针;已提交读:每次执行语句(每次select查询时)的时候,都重新生成一次快照(Read View)。
可重复读:同一个事务开启时生成一个当前事务全局性的快照(Read View),产生时机:第一次select查询时产生且只产生一次。
解决了可重复读?每次select生成的数据快照,其他事务虽然更新了最新数据,但当前数据select仍然查看的是自己的全局快照。
部分解决了幻读?每次select生成的数据快照,其他事务虽然更新了最新数据,但当前数据仍然查看的是自己的全局快照。当当前事务内对另一个事务中新增的数据进行update后,再次select则会看到多了一条数据,即出现了幻读。但如果不进行上述操作,则不会出现幻读。
注意:**当前事务内自己事务的修改,是可以读到的 **。(通过undo日志中的事务ID来获取)
记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。
当数据库出现任何故障导致无法正常使用时,可以首先查看此日志。
查询日志记录了客户端的所有语句。由于上线项目sql特别多,开启查询日志 IO 太多导致 MySQL 效率低,只有在调试时才开启,比如通过查看sql发现热点数据进行缓存。
二进制日志二进制日志 BIN LOG 记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是不包括数据查询语句。
语句以“事件”的形式保存,它描述了数据的更改过程,对于灾难时的数据恢复起着极其重要的作用。
两个重要的应用场景:主从复制、数据恢复(mysql 的数据恢复,一般需要 “数据备份~/data.sql
+ bin log
二进制” 共同完成)。
查看binlog:show binary logs
。
注意:通过 mysqlbinlog 工具(mysql原生自带的工具)可以快速解析大量的 binlog 日志文件。
举例:
mysqldump -u root -p database_name table_name > ~/data.sql
。mysql> source ~/data.sql
。详见 “MySQL优化/SQL和索引优化” 部分。
重做日志,用于记录事务操作的变化,确保事务的持久性。
redo log
是在事务开始后就开始记录,不管事务是否提交都会记录下来,在异常发生时(如数据持久化过程中掉电),重启后mysqld会恢复redo log
中的记录的数据,从而保证数据的完整性。innodb_log_buffer_size
默认是16M,就是redo log
缓冲区的大小,它随着事务开始,就开始写redo log
,如果事务比较大,为了避免事务执行过程中花费过多磁盘IO,可以设置比较大的redo log
缓存,节省磁盘IO。Buffer Pool
中的数据。InnoDB总是先把Buffer Pool
中的数据改变记录到redo log
中,用来进行崩溃后的数据恢复。redo log
,然后再慢慢的将Buffer Pool
中的脏数据刷新到磁盘上。innodb_log_group_home_dir
指定的目录下的两个文件:ib_logfile0
,ib_logfile1
,该文件被称作重做日志。
buffer pool
缓存池:加速读和加速写,直接操作data page
,写redo log
修改就算完成,有专门的线程去做把buffer pool
中的dirty page
写入磁盘。
注意:undo log也会记录在redo log中。
undo log
:回滚日志,保存了事务发生之前的数据的一个版本,主要作用:
开启慢查询日志,并设置慢查询时间,记录慢查询sql,进而用explain分析sql执行计划,进而给出优化措施。
分为观察Show Status
和行动Action
两部分。
总结:
show status like '参数';
常用的性能参数如下:
connections
:连接mysql服务器的次数
uptime
:mysql服务器上线时间
show_queries
:慢查询的次数
可结合慢查询日志,找出慢查询语句,针对该语句进行表结构优化或查询语句优化。
innodb_rows_insert、innodb_rows_delete、innodb_rows_update、innodb_rows_read
:执行增/删/改/查的行数
com_insert、com_delete、com_update、com_select
:增/删/改/查的次数
如果一条查询语句有多个执行计划,mysql会为每个执行计划计算成本,并从中选择成本最小的作为最终的执行计划。
show status like 'last_query_cost'
,可得到当前查询的成本(即sql语句所需要读取的页的数量),用来评价查询语句执行效率的指标之一。
SQL查询是一个动态的过程,从页加载的角度看:
位置决定效率
如果页在buffer pool中,效率是最高的,否则需要从内存或磁盘中读取。针对单个页的读取来说,如果页存在内存中,会比在磁盘中读取效率高很多。
批量决定效率
如果从磁盘中对单个页进行随机读取,效率很差(10ms),而采用顺序读取(批量读取),平均一页的读取效率会提升很多(甚至快于单个页在内存中的随机读取)。
总的来说,将常用的数据尽量要放在buffer pool中,其次可充分利用磁盘的吞吐能力进行一次性批量读取(平均单个页的读取效率提升很多)。
# 检查慢查询日志是否开启
show variables like '%slow_query_log';
# 设置慢查询日志的状态
set global slow_query_log = on/off;
MySQL的慢查询日志,用来记录在MySQL中响应时间超过阈值的语句,即运行时间超过long_query_time
值(默认为10s)的SQL查询,则会被记录到慢查询日志中。
# 慢查询的时间阈值
show variables like '%long_query_time%';
# 设置慢查询志的时间阈值
set long_query_time = 1;
set global long_query_time = 1;
# 修改配置文件my.cnf,可永久修改
[mysqld]
slow_query_log = on/off
slow_query_log_file = /var/lib/mysql/slow_query.log # 如果不指定慢查询日志文件名,默认为hostname-slow.log
log_query_time = 3
log_output=FILE
检查慢查询日志,找到执行时间特别长的SQL查询,针对性的进行优化,可提高系统的整体效率。
注意:非调优需要的话,一般不建议启动慢查询。
慢查询日志支持将日志记录写入文件:
# 慢查询日志文件"slow_query_log_file"
show variables like '%slow_query_log%';
查看慢查询数目:
show global status like '%Slow_queries%';
慢查询日志分析工具:
mysqldumpslow
重新生成慢查询日志文件:
mysqladmin -u root -p flush-logs slow
# 注意:如果需要旧的日志文件,则需要进行备份。
查看SQL执行成本show profiles:
# 查询近期的sql执行语句
show profiles;
# 查询Query_ID条sql语句的执行状态和开销
show profile for query ID
# 也可执行查询参数,如下:
show profile cpu, block io for query ID
# 常用的查询参数:
all # 查询所有的开销信息
block io # 显示快io开销
context switches # 上下文切换开销
cpu # 显示cpu开销
IPC # 显示发送和接受开销信息
memory # 显示内存开销信息
page faults # 显示页面错误开销信息
source # 显示和source_function、source_file、source_line相关的开销信息
swaps # 显示交换次数开销信息
除了优化SQL和索引,很多时候,在实际生产环境中,由于数据库服务器本身的性能局限,就必须要对上层的应用来进行一些优化,使得上层应用访问数据库的压力能够减到最小。
参考本人写的另一篇博客中,mysql连接池的实现
应用访问数据库,都要先和MySQL Server
创建连接(包括三次握手、权限验证等过程),然后发送SQL语句,并经Server处理完成后,再把结果通过网络返回给应用,之后再关闭与MySQL Server
的连接。因此,如果短时间大量的数据库访问,则建立和断开连接中TCP三次握手和四次挥手所花费的时间就很大了。
一般都会在应用访问数据库中间层,添加连接池模块,相当于应用与MySQL Server
事先创建一组连接,当需要请求MySQL Server
时,不需要再进行TCP连接和释放连接了,直接从连接池获取连接即可发送sql语句。
一般连接池都会维护以下资源:
业务上增加redis、memcache
,即用缓存把经常访问的热点数据缓存起来,提高其查询效率。
以redis为例,引入缓存后,需要考虑**缓存一致性、缓存击穿(加锁,避免并发查询同一个过期数据)、缓存雪崩(设置过期后的异步操作,一旦过期立即从磁盘获取并更新数据;将缓存数据失效时间分散开,避免key的批量过期出现)**等问题。
主要指的就是MySQL Server
启动时加载的配置文件my.ini
(windows中)或my.cnf
(linux中)中配置项内容的优化。
将select查询语句上一次的查询结果记录下来放在查询缓存中,下一次再查询相同内容时,直接从缓存中查询,不用再进行一遍真正的SQL查询。但是当两个select查询中间出现insert,update,delete语句的时候,查询缓存就会被清空。
适用场景:查询缓存适用更新不频繁的表,因为过多的查询缓存的数据添加和删除会不停的写入和删除查询缓存,这会影响MySQL的执行效率,还不如每次都从磁盘上查来得快(缓存指的就是一块内存,内存I/O比磁盘I/O快很多)。
查看查询缓存的设置:(通过修改配置文件,可改变变量的值,如query_cache_type
或query_cache_type
)
查看缓存的使用情况:
主要指配置文件中innodb_buffer_pool_size
配置项,其定义了 InnoDB 存储引擎的表数据和索引数据的最大内存缓冲区大小,值越高,访问表中数据需要的磁盘 I/O 就越少。
主要指配置文件中thread_cache_size
配置项。
MySQL Server 网络模块采用经典的 I/O复用+线程池模型,引入线程池就是在业务使用之前,先创建一组固定数量的线程,等待事件发生,当有 SQL 请求到达 MySQL Server 的时候,在线程池中取一个线程来执行该 SQL 请求就可以了,执行完成后把线程再归还到线程池中,并继续等待下一次任务的处理。
注意:MySQL会根据连接量,对线程池进行“扩/缩容”,保证 MySQL Server 的性能不受影响。
MySQL Server 作为一个服务器,可以设置客户端的最大连接量和连接超时时间(MySQL Server 对超时未通信的连接,进行主动关闭操作,单位是秒)。
在实际生产环境中,如果对mysql数据库的读和写都在一台数据库服务器中操作,无论是在安全性、高可用性,还是高并发等各个方面都是不能满足实际需求的。一般要通过主从复制的方式来同步数据,再通过读写分离来提升数据库的并发负载能力。
主从复制的流程:两个日志(bin log 二进制日志 & relay log 日志)和三个线程(master 的一个线程和 slave 的二个线程)
主库的更新操作写入 bin log 二进制日志中。
master 服务器创建一个 binlog 转储线程(binlog dump
),将二进制日志内容发送到从服务器。
slave 机器执行 START SLAVE
命令开启一个工作线程(I/O 线程),接收 master 的 bin log
并复制到其中继日志 relay log
。
注意:slave 从 master 的二进制日志中读取事件时,如果已经跟上 master,它会睡眠并等待 master 产生新的事件。
sql slave thread
(sql 线程)处理该过程的最后一步:从中继日志 redo log 中读取并重放其中的事件,从而更新 slave 机器的数据,使其与master 的数据一致。
中继日志通常会位于os缓存中,所以中继日志的开销很小。
多数项目开始时用单机数据库就够了,但随着服务器的请求越来越多,需要对数据库进行读写分离。 主库(Master)负责写、多个从库副本(Slave)负责读,并通过主从复制实现“数据同步更新”,保持数据一致。slave 从库可以不断的水平扩展,故读的压力会被不断的分摊,故不会存在太大的问题。