【Mysql】深分页问题、页分裂问题、加密/解密、执行计划
【Mysql】深分页问题、页分裂问题、加密/解密、执行计划
- 1、order by 是怎么实现的?
- 1.1 filesort 排序
- 1.2 全字段排序(也叫单路排序)
- 1.3 row_id 排序(也叫双路排序)
- 2、MySQL的深分页问题
- 2.1 limit执行顺序
- 2.2 如何优化深分页问题
- 2.2.1 使用子查询和JOIN优化
- 2.2.2 使用子查询和ID过滤优化
- 2.2.3 记录上一个ID
- 2.2.4 使用搜索引擎
- 3、SQL语句如何实现insertOrUpdate的功能?
- 4、什么是buffer pool?
- 4.1 读过程
- 4.2 写过程
- 5、页分裂问题
- 5.1 页分裂(合并)的危害
- 5.2 如何避免页分裂
- 6、数据库加密/解密
- 6.1 数据库怎么做加密和解密?
- 6.1.1 服务端加解密
- 6.1.2 数据库加密函数
- 6.2 数据库加密后怎么做模糊查询?
- 6.2.1 明文分词
- 6.2.2 数据库解密函数
- 7、SQL执行计划分析的时候,要关注哪些信息?
- 7.1 type
- 7.2 possible_keys 和 key
- 7.3 extra
1、order by 是怎么实现的?
order by 是做排序的,具体怎么排取决于优化器的选择,如果优化器认为走索引更快,那么就会用索引排序,否则,就会使用filesort (执行计划中extra中提示:using filesort),但是能走索引排序的情况并不多,并且确定性也没有那么强,很多时候,还是走的filesort。
filesort这种排序方式中,如果需要排序的内容比较少,就会基于内存中的sort_buffer,否则就需要使用临时文件进行排序了。并且在实际排序过程中,如果字段长度并不是特别长,那么就会使用全字段排序的方式直接在sort_buffer中排序后返回结果集。如果字段长度特别长,那么就可能基于空间考虑,采用row_id排序,这样就会在排序后进行二次回表后返回结果集。
1.1 filesort 排序
如果不能使用或者优化器认为索引排序效率不高时, MySQL 会执行filesort操作以读取表中的行并对它们进行排序。
在进行排序时,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer,它的大小是由sort_buffer_size控制的。
而根据sort_buffer_size的大小不同,会在不同的地方进行排序操作:
● 如果要排序的数据量小于 sort_buffer_size,那么排序就在内存中完成。
● 如果排序数据量大于sort_buffer_size,则需要利用磁盘临时文件辅助排序。
除了sort_buffer_size参数以外,影响排序的算法的还有一个关键参数:max_length_for_sort_data
max_length_for_sort_data是 MySQL 中控制<用于排序的行数据的长度>的一个参数,默认值是1024字节。如果单行的长度超过这个值,MySQL就认为单行太大,那么就会采用rowid 排序,否则就进行全字段排序。
1.2 全字段排序(也叫单路排序)
所谓全字段排序,就是将要查询的所有字段都放到sort_buffer中,然后再根据排序字段进行排序,排好之后直接把结果集返回给客户端。
全字段排序的好处就是只对原表进行了一次回表查询(每条记录只需要回表一次),之后的排序好以后就可以直接把需要的字段返回了。所以他的效率比较高。但是他的缺点就是,如果要查询的字段比较多,那么就会比较耗费sort_buffer的空间,使得空间中能存储的数据很少。那么如果要排序的数据量变大,就会要用到临时文件,导致整体的性能下降。
1.3 row_id 排序(也叫双路排序)
在构建sort_buffer的时候,不要把所有的要查询字段都放进去,只把排序字段的主键放进去就行了。这样就可以解决单路排序中耗费sort_buffer空间高的问题,避免使用到临时文件。缺点是要多一次回表操作。
2、MySQL的深分页问题
2.1 limit执行顺序
在一个SQL查询语句的顺序中,limit其实是在最后执行的,也就是说,在做完筛选、分组、排序等操作之后,最后进行的limit。因为它是对最终结果集的限制。所以在执行完其他所有操作后,才应用 LIMIT,从而确保查询返回的结果集已经是经过完整处理的。
还有就是,limit的查询中,如果是像 LIMIT 10000, 100 这种形式 ,他会先查询出全部数据(10000+100),然后丢弃前面的结果,再返回需要的部分。 这也是为什么深分页很慢的原因。
2.2 如何优化深分页问题
2.2.1 使用子查询和JOIN优化
假如我们这样一条SQL:
SELECT c1, c2, cn... FROM table WHERE name = "jack" LIMIT 1000000,10
可以基于子查询进行优化,如以下SQL:
SELECT c1, c2, cn...
FROM table
INNER JOIN (SELECT idFROM tableWHERE name = "jack"ORDER BY idLIMIT 1000000, 10
) AS subquery ON table.id = subquery.id
使用一个子查询来获取限定条件下的一小部分主键id,这部分 id 对应于我们分页的目标区域。然后,使用这些 id 在主查询中获取完整的行数据。
以上SQL,在name有索引的情况下,子查询中查询id是不需要回表的。而当我们查询出我们想要的10个ID之后,基于ID查询不仅快,而且要查的数据量也很少。
2.2.2 使用子查询和ID过滤优化
如果主键ID是递增的,还可以用下面这个方式,原理其实和第一种是类似的:
SELECT c1, c2, cn...
FROM table
WHERE name = "jack"AND id >= (SELECT id FROM table WHERE name = "jack" ORDER BY id LIMIT 1000000, 1)
ORDER BY id
LIMIT 10
2.2.3 记录上一个ID
还有一种方式,是上面这个方式的变种,就是如果能提前预估要查询的分页的条件的话,是可以很大程度提升性能的。比如记住上一页的最大ID,下一页查询的时候,就可以可以根据id > max_id_in_last_page 进行查询。
2.2.4 使用搜索引擎
另外,如果是基于文本内容的搜索,可以使用 Elasticsearch 这样的全文搜索引擎来优化深度分页性能。但是需要注意的是,ES也会有深度分页的问题,只不过他的影响比MySQL要小一些。
3、SQL语句如何实现insertOrUpdate的功能?
在 MySQL 中,可以使用 INSERT INTO … ON DUPLICATE KEY UPDATE 语句实现 insertOrUpdate 功能。
需要注意:在on duplicate key时,会在前一个索引值到当前值加临键锁,极容易造成死锁。
要使用 INSERT INTO … ON DUPLICATE KEY UPDATE 语句,需要满足以下条件:
- 表必须有主键或唯一索引;
- 插入的数据必须包含主键或唯一索引列;
- 主键或唯一索引列的值不能为 NULL。
假设有一个 student 表,包含 id、name 和 age 三列,其中 id 是主键。现在要插入一条数据,如果该数据的主键已经存在,则更新该数据的姓名和年龄,否则插入该数据。
INSERT INTO student (id, name, age) VALUES (1, 'Alice', 20)
ON DUPLICATE KEY UPDATE name='Alice', age=20;
实现原理:
INSERT INTO … ON DUPLICATE KEY UPDATE ,如果数据库中已存在具有相同唯一索引或主键的记录,则更新该记录。其底层原理和执行流程如下:
- 检查唯一索引或主键:当执行 INSERT INTO … ON DUPLICATE KEY UPDATE 语句时,数据库首先尝试插入新行。在此过程中,数据库会检查表中是否存在与新插入行具有相同的唯一索引或主键的记录。
- 冲突处理:如果不存在冲突的唯一索引或主键,新行将被正常插入。如果存在冲突,即发现重复的唯一索引或主键值,数据库将不会插入新行,而是转而执行更新操作。
- 执行更新:在检测到唯一索引或主键的冲突后,数据库将根据 ON DUPLICATE KEY UPDATE 后面指定的列和值来更新已存在的记录。这里可以指定一个或多个列进行更新,并且可以使用 VALUES 函数引用原本尝试插入的值。
注意:使用INSERT INTO … ON DUPLICATE KEY UPDATE 会导致主键发生跳跃。
在 MySQL 中使用 INSERT ON DUPLICATE KEY UPDATE 语句时,如果插入操作失败(因为主键或唯一键冲突),而执行了更新操作,确实会导致自增主键计数器增加,即使没有实际插入新记录。
4、什么是buffer pool?
MySQL的数据是存储在磁盘上面的(Memory引擎除外),但是如果每次数据的查询和修改都直接和磁盘交互的话,性能是很差的。
于是,为了提升读写性能,Innodb引擎就引入了一个中间层,就是buffer pool。buffer是在内存上的一块连续空间,他主要的用途就是用来缓存数据页的,每个数据页的大小是16KB。
4.1 读过程
当我们在MySQL执行一个查询请求的时候,他的过程是这样的:
- MySQL首先检查Buffer Pool中是否存在本次查询的数据。如果数据在Buffer Pool中,就直接返回结果。
- 如果数据不在Buffer Pool中,MySQL会从磁盘读取数据。
- 读取的数据页被放入Buffer Pool,同时MySQL会返回请求的数据给应用程序。
4.2 写过程
当我们执行一次更新语句,如INSERT、UPDATE或DELETE等时,会进行以下过程
- 当应用程序执行写操作时,MySQL首先将要修改的数据页加载到Buffer Pool中。
- 在Buffer Pool中,对数据页进行修改,以满足写请求。这些修改只在内存中进行,不会立即写回磁盘。
- 如果Buffer Pool中的数据页被修改过,MySQL会将这个页标记为“脏页”(Dirty Page)。
- 脏页被写回磁盘,此时写入操作完成,数据持久化。
但是需要注意的是,脏页写回磁盘是由一个后台线程进行的,在MySQL服务器空闲或负载较低时,InnoDB会进行脏页刷盘,以减少对用户线程的影响,降低对性能的影响。
5、页分裂问题
InnoDB的数据页是InnoDB存储引擎中用于存储数据的基本单位,通常大小为16KB。B+树的每个节点都对应着一个数据页,包括根节点、非叶子节点和叶子节点。B+树通过节点之间的指针连接了不同层级的数据页,从而构建了一个有序的索引结构。
B+树是按照索引字段建立的,并且在B+树中是有序的,假如有下面一个索引的树结构,其中的索引字段的值并不连续。
假如,现在我们插入一个新的一条记录,他的索引值是3,那么他就要按照顺序插入到页20中,在索引值为1,2的记录的后面。而如果这个索引页已经满了,那么就需要触发一次页分裂。
页分裂是指将该页面中的一部分索引记录移动到一个新的页面中,从而为新记录腾出空间。这样可以保持B+树的平衡和性能。
以下,就是一次页分裂的过程:
那么,当我们向Innodb中添加数据的时候,如果索引是随机无序的,那么就会导致页分裂。而且分裂这个动作还可能会引起连锁反应,从叶子节点沿着树结构一路分裂到根节点。
有分裂,就会有合并。在InnoDB中,当索引页面中的索引记录删除后,页面可能会变得过于稀疏。这时,为了节省空间和提高性能,可能会触发页合并操作。
页合并是指将两个相邻的索引页面合并成一个更大的页面,减少B+树的层级,从而提高查询性能。
5.1 页分裂(合并)的危害
首先,页分裂和合并是涉及大量数据移动和重组的操作。频繁进行这些操作会增加数据库的I/O负担和CPU消耗,影响数据库的整体性能。
分裂和合并可能导致B+树索引结构频繁调整,这个过程也会影响插入及删除操作的性能。
频繁的页分裂和合并可能会导致磁盘上存在较多的空间碎片,新分出的一个页一般会有很多空闲空间,使得数据库表占用更多的磁盘空间,而导致浪费。
5.2 如何避免页分裂
使用varchar或者使用UUID作为主键的话,都会导致页分裂。尽量选择使用自增的字段作为索引,尤其是主键索引,这样可以很大程度的避免页分裂。
如果要插入大量数据,尽量使用批量插入的方式,而不是逐条插入。这样可以减少页分裂的次数。
频繁删除操作可能导致页面过于稀疏,从而触发页合并。所以,一般建议使用逻辑删除而不是物理删除。
6、数据库加密/解密
6.1 数据库怎么做加密和解密?
很多时候,我们的数据库表中会存储很多敏感信息,如用户的手机号、身份证号、密码之类的,这些数据如果不做好加密的话,一旦数据泄漏就会导致重要信息泄露。
一般来说都需要对敏感字段进行加密,然后再在数据库中保存加密后的数据,这样即使被拖库也没关系,比如攻击者拿到的只是加密后的密码,并不知道真实密码是什么。
6.1.1 服务端加解密
服务端加解密指的就是数据库在存入数据库之前就加密好,然后再从数据库取出之后进行解密。这样可以保证数据库的数据绝对安全,因为数据库也不知道明文到底是什么。
一般来说我们可以选择各种各样的加密算法,如对称加密和非对称加密都可以,一般来说用对称加密就行了。
有些场景下也可以用MD5(MD5严格来说并不是加密算法,只是一种hash算法),但是需要注意的是MD5不支持解密,所以只能用于那种存储后只做匹配而不作查询展示的场景,如用户的密码。而需要展示的场景,如手机号等就需要支持解密。
6.1.2 数据库加密函数
MySQL提供了一些内置的加密函数,我们可以直接使用这些加密函数进行数据加密:
1、AES_ENCRYPT 和 AES_DECRYPT,这两个函数是对称加密算法,使用对称密钥加密,这意味着加密和解密都使用相同的密钥。因此,必须确保密钥的安全性。
○ AES_ENCRYPT(str, key) 用于使用AES算法对字符串 str 进行加密,key表示使用的密钥。
○ AES_DECRYPT(crypt_str, key) 用于解密已加密的字符串 crypt_str,使用相同的密钥 key。
2、ENCRYPT: ENCRYPT(str, salt) 函数使用UNIX crypt()函数对字符串 str 进行加密,其中 salt 是一个2字符的随机盐。这种加密方法通常用于密码存储,但不是最安全的加密方式。
3、MD5 和 SHA1:
○ MD5(str) 和 SHA1(str) 函数分别用于计算字符串 str 的MD5和SHA-1哈希值。这不是真正的加密,而是散列函数,无法逆向解密。
6.2 数据库加密后怎么做模糊查询?
6.2.1 明文分词
首先有一个比较简单的做法,那就是对明文进行分词,然后分别加密后存储到数据库中,比如tomas这个需要加密的字符串,我们就可以把他拆成to 、tom等这几个字符串,然后分别对他们进行加密,并保存到数据库中。
这样当我们使用to 、tom 进行查询的时候,就可以对明文加密后去数据库中匹配了。
这个方案的缺点也比较明显,第一个就是需要冗余很多字段,第二个就是不够灵活,如果我按照toma来查询的话就不支持了。
当然,也有一些改进的方式,比如并不需要增加多个字段,可以把这些需要用于模糊查询的信息都放到同一个字段中,如DECRYPT_NAME,拼接成一个字符串就行了。如71AAFD38484F3160708C6A6D2D5F736B,83B01A578395CE81AEAAC6A4FE70AA94,E90048FB068AA98B7EC751CBD6DC78B7 这样就只需要通过这个字段做模糊查询就行了(索引也会失效)。
6.2.2 数据库解密函数
加密的时候如果用了函数的话,解密的时候我们也可以借助函数来做解密,同时做模糊查询,比如加密时使用了AES_ENCRYPT算法:
-- 加密数据
INSERT INTO user_data (username, credit_card) VALUES ('jack', AES_ENCRYPT('1234-5678-9012-3456', 'secret_key'));
那么在做模糊查询的时候就可以这样做:
SELECT * FROM user_data WHERE AES_DECRYPT(credit_card, 'secret_key') like 'tom%';
这样也就能实现一个模糊查询的效果了,但是这个方案有个缺点,就是无法用到索引,不是因为用like,而是因为我们在字段上用了函数,索引就会失效。
这个方案适合于表中数据量不大,或者查询条件中还有其他查询字段可以走索引的情况。
7、SQL执行计划分析的时候,要关注哪些信息?
下面是一次explain返回的一条SQL语句的执行计划的内容:
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | t2 | NULL | index | NULL | idx_abc | 198 | NULL | 5 | 20.00 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+--------------------------+
7.1 type
表示查询时所使用的索引类型,包括ALL、index、range、ref、eq_ref、const、system等。
有以下几个取值内容:
-
system:系统表,少量数据,往往不需要进行磁盘IO
-
const:使用常数索引,MySQL 只会在查询时使用常数值进行匹配。
○ explain select * from t2 where f=‘jack’;
○ 使用唯一性索引做唯一查询 -
eq_ref:唯一索引扫描,只会扫描索引树中的一个匹配行。
○ explain select * from t1 join t2 on t1.id = t2.id where t1.f1 = ‘s’;
○ 当在连接操作中使用了唯一索引或主键索引,并且连接条件是基于这些索引的等值条件时,MySQL通常会选择 eq_ref 连接类型,以提高查询性能。 -
ref:非唯一索引扫描, 只会扫描索引树中的一部分来查找匹配的行。
○ explain select * from t2 where a = ‘jack’;
○ 使用非唯一索引进行查询 -
range:范围扫描, 只会扫描索引树中的一个范围来查找匹配的行。
○ explain select * from t2 where a > ‘a’ and a < ‘c’;
○ 使用索引进行性范围查询 -
index:全索引扫描, 会遍历索引树来查找匹配的行
○ explain select c from t2 where b = ‘s’;
○ 不符合最左前缀匹配的查询 -
ALL:全表扫描, 将遍历全表来找到匹配的行。
○ explain select * from t2 where d = “ni”;
○ 使用非索引字段查询
需要注意的是,这里的index表示的是做了索引树扫描,效率并不高。以上类型由快到慢:
system> const > eq_ref >ref>range> index >ALL
7.2 possible_keys 和 key
possible_keys 表示查询语句中可以使用的索引,而不一定实际使用了这些索引。这个字段列出了可能用于这个查询的所有索引,包括联合索引的组合。
key 字段表示实际用于查询的索引。如果在查询中使用了索引,则该字段将显示使用的索引名称。
7.3 extra
这个字段描述了 MySQL 在执行查询时所做的一些附加操作。下面是 Extra 可能的取值及其含义:
1、Using where:表示 MySQL 将在存储引擎检索行后,再进行条件过滤(使用 WHERE 子句);查询的列未被索引覆盖,where筛选条件非索引的前导列或者where筛选条件非索引列。
○ explain select * from t2 where d = “ni”; 非索引字段查询
○ explain select d from t2 where b = “ni”; 未索引覆盖,用联合索引的非前导列查询
2、Using index:表示 MySQL 使用了覆盖索引(也称为索引覆盖)优化,只需要扫描索引,而无需回到数据表中检索行;
○ explain select b,c from t2 where a = “ni”; 索引覆盖
3、Using index condition:表示查询在索引上执行了部分条件过滤。这通常和索引下推有关。
○ explain select d from t2 where a = “ni” and b like “s%”; 使用到索引下推。
4、Using where; Using index:查询的列被索引覆盖,并且where筛选条件是索引列之一,但不是索引的前导列,或者where筛选条件是索引列前导列的一个范围
○ explain select a from t2 where b = “ni”; 索引覆盖,但是不符合最左前缀
○ explain select b from t2 where a in (‘a’,‘d’,‘sd’); 索引覆盖,但是前导列是个范围
5、Using join buffer:表示 MySQL 使用了连接缓存;
○ explain select * from t1 join t2 on t1.id = t2.id where a = ‘s’;
6、Using temporary:表示 MySQL 创建了临时表来存储查询结果。这通常是在排序或分组时发生的;
○ explain select count(*),b from t2 group by b;
7、Using filesort:表示 MySQL 将使用文件排序而不是索引排序,这通常发生在无法使用索引来进行排序时;
○ explain select count(*),b from t2 group by b;
8、Using index for group-by:表示 MySQL 在分组操作中使用了索引。这通常是在分组操作涉及到索引中的所有列时发生的;
9、Using filesort for group-by:表示 MySQL 在分组操作中使用了文件排序。这通常发生在无法使用索引进行分组操作时;
10、Range checked for each record:表示 MySQL 在使用索引范围查找时,需要检查每一条记录;
11、Using index for order by:表示 MySQL 在排序操作中使用了索引,这通常发生在排序涉及到索引中的所有列时;
12、Using filesort for order by:表示 MySQL 在排序操作中使用了文件排序,这通常发生在无法使用索引进行排序时;
13、Using index for group-by; Using index for order by:表示 MySQL 在分组和排序操作中都使用了索引。
参考链接:
1、https://www.yuque.com/hollis666/wk6won/caou56
2、https://www.yuque.com/hollis666/wk6won/et8lo7l10rg7g7iy
3、https://www.yuque.com/hollis666/wk6won/lq17kh7gaf8ayipw
4、https://www.yuque.com/hollis666/wk6won/ri2ky6kb6pvxy656
面试题:
1、如何解决mysql的深分页问题