【MySQL数据库教程天花板,mysql安装到mysql高级,强!硬!-哔哩哔哩】
【阿里巴巴Java开发手册】https://www.w3cschool.cn/alibaba_java
目前互联网上常见的数据库管理软件有Oracle、MySQL、MS SQL Server、DB2、PostgreSQL、Access、Sybase、informix这几种,如下是2022年DB-Engines Ranking对数据库受欢迎程度进行调查后的统计结果:查看数据库最新排名:https://db-engines.com/en/ranking
CREATE、ALTER、DROP、RENAME、TRUNCATE
INSERT、DELETE、UPDATE、SELECT
COMMIT、ROLLBACK、SAVEPOINT、GRANT、REVOKE
非全部新特性,仅是自我感觉用得着,并且非常重要的。
性能峰值几乎是5.7的两倍。
在MySQL 8.0版本之前,默认字符集为latin1 ,utf8字符集指向的是utf8mb3 。网站开发人员在数据库设计的时候往往会将编码修改为utf8字符集。如果遗忘修改默认的编码,就会出现乱码的问题。从MySQL8.0开始,数据库的默认编码将改为utf8mb4 ,从而避免上述乱码的问题。
在MySQL8.0版本中,InnoDB表的DDL支持事务完整性,即DDL操作要么成功要么回滚。DDL操作回滚日志写入到data dictionary数据字典mysql.innodb_ddl_log(该表是隐藏的表,通过show tables无法看到)中,用于回滚操作。通过设置参数,可将DDL操作日志打印输出到MySQL错误日志中。
什么叫计算列?简单来说就是某一列的值是通过别的列计算得来的。例如:a列值为1、b列值为2,c列不需要手动插入,定于a+b的结果为c的值,那么c就是计算列,是通过别的列计算得来的。
在MySQL8.0中,CREATE TABLE和ALTER TABLE都支持增加计算列。
语法:GENERATED ALWAYS AS (计算公式) VIRTUAL
从MySQL8.0.17开始,整数数据类型不推荐使用显示宽度的属性。
//创建表
CREATE TABLE tb1(
id INT,
a INT,
b INT,
c INT GENERATED ALWAYS AS (a + b) VIRTUAL
);
//插入数据
INSERT INTO tb1(a,b) VALUES (100,200);
//查询结果
mysql> SELECT * FROM tb1;
+------+------+------+------+
| id | a | b | c |
+------+------+------+------+
| NULL | 100 | 200 | 300 |
+------+------+------+------+
1 row in set (0.00 sec)
窗口函数的作用类似在查询中对数据进行分组,不同的是,分组操作会把分组的结果聚合成一条记录,而窗口函数是将结果聚合成一条记录,而窗口函数是将结果置于每一条数据记录中。
窗口函数可以分为
静态窗口函数
和动态窗口函数
静态窗口函数的窗口大小是固定的, 不会因为记录的不同而不同;
动态窗口函数的窗口大小会随着记录的不同而变化;
函数 OVER(PARTITION BY 字段名 ORDER BY 字段名 ASC|DESC)
或者是:
函数 OVER 窗口名 ... WINDOW 窗口名 AS (PARTITION BY 字段名 ORDER BY 字段名 ASC|DESC)
OVER的关键字指定函数的窗口范围。
如果省略后面括号中的内容,则窗口会包含满足WHERE条件的所有记录,窗口函数会基于所有满足WHERE条件的记录进行计算。
如果OVER关键字后面的括号不为空,则可以使用如下语法设置窗口。
窗口名:为窗口设置一个别名,用来标识窗口。
PARTITION BY子句:指定窗口函数按照哪些字段进行分组。分组后,窗口函数可以在每个分组中分别执行。
ORDER BY子句:指定窗口函数按照哪些字段进行排序。执行排序操作使窗口函数按照排序后的数据记录的顺序进行编号。
FRAME子句:为分区中的某个子集定义规则,可以用来作为滑动窗口使用。
窗口函数的特点是可以进行分组,而且可以在分组内排序。另外,窗口函数不会因为分组而减少表中的行数,这对我们在原表数据基础上进行统计和排序非常有用。
公用表表达式(或通用表表达式)简称CTE(Common Table Expressions)。CTE是一个命名的临时结果集,作用范围是当前语句。CTE可以理解成一个可以复用的子查询,当然跟子查询还是有点区别的,CTE可以引用其他CTE,但子查询不能引用其他子查询。所以,可以考虑代替子查询。
如果您打算在同一查询中重用子查询,则建议使用“公用表表达式”,因为该子查询被临时保存到内存中,这意味着它不需要多次运行。它们通常看起来也更干净,因此,如果您共享代码,则有助于提高可读性。
语法结构
WITH CTE名称
AS (子查询)
SELECT|DELETE|UPDATE 语句;
降序索引以降序存储键值。虽然在语法上,从MySQL4版本开始就已经支持降序索引的语法了,但实际上该DESC定义是被忽略的,直到MySQL8.x版本才开始真正支持降序索引(仅限InnoDB存储引擎)。
MySQL在8.0版本之前创建的仍然是升序索引,使用时进行反向扫描,这大大降低了数据库的效率
。在某些场景下,降序索引意义重大。例如:如果一个查询,需要对多个列进行排序,且顺序要求不一致,那么使用降序索引将会避免数据库使用额外的文件排序操作,从而提高性能。
降序索引只对查询特定的排序顺序有效,如果使用不当,反而查询效率更低。
在MySQL5.7版本之前,只能通过显式索引的方式删除索引。此时,如果发现删除索引后出现错误,又只能通过显式创建索引的方式将删除的索引创建回来。如果数据库表中的数据量非常大,或者数据表本身比较大,这种操作就会消耗系统过多的资源,操作成本非常高。
从MySQL8.0x开始支持隐藏索引(invisible indexes)
,只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引(即使用force index(强制使用索引),优化器也不会使用该索引),确认将索引设置为隐藏索引后系统不受任何影响,就可以彻底删除索引。这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除
。
同时,如果你想验证某个索引删除之后的查询性能影响
,就可以暂时先隐藏该索引。
注意:主键不能被设置为隐藏索引。当表中没有显式主键时,表中第一个唯一非空索引将会成为隐式主键,也不能设置为隐藏索引。
-- 查看数据库最大连接数
show variables like '%max_connections%';
-- 查看目前正在使用的连接数量
show global status like 'Max_used_connections';
-- 查看会话连接
show processlist;
-- 查看
show status like 'last_query_cost';
-- 查看MySQL本次启动后的运行时间(单位:秒)
show status like 'uptime';
--查看select语句的执行数
show [global] status like 'com_select';
--查看insert语句的执行数
show [global] status like 'com_insert';
--查看update语句的执行数
show [global] status like 'com_update';
--查看delete语句的执行数
show [global] status like 'com_delete';
--查看试图连接到MySQL(不管是否连接成功)的连接数
show status like 'connections';
--查看线程缓存内的线程的数量。
show status like 'threads_cached';
--查看当前打开的连接的数量。
show status like 'threads_connected';
--查看当前打开的连接的数量。
show status like 'threads_connected';
--查看创建用来处理连接的线程数。如果Threads_created较大,你可能要增加thread_cache_size值。
show status like 'threads_created';
--查看激活的(非睡眠状态)线程数。
show status like 'threads_running';
--查看立即获得的表的锁的次数。
show status like 'table_locks_immediate';
-- 查看不能立即获得的表的锁的次数。如果该值较高,并且有性能问题,你应首先优化查询,然后拆分表或使用复制。
show status like 'table_locks_waited';
-- 查看创建时间超过slow_launch_time秒的线程数。
show status like 'slow_launch_threads';
-- 查看查询时间超过long_query_time秒的查询的个数。
show status like 'slow_queries';
-- QPS(每秒Query量)
-- QPS = Questions(or Queries) / seconds
show global status like 'Question%';
-- TPS(每秒事务量)
-- TPS = (Com_commit + Com_rollback) / seconds
show global status like 'Com_commit';
show global status like 'Com_rollback';
-- key Buffer 命中率
show global status like 'key%';
key_buffer_read_hits = (1-key_reads / key_read_requests) * 100%
key_buffer_write_hits = (1-key_writes / key_write_requests) * 100%
-- InnoDB Buffer命中率
show status like 'innodb_buffer_pool_read%';
innodb_buffer_read_hits = (1 - innodb_buffer_pool_reads / innodb_buffer_pool_read_requests) * 100%
-- Query Cache命中率
show status like 'Qcache%';
Query_cache_hits = (Qcahce_hits / (Qcache_hits + Qcache_inserts )) * 100%;
-- Table Cache状态量
show global status like 'open%';
比较 open_tables 与 opend_tables 值
-- Thread Cache 命中率
show global status like 'Thread%';
show global status like 'Connections';
Thread_cache_hits = (1 - Threads_created / connections ) * 100%
-- 锁定状态
mysql> show global status like '%lock%';
Table_locks_waited/Table_locks_immediate=0.3% 如果这个比值比较大的话,说明表锁造成的阻塞比较严重
Innodb_row_lock_waits innodb行锁,太大可能是间隙锁造成的
-- Tmp Table 状况(临时表状况)
show status like 'Create_tmp%';
Created_tmp_disk_tables/Created_tmp_tables比值最好不要超过10%,如果Created_tmp_tables值比较大,
可能是排序句子过多或者是连接句子不够优化
-- Binlog Cache 使用状况
show status like 'Binlog_cache%';
如果Binlog_cache_disk_use值不为0 ,可能需要调大 binlog_cache_size大小
-- Innodb_log_waits 量
show status like 'innodb_log_waits';
Innodb_log_waits值不等于0的话,表明 innodb log buffer 因为空间不足而等待
-- 显示所有表
SHOW TABLES;
-- 查看表结构
SHOW CREATE TABLE 表名;
DESC 表名;
SHOW INDEX FROM 表名;
1.
Table
表的名称。
2.Non_unique
如果索引不能包括重复词,则为0。如果可以,则为1。
3.Key_name
索引的名称。
4.Seq_in_index
索引中的列序列号,从1开始。
5.Column_name
列名称。
6.Collation
列以什么方式存储在索引中。在MySQL中,有值‘A’(升序)或NULL(无分类)。
7.Cardinality
索引中唯一值的数目的估计值。通过运行ANALYZE TABLE或myisamchk -a可以更新。基数根据被存储为整数的统计数据来计数,所以即使对于小型表,该值也没有必要是精确的。基数越大,当进行联合时,MySQL使用该索引的机会就越大。
8.Sub_part
如果列只是被部分地编入索引,则为被编入索引的字符的数目。如果整列被编入索引,则为NULL。
9.Packed
指示关键字如何被压缩。如果没有被压缩,则为NULL。
10.Null
如果列含有NULL,则含有YES。如果没有,则该列含有NO。
11.Index_type
用过的索引方法(BTREE, FULLTEXT, HASH, RTREE)。
12.Comment
多种评注。
EXPLAIN或DESCRIBE语句语法形式如下:
EXPLAIN SELECT 1;
或者
DESCRIBE SELECT 1;
如果我们想看看某个查询的执行计划,的话,可以具体的查询语句前面加一个
EXPLAIN
,就像这样:
EXPLAIN SELECT 1;
输出的上述信息就是所谓的
执行计划
。在这个执行计划的辅助下,我们需要知道应该怎样改进自己的查询语句以使查询执行起来更高效。其实除了以SELECT
开头的查询语句,其余DELETE、INSERT、REPLACE
以及UPDATE
语句等都可以加上EXPLAIN
,用来查看这些语句的执行计划,只是平时我们对SELECT
语句更感兴趣。
注意:执行EXPLAIN时并没有真正的执行该后面的语句,因此可以安全的查看执行计划。
EXPLAIN
语句输出的各个列的作用如下:
列名 | 描述 |
---|---|
id | 在一个大的查询语句中每个SELECT关键字都对应一个唯一的id |
select_type | SELECT关键字对应的那个查询的类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type【重点】 | 针对单表的访问方法 |
possible_keys | 可能用到的索引 |
key | 实际上使用的索引 |
key_len【重点】 | 实际使用到的索引长度 |
ref | 当使用索引等值查询时,与索引列进行等值匹配的对象信息 |
rows【重点】 | 预估的需要读取的记录条数 |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
Extra【重点】 | 一些额外的信息 |
id
id如果相同,可以认为是一组,从上往下顺序执行
在所有组中,id值越大,优先级越高,越先执行
关注点:id号每个号码,表示一趟独立的查询,一个sql的查询趟数越少越好
select_type
一条大的查询语句里边可以包含若干个SELECT关键字,
每个SELECT关键字代表着一个小的查询语句
,而每个SELECT关键字的FROM子句里都可以包含若干张表(这些表用来做连接查询),每一张表都对应着执行计划输出中的一条记录
,对于同一个SELECT关键字中的表来说,它们的id值都是相同的。
MySQL为每一个SELECT关键字代表的小查询都定义了一个称之为select_type
的属性,意思是只要我们知道了某个小查询的select_type属性
,就知道了这个小查询在整个大查询中扮演了一个什么角色
。
SIMPLE
:查询语句中不包含UNION或者子查询的查询都算作是SIMPLE类型,无论是单表查询还是联合查询这些查询的级别都是SIMPLE。顾名思义,这些查询都被 MySQL 认为是比较简单的查询模式。
PRIMARY
:对于包含UNION、UNION ALL或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type值就是PRIMARY。
UNION
:包含UNION、UNION ALL或者子查询的大查询来说,它是由几个小查询组成的嘛。除了第一个是 PRIMARY,其他的都是 UNION。
UNION RESULT
:如果 MySQL 中的 UNION 需要用到临时表进行去重的话,那么这个小查询的级别就是 UNION RESULT。
SUBQUERY
:如果我们的子查询不能转换对应 semi-join(多表连接)的形式,而且这个查询不是相关子查询的话,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,这个时候该子查询的第一个 SELECT 的级别就是 SUBQUERY。
DEPENDENT SUBQUERY
:如果包含子查询的查询语句不能够转为对应的semi-join(多表连接)的形式,并且该子查询是相关子查询,则该子查询的第一个SELECT关键字代表的那个查询的select_type就是DEPENDENT SUBQUERY。
DEPENDENT UNION
:在包含UNION或者UNION ALL的大查询中,如果各个小查询都依赖于外层查询的话,那除了最左边的那个小查询之外,其余的小查询的select_type的值就是DEPENDENT UNION。
DERIVED
:对于包含‘派生表’的查询,该派生表对应的子查询的select_type的值就是DERIVED。
MATERIALIZED
:当查询优化器在执行包含子查询的语句时,选择将子查询物理化之后与外层查询进行连接查询时,该子查询对应的select_type属性就是MATERIALIZED。
partitions(可略)
代表分区表中命中的情况,非分区表,该项为NULL。一般情况我们的查询语句执行计划的partitions列值都是NULL。
type【重点】
执行计划的一条记录就代表着MySQL对某个表的
执行查询时的访问方法
,又称“访问类型”,其中的type
列就表明了这个访问方法是啥,是较为重要的一个指标。比如,看到type
列的值是ref
,表明MySQL
即将使用ref
访问方法来执行对数据表的查询。
完整访问方法如下(性能由优到劣排序):
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
。
SQL性能优化的目标:至少要达到
range
级别,要求是ref
级别,最好是consts
级别(来源Alibaba开发手册)。
system
:当表中只有一条记录
并且该表使用的存储引擎的统计数据都是精确地,比如MyISAM、Memory,那么对该表的访问方法就是system
。
const
:当我们根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const
。
eq_ref
:在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较),则对该被驱动表的访问方法就是eq_ref
。
ref
:当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref
。
fulltext
:全文检索,不推荐,推荐使用搜索引擎替代。
ref_or_null
:当对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值时,那么对该表的访问方法就可能是ref_or_null
。
index_merge
:单表访问方法时在某些场景下可以使用Intersection、Union、Sort-Union这三种索引合并的方式来执行查询就是index_merge
。
unique_subquery
:是针对一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的type列的值就是unique_subquery
。
index_subquery
:该连接类型类似于unique_subquery。可以替换IN子查询,但只适合下列形式的子查询中的非唯一索引:value IN SELECT key_column FROM single_table WHERE some_expr)。
range
:如果使用索引获取某些范围区间
的记录,那么就可能使用到range
访问方法。
index
:当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是index
。
ALL
:全表扫描。
possible_keys和key
在EXPLAIN语句输出的执行计划中,
possible_keys
列标识在某个查询语句中,对某个表执行单表查询时可能用到的索引
有哪些。一般查询涉及到的字段若存在索引,则该索引将被列出,但不一定被查询使用,key
列表示实际用到的索引
有哪些,如果为NULL,则没有使用索引。
key_len【重点】
实际使用到的索引长度(即:字节数)帮你检查是否充分利用上了索引,值越大越好,主要针对于联合索引,有一定参考意义。
ref
当使用索引列等值查询时,与索引列进行等值匹配时的对象信息。比如只是一个常数或者某个列。
rows【重点】
预估的需要读取的记录条数,值越少越好。
filtered
某个表经过搜索条件过滤后剩余记录条数的百分比。
如果使用的索引执行的是单表扫描,那么计算时需要估计出满足除使用到对应索引的搜索条件外的其他搜索条件的记录有多少条。
对于单表查询来说,这个filtered
没什么意义,因为我们更关注在连接查询中驱动表对应的执行计划记录的filtered值
,它决定了被驱动表要执行的次数(即rows * filtered)。
Extra【重点】
顾名思义,
Extra
列是用来说明一些额外信息的,包含不适合在其他列中显示但十分重要的额外信息。我们可以通过这些额外信息来更准确的理解MySQL到底将如何执行给定的查询语句
。
No tables used
:当查询语句的没有FROM
子句时,在Extra
列中将会提示该额外信息。
Impossible WHERE
:查询语句的WHERE
子句永远为FALSE
时,在Extra
列中将会提示该额外信息。
Using where
:当我们使用全表扫描来执行对某个表的查询,并且该语句的WHERE
子句中游针对该表的搜索条件时,在Extra
列中将会提示该额外信息。
(NULL)
:
1、当使用索引访问来执行对某个表的查询,并且该语句的WHERE
子句中有除了该索引包含的列之外的其他搜索条件时,在Extra
列中将会提示该额外信息。
2、有一些情况下对结果集中的记录进行排序是可以使用到索引的,在Extra
列中将会提示该额外信息。
No matching min/max row
:当查询列表中有MIN
或者MAX
聚合函数,但是并没有符合WHERE
子句中的搜索条件的记录时,在Extra
列中将会提示该额外信息。
Select tables optimized away
:当查询列表中有MIN
或者MAX
聚合函数,并且有符合WHERE
子句中的搜索条件的记录时,在Extra
列中将会提示该额外信息。
Using index
:当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引的情况下,在Extra
列中将会提示该额外信息。
Using index condition
:有些搜索条件中虽然出现了索引列,但却不能使用到索引,在Extra
列中将会提示该额外信息。
Using where:Using join buffer(hash join)
:在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MyQL一般会为其分配一块叫join buffer
的内存块来加快查询速度,也就是我们所讲的基于块的嵌套循环算法
,在Extra
列中将会提示该额外信息。
Using where:Not exists
:当我们使用左(外)连接时,如果WHERE
子句中包含要求被驱动表的某个列等于NULL
值的搜索条件,而且那个列又是不允许存储NULL
值的,在Extra
列中将会提示该额外信息。
Using union(...):Using where
:如果执行计划的Extra
列出现了Using intersect(...)
提示,说明准备使用Intersect
索引合并的方式执行查询,括号中的...
表示需要进行索引合并的索引名称;如果出现了Using union(...)
提示,说明准备使用Union
索引合并的方式执行查询;出现Using sort union(...)
提示,说明准备使用Sort-Union
索引合并的方式执行查询。
Zero limit
:当我们LIMIT
子句的参数为0
时,表示压根儿不打算从表中读出任何记录,在Extra
列中将会提示该额外信息。
Using filesort
:很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,MySQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序(英文名:filesort
)。如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的Extra
列中显示Using filesort
提示。
Using tomporary
:在许多查询的执行过程中,MySQL可能会借住临时表来完成一些功能,比如去重、排序之类的,比如我们在执行许多包含DISTINCT、GROUP BY、UNION
等子句的查询过程中,如果不能有效利用索引来完成计划的Extra
列将会显示Using temporary
提示。
EXPLAIN四种输出格式
EXPLAIN可以输出四种格式:
传统格式、JSON格式、TREE格式、以及可视化输出
。用户可以根据需要选择适用于自己的格式。
传统格式
传统格式简单明了,输出是一个表格形式,概要说明查询计划。
JSON格式
传统格式中介绍的EXPLAINI
语句输出中缺少了一个衡量执行计划好坏的重要属性–成本
。而JSON格式是四种格式里面输出信息最详尽的格式,里面包含了执行的成本信息。
JSON格式:在EXPLAIN单词和真正的查询语句中间加上FORMAT=JSON
。
SHOW WARNINGS的使用
在我们使用EXPLAIN
语句查看了某个查询的执行计划后,紧接着还可以使用SHOW WARNINGS
语句查看与这个查询执行计划有关的一些扩展信息。
大家可以看到
SHOW WARNINGS
展示出来的信息有三个字段,分别是Level、Code、Message
。我们最常见的就是Code为1003的信息,当Code值为1003时,Message字段展示的信息类似于查询优化器将我们查询语句重写后的语句。
运算符 | 名称 | 作用 | 示例 |
---|---|---|---|
IS NULL | 为空运算符 | 判断值或表达式是否为空 | SELECT 1 IS NULL,NULL IS NULL; |
IS NOT NULL | 不为空运算符 | 判断值或表达式是否不为空 | SELECT I IS NOT NULL,NULL IS NOT NULL; |
LEAST | 最小运算符 | 在多个值里面返回最小值 | SELECT LEAST(‘A’,‘G’,‘P’); |
GREATEST | 最大运算符 | 在多个值里面返回最大值 | SELECT GREATEST(‘A’,‘G’,‘P’); |
BETWEEN 数字下限 AND 数字上限 | 两者之间运算符 | 在下限和上限之间取值 | SELECT * FROM employees WHERE salary BETWEEN 8000 AND 12000; |
ISNULL | 为空运算符 | 判断值或表达式是否为空 | SELECT 1 ISNULL; |
IN | 属于运算符 | 判断值或者表达式是否是列表中的一个值 | SELECT 1 IN(1,2,3); |
NOT IN | 不属于运算符 | 判断表达式或者值是否不是列表中的一个值 | SELECT 2 NOT IN(1,2,3); |
LIKE | 模糊匹配运算符 | 判断一个值是否符合模糊匹配规则 | SELECT * FROM employees WHERE last_name LIKE ‘%a%’; |
REGEXP | 正则表达式运算符 | 判断一个值是否符合正则表达式规则 | SELECT ‘xykstart’ REGEXP ‘^x’,‘xykstart’ REGEXP ‘$t’,‘xykstart’ REGEXP ‘st’, |
RLIKE | 正则表达式运算符 | 判断一个值是否符合正则表达式规则 | SELECT ‘zzzz’ RLIKE ‘^z’; |
一个同时插入多行记录的INSERT语句等同于多个单行插入的INSERT语句,但是多行的INSERT语句在处理过程中效率更高。因为MySQL执行单条INSERT语句插入多行数据比使用多条INSERT语句快,所以在插入多条记录时
最好选择使用单条INSERT语句的方式插入
。
1.查询缓存::Server如果在查询缓存中发现了这条SQL语句,就会直接将结果返回给客户端;如果没有就进入到解析器阶段,需要说明的是,因为查询缓存往往效率不高,所以在MySQL8.0之后就抛弃了这个功能。
2.解析器:在解析器中对SQL语法进行语法分析、语义分析。
3.优化器:在优化器中会确定SQL语句的执行路径,比如是根据全表检索
还是根据索引检索
等。
经过了解析器,MYSQL就知道你要做什么了。在开始执行之前,还要经过优化器的处理。一条查询可以由很多种执行方式,最后都返回了相同的结果。优化器的作用就是找到这其中最好的执行计划。
SQL查询优化分为两个部分,逻辑查询优化
和物理查询优化
。
逻辑查询优化则是通过SQL等价变换
提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高。
物理查询优化则是通过索引
和表连接方式
等技术来进行优化,这里终点需要掌握索引的使用。
4.执行器:截至到现在,还没有真正去读写真实的表,仅仅只是产出了一个执行计划,于是就进入了执行器阶段。在执行之前需要判断该用户是否具备权限,如果没有,就会返回权限错误。如果具备权限,就执行SQL查询并返回结果。在MySQL8.0以下的版本,如果设置了查询缓存,这时会将查询结果进行缓存。
SQL语句在MySQL中的流程是:SQL语句->查询缓存->解析器->优化器->执行器
。
默认数据页大小为
16kb
索引按照物理实现方式,索引可以分为2中:聚簇(聚集)和非聚簇(非聚集)索引。我们也把聚簇索引称为二级索引和辅助索引。
聚簇索引
并不是一种单独的索引类型,而是一种数据存储方式
(所有的用户记录都存储在了叶子节点),也就是所谓的索引及数据,数据及索引
。
术语“聚簇”表示数据和相邻的键值聚簇的存储在一起。
特点:
1、使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
页内
的记录是按照主键的大小顺序排成一个单向链表
。
各个存放用户记录的页
也是根据页中用户记录的主键大小顺序排成一个双向链表
。
存放目录项记录的页
分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表
。
2、B+树的
叶子节点
存储的是完整的用户记录。
所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。
我们把具有这两种特性的B+树称为聚簇索引
,所有完整的用户记录都存放在这个聚簇索引
的叶子节点处。这种聚簇索引并不需要我们在MySQL语句中显式的使用INDEX
语句去创建,InnoDB
存储引擎会自动
为我们创建聚簇索引。
优点:
数据访问更快
,因为聚簇索引将索引和数据保存在同一个B+树种,因此聚簇索引中获取数据比非聚簇索引更快。
聚簇索引对于主键的排序查找
和范围查找
速度非常快。
按照聚簇索引排列的顺序,查询显示一定范围数据的时候,由于数据都是紧密相连,数据库不用从多个数据块中提取数据,所以节省了大量的io操作
。
缺点:
插入速度严重依赖插入顺序
,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义为一个自增的ID列为主键
。
更新主键的代价很高
,因为将会导致被动更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。
二级索引访问需要两次索引查找
,第一次找到主键值,第二次根据主键值找到行数据。
限制:
对于MySQL数据库目前只有InnoDB数据引擎支持聚簇索引,而MyISAM并不支持聚簇索引。
由于物理存储排序方式只能有一种,所以每个MySQL的表只能有一个聚簇索引
。一般情况下就是该表的主键。
如果没有定义主键,Innodb会选择非空的唯一索引代替,如果没有这样的索引,Innodb会隐式的定义一个主键来作为聚簇索引。
为了充分利用聚簇索引和聚簇的特性,所以Innodb表的主键尽量选用有序的顺序id
,而不建议无序的id,比如UUID、MD5、HASH、字符串列作为主键无法保证数据的顺序增长。
聚簇索引与非聚簇索引原理不同,在使用上也有一些区别:
1、聚簇索引的叶子结点
存储的就是我们的数据记录
,非聚簇索引的叶子节点上存储的是数据位置
。非聚簇索引不会影响数据表的物理存储顺序。
2、一个表只能有一个聚簇索引
,因为只能有一种排序存储方式,但可以有多个非聚簇索引
,也就是多个索引目录提供数据检索。
3、使用聚簇索引的时候,数据的查询效率高,但如果对数据进行插入、删除、更新的操作,效率会比非聚簇索引低。
1、根页面位置万年不动
2、内节点中目录项记录的唯一性
3、一个页面最少存储2条记录
MySQL会自动给每个页里加两条记录,由于这两条记录不是我们自己插入的,所以有时候也称
伪记录
和虚拟记录
。这两个伪记录一个代表最小记录
,一个代表最大记录
。
MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。
从功能逻辑
上说,索引主要有4种,分别是普通索引、唯一索引、主键索引、全文索引。
按照物理实现方式
,索引可以分为2种:聚簇索引和非聚簇索引。
按照作用字段个数
进行划分,分成单列索引和联合索引。
普通索引
在创建索引时,不附加任何条件,用于提高查询效率,这类索引可以创建在
任何数据类型
中,其值是否唯一和非空,要由字段本身的完整性约束条件决定,建立索引后,可以通过索引进行查询。
唯一性索引
使用
UNIQUE参数
可以设置索引的唯一性索引,在创建唯一性索引时,限制该索引必须是唯一的,但允许有空值,在一张数据表中可以有多个
唯一索引。
主键索引
主键索引就是一种
特殊的唯一性索引
,在唯一性索引的基础上增加了不为空的约束,也就是NOT NULL + UNIQUE,一张表里最多只有一个
主键索引。
单列索引
在表中的单个字段上创建索引。单列索引只根据该字段进行索引。单列索引可以是普通索引,也可以是唯一性索引,还可以是全文索引。只要保证该索引只对应一个字段即可,一个表可以有多个单列索引。
多列(组合、联合)索引
多列索引是在表的
多个字段组合
上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行查询,但只有查询条件中使用了这些字段的第一个字段时才会被使用。例如,在表中的字段id、name和gender上建立了一个多列索引idx_id_name_gender
,只有在查询条件中使用了字段id时该索引才会被使用,使用组合索引时遵循最左前缀集合
。
全文索引
全文索引(也称全文检索)是目前
搜索引擎
使用的一种关键技术。他能利用【分词技术
】等多种算法智能分析出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。全文索引非常适合大型数据集,对于小的数据集,他的用处比较小。
随着大数据时代的到来,关系型数据库应对全文索引的需求已力不从心,逐渐被solr、ElasticSearch
等专门的搜索引擎所替代。
补充:空间索引
使用
参数SPATIAL
可以设置索引为空间索引
。空间索引只能建立在空间数据类型上,这样可以提高系统获取空间数据的效率。MySQL中的空间数据类型包括GEOMETRY、POINT、LINSTRING和POLYGON等。目前只有MyISAM存储引擎支持空间检索,而且索引的字段不能为空值,这类索引很少会用到。
1、字段的数值有唯一性限制
索引本身可以起到约束的作用,比如唯一索引、主键索引都是可以起到唯一性约束的,因此在我们数据表中,如果
某个字段是唯一性的
,就可以直接创建唯一性索引
,或者主键索引
,这样可以更快速的通过该索引来确定某条记录。
业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)
说明:不要以为唯一索引影响了insert速度,这个速度损耗可以忽略,但提高查询速度是明显的。
2、频繁作为WHERE查询条件的字段
某个字段在SELECT语句的WHERE条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在数据量大的情况下,创建普通索引就可以大 幅提升数据查询的效率。
3、经常GROUP BY 和 ORDER BY的列
索引就是让数据按照某种顺序进行存储或检索,因此当我们使用GROUP BY对数据进行分组查询,或者使用ORDER BY对数据进行排序的时候,就需要
对分组或排序的字段进行索引
。如果分组或排序的列有多个,那么可以在这些列上建立组合索引
。
4、UPDATE、DELETE的WHERE条件列
对数据按照某个条件进行查询后在进行UPDATE或DELETE的操作,如果为WHERE字段创建了索引,就能大幅提升效率。原理是因为我们先根据WHERE条件列检索出来这条记录,然后再对它进行更新或删除。
如果进行更新的时候,更新的字段是非索引字段,提升效率会更明显,这是因为非索引字段更新不需要对索引进行维护。
5、DISTINCT字段需要创建索引
有时候我们需要对某个字段进行去重,使用DISTINCT,那么对这个字段创建索引,也会提升查询效率。
6、多表JOIN连接操作时,创建索引注意事项
首先,
连接表的数量尽量不要超过3张
,因为每增加一张表就相当于增加了一次嵌套循环,数量级增长非常快,严重影响查询的效率。
其次,对WHERE条件创建索引
,因为WHERE才是对数据条件的过滤。如果在数据量非常大的情况下,没有WHERE条件过滤是非常可怕的。
最后,对于连接的字段创建索引
,并且该字段在多张表中类型必须一致
。
7、使用列的类型小的创建索引
我们这里所说的
类型大小
指的就是该类型表示的数据范围的大小。
我们在定义表结构的时候要显式的指定列的类型,以整数类型为例,有TINYINT、MEDIUMINT、INT、BIGINT
等,它们占用的存储空间是依次递增,能表示的整数范围当然也是依次递增。如果我们想对某个整数列建立索引的话,在表示整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用INT
就不要使用BIGINT
,能使用MEDIUMINT
就不要使用INT
。
数据类型越小,在查询时进行的比较操作越快。
数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘IO带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中了,从而加快读写效率。
这个建议对于表的主键来说更加适用
,因为不仅是聚簇索引中的存储主键值,其他所有二级索引的节点处都会存储一份记录的主键值,如果主键值使用更小的数据类型 ,也就意味着节省了更多的存储空间和更高效的IO。
8、使用字符串前缀创建索引
假设我们字符串很长,那么存储一个字符串就需要占用很大的存储空间,在我们需要为这个字符串建立索引时,那就意味着在对应的B+树种有这么两个问题:
B+树索引的记录需要把该列完整字符串存储起来,更费时。而且字符串越长,在索引中占用的存储空间越大
。
如果B+树索引中索引存储的字符串很长,那在做字符串比较时会占用更多的时间
。
我们可以通过截取字段前面一部分内容建立索引,这个就叫前缀索引
。这样在查找记录时虽然不能精确的定位到记录的位置,但能定位到相应前缀所在的位置,然后根据前缀相同的记录的主键值回表查询完整的字符串值。既节约空间
,又减少了字符串的比较时间
,还大体能解决排序的问题。
例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间,如果只检索字段前面若干字符,这样可以提高检索速度。
创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引:
create table shop(address varchar(120) not null);
alter table shop add index(address(12));
问题是,截取多少呢?截取的多了,达不到节省索引存储空间的目的;截取的少了,重复内容太多,字段的散列度(选择性)会降低。
怎么计算不同的长度的选择性呢?
先看一下字段在全部数据中的选择度:
select count(distinct address)/count(*) from shop;
通过不同的长度去计算,与全表的选择性对比:
公式:
select count(distinct left(列名,索引长度))/count(*);
例如:
select count(distinct left(address,10))/count(*) as sub10, -- 截取前10个字符的选择度
count(distinct left(address,15))/count(*) as sub15, -- 截取前15个字符的选择度
count(distinct left(address,20))/count(*) as sub20, -- 截取前20个字符的选择度
count(distinct left(address,25))/count(*) as sub25 -- 截取前25个字符的选择度
from shop;
引申另一个问题:索引列前缀对排序的影响
如果使用了索引列前缀,比方说前边只把address列的前12个字符
放到了二级索引中,下边这个查询可能就有点尴尬了:
SELECT * FROM shop
ORDER BY address
LIMIT 12;
因为二级索引不包含完整的address列信息,所以无法对前12个字符相同,后边的字符不同的记录进行排序,也就是使用索引列前缀的方式
无法支持使用索引排序
,只能使用文件排序。
Alibaba《Java开发手册》
【强制】在varcahr字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分别决定索引长度。
说明:索引的长度与区分长度是一堆矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上
,可以使用count(distinct left(列名,索引长度))/count(*)的区分度来确定,越靠近1越好
。
9、区分度高(散列性高)的列适合作为索引
列的基数
指的是某一列中不重复数据的个数,比方说某个列包含值2,5,8,2,5,8,2,5,8
虽然有9
条记录,但该列的基数却是3
。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列的值越集中。
这个列的基数指标非常重要,直接影响我们是否能有效的利用索引。最好为列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。
10、使用最频繁的列放到联合索引的最左侧
这样可以较少的建立一些索引。同时,由于“最左前缀原则”,可以增加索引的使用率。
11、在多个字段都创建索引的情况下,联合索引优于单值索引
在实际工作中,我们也需要注意平衡,索引的数目不是越多越好,我们需要限制每张表上的索引数量,建议单张表索引数量
不超过6个
。原因:
1、每个索引都需要占用磁盘空间
,索引越多,需要的磁盘空间就越大。
2、索引会影响INSERT、DELETE、UPDATE等语句的性能
,因为表中的数据更改的同时,索引也会进行调整和更新,会造成负担。
3、优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估
,以生成 出一个最好的执行计划,如果同时有很多个索引都可以用于查询,会增加MySQL优化器生成执行计划时间,降低查询性能。
1、在WHERE中使用不到的字段,不要设置索引
WHERE条件(包括GROUP BY、ORDER BY)里用不到的字段不需要创建索引,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的。
2、数据量小的表最好不要使用索引
如果表记录太少了,比如少于1000个,那么是不需要创建索引的。表记录太少,是否创建索引
对查询效率的影响并不大
。甚至说,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。
3、有大量重复数据的列上不要建立索引
在条件表达式中经常用到的不同值较多的列上建立索引,但字段中如果有大量重复数据,也不用创建索引。比如在学生表的
“性别”
字段上只有“男”与“女 ”两个不同的值,因此无需创建索引。如果建立索引,不但不会提高查询效率,反而会严重降低数据更新速度
。
索引的价值是帮助快速定位,如果定位的数据有很多,那么索引就失去了它的使用价值,当数据重复度大,比如高于10%
的时候,也不需要对这个字段使用索引。
4、避免对经常更新的表创建过多的索引
第一层含义:频繁更新的字段不一定要创建索引。因为更新数据的时候,也需要更新索引,如果索引太多,在更新索引的时候也会造成负担,从而影响效率。
第二层含义:避免对经常更新的表创建过多的索引,并且索引中的列尽可能少。因此,虽然提高了查询速度,同时却会降低更新表的速度。
5、不建议用无序的值作为索引
例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等。
6、删除不再使用或者很少使用的索引
表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。
7、不要定义冗余或重复的索引
1、冗余索引
有时候有意或者无意的就对同一个列创建了多个索引,比如:index(a,b,c)相当于index(a)、index(a,b)、index(a,b,c)。
举例:建表语句如下:
我们知道,通过idx_name_birthday_phone_number
索引就可以对name
列进行快速搜索,在创建一个专门针对name
列的索引就算是一个冗余索引
,维护这个索引只会增加维护的成本,并不会对搜索有什么好处。
2、重复索引
另一种情况,我们可能会对某个列
重复建立索引
,比方说这样:
我们看到,col1即是主键、又给它定义为一个唯一索引,还给它定义了一个普通索引,可是主键本身就会生成聚簇索引,所以定义的唯一索引和普通索引是重复的,这种情况要避免。
SQL语句是否使用索引,跟
数据库版本、数据量、数据选择度
都有关系。
过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段就无法被使用;索引文件具有
B-Three的最左前缀匹配特性
,如果左边的值未确定,那么无法使用此索引。
计算、函数、类型转换(自动或手动)
导致索引失效。
范围条件右边的列
索引失效,应用开发范围查询中,例如:金额查询、日期查询往往都是范围查询,应将查询条件放置where语句最后
。(创建的联合索引中,务必把范围涉及到的字段写在最后
)
不等于(
!=
或者<>
)操作后获取的结果集在总结果集中占据的比例也是关键因素,如果返回的结果集超过全表的10%-30%时,优化器可能会放弃走索引。
IS NULL
可以使用索引,IS NOT NULL
无法使用索引。
LIKE
以通配符%开头导致索引失效。
【强制】页面搜索严禁左模糊或全模糊,如果需要请走搜索引擎来解决。
OR
前后存在非索引的列,索引失效。
数据库和表的字符集统一使用utf8mb4
,统一字符集可以避免由于字符集转换产生的乱码。不同的字符集
,进行比较前需要进行转换
会造成索引失效。
一般性建议
对于单列索引,尽量选择针对当前query过滤性更好的索引。
在选择组合索引的时候,当前query中过滤性最好的字段在索引顺序中,位置越靠前越好。
在选择组合索引的时候,尽量选择能够包含当前query中where子句中更多字段的索引。
在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面。
总之,书写SQL语句时,尽量避免造成索引实效的情况。
如果只能添加一条索引的情况下,一定要添加给被驱动表,当然给主驱动表、被驱动表都加上索引是更好的选择,但
连接字段的类型一定一致
,否则会涉及函数转换导致索引失效。
对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表出现。
对于内连接来讲,如果表的连接条件中只能有一个字段作为索引,则有索引的字段所在的表作为被驱动表。
对于内连接来说,在两个表的连接条件都存在索引的情况下,会选择数据量少的表作为驱动表“小表驱动大表”。
对于外连接来说,在特殊情况下也会被查询优化器转换为内连接。
JOIN语句原理
JOIN方式连接多个表,本质上就是哥哥表之间的数据循环匹配。MySQL5.5版本之前,MySQL只支持一种表间关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则join关联的执行时间会非常长。MySQL5.5以后的版本中,MySQL通过引入BNLJ算法来优化嵌套执行。
驱动表和被驱动表
驱动表就是主表,被驱动表就是从表,费驱动表。
Simple Nested-Loop Join(简单嵌套循环连接)
算法相当简单,从表A中取出一条数据1,遍历表B,将匹配到的数据放到result…以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断:
可以看到这种方式效率非常低级,以上述表A数据100条,表B数据1000条计算,则A*B=10万次。开销统计如下:
当然MySQL肯定不会这么粗暴的去进行表的连接,所以就出现了后面两种对Nested-Loop Join优化算法。
Index Nested-Loop Join(索引嵌套循环连接)
Index Nested-Loop Join其优化的思路主要是为了
减少内层表数据的匹配次数
,所以要求在被驱动表上必须有索引
才行。通过外层表匹配条件直接与内层表索引进行匹配,避免了内层表的每条记录去进行比较,这样极大的减少了对内层表匹配的次数。
驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本比较固定,故MySQL优化器都倾向于使用记录数少的表作为驱动表(外表)。
Block Nested-Loop Join(块嵌套循环链接)
如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了。每次访问被驱动表,其表的记录都会被加载到内存中,然后再从被驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后被驱动表的记录在加载到内存匹配,这样周而复始,大大增加了IO的次数,为了减少被驱动表IO的次数,就出现了Block Nested-Loop Join的方式。
不再是逐条获取驱动表的数据,而是一块一块的获取,引入了
join buffer缓冲区
,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每一条记录一次性和join buffer中所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合成一次,降低了被驱动表的访问频率。
注意:
这里缓存的不只是关联表的列,select后面的列也会缓存起来。
在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量减少不必要的字段,可以让join buffer中可以存放更多的列。
参数设置:block_nested_loop
通过show variables like '%optimizer_switch%'
查看block_nested_loop
状态。默认是开启的。
join_buffer_size
驱动表能不能一次加载完,要看join buffer能不能存储所有的数据,默认情况下join_buffer_size=256k
。
join_buffer_size的最大值在32位系统可以申请4G,而在64位操作系统下可以申请大于4G的Join Buffer空间(64位Windows除外,其大值会被截断为4GB并发出警告)。
Join小结
1、
整体效率比较:INLJ > BNLJ > SNLJ
2、永远用小结果集驱动大结果集(其本质就是减少外层循环的数据数量)(小的度量单位指的是 表行数*每行大小)。
3、为了被驱动表匹配的条件增加索引(减少内层表的循环匹配次数)。
4、增大join buffer size的大小(一次缓存的数据越多,那么内层包的扫描次数就越少)。
5、减少驱动表不必要的字段ch挨训(字段越少,join buffer所缓存的数据就越多)。
Hash Join
从MySQL的8.0.20版本开始将废弃BNLJ,因为MySQL8.0.18版本开始就加入了hash join默认都会使用hash join
Nested Loop:
对于被连接的数据子集较少的情况,Nested Loop是个较好的选择。
Hash Join是做大数据集连接
时的常用方式,优化器使用两个表中较小(相对较小)的表利用Join key在内存中建立散列表
,然后扫描较大的表被探测散列表,找出Hash表匹配的行。
这种方式适用于较小的表可以放入内存,这时优化器将它分割成若干不同的分区
,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/O的性能。
它能够很好的工作没有索引的大表和并查询的环境中,并提供最好的性能。大多数人都说它是Join的重型升降机。Hash Join只能应用于等值连接(如WHERE A.COL1 = B.COL2),这是由Hash的特点决定的。
MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT语句查询的结果作为另一个SELECT语句的条件。
子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作
。
子查询是MySQL的一项重要的功能,可以帮助我们通过一个SQL语句实现比较复杂的查询。但是子查询的执行效率不高。
原因:
1、执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表
,然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表
。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
2、子查询的结果集存储的临时表,不论内存临时表还是磁盘临时表都不会存在索引
,所以查询性能会受到一定的影响。
3、对于返回结果集比较大的子查询,其对查询性能的影响也就越大。
在MySQL中,可以使用连接(JOIN)查询来替代子查询。
连接查询不需要建立临时表
,其速度比查询要快
,如果查询中使用索引的话,性能就会更好。
尽量不要使用NOT IN或者NOT EXISTS,用LEFT JOIN xxx ON xxx WHERE xx IS NULL替代。
问题:在WHERE条件字段上加索引,但为什么在ORDER BY字段上还要加索引呢?
回答:
在MySQL中,支持两种排序方式,分别是FileSort
和Index
排序。
FileSort排序则一般在内存中
进行排序,占用CPU较多
。如果待排序结果较大,会产生临时文件I/O到磁盘进行排序的情况,效率较低。
优化建议:
1、SQL中, 可以在WHERE子句和ORDER BY子句中使用索引,目的是在WHERE子句中避免全表扫描
,在ORDER BY子句避免使用FileSort排序
。当然,某些情况下全表扫描,或者FileSort排序不一定比索引慢,但总的来说,我们还是要避免,以提高查询效率。
2、尽量使用Index完成ORDER BY排序。如果WHERE和ORDER BY后面是相同的列就使用单索引列;如果不同就使用联合索引。
3、无法使用Index时,需要对FileSort方式进行调优。
ORDER BY时不LIMIT则索引失效,除非不涉及回表操作,仅查询索引列。
ORDER BY时规则不一致,索引失效(顺序错,不索引;方向反,不索引)。
ORDER BY时,无过滤,不索引,除非使用LIMIT。
结论:
1、两个索引同时存在,MySQL自动选择最优的方案。
2、当【范围条件】和【group by或者order by】的字段出现二选一时,优先观察条件的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上,反之,依然。
GROUP BY使用索引的原则几乎跟ORDER BY一致,GROUP BY即时没有过滤条件用到索引,也可以直接使用索引。
GROUP BY先排序在分组,遵照索引建的最佳左前缀法则。
当无法使用索引列,增大max_length_for_sort_data
和sort_buffer_size
参数的设置。
WHERE效率高于HAVING,能写在WHERE限定的条件就不要写在HAVING中了。
减少使用ORDER BY,和业务沟通能不排序就不排序,或将排序放到程序端去做。ORDER BY、GROUP BY、DISTINCT这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
包含了ORDER BY、GROUP BY、DISTINCT这些查询的语句,WHERE条件过滤出来的结果集请保持在1000行以内
,否则SQL会很慢。
一般分页查询时,通过创建覆盖索引能够比较好地提高性能。一个常见又非常头疼的问题就是
limit 2000000,10
,此时需要MySQL排序前2000010记录,仅仅返回2000000-2000010的记录,其他记录丢弃,查询排序的代价非常大。
优化思路一
在索引上完成排序分页操作,最后根据主键关联回原表查询所需的其他列内容。
EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY LIMIT 2000000,10) a WHERE t.id = a.id;
优化思路二
该方案适用于主键自增的表,可以把LIMIT查询转换成某个位置的查询。
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;
一个索引包含了满足查询结果的数据就叫做覆盖索引,简单说就是
索引列+主键
包含SELECT到FROM之间查询的列
。
覆盖索引的利弊
好处:
1、避免Innodb表进行索引的二次查询(回表)
Innodb是以聚集索引的顺序来存储的,对于Innodb来说,二级索引在叶子结点中所保存的是行的主键信息,如果是二级索引查询数据,在查找相应的键值后,还需通过主键进行二次查询才能获取我们真实所需要的数据。
在覆盖索引中,二级索引的键值可以获取所要的数据,避免了对主键的二次查询,减少了IO操作
,提升了查询效率。
2、可以把随机IO变成顺序IO加快查询效率
由于覆盖索引是按键值的顺序存储的,对于IO密集型的范围查询来说,对于随机从磁盘读取每一行的数据IO要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的IO
转变索引查询的顺序IO
。
弊端:
索引字段的维护
总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务DBA,或者成为业务数据架构师的工作。
Index Condition Pushdown(ICP)是MySQL5.6中新特性,是一种在存储引擎层使用索引过滤数据的优化方式。
如果没有ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给MySQL服务器,由MySQL服务器评估WHERE
后面的条件是否保留行。
启用ICP后,如果部分
WHERE
条件可以仅使用索引中的列进行筛选,则MySQL服务器会把这部分WHERE
条件放到存储引擎筛选,然后存储引擎通过使用索引条目来筛选数据,并且只有在满足这一条件时才从表中取行。
好处:ICP可以减少存储引擎必须访问基表的次数和MySQL服务器必须访问存储引擎的次数。
但是,ICP的加速效果
取决于在存储引擎内通过ICP筛选
掉的数据比例。
ICP的开启/关闭
默认情况下启用索引条件下推。可以通过设置系统变量
optimizer_switch
控制:
index_condition_pushdown
# 打开索引下推
SET optimizer_switch = 'index_condition_pushdown=off';
# 关闭索引下推
SET optimizer_switch = 'index_condition_pushdown=on';
当使用索引下推时,
EXPLAIN
语句输出结果中Extra
列内容显示为Using index condition
。
ICP的使用条件
1、如果表访问的类型为range、ref、eq_ref和ref_or_null可以使用ICP。
2、ICP可以用于InnoDB
和MyISAM
表,包括分区表InnoDB
和MyISAM
表。
3、对于InnoDB
表,ICP仅用于二级索引
。ICP的目标是减少全行读取次数,从而减少I/O操作。
4、当SQL使用覆盖索引时,不支持ICP。因为这种情况下使用ICP不会减少I/O。
5、相关子查询的条件不能使用ICP。
EXISTS和IN的区分
索引是个前提,实际选择与否还是要看表的大小。你可以选择的标准理解为
小表驱动大表
。在这种方式下效率是最高的。
比如下面这样:
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
SELECT * FROM A WHERE EXISTS(SELECT cc FROM B WHERE B.cc=A.cc)
当A小于B时,用EXISTS。因为EXISTS的实现,相当于外表循环,实现的逻辑类似于:
for i in A
for j in B
if j.cc == i.cc then ...
当B小于A时用IN,因为实现的逻辑类似于:
for i in B
for j in A
if j.cc = i.cc then ...
哪个表小就用哪个表来驱动,A表小就用EXISTS,B表小就用IN。
COUNT(*)与COUNT(1)与COUNT(具体字段)效率
问:在MySQL中统计数据表的行数,可以使用三种方式:SELECT COUNT(*)、SELECT COUNT(1)和SELECT (具体字段),使用这三者之间的查询效率是怎样的?
答:
前提:如果你要统计的是某个字段的非空数据行数,则另当别论,毕竟比较执行效率的前提是结果一样才可以。
环节1:
COUNT(*)
和COUNT(1)
都是对所有结果进行COUNT
,COUNT(*)
和COUNT(1)
本质上并没有区别(二者)执行时间可能略有差别,不过你还是可以把它俩的执行效率看成是相等的。如果有WHERE子句,则是对所有符合筛选条件的数据行进行统计;如果没有WHERE子句,则是对数据表的数据行进行统计。
环节2:如果是MyISAM存储引擎,统计数据表的行数只需要
0(1)
的复杂度,这是因为每张表MyISAM的数据表都有一个meta信息存储了row_count
值,而一致性则由表级锁来保证。
如果是InnoDB存储引擎,因为InnoDB支持事务,采用行级锁和MVCC机制,所以无法像MyISAM一样,维护一个row_count变量,因此需要采用
扫描全表
,是0(n)
的复杂度,进入循环+计数的方式来完成统计。
环节3:在InnoDB引擎中,如果采用
COUNT(具体字段)
来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于COUNT(*)
和COUNT(1)
来说,他们不需要查找具体的行,只是统计行数,系统会自动
采用占用空间更小的二级索引来进行统计。
如果有多个二级索引,会使用key_len小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。
关于SELECT(*)
在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT<字段列表>查询。原因:
1、MySQL在解析的过程中,会通过查询数据字典
将“*
”按序转换成所有列名,这会大大的耗费资源和时间。
2、无法使用覆盖索引
。
LIMIT 1 对优化的影响
针对的是会扫描全表的SQL语句,如果你可以确定结果集只有一条,那么加上
LIMIT 1
的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。
如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上LIMIT 1
了。
多使用COMMIT
只要有可能,在程序中尽量多使用COMMIT,这样程序的性能得到提高,需求会因为COMMIT所释放的资源而减少。
COMMIT所释放的资源:
回滚段上用于恢复数据的信息
被程序语句获得的锁
redo / undo log buffer 中的空间
管理上述3中资源中的内部花费
MySQL默认是开启自动提交
的,即每一条DML(增删改)语句都会被作为一个单独的事务进行隐式提交。
自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现了各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:
1、可靠性不高
存在自增ID回溯的问题,这个问题直到最新版本的MySQL8.0才修复。
2、安全性不高
对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。
3、性能差
自增ID的性能较差,需要在数据库服务端生成。
4、交互多
业务还需要额外执行一次类似last_insert_id()
的函数才能知道刚才插入的自增值,这需要多一次网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。
5、局部唯一性
最重要的一点,自增ID是局部唯一,只在当数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦。
主键设计至少应该是
全局唯一
且单调递增
。全局唯一保证在各系统之间都是唯一的,单调递增是希望插入时不影响数据库性能。
在关系型数据库中,关于数据表设计的基本原则、规则就称为范式。
可以理解为,一张数据表的设计结构需要满足的某种设计标准的级别
。想要设计一个结构合理的关系型数据库,必须满足一定的范式。
范式的英文名是
Noraml Form
,简称NF
。它是英国人E.F.Codd在上个世纪70年代提出关系数据库模型后总结出来的。范式是关系数据库理论的基础,也是我们在设计数据库结构过程中所要遵循的规则
和指导方法
。
目前关系型数据库有六种常见范式,按照范式级别,从低到高分别是:
第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF,又称完美范式)。
数据库的范式设计越高阶,冗余度就越低,同时高阶的范式一定符合低阶范式的要求,满足最低要求的范式是第一范式(1NF)。在第一范式的基础上进一步满足更多规则要求的称为第二范式(2NF),其余范式以此类推。
一般来说,在关系型数据库设计中,最高也就遵循到
BCNF
,普遍还是3NF
。但也不绝对,有时候为了提高某些查询性能,我们还需要破坏范式规则,也就是反规范化
。
范式的定义会使用到主键和候选键,数据库中的键(Key)由一个或者多个属性组成。数据表中常用的几种键和属性的定义:
超键
:能唯一标识元组的属性集叫做超键。
候选键
:如果超键不包括多余的属性,那么这个超键就是候选键。
主键
:用户可以从候选键中选择一个作为主键。
外键
:如果数据表R1中的某属性集不是R1的主键,而是另一个数据表R2的主键,那么这个属性集就是数据表R1的外键。
主属性
:包含在任一候选键中的属性称为主属性。
非主属性
:与主属性相对,指的是不包含在任何一个候选键的属性。
通常,我们也将候选键称之为“码
”,把主键也称为“主码
”。因为键可能是由多个属性组成的,针对单个属性,我们还可以用主属性和非主属性来进行区分。
第一范式主要是确保数据表中每一个字段值必须具有
原子性
,也就是说数据表中每个字段的值为不可再次拆分
的最小数据单元。
我们再设计某个字段的时候,对于字段X来说,不能把字段X-1和字段X-2,事实上,任何的DBMS都会满足第一范式的要求,不会将字段进行拆分。
第二范式要求,在满足第一范式的基础上,还要
满足数据表里每一条数据记录,都是可唯一标识的。而且所有非主键字段,都必须完全依赖主键,不能只依赖主键的一部分
。如果知道主键的所有属性的值,就可以检索到任何元组(行)的任何属性的任何值。(要求中的主键,其实可以扩展替换为候选键)。
小结:第二范式要求实体的属性完全依赖主关键字。如果存在不完全依赖,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与元实体之间是一对多的关系。
第三范式是在第二范式的基础上,确保数据表中的每一个非主键字段和主键字段直接相关,也就是说,
要求数据表的所有非主键字段不能依赖于其他非主键字段
。(即,不能存在非主属性A依赖于非主属性B,非主属性B依赖于主键C的情况,即存在“A->B->C"的决定关系)通俗地讲,该规则的意思是所有非主键属性
之间不能有依赖关系,必须相互独立
。
这里的主键可以拓展为候选键。
小结:符合第三范式后的数据模型通俗地讲,第二范式和第三范式通常以这句话概括:“每个非主键属性依赖于键,依赖于整个键,并且除了键别无他物”。
关于数据表的设计,有三个范式要遵循。
1、第一范式,确保每列保持原子性
。
数据库的每一列都是不可分割的原子数据项,不可再分的最小数据单元,而不能是集合、数组、记录等非原子数据项。
2、第二范式,确保每列都和主键
完全依赖
。
尤其在复合主键的情况下,非主键部分不应该依赖于部分主键。
3、第三范式,确保每列都和主键列
直接相关
,而不是间接相关。
范式的优点:
数据的标准化有助于消除数据库中的数据冗余
,第三范式通常被人为在性能、扩展性和数据完整性方面达到了最好的平衡。
范式的缺点:
范式的使用,可能降低查询的效率
。因为范式等级越高,设计出来的数据表就越多、越精细,数据的冗余度就越低,进行数据查询的时候就可能需要关联多张表
,这不但代价昂贵,也可能使一些索引策略无效
。
范式只是提出了设计的标准,实际上设计数据表时,未必一定要符合这些标准。开发中,我们出现为了性能和读取效率违反范式化的原则,通过
增加少量冗余
或重复的数据来提高数据库读性能
,减少关联查询,join表的次数,实现空间换取时间
的目的。因此在实际的设计过程中要理论结合实际,灵活运用。
范式本身没有优劣之分,只有适合场景不同。没有完美的设计,只有合适的设计,我们在数据表的设计中,还需要根据需求将范式和反范式混合使用。
有的时候不能简单按照规范要求设计数据表,因为有的数据看似冗余,其实对于业务来说时分重要。这个时候,我们就要遵循
业务优先
的原则,首先满足业务需求,再尽量减少冗余。
如果数据库中的数据量比较大,系统的UV和PV访问频次比较高,则完全按照MySQL的三大范式设计数据库,读数据时产生大量的关联查询,在一定程度上会影响数据库的读取性能。如果我们想对查询效率进行优化,
反范式优化
也是一种优化思路。此时,可以通过在数据表中增加冗余字段
来提高数据库的读性能。
规范化 vs 性能
1、为满足某种商业目标,数据库性能比规范化数据库更重要。
2、在数据规范化的同时,要综合考虑数据库的性能。
3、通过在给定的表中添加额外的字段,以大量减少需要从搜索信息所需的时间。
4、通过再给定的表中插入计算列,以方便查询。
人们在第三范式的基础上进行了改进,提出了
巴斯范式(BCNF),也叫做巴斯 - 科德范式(Boyce-Codd Normal Form)
。BCNF被认为没有新的设计规范加入,只是对第三范式中范式规范要求更强,使得数据库冗余更小。所以,称为是修正的第三范式
,或扩充的第三范式
,BCNF不被称为第四范式。
若一个关系达到了第三范式,并且它只有一个候选键,或者它的每个候选键都是单属性,则该关系自然达到BC范式。
一般来说,一个数据设计符合第三范式或者巴斯范式就可以了。
多值依赖的概念:
多值依赖
即属性之间的一对多关系,记为K->->A。
函数依赖
事实上单值依赖,所以不能表达属性值之间的一对多关系。
平凡的多值依赖
:全集U=K+A,一个K可以对应多于多个A,即->->A。此时整个表就是一组一对多关系。
非平凡的多值依赖
:全集U=K+A+B,一个K可以对应于多个A,也可以对应多个B,A与B互相独立,即K->->A,K->->B。整个表有多组一对多关系,且有:“一”部分是相同的属性集合,“多”部分是互相独立的属性集合。
第四范式即在满足巴斯-科德范式(BCNF)的基础上,消除非平凡且非函数依赖的多值依赖(即把同一表内的多对多关系删除)。
除了第四范式外,我们还有更高级的第五范式(又称完美范式)和域键范式DKNF)。
在满足第四范式(4NF)的基础上,消除了不是由候选键所蕴含的连接依赖。如果关系模式R中的每一个连接依赖均由R的候选键所隐含
,则称此关系模式符合第五范式。
函数依赖是多值依赖的一种特殊的情况,而多值依赖实际上是连接依赖的一种特殊情况。但连接依赖不像函数依赖和多值依赖可以由
语义直接导出
,而是在关系连接运算
时才反映出来。存在连接依赖的关系模式仍可能遇到数据冗余及插入、修改、删除异常等问题。
第五范式处理的是
无损连接问题
,这个范式基本没有实际意义
,因为无损连接很少出现,而且难以察觉。而域键范式视图定义一个终极范式
,该范式考虑所有的依赖和约束类型,但实用价值也是最少的,只存在理论研究中。
数据库设计是牵一发而动全身的。那有没有办法提前看到数据库的全貌呢?比如需要哪些数据表、数据表中应该有哪些字段,数据表与数据表之间有什么关系、通过什么字段进行连接,等等。这样我们才能进行整体的梳理和设计。
其实,ER模型就是一个这样的工具。ER模型也叫做
实体关系模型
,是用来描述现实生活中客观存在的事物、事物的属性,以及事物之间关系的一种数据模型。在开发基于数据库的信息系统的设计阶段,通常使用ER模型来描述信息需求和信息特性,帮助我们清理业务逻辑,从而设计出优秀的数据库。
ER模型中有三个要素,分别是实体、属性和关系。
实体
,可以看做是数据对象,往往对应于现实生活中的真实存在的个体。在ER模型中,在矩形
来表示。实体分为两类,分别是强实体
和弱实体
。强实体是指不依赖于其他实体的实体;弱实体是指对另一个实体有很强的依赖关系的实体。
属性
,则是指实体的特性。比如超市的地址、联系电话、员工数等。在ER模型中用椭圆形
来表示。
关系
,则是指实体之间的联系。比如超市把商品卖给顾客,就是一种超市与顾客之间的联系。在ER模型中用菱形
来表示。
注意:实体和属性不容易区分。这里提供一个原则:我们从系统整体的角度出发去看,
可以独立存在的实体,不可再分的属性
。也就是说,属性不能包含其他属性。
在ER模型的3个要素中,关系又可以分为3种类型,分别是 一对一、一对多、多对多。
一对一
:指实体之间的关系是 一 一 对应的,比如个人与身份证信息之间的关系就是一对一的关系。一个人只能有一个身份证信息,一个身份证信息也只属于一个人。
一对多
:指一边的实体通过关系,可以对应多个另外一边的实体。相反,另外一边的实体通过这个关系,则只能对应唯一的一边的实体。比如说,我们新建一个班级表,而每个班级都有多个学生,每个学生则对应一个班级,一个班级对学生就是一对多的关系。
多对多
:指关系两边的实体都可以通过关系对应多个对方的实体。比如在进货模块中,供货商与超市之间的关系就是多对多关系,一个供货商可以给多个超市供货,一个超市也可以从多个供货商那里采购商品。再比如一个选课表,有许多科目,每个科目有很多学生选,而每个学生又可以选择多个科目,这就是多对多的关系。
1、数据表的个数越少越好
RDBMS的核心在于对实体和联系的定义,也就是E-R图(Entity Relationship Diagram),数据表越少,证明实体和联系设计得越简洁,既方便理解又方便操作。
2、数据表的字段个数越少越好
字段个数越多,数据冗余的可能性越大。设置字段个数少的前提是各个字段互相独立,而不是某个字段的取值可以由其他字段计算出来。当然字段个数少是相对的,我们通常在数据冗余
和检索效率
中进行平衡。
3、数据表中联合主键的字段个数越少越好
设置主键是为了确定唯一性,当一个字段法确定唯一性的时候,就需要采用联合主键的方式(也就是用多个字段来定义一个主键)。联合主键的字段越多,占用的索引空间越大
,不仅会加大理解难度,还会增加运行时间和索引空间,因此联合主键的字段个数越少越好。
4、使用主键和外键越多越好
数据库的设计实际上就是定义各种表,以及各种字段之间的关系。这些关系越多,证明这些实体之间的冗余度越低,利用度越高
。这样做的好处在于不仅保证了数据表之间的独立性
,还能提升相互之间的关联使用率。
“三少一多”原则的核心就是
简单可复用
。简单指的是用更少的表、更少的字段、更少的联合主键字段来完成数据表的设计。可复用则是通过主键,外键的使用来增强数据表之间的复用率。因为一个主键可以理解是一张表的代表。键设计得越多,证明它们之间的利用率越高。
注意:这个原则并不是绝对的,有时候,我们需要牺牲数据的冗余度来换取数据处理的效率。
【强制】库的名称必须控制在32个字符以内,只能使用英文字母、数字和下划线,建议以
英文字母开头
。
【强制】库名英文一律小写
,不同单词采用下划线
分割。须见名知意。
【强制】库的名称格式:业务系统名称_子系统名。
【强制】库名禁止使用关键字(如type,order等)。
【强制】创建数据库时必须显式指定字符集
,并且字符集只能是utf8或者utf8mb4。
【建议】对于程序连接数据库账号,遵循权限最小原则
。
使用数据库账号只能在一个DB下使用,不准跨库。程序使用的账号原则上不准有drop权限
。
【建议】临时库以tmp_
为前缀,并以日期为后缀;备份库以bak_
为前缀,并以日期为后缀。
【强制】表和列的名称必须控制在32个字符以内,表名只能使用英文字母、数字和下划线,建议以
英文字母开头
。
【强制】表名、列名英文一律小写
,不同单词采用下划线
分割。须见名知意。
【强制】表名要求有模块名强相关,同一模块的表名尽量使用统一前缀
。
【强制】创建表时必须显式指定字符集
为utf8和utf8mp4。
【强制】表名、列名禁止使用关键词。
【强制】创建表时必须显式指定表存储引擎
类型。如无特殊要求,一律为InnoDB。
【强制】建表必须要有comment。
【强制】字段命名应尽可能使用表达实际含义的英文单词或缩写
。
【强制】布尔值类型的字段命名为is_描述
。
【强制】禁止在数据库中存储图片、文件等大的二进制数据,只存储文件地址信息。
【建议】建表时关于主键:表必须有主键
。
【建议】核心表必须有行数据的创建时间段
和最后更新时间段
,便于查问题。
【建议】表中所有字段尽量都是NOT NULL
属性,业务可以根据需要定义DEFAULT值
。
【建议】所有存储相同数据的列名和列类型必须一致
(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)。
【建议】中间表(或临时表)用于保存中间结果集,名称为tmp_
开头。备份表用于备份或抓取源表快照,名称以bak_
开头。中间表和备份表定期清理。
【建议】创建表时,可以使用可视化工具。这样可以确保表、字段相关的约定都能设置上。
实际上,我们通常很少自己写DDL语句,可以使用一些可视化工具来创建和操作数据库和数据表。可视化工具除了方便,还能直接帮我们将数据库的结构定义转化成SQL语言,方便数据库和数据表结构的导出和导入。
【强制】InnoDB表必须主键为id int/bigint auto_increment,且主键值
禁止被更新
。
【强制】InnoDB和MyISAM存储引擎表,索引类型必须为BTREE
。
【建议】主键的名称以pk_
开头,唯一键以uni_
或uk_
开头,普通索引以idx_
开头,一律使用小写格式,以字段的名称或缩写作为后缀。
【建议】多单词组组成的columnname,取前几个单词首字母,加末单词组成column_name。
【建议】单个表的索引个数不能超过6个
。
【建议】在建立索引时,多考虑建立联合索引
,并把区分度最高的字段放在最前面。
【建议】在多表JOIN的SQL里,保证被驱动表的连接列上有索引,这样JOIN执行效率最高。
【建议】建表或加索引时,保证表里互相不存在冗余索引
。
【强制】程序端SELECT语句必须指定具体字段名称,禁止写成
*
。
【建议】程序端insert语句指定具体字段名称,不要写成INSERT INTO t1 VALUES(…)。
【建议】除静态表或小表(100行以内),DML语句必须有WHERE条件,且索引查找。
【建议】INSERT INTO …VALUES(xx),(xx),(xx)…这里的xx的值不要超过5000个。值过多虽然上线很快,单会引起主从同步延迟。
【建议】SELECT语句不要使用UNION,推荐使用UNION ALL,并且UNION子句个数限制在5个以内。
【建议】线上环境,多表JOIN不要超过5个表。
【建议】减少使用ORDER BY,和业务沟通能不排序就不排序,或将排序放到程序端去做。ORDER BY、GROUP BY、DISTINCT这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
【建议】包含了ORDER BY、GROUP BY、DISTINCT这些查询的语句,WHERE条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
【建议】对单表的多次alert操作必须合并为一次。
对于超过100w行的大表进行alter table,必须经过DBA审核,并在业务低峰期执行,多个alter需整合在一起。因为alter table会产生表锁
,期间阻塞对于该表的所有写入,对于业务可能会产生极大影响。
【建议】批量操作数据时,需要控制事务处理间隔时间,进行必要的sleep。
【建议】事务里包含SQL不超过5个。
因为过长的事务会导致锁数据较久,MySQL内部缓存、连接消耗过多等问题。
【建议】事务更新语句尽量基于主键或UNIQUE KEY,如UPDATE… WHERE id=xx;
否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁。
优化MySQL服务器主要从两个方面来优化,一方面是对
硬件
进行优化;另一方面是对MySQL服务的参数
进行优化。这部分的内容需要较全面的知识,一般只有专业的数据库管理员
才能进行这一类的优化。
服务器的硬件性能直接决定着MySQL数据库的性能。
硬件的性能瓶颈直接决定MySQL数据库的运行速度和效率。针对性能瓶颈提高硬件配置,可以提高MySQL数据库查询、更新的速度。
1、配置较大的内存
。足够大的内存是提高MySQL数据库性能的方法之一。内存的速度比磁盘I/O快得多,可以通过增加系统的缓冲区容量
使数据哎内存中停留的时间更长,以减少磁盘I/O
。
2、
配置高速磁盘系统
,以减少读盘的等待时间,提高响应速度。磁盘的I/O能力,也就是它的寻道能力,目前的SCSI
高速旋转是7200转/分钟。这样的速度,一旦访问的用户量上去,磁盘的压力就会过大,如果每天的网站pv(page view)在150w,这样的一般的配制就无法满足这样的需求了。现在SSD
盛行,在SSD上随机访问和顺序访问性能几乎差不多,使用SSD可以减少随机IO带来的性能损耗。
3、
合理分布磁盘I/O
,把磁盘I/O分散在多个设备上以减少资源竞争,提高并行操作能力。
4、配置多处理器
,MySQL是多线程的数据库,多处理器可同时执行多个线程。
通过优化MySQL的参数可以提高资源利用率,从而达到提高MySQL服务器性能的目的。
MySQL服务的配置参数都在my.cnf
或者my.ini
文件的[mysqlId]组中。配置完参数以后需要重启MySQL服务才会生效。
下面几个对性能影响比较大的参数进行详细介绍。
innodb_buffer_pool_size
:这个参数是MySQL数据库最重要的参数之一,表示InnoDB类型的表和索引的最大缓存
。它不仅仅缓存索引数据
,还会缓存表的数据
。这个值越大,查询的速度就会越快。但是这个值太大会影响系统的性能。
key_buffer_size
:表示索引缓冲区的大小
。索引缓冲区是所有的线程共享
。增加索引缓冲区可以得到更好处理的索引(对所有读和多重写)。当然,这个值不是越大越好,他的大小取决于内存的大小。如果这个值太大,就会导致操作系统频繁换页,也会降低系统性能。对于内存在4GB
左右的服务器该参数可设置为256M
或384M
。
table_cache
:表示同时打开的表的个数
。这个值越大,能够同时打开的表的个数越多。物理内存越大,设置就越大。默认为2402,调到512-1024最佳。这个值不是越大越好,因为同时打开的表太多会影响操作系统的性能。
query_cache_size
:表示查询缓冲区的大小
。可以通过在MySQL控制台观察,如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,就要增加Query_cache_size的值;如果Qcache_hits的值非常大,则表明查询缓冲使用非常频繁,如果该值较小反而会影响效率,那么可以考虑不用查询缓存;Qcache_free_blocks,如果该值非常大,则表明缓冲区中碎片很多。MySQL8.0之后失效
该参数需要和query_cache_type配合使用。
query_cache_type
:的值是0时,所有的查询都不使用查询缓冲区。但是query_cache_type=0并不会导致MySQL释放query_cache_size所配置的缓冲区内存,MySQL8.0之后失效
。
当query_cache_type=1时,所有的查询都将使用查询缓存区,除非在查询语句中指定SQL_NO_CACHE
,如SELECT SQL_NO_CACHE * FROM tbl_name。
当query_cache_type=2时,只有在查询语句中使用SQL_CACHE
关键词,查询才会使用查询缓存区。使用查询缓存区可以提高查询的速度,这种方式只适合于修改操作少且经常执行相同的查询操作的情况。
sort_buffer_size
:表示每个需要进行排序的线程分配的缓冲区的大小
。增加这个参数的值可以提高ORDER BY
或GROUP BY
操作的速度。默认数值是2 097 144字节(约2MB)。对于内存在4GB左右的服务器推荐设置为6-8M,如果有100个连接,那么实际分配的总共排序缓冲区大小为100 * 6 = 600MB。
join_buffer_size = 8M
:表示联合查询操作所能使用的缓冲区大小
,和sort_buffer_size一样,该参数对应的分配内存也是每个连接独享。
read_buffer_size
:表示每个线程连续扫描时为扫描的每个表分配的缓冲区的大小(字节)
。当线程从表中连续读取记录时需要用到这个缓冲区。SET SESSION read_buffer_size=n可以临时设置该参数的值。默认为64k,可以设置为4M。
innodb_flush_log_at_trx_commit
:表示何时将缓冲区的数据写入日志文件
,并且将日志文件写入磁盘中。该参数对于InnoDB引擎非常重要。该参数有3个值,分别为0、1和2.该参数的默认值为1。
值为0
时,表示每秒1次
的频率将数据写入日志文件并将日志文件写入磁盘。每个事务的ccommit并不会触发前面的任何操作。该模式速度最快,但不太安全,mysqlId进程的崩溃会导致上一秒钟所有事务数据的丢失。
值为1
时,表示每次提交事务时
将数据写入日志文件并将日志文件写入磁盘进行同步。该模式是最安全的,但也是最慢的一种方式。因为每次事务提交或是屋外的指令都需要把日志写入(flush)硬盘。
值为2
时,表示每次提交事务时
将数据写入日志文件,每隔1秒
将日志文件写入磁盘。该模式速度较快,也比0安全,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。
innodb_log_buffer_size
:这是InnoDB存储引擎的事务日志所使用的缓冲区
。为了提高性能,也是将信息写入Innodb Log Buffer中,当满足innodb_flush_log_trx_commit参数所设置的相应条件(或者日志缓冲区写满)之后,才会将日志写到文件(或者同步到磁盘)中。
max_connections
:表示允许连接到MySQL数据库的最大数量
,默认值是151
。如果状态变量connection_errors_max_connections不为零,并且一直增长,则说明不断有连接请求因数据库连接数已达到允许最大值而失败,这是可以考虑增大max_connections的值。在Linux平台下,性能好的服务器,支持500-1000个连接不是难事,需要根据服务器性能进行评估设定。这个链接不是越大越好
,因为这些连接会浪费内存的资源。过多的连接可能会导致MySQL服务器僵死。
back_log
:用于控制MySQL监听TCP端口时设置的积压请求栈大小
。如果MySQL的连接数达到max_connections时,新进来的请求将会被存在堆栈中,以等待某一连接释放资源,该堆栈的数量即back_log,如果等待连接的数(max_connections / 5),对于Linux系统推荐设置为小于512的整数,但最大不超过900。
thread_cache_size
:线程池缓存线程数量的大小
,当客户端断开连接后将当前线程缓存起来,当在接到新的连接请求时最快速响应无需创建新的线程。这尤其对那些使用短连接的应用程序来说可以极大的提高创建连接的效率。那么为了提高性能可以增大该参数的值。默认为60,可以设置为120。
可以通过如下几个MySQL状态值来适当调整线程池的大小:show global status like 'Thread%';
当Threads_cached越来越少,但Threads_connected始终不降,且Threads_created持续升高,可适当增加thread_cache_size的大小。
wait_timeout
:指定一个请求的最大连接时间
,对于4GB左右内存的服务器可以设置为5-10.
interactive_timeout
:表示服务器在关闭连接前等待行动的描述。
遇到整数类型的字段可以用
INT型
。这样做的理由是INT型数据有足够大的取值范围,不用担心数据超出取值范围的问题。刚开始做项目的时候,首先要保证系统的稳定性,这样设计字段类型是可以的,但在数据量很大的时候,数据类型的定义,在很大程度上会影响到系统整体的执行效率。
对于
非负型
的数据(如自增ID、整型IP)来说,要优先使用无符号UNSIGNED
来存储。因为无符号相对于有符号,同样的字节数,存储的数值范围更大。如tinyint有符号为-128-127,无符号位0-255,多出一倍的存储空间。
跟文本类型数据相比,大整数往往占用
更少的存储空间
,因此,在存取和对比的时候,可以占用更少的内存空间。所以,在二者皆可用的情况下,尽量使用整数类型,这样可以提高查询效率。
MySQL
内存临时表
不支持TEXT、BLOB这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表
进行。并且对于这种数据,MySQL还是要进行二次查询
,会使SQL性能变得很差,但是不是说一定不能使用这种数据类型。
如果一定要使用,建议把BLOB或是TEXT列分离到单独的扩展表
中,查询时一定不要使用select *,而只需要取出必要的列,不需要TEXT列的数据时不要对该表进行查询。
修改ENUM值需要使用ALTER语句。
ENUM类型的ORDER BY操作效果很低,需要额外操作。使用TINYINT来代替ENUM类型。
非精准浮点:float,double
精准浮点:decimal
Decimal类型为精准浮点数,在计算时不会丢失精度,尤其是财务相关的金融类数据。占用空间由定义的宽度决定,每4个字节可以存储9位数字,并且小数点要占用一个字节。可用于存储比bigint更大的整型数据。
总值,遇到数据量大的项目时,一定要在充分了解业务需求的前提下,合理优化数据类型,这样才能充分发挥资源的效率,使系统达到最优。
原子性(atomicity):
原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。即要么转账成功,要么转账失败,是不存在中间状态。如果无法保证原子性会怎么样?就会出现数据不一致的情形,A账户减去100元,而B账户增加100元操作失败,系统将无故丢失100元。
一致性(consistency):
(国内很多网站上对一致性的阐述有误,具体你可以参考Wikipedia对Consistency的阐述)
根据定义,一致性是指事务执行前后,数据从一个
合法性状态
变换到另外一个合法性状态
。这种状态是语义上
的而不是语法上的,跟具体的业务有关。
那什么是合法的数据状态呢?满足
预定的约束
的状态就叫做合法的状态。通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。
举例1:A账户有200元,转账300元出去,此时A账户余额为-100元。你自然就发现了此时数据是不一致的,为什么呢?因为你定义了一个状态,余额这列必须>=0。
举例2:A账户200元,转账50元给B账户,A账户的钱扣了,但B账户因为各种意外,余额并没有增加,你也知道此时数据是不一致的,为什么?因为你定义了一个状态,要求A+B的总余额必须不变。
举例3:在数据表中我们将
姓名
字段设置为唯一性约束
,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名不唯一,就破坏了事务的一致性要求。
隔离性(isolation)
事务的隔离性是指一个事务的执行
不能被其他事务干扰
,即一个事务内部的操作及使用的数据对并发
的其他事务时隔离的,并发执行的各个事务直接不能互相干扰。
如果无法保证隔离性会怎么样?假设A账户有200元,B账户0元。A账户往B账户转账两次,每次金额为50元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形:
持久性
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是
永久性的
,接下来的其他操作和数据库故障不应该对其有任何影响。
持久性是通过事务日志
来保证的。日志包括了重做日志
和回滚日志
。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后在对数据库中对应的行进行修改。这样做的好处是,即时数据库系统崩溃,数据库重启后也能找到没有更新到的数据库系统中的重做日志,重新执行,从而使事务具有持久性。
总结
ACID是事务的四大特性,在这四个特性中,原子性的基础,隔离性是手段,一致性是约束条件,而持久性是我们的目的。
数据库事务,其实就是数据库设计者为了方便起见,把需要保证原子性、隔离性、一致性、持久性
的一个或多个数据库操作称为一个事务。
我们现在知道
事务
是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL根据这些操作所执行的不同阶段把事务
大致划分成几个状态:
活动的(active)
事务对应的数据库操作正在执行过程中时,我们就说该事务处在
活动的
状态。
部分提交的(partially committed)
当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并
没有刷新到磁盘
时,我们就说该事务处在部分提交的
状态。
失败的(failed)
当事务处在
活动的
或者部分提交的
状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的
状态。
中止的(aborted)
如果事务执行了一部分而变为
失败的
状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为回滚
。当回滚
操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的
状态。
提交的(committed)
当一个处在
部分提交的
状态的事务将修改过的数据都同步到磁盘
上之后,我们就可以说该事务处在了提交的
状态。
图中可见,只有当事务处在
提交的
或者中止的
状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久失效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。
MySQL是一个
客户端/服务器
,对于同一个服务器来说,可以u若干个客户端之连接,每个客户端与服务器连接上后,就可以称为一个会话(Session
)。每个客户端都可以在自己的会话中向服务器发送请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务,事务有隔离性
的特性,理论上在某个事务对某个数据进行访问
时,其他事务应该进行排队
,当该事务提交之后,其他事务才可以继续访问这个数据。但这样对性能影响太大
,我们即想保持事务隔离性,又想让服务器在处理访问同一数据的多个事务性能尽量高些
,那就看二者如何权衡取舍了。
脏读(Dirty Read)
对于两个事务Session A、Session B、Session A
读取
了已经被Session B更新但还没有被提交
的字段时。之后若Session B回滚
,Session A读取
的内容就是临时且无效的。
不可重复读(Non-Repeatable Read)
对于两个事务Seesion A、Session B,Session A
读取
了一个字段,然后Session B更新
了该字段。之后Session A再次读取
同一个字段,值就不同
了。那就意味着发生了不可重复读。
幻读(Phantom)
对于两个事务Session A、Session B,Session A从一个表中
读取
一个字段,然后Session B在该表插入
了一些新的行。之后,如果Session A再次读取
同一个表,就会多出几行,那就意味着发生了幻读。
READ UNCOMMITTED
:读未提交,该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。
READ COMMITTED
:读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别。可以避免脏读,但不可重复读、幻读问题仍然存在。
REPEATABLE READ
:可重复读,事务A在读到一条记录之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。
SERIALIZABLE
:可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除的操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
SQL标准
中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:
脏写
怎么没涉及到?因为脏写这个问题太严重了,不论哪种隔离级别,都不允许脏写的情况发生。
不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4种事务隔离级别与并发性能关系如下:
不同的数据库厂商对SQL标准中规定的四种隔离级别支持不一样。比如,
Oracle
就支持READ COMMITTED(默认隔离级别)
和SERIALIZABLE隔离级别
。MySQL支持4种隔离级别,但与SQL标准中所规定的各级隔离级别允许发生的问题却有些出入,MySQL在REPEATABLE READ隔离级别
下,是可以禁止幻读的问题发生的。
MySQL的默认隔离级别为
REPEATABLE READ
,在MySQL8.0
中默认隔离级别为READ COMMITTED
。
# 查看隔离级别,MySQL5.7.20的版本及之后;
SHOW VARIABLES LIKE 'transaction_isolation';
# 不同MySQL版本中都可以使用的;
SELECT @@transaction_isolation;
事务日志4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?
事务的隔离性由锁机制
实现。
而事务的原子性、一致性和持久性由事务的redo日志和undo日志来保证。
REDO LOG称为重做日志
,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。
UNDO LOG称为回滚日志
,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
有的DBA或许会认为UNDO是REDO的逆过程,其实不然。REDO和UNDO都可以视为一种恢复操作
,但是:
redo log:是存储引擎层(innodb)生成的日志,记录的是
物理级别
上的页修改操作,比如页号xxx、偏移量yyy写入了‘zzz’数据。主要为了保证数据的可靠性;
undo log:是存储引擎(innodb)生成的日志,记录的是
逻辑操作
日志,比如对某一行数据进行了INSERT语句操作,那么undo log就记录一条与之相反的DELETE操作。主要用于事务的回滚
(undo log记录的是每个修改操作的逆操作
)和一致性非锁定读
(undo log回滚行记录到某种特定的版本—MVCC,即多版本并发控制)。
InnoDB存储引擎是以
页为单位
来管理存储空间的。在真正访问页面之前,需要把磁盘上
的页缓存到内存中的Buffer Pool
之后才可以访问。所有的变更都必须先更新缓冲池
中的数据,然后缓冲池中脏页
会以一定的频率被刷入磁盘(chekPoint机制
),通过缓冲池来优化CPU和磁盘之间的鸿沟,这样就可以保证整体的性能不会下降太快。
好处
redo日志降低了刷盘频率
redo日志占用的空间非常小
存储表空间ID、页号、偏移量以及需要更新的值,所需的存储空间是很小的,刷盘快。
特点
redo日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的
,也就是使用顺序IO,效率比随机IO快。
事务执行过程中,redo log不断记录
redo log跟bin log的区别,redo log是存储引擎层
产生的,而bin log是数据库层
产生的。假设一个事务,对表做10万行的记录插入,在这个过程中,一直不断的往redo log顺序记录,而bin log不会记录,直到这个事务提交,才会写入到bin log文件中。
Redo log可以简单分为以下两个部分:
重做日志的缓冲(redo log buffer)
,保存在内存中,是易失的。
在服务器启动时就像操作系统申请了一大片称之为redo log buffer的连续内存
空间,翻译成中文就是redo日志缓冲区。这片内存空间被划分成若干个连续的redo log block
。一个redo log block占用512字节
大小。
参数设置:innodb_log_buffer_size
redo log buffer大小,默认16M
,最大值是4096M,最小值为1M。
重做日志文件(redo log file)
,保存在硬盘中,是持久的。
体会:Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化。
redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后
一定的频率
刷入到真正的redo log file中。这里的一定频率怎么看待呢?这就是我们要说的刷盘策略。
注意,redo log buffer刷盘到redo log file的过程并不是真正刷到磁盘中去,只是刷到了文件系统缓存
(page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。
针对这种情况,InnoDB给出
innodb_flush_log_at_trx_commit
参数,该参数控制commit提交事务时,如何将redo log buffer中的日志刷新到redo log file中。它支持三种策略:
设置为0
:表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)。
设置为1
:表示每次事务提交时都将进行同步,刷盘操作(默认值
)。
设置为2
:表示每次事务提交时都只把redo log buffer内容写入page cache,不进行同步。由os自己决定什么时候同步到磁盘文件。
另外,InnoDB存储引擎有一个后台线程,每个1秒
,就会把redo log buffer
中的内容写到文件系统缓存(page cache
),然后调用刷盘操作。
也就是说一个没有提交事务的redo log
记录,也可能会刷盘。因为事务执行过程redo log记录是会写入redo log buffer
中,这些redo log记录会被后台线程
刷盘。
除了后台线程每秒1次
的轮询操作,还有一种情况,当redo log buffer
占用的空间即将达到innodb_log_buffer_size
(这个参数默认是16M)的一半的时候,后台线程会主动刷盘。
可通过show variables like 'innodb_flush_log_at_trx_commit';
查看
小结:innodb_flush_log_at_trx_commit=1
为1
时,只要事务提交成功,redo log
记录就一定在硬盘里,不会有任何数据丢失。
如果事务执行期间MySQL
挂了或宕机,这部分日志丢了,但事务并没有提交,所以日志丢了也不会有任何损失,可以保证ACID的D,数据绝对不会丢失,但是效率最差
的。
建议使用默认值
,虽然操作系统宕机的概率理论小于数据库宕机的概率,但是一般既然使用了事务,那么数据的安全相对来说更重要一些。
小结:innodb_flush_log_at_trx_commit=2
为2
时,只要事务提交成功,redo log buffer中的内容只写入文件缓存(page cache
)。
如果仅仅只是MySQL
挂了不会有任何数据丢失,但是操作系统宕机可能会有1
秒数据的丢失,这种情况下无法满足ACID中的D。但数值2肯定是效率最高的。
小结:innodb_flush_log_at_trx_commit=0
为0
时,master thread中每1
秒进行一次重做日志的fsync操作,因此实例crash最多丢失1秒钟内
的事务。(master thread是负责将缓冲池中的数据异步刷新到磁盘,保持数据的一致性)。
数值0的话,是一种折中的做法,它的IO效率理论是高于1的,低于2的,这种策略也有丢失数据的风险,也无法保证D。
redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中
更新数据
的前置操作
其实是要先写入一个undo log
。
事务需要保证
原子性
,也就是事务中操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:
情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误
,操作系统错误
,甚至是突然断电
导致的错误。
情况二:程序员可以在事务执行过程中手动输入ROLLBACK
语句结束当前事务的执行。
以上情况出现,我们需要把数据改回原先的样子,这个过程称之为回滚
,这样就可以造成一个假象:这个事务看起来什么都没做,所以符合原子性
要求。
每当我们要对一条记录做改动时(这里的
改动
可以指INSERT、DELETE、UPDATE
),都需要“留一手” 一 一 把回滚时所需的东西记下来。比如:
你插入一条记录
时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉
就好了。(对于每个INSERT,InnoDB存储引擎会完成一个DELETE)。
你删掉了一条记录
,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入
到表中就好了。(对于每个DELETE,InnoDB存储引擎会执行一个INSERT)。
你修改了一条记录
,至少要把修改这条记录前的旧值都记录下来,这样之后回滚再把这条记录更新为旧值
就好了。(对于每个UPDATE,InnoDB存储引擎会执行一个相反的UPDATE,将修改前的行放回去)。
MySQL把这些为了回滚而记录的这些内容称之为
撤销日志
或者回滚日志
(即undo log
)。注意,由于查询操作(SELECT
)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录
相应的undo日志。
此外,undo log
会产生redo log
,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。
作用1:回滚数据
用户对undo日志可能
有误解
:undo用于将数据库物理地恢复到执行语句或事物之前的样子。但事实并非如此。undo是逻辑日志
,因此只是将数据库逻辑恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。
这是因为在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。
作用2:MVCC
undo的另一个作用是MVCC,即InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。
undo log
是逻辑日志,对事务回滚时,只是将数据库逻辑地恢复到原来的样子。
redo log
是物理日志,记录的是数据页的物理变化,undo log不是redo log的逆过程。
锁
是计算机协调多个进程或线程并发访问某一资源
的机制。在程序开发中会存在多线程同步的问题,当多个线程并发访问某个数据的时候,尤其是针对一些敏感的数据(比如订单、金额等),我们就需要保证这个数据在任何时刻最多只有一个线程
在访问,保证数据的完整性
和一致性
。在开发过程中加锁是为了保证数据的一致性,这个思想在数据库领域中同样很重要。
在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供多用户共享的资源。为保证数据的一致性,需要对
并发操作进行控制
,因此产生了锁
。同时锁机制
也为实现MySQL的各个隔离级别提供了保证。锁冲突
也是影响数据库并发访问性能
的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。
并发事务访问相同记录的情况大致可以划分为3种:
读-读
情况,即并发事务相继读取相同的记录
。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生。
写-写
情况,即并发事务相继对相同的记录做出改动。
在这种情况下会发生脏写
的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行
,这个排队的过程其实是通过锁
来实现的。这个所谓的锁其实是一个内存中的结构
,在事务执行前本来是没有锁的,也就是说一开始是没有锁结构
和记录进行关联的,如图所示:
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的
锁结构
,当没有的时候就会在内存中生成一个锁结构
与之关联。比如,事务T1
要对这条记录做改动,就需要生成一个锁结构
与之关联:
在锁结构
里有很多信息,为了简化理解,只把两个比较重要的属性拿了出来:
trx信息
:代表这个锁结构是哪个事务生成的。
is_waiting
:代表当前事务是否在等待。
当事务T1
改动了这条记录后,就生成了一个锁结构
与该记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting
属性就是false
,我们把这个场景就称之为获取锁成功
,或者加锁成功
,然后就可以继续执行操作了。
在事务
T1
提交之前,另一个事务T2
也想对该记录做改动,那么先看看有没有锁结构
与这条记录关联,发现有一个锁结构
与之关联后,然后也生成了一个锁结构与这条记录关联,不过锁结构的is_waiting
属性值为true
,表示当前事务需要等待,我们把这个场景称之为获取锁失败
,或者加锁失败
,图示:
在事务T1提交之后,就会把该事务生成的
锁结构释放掉
,然后看看还有没有别的事务在等待获取锁,发现了事务T2还在等待获取锁,所以把事务T2
对应的锁结构is_waiting
属性设置为false
,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到了锁,效果图就是这样:
小结几种说法:
不加锁
:意思就是不需要在内存中生成对应的锁结构
,可以直接执行操作。
获取锁成功,或者加锁成功
:意思就是在内存中生成了对应的锁结构
,而且锁结构的is_waiting
属性为false
,也就是事务可以继续执行操作。
获取锁失败,或者加锁失败,或者没有获取到锁
:意思就是在内存中生成了对应的锁结构
,不过锁结构的is_waiting
属性为true
,也就是事务需要等待,不可以继续执行操作。
读-写
或写-读
,即一个事务进行读取操作,另一个进行改动操作,这种情况可能发生脏读
、不可重复读
、幻读
的问题。
各个数据库厂商对SQL标准
的支持都可能不一样。比如MySQL在REPEATABLE READ
隔离级别上就已经解决了幻读
问题。
怎么解决
脏读
、不可重复读
、幻读
这些问题呢?其实有两种可选的解决方案:
方案一:读操作利用多版本并发控制MVCC
,写操作进行加锁
所谓的
MVCC
,就是生成一个ReadView
,通过ReadView找到符合条件的记录版本(历史版本由undo日志
构建)。查询语句只能读
到在生成ReadView之前已提交事务所做的更改
,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作
肯定针对的是最新版本的记录
,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写
操作并不冲突。
普通的SELECT语句在READ COMMITED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。
在READ COMMITTED
隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改
,也就是避免了脏读现象;
在REPEATABLE READ
隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作
才会生成一个ReadView,之后的SELECT操作都复用
这个ReadView,这样也就避免了不可重复读和幻读的问题。
方案二:读、写操作都采用加锁
的方式
如果我们的一些业务场景不允许读取记录的旧版本,而是每次都必须去
读取记录的最新版本
。比如,在银行存款的事务中,你需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额,直到本次存款事务执行完成,其他事务才可以访问账户的余额。这样在读取记录的时候就需要对其进行加锁
操作,这样也就意味着读
操作和写
操作也像写一写
操作那样排队
执行。
脏读
的产生是因为当前事务读取了另一个未提交事务写的一条记录,如果另一个事务在写记录的时候就给这条记录加锁,那么当前事务就无法继续读取该记录了,所以也就不会有脏读问题的产生了。
不可重复读
的产生是因为当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务再次读取时会获得不同的值,如果在当前事务读取记录时就该给记录加锁,那么另一个事务就无法修改该记录,自然也不会发生不可重复读了。
幻读
问题的产生是因为当前读取一个范围的记录,然后另外的事务向该范围内插入了新纪录,当前事务再次读取该范围的记录发现了新插入的新纪录。采用加锁的方式解决幻读问题就有一些麻烦,因为当前事务在第一次读取记录时幻影记录并不存在,所以读取的时候加锁就有点尴尬(因为你并不知道给谁加锁)。
小结对比发现:
采用MVCC
方式的话,读-写
操作彼此并不冲突,性能更高
。
采用加锁
方式的话,读-写
操作彼此需要排队执行
,影响性能。
一般情况下我们当然愿意采用MVCC
来解决读-写
操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用加锁
的方式执行。
对于数据库中并发事务
读-读
情况并不会引起什么问题。对于写-写
、读-写
或写-读
这些情况可能会引起一些问题,需要使用MVCC
或者加锁的方式来解决它们。在使用加锁
的方式解决问题时,由于既要允许读一读
情况不受影响,又要使写-写
、读-写
或写-读
情况中的操作相互阻塞
,所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁(Shared Lock S Lock)
和排他锁(Exclusive Lock,X Lock)
,也叫读锁(read lock)
和写锁(write lock)
。
读锁
:也称为共享锁
、英文用S
表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
写锁
:也称为排他锁
、英文用X
表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。
需要注意的是对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。
在采用
加锁
方式解决脏读、不可重复读、幻读
这些问题时,读取一条记录时需要获取该记录的S锁
,其实是不严谨的,有时候需要在读取记录时就获取记录的X锁
,来禁止别的事务读写该记录,为MySQL提供了两种比较特殊的SELECT
语句格式:
对读取的记录加
S锁
:
SELECT ... LOCK IN SHARE MODE;
# 或
SELECT ... FOR SHARE; #(8.0新增语法)
在普通的SELECT语句后边加
LOCK IN SHARE MODE
,如果当前事务执行了该语句,那么它会为读取到的记录加S锁
,这样允许别的事务继续获取这些记录的S锁
(比方说别的事务也使用SELECT ... LOCK IN SHARE MODE
语句来读取这些记录),但是不能获取这些记录的X锁
(比如使用SELECT ... FOR UPDATE
语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的X锁
,那么它们会阻塞,直到当前事务提交之后将这些记录上的S锁
释放掉。
对读取的记录加
X锁
:
SELECT ... FOR UPDATE;
在普通的SELECT语句后边加
FOR UPDATE
,如果当前事务执行了该语句,那么它会为读取到的记录加X锁
,这样既不允许别的事务获取这些记录的S锁
(比方说别的事务使用SELECT ... LOCK IN SHARE MODE
语句来读取这些记录),也不允许获取这些记录的X锁
(比如使用SELECT ... FOR UPDATE
语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的S锁
或者X锁
,那么它们会阻塞,直到当前事务提交之后将这些记录上的X锁
释放掉。
MySQL8.0新特性
在5.7及之前的版本,SELECT … FOR UPDATE,如果获取不到锁,会一直等待,直到
innodb_lock_wait_timeout
超时。在8.0版本中,SELECT … FOR UPDATE,SELECT … FOR SHARE添加NOWAIT、SKIP、LOCKED
语法,跳过锁等待,或者跳过锁定。
通过添加NOWAIT、SKIP、LOCKED语法,能够立即返回。如果查询的行已经加锁:
那么NOWAIT会立即报错返回。
而SKIP LOCKED也会立即返回,只是返回的结果中不包含被锁定的行。
平常所用到的
写操作
无非是DELETE、UPDATE、INSERT
这三种:
DELETE
:对一条记录做DELETE操作的过程其实是先在B+树
中定位这条记录的位置,然后获取这条记录的X锁
,再执行delete mark
操作。我们也可以把这个定位待删除记录在B+树中的过程看成是一个获取X锁
的锁定读
。
UPDATE
:在对一条记录做UPDATE操作时分为三种情况:
情况1:未修改记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化。
则现在B+树
中定位到这条记录的位置,然后再获取一下记录的X锁
,最后原纪录的位置进行修改操作。我们也可以把这个定位待修改记录在B+树
中位置的过程看成是一个获取X锁
的锁定读
。
情况2:未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。
则先在B+树
中定位到这条记录的位置,然后获取了一下记录的X锁
,将记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新纪录。这个定位待修改记录在B+树
中位置的过程看成是一个获取X锁
的锁定读
,新插入的记录由INSERT
操作提供的隐式锁
进行保护。
情况3:修改了该记录的键值,则相当于在原记录上做
DELETE
操作之后再来一次INSERT
操作,加锁操作就需要按照DELETE
和INSERT
的规则进行了。
INSERT
:一般情况下,新插入一条记录的操作并不加锁,通过一种称之为隐式锁
的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。
为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很
耗资源
的事情(涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应
和系统性能
两方面进行平衡,这样就产生了锁粒度(Lock granularity)
的概念。
对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在
表级别
进行加锁,自然就被称之为表级锁
或者表锁
,对一个表加锁影响整个表的记录,我们就说这个锁的粒度比较粗。锁的粒度主要分为表级锁、页级锁和行锁。
该锁会锁定整张表,它是MySQL中最基本的锁策略,并
不依赖于存储引擎
(不管你是MySQL的什么存储引擎,对于表锁的策略都是一样的),并且表锁是开销最小
的策略(因为粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁
问题。当然,锁的粒度大所带来的最大的负面影响就是出现锁资源争用的概率也会最高,导致并发率大打折扣
。
表级别的S锁、X锁
在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的
S锁
或者X锁
的。在对某个表执行一些诸如ALTER TABLE、DROP TABLE
这类的DDL
语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL
语句会发生阻塞。这个过程其实是通过在server层
使用一种称之为元数据锁
(英文名:Metadata Locks
,简称MDL
)结构来实现的。
一般情况下,不会使用InnoDB存储引擎提供的表级别的
S锁
和X锁
。只会在一些特殊情况下,比方说崩溃恢复
过程中用到。比如,在系统变量autocommit=0,innodb_table_locks=1
时,手动
获取InnoDB存储引擎提供的表t的S锁
或者X锁
可以这么写:
LOCK TABLES t READ
:InnoDB存储引擎会对表t
加表级别的S锁
。
LOCK TABLES t WRITE
:InnoDB存储引擎会对表t
加表级别的X锁
。
不过尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES
这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的行锁
,关于InnoDB表级别的S锁
和X锁
大家了解一下就可以了。
# 查看表上加过的锁,主要关注In_use字段的值
SHOW OPEN TABLES;
# 或者
SHOW OPEN TABLES where In_use > 0;
# 手动增加表锁命令
LOCK TABLES t READ;# 存储引擎会对表t加表级别的共享锁。共享锁也叫读锁或S锁(Share的缩写)
LOCK TABLES t WRITE;# 存储引擎会对表t加表级别的排他锁。排他锁也叫独占锁、写锁或 X锁(是eXclusive的缩写)
# 解锁当前加锁的表
UNLOCK TABLES;
意向锁(intention lock)
InnoDB支持
多粒度锁(multiple granularity locking)
,它允许行级锁
与表级锁
共存,而意向锁
就是其中的一种表锁
。
1、意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
2、意向锁是一种不与行级锁冲突表级锁
,这一点非常重要。
3、表明“某个事务正在某些行持有了锁或该事务准备去持有锁”。
意向锁分为两种:
意向共享锁
(intention shared lock,IS):事务有意向对表中的某些行加共享锁
(S锁)。
# 事务要获取某些行的S锁,必须先获得表的IS锁
SELECT column FROM table ... LOCK IN SHARE MODE;
意向排他锁
(intention exclusive lock,IX):事务有意向对表中的某些行加排他锁
(X锁)。
# 事务要获取某些行的X锁,必须先获得表的IX锁。
SELECT column FROM table ... FOR UPDATE;
即:意向锁是由存储引擎
自己维护的
,用户无法手动操作意向锁,在位数据行加共享/排他锁之前,InnoDB会先获取该数据行所在数据表的对应意向锁
。
意向锁的并发性
:意向锁不会与行级的共享/排他锁互斥!正因为如此,意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性(不然我们直接用普通的表锁就行了)。
总结
:
1、InnoDB支持多粒度锁
,特定场景下,行级锁可以与表级锁共存。
2、意向锁之间互不排斥,但除了IS与S兼容外,意向锁会与 共享锁/排他锁 互斥
。
3、IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
4、意向锁在保证并发性的前提下,实现了行锁和表锁共存
且满足事务隔离性
的要求。
自增锁(AUTO-INC锁)
所有插入数据的方式总共分为三类,分别是“
Simple inserts
”,“Bulk inserts
”和“Mixed-mode inserts
”。
1、“Simple inserts”(简单插入)
可以预先确定要插入的行数
(当语句被初始处理时)的语句。包括没有嵌套子查询的单行和多行INSERT ... VALUES()
和REPLACE
语句。
2、“Bulk inserts”(批量插入)
事先不知道要插入的行数
(和所需自动递增值的数量)的语句。比如INSERT ... SELECT
,REPLACE ... SELECT
和LOAD DATA
语句,但不包括纯INSERT。InnoDB在每处理一行,为AUTO_INCREMENT列分配一个新值。
3、“Mixed-mode inserts”(混合模式插入)
这些是“Simple inserts”语句但是指定部分新行的自动递增值。例如INSERT INTO teacher (id,name) VALUES (1,'a'),(NULL,'b'),(5,'c'),(NULL,'d');
只是指定了部分id的值。另一种类型的“混合模式插入”是INSERT ... ON DUPLICATE KEY UPDATE
。
对于上面数据插入的案例,MySQL中采用了
自增锁
的方式来实现,AUTO-INC锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁
,在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞
,可以保证一个语句中分配的递增值是连续的。也正因为此,其并发性显然并不高,当我们向一个由AUTO_INCREMENT关键字的主键插入值的时候,每条语句都要对这个表锁进行竞争
,这样的并发潜力其实是很低下的,所以innodb通过innodb_autoinc_lock_mode
的不同取值来提供不同的锁定机制,来显著提高SQL语句的可伸缩性和性能。
innodb_autoinc_lock_mode有三种取值,分别对应与不同锁定模式:
(1)innodb_autoinc_lock_mode = 0(“传统”锁定模式)
在此锁定模式下,所有类型的insert语句都会获得一个特殊的表级AUTO-INC锁,用于插入具有AUTO_INCREMENT列的表。这种模式其实就如我们上面的例子,即每当执行insert的时候,都会得到一个表级锁(AUTO-INC锁),使得语句中生成的auto_increment为顺序,且在binlog中重放的时候,可以保证master与slave中数据的auto_increment是相同的。因为表级锁,当在同一时间多个事务中执行insert的时候,对于AUTO-INC锁的争夺会限制并发
能力。
(2)innodb_autoinc_lock_mode = 1(“连续”锁定模式)
在MySQL8.0之前,连续锁定模式是默认
的。
在这个模式下,“bulk inserts”仍然使用AUTO-INC表级锁,并保持语句结束。这适用于所有INSERT … SELECT,REPLACE … SELECT和LOAD DATA语句。同一时刻只有一个语句可以持有AUTO-INC锁。
对于“Simple inserts”(要插入的行数事先已知),则通过mutex(轻量锁)
的控制下获得所需数量的自动递增值来避免表级AUTO-INC锁,它只在分配过程的持续时间内保持,而不是直到语句完成。不使用表级AUTO-INC锁,除非AUTO-INC锁由另一个事务保持。如果另一个事务保持AUTO-INC锁,则“Simple inserts”等待AUTO-INC锁,如果它是一个“bulk inserts”。
(3)innodb_autoinc_lock_mode = 2(“交错”锁定模式)
从MySQL8.0
开始,交错模式是默认
设置。
在这种锁定模式下,所有类INSERT语句都不会使用表级AUTO-INC锁,并且可以同时执行多个语句。这是最快和最可扩展的锁定模式,但是当使用基于语句的复制或恢复方案时,从二进制日志重播SQL语句时,这是不安全的。
在此锁定模式下,自动递增值保证
在所有并发执行的所有类型的insert语句中是唯一
且单调递增
的。但是,由于多个语句可以同时生成数字(即,跨语句交叉编号),为任何给定语句插入的行生成的值可能不是连续的
。
如果执行的语句是“simple inserts”,其中要插入的行数已提前知道,除了“Mixed-mode inserts”之外,为单个语句生成的数字不会有间隙。然而,当执行“bulk inserts”时,在由任何给定语句分配的自动递增值可能存在间隙。
元数据锁(MDL锁)
MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL的作用是,保证读写的正确性。比如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个
表结构做变更
,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
因此,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥呃,用来保证变更表结构操作的安全性,解决了DML和DDL操作之间的一致性问题。不需要显式使用
,在访问一个表的时候会被自动加上。
行锁(Row Lock)也称为记录锁,顾名思义,就是锁住某一行(某条记录row)。需要注意的是,MySQL服务器并没有实现行锁机制,
行级锁只在存储引擎层实现
。
优点
:锁定粒度小,发生锁冲突概率低
,可以实现的并发度高
。
缺点
:对于锁的开销比较大
,加锁会比较慢,容易出现死锁
情况。
1、记录锁(Record Locks)
记录锁也就是仅仅一条记录锁上,官方的类型名称为:
LOCK_REC_NOT GAP
。比如我们把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。
记录锁是有S锁和X锁之分的,称之为S型记录锁
和X型记录锁
。
当一个事务获取了一条记录的S型锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
当一个事务获取了一条记录的X型记录锁后,其他事务即不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
2、间隙锁(Gap Locks)
MySQL
在REPEATABLE READ
隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC
方案解决,也可以采用加锁
方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录上不存在,我们无法给这些幻影记录
加上记录锁
。InnoDB提出了一种称之为Gap Locks
的锁,官方的类型名称为:LOCK_GAP
,我们可以简称为gap锁
。比如,把id值为8的那条记录加一个gap锁的示意图如下。
图中id值为8的记录加了gap锁,意味着不允许别的事务在id值为8的记录前后边的间隙插入新纪录
,其实就是id列的值(3,8)、(8,15)这个区间的新纪录是不允许立即插入的。比如,有另外一个事务想在插入一条id值为4的新纪录,它定位到该条新纪录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3,8)、(15,20)及20之后,中的新纪录才可以被插入。
gap锁的提出仅仅是为了防止插入幻影记录而提出的。
虽然有共享gap锁
和独占gap锁
这样的说法,但是它们起到的作用是相同的。而且如果对一条记录加了gap锁(不论是共享gap锁还是独占gap锁),并不会限制其他事务对这条记录加记录锁或者继续加gap锁。
3、临键锁(Next-Key Locks)
有时候我们即想
锁住某条记录
,又想阻止
其他事务在该记录前边的间隙插入新纪录
,所以InnoDB就提出了一种称之为Next-Key Locks
的锁,官方的类型名称为:LOCK_ORDINARY
,我们也可以简称为net-key锁
。Next-Key Locks是存储引擎innodb
、事务级别在可重复读
的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。比如,我们把id值为8的那条记录加一个next-key锁的示意图如下:
next-key锁
的本质就是一个记录锁
和一个gap锁
的合体,它即能保护该条记录,又能阻止别的事务将新纪录插入被保护纪录前边的间隙
。
4、插入意向锁(Insert Intention Locks)
我们说一个事务
插入
一条记录时需要判断一下插入位置是不是被别的事务加了gap锁
(next-key
也包含gap锁
),如果有的话,插入操作需要等待,直到拥有gap锁
的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构
,表明有事务想在某个间隙
中插入
新纪录,但是现在在等待。InnoDB就把这种类型的锁命名为Insert Intention Locks
,官方的类型名称为:LOCK_INSERT_INTENTION
,我们称为插入意向锁
。插入意向锁是一种Gap锁
,不是意向锁,在insert操作时产生。
插入意向锁是在插入一条记录行前,由
INSERT操作产生的一种间隙锁
。该锁用以表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间不需要互相等待。假设存在两条值分别为4和7的记录,两个不同的事务分别视图插入值为5和6的两条记录,每个事务在获取插入行独占的(排他)锁前,都会获取(4,7)之间的间隙锁,但是因为数据行之间并不冲突
,所以两个事务之间并不会产生冲突(阻塞等待)。
页锁就是在
页的粒度
上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
每个层级的锁数量是有限制的,因为锁会占用内存空间,
锁空间的大小是有限的
。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级
。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB中行锁升级为表锁,这样做的好处是占用锁空间降低了,但同时数据的并发度也下降了。
从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待
数据并发的思维方式
。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想
。
悲观锁是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会
阻塞
直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
)。比如行锁、表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
悲观锁不适用的场景较多,它存在一些不足,因为悲观锁大多数情况下依靠数据库的锁机制来实现,以保证程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是
长事务
而言,这样的开销往往无法承受
,这时就需要乐观锁。
乐观锁认为同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是
不采用数据库自身的锁机制,而是通过程序来实现
。在程序上,我们可以采用版本号机制
或者CAS机制
实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量
。在Java中java.util.concurrent.atomic
包下的原子变量类就是使用了乐观锁的一种实现方式:CAS实现的。
从这两种锁的设计思想中,我们总结一下乐观锁和悲观锁的适用场景:
1、乐观锁
适合读操作多
的场景,相对来说写的操作比较少。它的优点在于程序实现
,不存在死锁
问题,不过适用场景也会相对乐观,因为它阻止不了除程序以外的数据库操作。
2、悲观锁
适合写操作多
的场景,因为写的操作具有排它性
。采用悲观锁的方式,可以在数据库层面阻止其他事务对数据的操作权限,防止读 - 写
和写 - 写
的冲突。
一个事务在执行
INSERT
操作时,如果即将插入的间隙
已经被其他事务加了gap锁
,那么本次INSERT
操作会阻塞,并且当前事务会在该间隙上加一个插入意向锁
,否则一般情况下INSERT
操作时不加锁的。那如果一个事务首先插入了一条记录(此时并没有在内存生产该记录关联的锁结构),然后另一个事务:
立即使用
SELECT ... LOCK IN SHARE MODE
语句读取这条记录,也就是要获取这条记录的S锁
,或者使用SELECT ... FOR UPDATE
语句读取这条记录,也就是获取这条记录的X锁
,怎么办?
如果允许这种情况的发生,那么可能产生脏读
问题。
立即修改这条记录,也就是要获取这条记录的X锁
,怎么办?
如果允许这种情况的发生,那么可能产生脏写
问题。
这时候我们前边提过的事务id
又要起作用了。我们把聚簇索引和二级索引中的记录分开看一下:
情景一:
对于聚簇索引记录来说,有一个trx_id
隐藏列,该隐藏列记录着最后改动该记录的事务id
。那么如果当前事务中新插入一条聚簇索引记录后,该记录的trx_id
隐藏列代表的就是当前事务的事务id
,如果其他事务此时想对该记录添加S锁
或者X锁
时,首先会看一下该记录的trx_id
隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个X锁
(也就是为当前事务创建一个锁结构,is_waiting
属性false
),然后自己进入等待状态(也就是为自己也创建一个锁结构,is_waiting
属性是true
)。
情景二:
对于二级索引记录来说,本身并没有trx_id
隐藏列,但是在二级索引页面的Page Header
部分有一个PAGE_MAX_TRX_ID
属性,该属性代表对该页面做改动的最大的事务id
,如果PAGE_MAX_TRX_ID
属性值小于当前最小的活跃事务id
,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复情景一
的做法。
即:一个事务对新插入的记录可以不显式的加锁(生成一个锁结构),但是由于
事务id
的存在,相当于加了一个隐式锁
。别的事务在对这条记录加S锁
或者X锁
时,由于隐式锁
的存在,会先帮助当前事务生成一个锁结构,然后自己再生成一个锁结构后进入等待状态。隐式锁是一种延迟加锁
的机制,从而来减少加锁的数量。
隐式锁在实际内存对象中并不含有这个锁信息。只有当产生锁等待时,隐式锁转化为显示锁。
通过特定的语句进行加锁,我们一般称之为显示加锁,例如:
显示加共享锁:
select .... lock in share mode
显示加排它锁:
select .... for update
全局锁就是对
整个数据库实例
加锁。当你需要让整个库处于只读状态
的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用场景
是:做全库逻辑备份
。
全局锁的命令:Flush tables with read lock
概念
产生死锁的必要条件
1、两个或者两个以上的事务。
2、每个事务都已经持有锁并且申请新的锁。
3、锁资源同时只能被同一个事务持有或者不兼容。
4、事物之间因为持有锁和申请锁导致彼此循环等待。
死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。
如何处理死锁
方式1:
等待,直到超时(innodb_lock_wait_timeout=50s)。
即当两个事务互相等待时,当一个事务等待时间超过设置的阈值时,就将其回滚
,另外事务继续进行。这种方法简单有效,在innodb中,参数innodb_lock_wait_timeout
用来设置超时时间。
缺点:对于在线服务来说,这个等待时间往往是无法接受的。
那将此值修改短一些,比如1s,0.1s是否合适?不合适,容易误伤到普通的锁等待。
方式2
:使用死锁检测进行死锁处理
方式1检测死锁太过被动,innodb还提供了wait-for graph算法
来主动进行死锁检测,每当加锁请求无法立即满足需要进入等待时,wait-for graph算法都会被触发。
这是一种较为主动的死锁检测机制
,要求数据库保存锁的信息链表
和事务等待链表
两部分信息。
死锁检测的原理是构建一个以事务为顶点,锁为边的有向图,判断有向图是否存在环,存在即有死锁。
一旦检测到回路,有死锁,这时候InnoDB存储引擎会选择回滚undo量最小的事务
,让其他事务继续执行(innodb_deadlock_detect=on
表示开启这个逻辑)。
缺点:每个新的被阻塞的线程,都要判断是不是由于自己的加入导致了死锁,这个操作时间复杂度是0(n)。如果100个并发线程同时更新同一行,意味着要检测100*100=1万次,1万个线程就会有1千万次检测。
如何解决?
方式1:关闭死锁检测,但意味着可能会出现大量的超时,会导致业务有损。
方式2:控制并发访问的数量。比如在中间件中实现对于相同行的更新,在进入引擎之前排队,这样在InnoDB内部就不会有大量的死锁检测工作。
进一步的思路:
可以考虑通过将一行改成逻辑上的多行来减少锁冲突
。比如,连锁超市账户总额的记录,可以考虑放到多条记录上。账户总额等于这多个记录的值的总和。
如何避免死锁
1、合理设置索引,使业务SQL尽可能通过索引定位更少的行,减少锁竞争。
2、调整业务逻辑SQL执行顺序,避免update/delete长时间持有锁的SQL在事务前面。
3、避免大事务,尽量将大事务拆成多个小事务来处理,小事务缩短锁定资源的时间,发生锁冲突的几率也更小。
4、在并发比较高的系统中,不要显式加锁,特别是是在事务里显示加锁。如select … for update语句,如果是在事务里运行了start transaction或设置了autocommit等于0,那么就会锁定所查找到的记录。
5、降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
我们前边说对一条记录加锁的本质就是内存中创建一个
锁结构
与之关联,那么是不是一个事务对多条记录加锁,就要创建多个锁结构
呢?比如:
# 事务T1
SELECT * FROM user LOCK IN SHARE MODE;
理论上创建多个
锁结构
没问题,但是如果一个事务要获取10000条记录的锁,生成10000个锁结构也太崩溃了!所以决定在对不同记录加锁时,如果符合下边这些条件的记录会放到一个锁结构
中。
1、在同一个事务中进行加锁操作。
2、被加锁的记录在同一个页面中。
3、加锁的类型是一样的。
4、等待状态是一样的。
InnoDB
存储引擎中的锁结构
如下:
MVCC(Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC是通过数据行的多个版本管理来实现数据库的
并发控制
。这项技术使得在InnoDB的事务隔离级别下执行一致性读
操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
MVCC没有正式的标准,在不同的DBMS中MVCC的实现方式可能是不同的,也不是普通使用的(大家可以参考相关的DBMS文档)。这里讲解InnoDB中MVCC的实现机制(MySQL其它的存储引擎并不支持它)。
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理
读-写冲突
,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
,而这个读指的就是快照读
,而非当前读
。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
快照读又叫一致性读,读取的快照数据。
不加锁的简单的SELECT都属于快照读
,即不加锁的非阻塞读;比如这样:
SELECT * FROM player WHERE ...
之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。
既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。
当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的SELECT,或者对数据进行增删改都会进行当前读。比如:
SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁
回顾一下undo日志的版本链,对于使用
InnoDB
存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。
trx_id
:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id
赋值给trx_id
隐藏列。
roll_pointer
:每次对某条聚簇索引进行改动时,都会把旧的版本写入到undo日志
中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
MVCC的实现依赖于:
隐藏字段、Undo Log、Read View
在MVCC机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在Undo Log里。如果一个事务想查询这个行记录,需要读取哪个版本的行记录呢?这时就需要用到ReadView了,它帮我们解决了行的可见性问题。
ReadView就是事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数组,用来记录并维护系统当前
活跃事务
的ID(“活跃”指的就是,启动了但还没提交)。
使用
READ UNCOMMITTED
隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
使用
SERIALIZABLE
隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。
使用
READ COMMITTED
和REPEATABLE READ
隔离级别的事务,都必须保证读到已经提交了的
事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。
这个ReadView中主要包含4个比较重要的内容,分别如下:
1、
creator_trx_id
,创建这个Read View的事务ID。
说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个读事务中的事务id值都默认为0。
2、
trx_ids
,表示在生成ReadView时当前系统中活跃的读写事务的事务id列表
。
3、
up_limit_id
,活跃的事务中最小的事务ID。
4、
low_limit_id
,表示生成ReadView时系统中应该分配给下一个事务的id
值。low_limit_id是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在又id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。
有了这个ReadView,这样的访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。
如果被访问的trx_id属性值与ReadView中
creator_trx_id
值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值小于ReadView中
up_limit_id
值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
如果访问版本的trx_id属性值大于或等于ReadView中的
low_limit_id
值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
如果被访问版本的trx_id属性值在ReadView的
up_limit_id
和low_limit_id
之间,那就需要判断一下trx_id属性是不是在trx_ids
列表中。
如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过MVCC找到它:
1、首先获取事务自己的版本号,也就是事务ID;
2、获取ReadView;
3、查询得到的数据,然后与ReadView中的事务版本号进行比较;
4、如果不符合ReadView规则,就需要从Undo Log中获取历史快照;
5、最后返回符合规则的数据。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依次类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
InnoDB中,MVCC是通过Undo Log + Read View进行数据读取,Undo Log保存了历史快照,而Read View规则帮我们判断当前版本的数据是否可见。
在隔离级别为读已提交(Read Committed)时,一个事务中的每一次SELECT查询都会重新获取第一次Read View。
如图所示:
注意,此时同样的查询语句都会重新获取一次Read View,这时如果Read View不同,就可能产生不可重复读或者幻读的情况。
当隔离级别为可重复读的时候,就避免不了不可重复读,这时因为一个事务只在第一次SELECT的时候会获取一次Read View,而后面所有的SELECT都会复用这个Read View,如下表所示:
这里介绍了
MVCC
在READ COMMITTD、REPEATABLE READ
这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的读-写、写-读
操作并发执行,从而提升系统性能。
核心点在于ReadView的原理,
READ COMMITTD、REPEATABLE READ
这两个隔离级别的一个很大不同就是生成ReadView的时机不同:
READ COMMITTD
在每一次进行普通SELECT操作前都会生成一个ReadView。
REPEATABLE READ
只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
说明:我们之前说执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对记录打了一个删除标志位,这主要就是为MVCC服务的。
通过MVCC我们可以解决:
1、读写之间阻塞的问题
。通过MVCC可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
2、
降低了死锁的概率
。这是因为MVCC采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
3、
解决快照读的问题
。当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。