mysql知识点

参考:
MySQL性能调优与架构设计-备课笔记 枫叶云笔记
索引失效的10种场景,你知道几个呢?(面试必刷!)_索引失效的场景-CSDN博客
【mysql进阶】MTS主从同步原理及实操指南(七)_无线mts控制器、备用控制器站点参照,不同步-CSDN博客
MySQL数据库之分库分表方案_51CTO博客_数据库分库分表方案
MySQL-进阶篇-SQL优化(插入数据优化、主键优化、order by优化、group by
优化、limit优化、count优化、update优化)-CSDN博客

1.计数器表如何实现

用一张表来存储计数器,如:id、模块名称、点击总次数。

但是随之而来会发生,并发情况下,所有操作倒要串行化处理,使得效率降低。可以采用以下方法来优化:

新增字段:slot(槽)字段。

然后一个模块可以有n条记录,用槽字段来区分,然后再需要增加计数器的时候,就随机选择一条记录来处理。要统计的时候,就求和。

2.字段优化

2.1.基本原则

尽可能选择存储数据少的字段类型;优先选择整形,避免NULL的情况。

通常情况下最好指定列为NOT NULL,除非真的需要存储NULL值。

如果查询中包含可为NULL的列,对MySQL来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊处理。当可为NULL的列被索引时,每个索引记录需要一个额外的字节。

通常把可为NULL的列改为NOT NULL带来的性能提升比较小,所以(调优时)没有必要首先在现有schema中查找并修改掉这种情况,除非确定这会导致问题。但是,如果计划在列上建索引,就应该尽量避免设计成可为NULL的列。

2.2.整形的注意点

MySQL可以为整数类型指定宽度,例如INT(11),对大多数应用这是没有意义的,它不会限制值的合法范围,只是规定了MySQL的一些交互工具(例如MySQL命令行客户端)用来显示字符的个数。对于存储和计算来说,INT(1)和INT(20)是相同的。

2.3.实数的注意点

实数是带有小数部分的数字。MySQL既支持精确类型的存储DECIMAL类型,也支持不精确类型存储FLOAT和 DOUBLE类型(浮点类型)。DECIMAL类型用于存储精确的小数,本质上MySQL是以字符串形式存放的。所以CPU不支持对DECIMAL的直接计算,只是在MySQL中自身实现了DECIMAL的高精度计算。相对而言,CPU直接支持原生浮点计算,所以浮点运算明显更快。

浮点类型在存储同样范围的值时,通常比DECIMAL使用更少的空间。FLOAT使用4个字节存储,DOUBLE占用8个字节,DECIMAL里面存储65个数字,DECIMAL对于列的空间消耗比较大,另外DOUBLE比 FLOAT有更高的精度和更大的范围。

如何选择?

在精度不敏感和需要快速运算的时候,选择FLOAT和 DOUBLE。

应该尽量只在对小数进行精确计算时才使用DECIMAL,例如存储财务或金融数据。

但在数据量比较大的而且要求精度时,可以考虑使用BIGINT代替DECIMAL,将需要存储的货币单位根据小数的位数乘以相应的倍数即可。假设要存储财务数据精确到万分之一分,则可以把所有金额乘以一百万,然后将结果存储在BIGINT里,这样可以同时避免浮点存储计算不精确和 DECIMAL精确计算代价高的问题。

2.4.字符串类型的注意点

2.4.1.char和varchar

VARCHAR

VARCHAR类型用于存储可变长字符串,是最常见的字符串数据类型。它比定长类型更节省空间,因为它仅使用必要的空间(例如,越短的字符串使用越少的空间)。在内部实现上,既然是变长,VARCHAR需要使用1或2个额外字节记录字符串的长度,如果列的最大长度小于或等于255字节,则只使用1个字节表示,否则使用2个字节。

VARCHAR节省了存储空间,所以对性能也有帮助。但是,由于行是变长的,在UPDATE时新值比旧值长时,使行变得比原来更长,这就肯能导致需要做额外的工作。如果一个行占用的空间增长,并且在页内没有更多的空间可以存储,在这种情况下,MyISAM会将行拆成不同的片段存储,InnoDB则需要分裂页来使行可以放进页内。

CHAR

CHAR类型是定长的,MySQL总是根据定义的字符串长度分配足够的空间。当存储CHAR值时,MySQL会删除所有的末尾空格,CHAR值会根据需要采用空格进行填充以方便比较。

CHAR与VARCHAR如何选择?

在CHAR和VARCHAR的选择上,这些情况下使用VARCHAR是合适的:

字符串列的最大长度比平均长度大很多,列的更新很少;使用了像UTF-8这样复杂的字符集,每个字符都使用不同的字节数进行存储。

CHAR适合存储很短的字符串,或者所有值定长或都接近同一个长度。例如,CHAR非常适合存储密码的MD5值,因为这是一个定长的值。对于经常变更的数据,CHAR也比VARCHAR更好,因为定长的CHAR类型不容易产生碎片。

对于非常短的列,CHAR比VARCHAR在存储空间上也更有效率。例如用CHAR( 1)来存储只有Y和N的值,如果采用单字节字符集只需要一个字节,但是VARCHAR(1)却需要两个字节,因为还有一个记录长度的额外字节。

另外,使用VARCHAR(5)和VARCHAR(200)存储'hello'在磁盘空间上开销是一样的。我们随便选择一个就好?应该使用更短的列,为什么?

更长的列会消耗更多的内存,因为MySQL通常会分配固定大小的内存块来保存内部值。尤其是使用内存临时表进行排序或操作时会特别糟糕。在利用磁盘临时表进行排序时也同样糟糕。

所以最好的策略是只分配真正需要的空间。

2.4.2.blob和text

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

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

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

注意:

(1)BLOB和 TEXT 值会引起一些性能问题,所以尽量避免使用BLOB和TEXT类型;

(2)一定要用,建议把BLOB或TEXT 列分离到单独的表中;

(3)在不必要的时候避免检索大型的 BLOB或TEXT值。例如,SELECT *查询就不是很好的想法,除非能够确定作为约束条件的WHERE子句只会找到所需要的数据行。否则,很可能毫无目的地在网络上传输大量的值。建议可以搜索索引列,决定需要的哪些数据行,然后从符合条件的数据行中检索BLOB或 TEXT值;

(4)还可以使用合成的(Synthetic)索引来提高大文本字段(BLOB或TEXT)的查询性能。简单来说,合成索引就是根据大文本字段的内容建立一个散列值,并把这个值存储在单独的数据列中,接下来就可以通过检索散列值找到数据行了。但是,要注意这种技术只能用于精确匹配的查询(散列值对于类似"<"或">="等范围搜索操作符是没有用处的)。可以使用MD5函数生成散列值,也可以使用SHA1(或CRC32),或者使用自己的应用程序逻辑来计算散列值。

2.4.3.时间

datetime 存储日期范围:1001年~9999年

timestamp 存储日期范围:1970年~2038年,并且跟时区有关系。

2.5.命名注意

说明:MySQL在Windows下不区分大小写,但在Linux下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。

表达是与否概念的字段,应该使用is_xxx的方式命名,数据类型是unsigned tinyint(1表示是,0表示否)。

3.索引

3.1.Hash索引

为什么HashMap不适合做数据库索引?

1、hash表只能匹配是否相等,不能实现范围查找;

2、当需要按照索引进行order by时,hash值没办法支持排序;

3、组合索引可以支持部分索引查询,如(a,b,c)的组合索引,查询中只用到了a和b也可以查询的,如果使用hash表,组合索引会将几个字段合并hash,没办法支持部分索引;

4、当数据量很大时,hash冲突的概率也会非常大。

3.2.B+Tree

一棵m阶的B+树完整定义如下:

每个节点最多可以有 m 个元素;

除了根节点外,每个节点最少有 (m/2) 个元素;

如果根节点不是叶节点,那么它最少有 2 个孩子节点;

所有的叶子节点都在同一层;

非叶子节点只存放关键字和指向下一个孩子节点的索引,记录只存放在叶子节点中;

一个有 k 个孩子节点的非叶子节点有(k-1) 个元素,按升序排列;

某个元素的左子树中的元素都比它小,右子树的元素都大于或等于它(二叉排序树的特征);

相邻的叶子节点之间用指针相连。

3.2.1.BTree和B+Tree的区别

B树 的非叶子节点也会存储数据。

B+树底层的相邻叶子节点存在链表的结构。

3.2.2.为什么选择B+Tree?

因为B数据每个节点都存储数据,每次查询的数据大小固定,就会造成每次查询返回的数据的条数变少,相同数据规模的情况下B树会增加io次数,而B+树,则数据量较小,一次可以返回多条记录,io次数较少。

范围查询B+树明显优于B树。

相较于二叉树,B+Tree的深度小,所以对应的IO也会相应减少。减少IO则效率变高。

详细参考以下描述:

https://cloud.fynote.com/share/d/XzdbOSw#1-1-8-MySQL与B-树_207

3.2.3.聚集索引和非聚集索引

聚集索引,就是将表的主键用来构造一棵B+树,并且将整张表的行记录数据存放在该B+树的叶子节点中。也就是所谓的索引即数据,数据即索引。由于聚集索引是利用表的主键构建的,所以每张表只能拥有一个聚集索引。

聚集索引的叶子节点就是数据页。换句话说,数据页上存放的是完整的每行记录。因此聚集索引的一个优点就是:通过过聚集索引能获取完整的整行数据。另一个优点是:对于主键的排序查找和范围查找速度非常快。

非聚集索引的叶子节点存储的是主键。

3.2.4.二级索引/辅助索引

聚簇索引只能在搜索条件是主键值时才能发挥作用,因为B+树中的数据都是按照主键进行排序的。

如果我们想以别的列作为搜索条件怎么办?我们一般会建立多个索引,这些索引被称为辅助索引/二级索引(即除主键索引之外的索引)。

这里引申出回表的概念:先通过二级索引查找到指定数据的主键,再通过主键索引获取到真正的数据行。

3.2.5.联合索引/复合索引

比如index(a,b)就是将a,b两个列组合起来构成一个索引。

这里引出覆盖索引:在联合索引上包含了需要查询的字段,可以避免回表。

最佳左前缀法则:

必须从索引的最左字段开始匹配,才能有效利用索引。

若查询条件未包含索引的最左字段,则无法利用该联合索引。

注意:

若查询条件包含范围查询(如>、<、BETWEEN、LIKE),则后续字段可能无法利用索引。

最佳左前缀法则示例:

假设表users有联合索引INDEX idx_name_age_email (name, age, email):

查询条件 是否利用索引 原因
WHERE name = 'Alice' ✅ 是 匹配最左字段name,可利用索引。
WHERE name = 'Alice' AND age = 20 ✅ 是 匹配最左字段name和次左字段age,可利用索引。
WHERE name = 'Alice' AND email = '[email protected]' ✅ 是 匹配最左字段name和第三字段email,可利用索引(但age部分未匹配)。
WHERE age = 20 ❌ 否 未匹配最左字段name,无法利用索引。
WHERE age = 20 AND email = '[email protected]' ❌ 否 未匹配最左字段name,无法利用索引。
WHERE name LIKE 'A%' ✅ 是 匹配最左字段name的前缀,可利用索引(但LIKE '%A'无法利用索引)。

3.2.6.索引的选择性

创建索引应该选择选择性/离散性高的列。索引的选择性/离散性是指,不重复的索引值(也称为基数,cardinality)和数据表的记录总数(N)的比值,范围从1/N到1之间。索引的选择性越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

很差的索引选择性就是列中的数据重复度很高,比如性别字段,不考虑政治正确的情况下,只有两者可能,男或女。那么我们在查询时,即使使用这个索引,从概率的角度来说,依然可能查出一半的数据出来。

3.2.7.索引的失效情况

参考:

https://blog.csdn.net/weixin_46224056/article/details/137597431

注意:在判断最左侧原则时,即使查询字段符合索引中字段的顺序,需要考虑范围查询是否会破坏

3.3.全文索引

全文索引(Full-Text Index)是一种用于高效检索文本数据中特定词汇或短语的数据库技术,特别适用于需要快速搜索大量文本内容的场景(如文章、评论、产品描述等)。

例:

sql 复制代码
CREATE TABLE articles ( 
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 
title VARCHAR(200), 
content TEXT, 
FULLTEXT (title, content) -- 在title和content字段上创建联合全文索引 
) ENGINE=InnoDB; -- MySQL 5.6+支持InnoDB的全文索引

3.3.1.前缀索引

针对blob、text、很长的varchar字段,mysql不支持索引他们的全部长度,需建立前缀索引。

例:

语法:Alter table tableName add key/index (column(X))

3.3.2.后缀索引

Mysql不支持后缀索引,但是可以通过将字符串倒置存储,再通过前缀索引可以实现后缀索引的功能。

3.4.三星索引

三星索引需满足的条件如下:

· 索引将相关的记录放到一起则获得一星 (比重27%)

· 如果索引中的数据顺序和查找中的排列顺序一致则获得二星(排序星) (比重27%)

· 如果索引中的列包含了查询中需要的全部列则获得三星(宽索引星) (比重50%)

一星:

一星的意思就是:如果一个查询相关的索引行是相邻的或者至少相距足够靠近的话,必须扫描的索引片宽度就会缩至最短,也就是说,让索引片尽量变窄,也就是我们所说的索引的扫描范围越小越好。等值查询可以匹配索引列的顺序。

二星(排序星) :

在满足一星的情况下,当查询需要排序,group by、 order by,如果查询所需的顺序与索引是一致的(索引本身是有序的),是不是就可以不用再另外排序了,一般来说排序可是影响性能的关键因素。

三星(宽索引星) :

在满足了二星的情况下,如果索引中所包含了这个查询所需的所有列(包括 where 子句和 select 子句中所需的列,也就是覆盖索引),这样一来,查询就不再需要回表了,减少了查询的步骤和IO请求次数,性能几乎可以提升一倍。

3.4.1.例1

现在有表,SQL如下

sql 复制代码
CREATE TABLE customer (
	cno INT,
	lname VARCHAR (10),
	fname VARCHAR (10),
	sex INT,
	weight INT,
	city VARCHAR (10)
);
CREATE INDEX idx_cust ON customer (city, lname, fname, cno);

对于下面的SQL而言,这是个三星索引

sql 复制代码
select cno,fname from customer where lname='xx' and city ='yy' order by fname;

一星:所有等值谓词的列,是组合索引的开头的列,可以把索引片缩得很窄,符合。

二星:order by的fname字段在组合索引中且是索引自动排序好的,符合。

三星:select中的cno字段、fname字段在组合索引中存在,符合。

3.4.2.例2

现有表:

sql 复制代码
CREATE TABLE `test` ( 
`id` INT (11) NOT NULL AUTO_INCREMENT, 
`user_name` VARCHAR (100) DEFAULT NULL, 
`sex` INT (11) DEFAULT NULL, 
`age` INT (11) DEFAULT NULL, 
`c_date` datetime DEFAULT NULL, 
PRIMARY KEY (`id`),
 ) ENGINE = INNODB AUTO_INCREMENT = 12 DEFAULT CHARSET = utf8;

索引:(user_name,sex,age)

对于sql:

sql 复制代码
select user_name,sex,age from test where user_name like 'test%'  and sex =1 ORDER BY age

来说

一星:通过user_name字段的范围查找和sex字段点的等值查找,可以查询出可以将查询范围缩小,符合。

二星:由于user_name是like查询,sex是等值查询,此时索引查询出的结果不保证按照age排序,不符合。(这里需要展开解释:当user_name和sex是等值查询时,查询的结果是根据age排序的,但是由于user_name是范围查询,B+树的排序顺序是user_name,sex,age,所以在多个符合条件的user_name下的age不能保证整体的有序性。)

三星:不存在回表,满足。

4.Mysql性能调优

4.1.开启慢查询配置

可以 通过开启慢查询日志,帮助定位慢SQL问题。MySQL默认关闭该功能。

通过下面的命令可以查看,系统是否开起来慢SQL日志。

sql 复制代码
show VARIABLES like 'slow_query_log';

开启慢SQL日志:

sql 复制代码
set GLOBAL slow_query_log=1;

查看阈值(单位,秒)

sql 复制代码
show VARIABLES like '%long_query_time%';

设置阈值

sql 复制代码
set global long_query_time=3;

查看是否开启:没有走索引的SQL也计入慢SQL日志

sql 复制代码
show VARIABLES like '%log_queries_not_using_indexes%';

查看日志文件名称

sql 复制代码
show VARIABLES like '%slow_query_log_file%';

4.2.Explain

EXPLAIN语句来帮助我们查看某个查询语句的具体执行计划。

总的来说通过EXPLAIN我们可以:

表的读取顺序

数据读取操作的操作类型

哪些索引可以使用

哪些索引被实际使用

表之间的引用

每张表有多少行被优化器查询

Explain语句的返回值:

id : 在一个大的查询语句中每个SELECT关键字都对应一个唯一的id

select_type : SELECT****关键字对应的那个查询的类型

table :表名

partitions :匹配的分区信息

type :针对单表的访问方法

possible_keys :可能用到的索引

key :实际上使用的索引

key_len :实际使用到的索引长度

ref :当使用索引列等值查询时,与索引列进行等值匹配的对象信息

rows :预估的需要读取的记录条数

filtered :某个表经过搜索条件过滤后剩余记录条数的百分比

Extra :---些额外的信息

4.2.1.Id

每个select都会有一个id。

SELECT* FROM s1 WHERE id IN ( SELECT * FROM s2);

上述sql就会有两个select,理论上应该有两个id,但是mysql可能会对sql进行优化,所以使用explain语句可能只有一个id。

EXPLAIN

select id

from user

union

select user_id

from user_book_rel

上述sql会返回三个id。

4.2.2.Select_type

Select对应的查询的类别,值和其含义:

含义 示例
SIMPLE 简单SELECT查询,不包含子查询、UNION或关联查询。 EXPLAIN SELECT * FROM t1;
PRIMARY 最外层的SELECT查询,通常出现在包含UNION或子查询的语句中。 EXPLAIN SELECT * FROM t1 UNION SELECT * FROM t2;第一个SELECT标记为PRIMARY,第二个SELECT标记为UNION。
UNION UNION操作中的第二个或后续SELECT查询。 EXPLAIN SELECT * FROM t1 UNION SELECT * FROM t2;第二个SELECT标记为UNION。
DEPENDENT UNION UNION操作中的第二个或后续SELECT查询,且依赖外部查询的结果。 EXPLAIN SELECT * FROM t1 WHERE a IN (SELECT b FROM t2 UNION SELECT c FROM t3 WHERE t3.d = t1.e);第二个SELECT(SELECT c FROM t3)可能标记为DEPENDENT UNION。
UNION RESULT UNION操作的结果集,表示合并后的临时表。 EXPLAIN SELECT * FROM t1 UNION SELECT * FROM t2;最终结果集标记为UNION RESULT
SUBQUERY 子查询中的第一个SELECT,不依赖外部查询。 EXPLAIN SELECT * FROM t1 WHERE a IN (SELECT b FROM t2);子查询SELECT b FROM t2标记为SUBQUERY。
DEPENDENT SUBQUERY 子查询中的第一个SELECT,且依赖外部查询的结果(相关子查询)。 EXPLAIN SELECT * FROM t1 WHERE a IN (SELECT b FROM t2 WHERE t2.c = t1.d);子查询SELECT b FROM t2标记为DEPENDENT SUBQUERY。
DERIVED 派生表(FROM子句中的子查询),MySQL会将其结果物化为临时表。 EXPLAIN SELECT * FROM (SELECT * FROM t1) AS derived_t1;EXPLAIN SELECT * FROM (SELECT * FROM t1) AS derived_t1;
DEPENDENT DERIVED 依赖外部查询的派生表(相关派生表)。 EXPLAIN SELECT * FROM t1 WHERE a IN (SELECT b FROM (SELECT * FROM t2 WHERE t2.c = t1.d) AS derived_t2);派生表derived_t2标记为DEPENDENT DERIVED
MATERIALIZED 物化子查询,表示子查询的结果被缓存到临时表以提高性能。 EXPLAIN SELECT * FROM t1 WHERE a IN (SELECT b FROM t2);若子查询被物化,则标记为MATERIALIZED。
UNCACHEABLE SUBQUERY 无法缓存的子查询,结果可能因外部数据变化而变化(如包含非确定性函数)。 EXPLAIN SELECT * FROM t1 WHERE a = (SELECT RAND());子查询标记为UNCACHEABLE SUBQUERY。
UNCACHEABLE UNION 无法缓存的UNION子查询,属于UNCACHEABLE SUBQUERY的一部分。 EXPLAIN SELECT * FROM t1 WHERE a IN (SELECT b FROM t2 UNION SELECT RAND());包含非确定性函数的UNION子查询标记为UNCACHEABLE UNION。

4.2.3.Table

每个select查询了哪张表

4.2.4.Partitions

根据具体的分区有关

4.2.5.Tpye

我们前边说过执行计划的一条记录就代表着MySQL对某个表的执行查询时的访问方法/访问类型,其中的type列就表明了这个访问方法/访问类型是个什么东西,是较为重要的一个指标,结果值从最好到最坏依次是:

出现比较多的是system>const>eq_ref>ref>range>index>ALL

一般来说,得保证查询至少达到range级别,最好能达到ref。

system:表只有一行数据(系统表),这是const类型的特例,查询效率最高。

const:通过主键或唯一索引查找,且最多返回一行数据,查询条件是常量。

eq_ref:在联表查询中,通过主键或唯一非空索引进行等值匹配,每行只匹配一行数据。

ref:通过非唯一索引或唯一索引的前缀进行等值匹配,可能返回多行数据。

fulltext:使用全文索引进行全文检索。

ref_or_null:与ref类似,但允许查找列中值为NULL的行。

index_merge:使用多个索引的合并优化。

unique_subquery:在IN/EXISTS子查询中使用唯一索引查询。

index_subquery:在IN/EXISTS子查询中使用索引查询。

range:对索引进行范围扫描,适用于BETWEEN、IN()等操作符。

index:全索引扫描,相当于ALL,但扫描的是索引而非数据行。

ALL:全表扫描,意味着MySQL需要遍历整个表的所有数据行,性能最差。

4.2.6.possible_keys与key

在EXPLAIN 语句输出的执行计划中,possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些,key列表示实际用到的索引有哪些,如果为NULL,则没有使用索引。

4.2.7.Key_len

key_len列表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度,计算方式是这样的:

对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就是该固定值,对于指定字符集的变长类型的索引列来说,比如某个索引列的类型是VARCHAR(100),使用的字符集是utf8,那么该列实际占用的最大存储空间就是100 x 3 = 300个字节。

如果该索引列可以存储NULL值,则key_len比不可以存储NULL值时多1个字节。

对于变长字段来说,都会有2个字节的空间来存储该变长列的实际长度。

4.2.8.Row

如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的rows列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的rows列就代表预计扫描的索引记录行数。

4.2.9.Filtered

查询优化器预测有多少条记录满⾜其余的搜索条件。

例:

sql 复制代码
`explain select name,age,sex from user where id = 1 and sex = 1;`

上述sql通过主键查询,且分析走主键索引,sex不在主键索引中,但是在查询条件内。Filtered表示在符合id=1的记录中有多少百分比的数据满足sex=1。

4.2.10.Extra

一些补充信息

4.3.Count查询优化

COUNT()都需要扫描大量的行(意味着要访问大量数据)才能获得精确的结果,因此是很难优化的。在MySQL层面能做的基本只有索引覆盖扫描了。如果这还不够,就需要考虑修改应用的架构,可以用估算值取代精确值,可以增加汇总表,或者增加类似Redis这样的外部缓存系统。

4.4.Limit优化

会先查询翻页中需要的N条数据的主键值,然后根据主键值回表查询所需要的N条数据,在此过程中查询N条数据的主键id在索引中完成,所以效率会高一些。

例:

sql 复制代码
EXPLAIN SELECT * FROM (select id from order_exp limit 10000,10) b,order_expa where a.id = b.id;

特殊情况下还可以这样优化:

sql 复制代码
EXPLAIN select * from order_exp where id > 67 order by id limit 10;

5.事务

5.1.事务的特性

原子性

一致性

隔离性

持久性

5.2.并发面临的问题

脏读:一个事务A读取到了另一个事务B未提交的数据,B回滚之后,导致A读取的数据毫无意义。

幻读:同一个查询条件,第一次读到n条,第二次读变成了m条。

不可重复读:同一条数据,第一次和第二次读取到的内容不一致。

5.3.事务隔离级别

读未提交:可能发生脏读、幻读、不可重复读

读已提交:可能发生不可重复读和幻读,但是能避免脏读

可重复读:可能发生幻读,但是能避免不可重复读和脏读(mysql默认级别)

可串行化:所有问题都能避免

5.4.隐式提交

5.4.1.修改数据库对象

当我们使用CREATE、ALTER、DROP等语句去修改这些所谓的数据库对象时,就会隐式的提交前边语句所属于的事务。

5.4.2.修改mysql表

当我们使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD等语句时也会隐式的提交前边语句所属于的事务。

5.4.3.上个事务未提交又开始一个事务

当我们在一个会话里,一个事务还没提交或者回滚时就又使用START TRANSACTION或者BEGIN语句开启了另一个事务时,会隐式的提交上一个事务。

5.4.4.加载数据

LOAD DATA

5.4.5.复制数据

使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等语句时也会隐式的提交前边语句所属的事务。

5.4.6.其它

使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、 LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。

6.MVCC

MVCC-多版本并发控制

6.1.版本链

对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:

trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。(即上一个修改该数据的事务id)

roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。(指向事务改动之前状态的指针)

参考:

https://cloud.fynote.com/share/d/XzdbOSw#1-5-1-2-版本链_967

7.锁

7.1.锁定读LBCC(当前读)

select lock in share mode (共享锁)、select for update (排他锁)、update (排他锁)、insert (排他锁/独占锁)、delete (排他锁)、串行化事务隔离级别都是当前读。

当前读这种实现方式,也可以称之为LBCC(基于锁的并发控制,Lock-Based Concurrency Control)。

7.1.1.共享锁和独享锁

共享锁英文名:Shared Locks,简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁。

假如事务E1首先获取了一条记录的S锁之后,事务E2接着也要访问这条记录:

如果事务E2想要再获取一个记录的S锁,那么事务E2也会获得该锁,也就意味着事务E1和E2在该记录上同时持有S锁。

独占锁,也常称排他锁,英文名:Exclusive Locks,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁。

如果事务E2想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务E1提交之后将S锁释放掉。

如果事务E1首先获取了一条记录的X锁之后,那么不管事务E2接着想获取该记录的S锁还是X锁都会被阻塞,直到事务E1提交。

所以我们说S锁和S锁是兼容的,S锁和X锁是不兼容的,X锁和X锁也是不兼容的,画个表表示一下就是这样:

X 不兼容X 不兼容S

S 不兼容X 兼容S

7.1.2.锁定读sql

共享锁:

SELECT * from test LOCK IN SHARE MODE;

独享锁:

SELECT * from test where Id = 1 for update

也就是在普通的SELECT语句后边加LOCK IN SHARE MODE,如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的S锁(比方说别的事务也使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录),但是不能获取这些记录的X锁(比方说使用SELECT ... FOR UPDATE语句来读取这些记录,或者直接修改这些记录)。

如果别的事务想要获取这些记录的X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的S锁释放掉。

7.1.3.意向锁

意向共享锁 ,英文名:Intention Shared Lock,简称IS锁。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。

意向独占锁 ,英文名:Intention Exclusive Lock,简称IX锁。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。

7.1.4.表级别的AUTO-INC锁

在使用MySQL过程中,我们可以为表的某个列添加AUTO_INCREMENT属性,之后在插入记录时,可以不指定该列的值,系统会自动为它赋上递增的值系统实现这种自动给AUTO_INCREMENT修饰的列递增赋值的原理主要是两个:

1、采用AUTO-INC锁,也就是在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。这样一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。

如果我们的插入语句在执行前不可以确定具体要插入多少条记录(无法预计即将插入记录的数量),比方说使用INSERT ... SELECT、REPLACE ... SELECT或者LOAD DATA这种插入语句,一般是使用AUTO-INC锁为AUTO_INCREMENT修饰的列生成对应的值。

2、采用一个轻量级的锁,在为插入语句生成AUTO_INCREMENT修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的AUTO_INCREMENT列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁。

如果我们的插入语句在执行前就可以确定具体要插入多少条记录,比方说我们上边举的关于表t的例子中,在语句执行前就可以确定要插入2条记录,那么一般采用轻量级锁的方式对AUTO_INCREMENT修饰的列进行赋值。这种方式可以避免锁定表,可以提升插入性能。

InnoDB提供了一个称之为innodb_autoinc_lock_mode的系统变量来控制到底使用上述两种方式中的哪种来为AUTO_INCREMENT修饰的列进行赋值,当innodb_autoinc_lock_mode值为0时,一律采用AUTO-INC锁;当innodb_autoinc_lock_mode值为2时,一律采用轻量级锁;当innodb_autoinc_lock_mode值为1时,两种方式混着来(也就是在插入记录数量确定时采用轻量级锁,不确定时使用AUTO-INC锁)。

不过当innodb_autoinc_lock_mode值为2时,可能会造成不同事务中的插入语句为AUTO_INCREMENT修饰的列生成的值是交叉的,在有主从复制的场景中是不安全的。

7.2.MySQL行级锁

7.2.1.Record Locks

也叫记录锁,就是仅仅把一条记录锁上

7.2.2.Gap Locks

间隙锁,是 InnoDB 引擎在 可重复读(REPEATABLE READ) 隔离级别下,为防止 幻读(Phantom Read) 而设计的一种行级锁。

7.2.3.Next-Key Locks

有时候我们既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录,所以InnoDB就提出了一种称之为Next-Key Locks的锁,官方的类型名称为:LOCK_ORDINARY,我们也可以简称为next-key锁。next-key锁的本质就是一个记录锁和一个gap锁的合体。

默认情况下,InnoDB以REPEATABLE

READ隔离级别运行。在这种情况下,InnoDB使用Next-Key Locks锁进行搜索和索引扫描,这可以防止幻读的发生。

7.2.4.Insert Intention Locks

我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的gap锁(next-key锁也包含gap锁,后边就不强调了),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。

但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在处于等待状态。这种类型的锁命名为Insert Intention Locks,官方的类型名称为:LOCK_INSERT_INTENTION,我们也可以称为插入意向锁。

可以理解为插入意向锁是一种锁的的等待队列,让等锁的事务在内存中进行排队等待,当持有锁的事务完成后,处于等待状态的事务就可以获得锁继续事务了。

8.主键优化

8.1.插入优化

批量插入优化,用批量插入代替多条插入语句。

批量插入关闭自动提交。

大数据导入可以使用load命令。

8.2.主键优化

8.2.1.Innodb存储结构

最外层是表空间,表空间包含多个段,段包含多个分区,分区包含多个页,页包含多个行,行中存储的是各个字段的值。

相邻的页之间会存在双向指针。

每一行的存储是根据主键来排序的。

8.2.2.页分裂

由于每一行都根据主键排序存储,所以所以一旦是乱序插入,就需要将数据根据主键找到对应的位置进行插入,但是插入的位置所在的页可能已经满了,所以此时需要将该页进行拆分。改行为就是页分裂,页分裂比较消耗性能。

8.2.3.页合并

与页分裂相对的是,页合并。在数据库删除的时候,数据库并不是直接把这一行的数据删除掉,而是标记这块空间能够被使用。当一页中被标记删除的空间达到一半以上,innodb则会尝试将该页前后的页合并起来,优化空间利用。

8.2.4.主键设计原则

降低主键长度

主键长,二级索引多则会消耗更多的磁盘空间,并且系统io也会增大。

自增主键,避免采用随机字符串

顺序插入,尽可能避免页分裂和页合并。

尽可能减少主键修改

主键最好不要受业务影响。

8.2.5.Update优化

Update尽可能命中索引,因为如果没有命中索引,那么会导致全表扫描,使得行级锁范围增大,影响其他读写。

9.主从复制

9.1.基于binlog的主从复制

9.1.1.Binlog的三种模式

(1)ROW: 行记录形式,会记录每一行的数据变化情况,记录每一行数据修改前后的具体值。优点是记得细,能够重现实现数据修改细节。缺点是会产生大量日志,消耗性能。

(2)STATEMENT:状态记录形式,会记录每一条执行的SQL,然后通过SQL重放来实现数据同步。仅记录修改数据的 SQL 语句(如 UPDATE、DELETE、INSERT)。

优点:

日志量小:仅记录 SQL 语句,占用磁盘空间少。

性能开销低:对主库性能影响较小。

缺点:

主从复制不一致:若 SQL 语句包含非确定性函数(如 NOW()、UUID())或依赖上下文(如 AUTO_INCREMENT),可能导致主从数据不一致。如

-- 主库执行

INSERT INTO logs (message, created_at) VALUES ('test', NOW());

-- 从库因执行时间不同,created_at 值可能不同

(3)MIXED:混合模式,一般情况使用STATEMENT,当STATEMENT不能满足的则自动切换为ROW进行记录

9.1.2.主从复制过程

1)主库将数据的变化写入到binlog中

2)从库定期检测binlog文件是否有发生变化(检测是不需要读取文件详细内容的,可以通过文件最近修改时间和Etag来实现),如果有变化则开启一个IO线程来获取主库binlog数据

3)同时主库会为每个IO线程启动一个dump线程,用于先IO线程发送二进制事件(也就是binlog日志数据)。发送获取的数据并不会直接给到从库进行重放,而是先放到一个中继日志(relay log)中,然后从库再从中继日志读取数据,进行重发,从而实现数据同步

4)执行完成后,IO线程和dump线程进行睡眠状态,等待下一次数据同步再被唤醒

9.1.3.为什么需要中继日志?

解耦:防止重放binlog导致线程阻塞,造成整个同步的过程阻塞。将接收数据和重放数据分解开,让各个线程专心于自己的工作。

9.1.4.主从复制的延迟问题

从库SQL线程重放binlog时,相当于重新执行sql,所以此时的读写是随机的。随机读写相较于写binglog的顺序读写,速度会慢。

重放时,会面临加锁等操作,而SQL线程又是单线程,所以速度会慢。

9.1.5.解决延迟的办法

1 使用更高性能的硬件

2 一主多从

3 多主多从,业务拆分

4 集群部署

9.2.MTS主从同步

9.2.1.原理

MTS的核心概念在于多线程并行重放binlog,但是并不是所有的binlog都能并行重放,有些操作可能涉及锁竞争,那么就不能并行执行。

MTS中除了SQL线程,还创建了多个WORK线程,IO线程不断接收bin log,写入到relay log中,SQL线程读取relay log,并且判断哪些事件可以并行回放,哪些只能串行回放。并行回放的会分发给WORK线程并行回放。串行回放的就由SQL线程自己回放。

9.2.2.如何判断哪些事务能够并行回放

通过组提交来实现,即对事务进行分组,我们认为一个组提交的事务都是可以并行回放的,那么怎么判定事务可以处于同一个组呢?那就是看主库中事务执行时,是否能够同时提交成功,或者说同时处于prepare阶段的所有事务,都是可以同时提交的

这里需要大家了解mysql事务两阶段提交的流程,简单来说就是一个事务提交完成是需要经历两个阶段的,事务执行,将数据更新到内容后,会先写入redo log,这时事务处于prepare状态,再写入bin log,然后提交事务,并将redo log标注为commit状态,这样整个事务才算提交完成。经历了prepare,commit两个阶段

只要能同时处于prepare状态的事务,说明事务间是没有锁竞争的,那么就是可以并行执行的,同时也是可以并行回放的。如果有锁竞争的,那么该事务肯定要等待竞争事务先执行完,释放锁后才能执行,也就不可能同时处于prepare状态

9.2.3.如何知道哪些事务处于prepare状态?

mysql5.7引入了两个变量

  1. sequence_number(序列号)
    定义:每个事务在提交时会被分配一个全局唯一且单调递增的序列号。
    作用:
    唯一标识事务:通过序列号可以唯一确定一个事务的提交顺序。
    构建事务依赖链:序列号隐式表示了事务的提交顺序,为 last_committed 提供了基础。
  2. last_committed(最后提交事务的序列号)
    定义:事务在进入 prepare 阶段(即准备提交阶段)时,会记录当前"最后提交事务的序列号"。
    作用:
    标记事务组:具有相同 last_committed 值的事务被视为属于同一组,可并行执行。
    避免冲突:通过依赖关系确保同一组内的事务无冲突(如修改不同行)。
    组提交(Group Commit):
    主库在提交事务时,会将多个可以并行执行的事务合并为一个"组",一次性提交。
    组内事务共享同一个 last_committed 值(即组内第一个事务的 sequence_number)。

9.2.4.在binlog中如何标注哪些事务是同一组的?

其实我们上面已经讲到了,是通过last_committed信息来标注,但这里我们要拓展一下,mysql5.7中将sequence_number和last_committed信息属于组提交信息,组提交信息是存放到GTID事件中的,每个事务会有自己的GTID事件。

GTID默认是关闭的,如果关闭时会讲组提交信息存放到匿名GTID事件中(Anonymous_Gtid),如果开启了,就会存储到每个事务自己的GTID事件中,每个事务执行前都会添加一个GTID事件,用于记录当前的全局事务ID

9.2.5.主从复制流程

(1)主库将数据的变化写入到binlog中

(2)从库定期检测binlog文件是否有发生变化,如果有变化则开启一个IO线程来获取主库binlog数据

(3)同时主库会为每个IO线程启动一个dump线程,用于先IO线程发送二进制事件(也就是binlog日志数据)。发送获取的数据并不会直接给到从库进行重放,而是先放到一个中继日志(relay log)中

(4)从库的SQL线程从relay log中读取事务后,会获取该事务的组信息,拿到sequence_number和last_committed

(5)从库会记录已经执行了的事务的sequence_number的最小值,将其存放到low water mark变量中,简称lwm

(6)lwm与取出事务的last_committed比较,如果last_committed比lwm更小,说明取出事务与当前执行组为同组(本组事务的sequence_number的最小值肯定大于last_committed)。则SQL线程会找到一个空闲的WORK线程,如果有空闲的,就会直接重放这个事务,如果没有空闲的,SQL线程就会处于等待状态,直到有一个空闲的WORK线程为止。

(7)如果last_committed等于大于lwm,则说明取出事务与当前执行组不是同一组,则取出事务需要等待。

10.分库分表

10.1.瓶颈

10.1.1.I/O瓶颈

磁盘读写:分库,垂直分表

网络IO:分库

10.1.2.CPU瓶颈

SQL问题:优化SQL、索引,业务逻辑层处理

单表数据量太大:水平分表

10.2.分库分表

10.2.1.水平分库

以字段为依据,按照一定策略,将一个库的数据拆分到多个库。每个库的表结构完全一样,但是数据没有交集。所有库加在一起就是全量数据。

10.2.2.水平分表

以字段为依据,按照一定策略,将一个表的数据拆分到多个表。每张表的结构一模一样,但是数据没有交集。所有的表加在一起就是全量数据。

10.2.3.垂直分库

根据业务归属不同,将不同的表拆分到不同的数据库中。

10.2.4.垂直分表

根据业务归属不同,将字段拆分到不同的表中。

10.3.工具

Sharding-sphere

TDDL

MyCat

MyCat属于Proxy架构,Sharding-sphere、TDDL属于客户端架构。

区别:

Proxy 客户端
优点 集中管理监控;升级方便;解决连接数问题
缺点 有中间层,有成本开销;中间层必须具备高可用

10.4.问题

10.4.1.对非分片键(Non-Partition Key)的查询

在分库分表之后,要根据某个字段查询数据,但是这个字段跟分库分表无关。此时需要查询多个分片。但分片越多,效率越低。且需要扫描海量的数据。

基因法

将分片字段信息嵌入非分片字段。如分片字段是user_id,而非分片字段是order_no。可以选择将user_id的哈希值后四位嵌入order_no,如order_no = 时间戳 + 随机数 + user_id_hash。此时可以通过order_no直接找到对应的分片。

其做法是将相关数据存储在同一路由下,并使查询的字段携带分片路由信息。

冗余法

在每个分片上维护非分片字段的索引表,通过异步消息同步。

搜索引擎集成

通过Elasticsearch等工具建立倒排索引,将需要查询的字段和数据对应的表分区存储在搜索引擎中。

10.4.2.扩容问题

水平扩容库(升级从库法)

主从复制,增加从库节点,读写分离。

水平扩容表(双写迁移法)

第一步:应用配置双写,部署;

第二步:将老库中的老数据复制到新库中;

第三步:以老库为准校对新库中的老数据;

第四步:应用去掉双写,部署;

事务一致性问题

强一致性:二阶段提交和三阶段提交问题

最终一致性:可靠的消息

跨节点join问题

应用层解决

在应用层执行join操作。
数据冗余

复制关联数据到同一分片。
全局表

将字典表等公共数据,全量复制到每一个分片。
字段冗余

将部分字段冗余存储到表中,反规范化设计

跨节点排序、分页、函数问题

分页

1 在每个节点查询分页数据,之后在应用层或者中间层合并之后再分页。

2 业务优化,将分页转换为范围查询(比如时间范围)。游标分页(比如使用id代替limit)
排序

1 每个分片返回局部有序数据,然后在应用层/中间层进行排序。

2 将排序字段冗余到es中,在es中进行排序

主键全局唯一问题

UUID:但是字符串长,而且无序

Snowflake分布式自增ID算法:时间戳+机器编号+序列号。但是要注意机器时钟,防止时钟回拨。

相关推荐
潘yi.1 小时前
NoSQL之Redis配置与优化
数据库·redis·nosql
zdkdchao2 小时前
hbase资源和数据权限控制
大数据·数据库·hbase
伤不起bb2 小时前
NoSQL 之 Redis 配置与优化
linux·运维·数据库·redis·nosql
leo__5202 小时前
PostgreSQL配置文件修改及启用方法
数据库·postgresql
南風_入弦3 小时前
优化09-表连接
数据库·oracle
Snk0xHeart4 小时前
极客大挑战 2019 EasySQL 1(万能账号密码,SQL注入,HackBar)
数据库·sql·网络安全
····懂···5 小时前
数据库OCP专业认证培训
数据库·oracle·ocp
学习中的码虫5 小时前
数据库-MySQL
数据库
天天摸鱼的java工程师6 小时前
高考放榜夜,系统别崩!聊聊查分系统怎么设计,三张表足以?
java·后端·mysql
Karry的巡洋舰6 小时前
【数据库】安全性
数据库·oracle