
目录
- 一、索引有什么用
- 二、认识磁盘
-
- [2.1 MySQL与存储](#2.1 MySQL与存储)
- [2.2 磁盘](#2.2 磁盘)
- [三、MySQL 与磁盘交互基本单位](#三、MySQL 与磁盘交互基本单位)
- 四、建立共识
- 五、索引的理解
-
- [5.1 测试并简单理解索引](#5.1 测试并简单理解索引)
- [5.2 MySQL选择page方案进行IO交互的原因](#5.2 MySQL选择page方案进行IO交互的原因)
- [5.3 理解单个page](#5.3 理解单个page)
- [5.4 理解多个page](#5.4 理解多个page)
- [5.5 引入页目录](#5.5 引入页目录)
-
- [5.5.1 对于单个page的情况](#5.5.1 对于单个page的情况)
- [5.5.2 对于多个page的情况](#5.5.2 对于多个page的情况)
- [5.6 深入理解索引](#5.6 深入理解索引)
- [5.7 使用其他数据结构建立索引为何不行?](#5.7 使用其他数据结构建立索引为何不行?)
-
- [5.7.1 其他数据结构建立索引为何不行?](#5.7.1 其他数据结构建立索引为何不行?)
- [5.7.2 为什么选择B+树,而不选择B树](#5.7.2 为什么选择B+树,而不选择B树)
- [5.8 聚簇索引 VS 非聚簇索引](#5.8 聚簇索引 VS 非聚簇索引)
- 六、索引操作
-
- [6.1 创建索引](#6.1 创建索引)
-
- [6.1.1 创建主键索引](#6.1.1 创建主键索引)
- [6.1.2 创建唯一索引](#6.1.2 创建唯一索引)
- [6.1.3 创建普通索引](#6.1.3 创建普通索引)
- [6.1.4 创建全文索引](#6.1.4 创建全文索引)
- [6.2 查询索引](#6.2 查询索引)
- [6.3 删除索引](#6.3 删除索引)
-
- [6.3.1 删除主键索引](#6.3.1 删除主键索引)
- [6.3.2 删除其他索引(普通索引、唯一索引、全文索引)](#6.3.2 删除其他索引(普通索引、唯一索引、全文索引))
- [6.4 索引创建原则](#6.4 索引创建原则)
- 结尾
一、索引有什么用
索引 :提高数据库的性能,索引是物美价廉的东西了。不用加内存,不用改程序,不用调sql,只要执行正确的 create index
,查询速度就可能提高成百上千倍。但是天下没有免费的午餐,查询速度的提高是以插入、更新、删除的速度为代价的,这些写操作,增加了大量的IO。所以它的价值,在于提高一个海量数据的检索速度。
常见索引分为:
- 主键索引(primary key)
- 唯一索引(unique)
- 普通索引(index)
- 全文索引(fulltext)
案例:
先整一个海量表,在查询的时候,看看没有索引时有什么问题?
sql
drop database if exists `user_index`;
create database if not exists `user_index` default character set utf8;
use `user_index`;
-- 产生随机字符串
delimiter $$
create function rand_string(n INT)
returns varchar(255)
DETERMINISTIC
READS SQL DATA
begin
declare chars_str varchar(100) default 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
declare return_str varchar(255) default '';
declare i int default 0;
while i < n do
set return_str = concat(return_str, substring(chars_str, floor(1 + rand() * 52), 1));
set i = i + 1;
end while;
return return_str;
end $$
delimiter ;
-- 产生随机数字
delimiter $$
create function rand_num( )
returns int(5)
DETERMINISTIC
READS SQL DATA
begin
declare i int default 0;
set i = floor(10 + rand() * 500);
return i;
end $$
delimiter ;
-- 创建存储过程,向雇员表添加海量数据
delimiter $$
create procedure insert_emp(in start int(10), in max_num int(10))
begin
declare i int default 0;
set autocommit = 0;
repeat
set i = i + 1;
insert into EMP values ((start + i), rand_string(6), 'SALESMAN', 0001, curdate(), 2000, 400, rand_num());
until i = max_num
end repeat;
commit;
end $$
delimiter ;
-- 雇员表
CREATE TABLE `EMP` (
`empno` int(6) unsigned zerofill NOT NULL COMMENT '雇员编号',
`ename` varchar(10) DEFAULT NULL COMMENT '雇员姓名',
`job` varchar(9) DEFAULT NULL COMMENT '雇员职位',
`mgr` int(4) unsigned zerofill DEFAULT NULL COMMENT '雇员领导编号',
`hiredate` datetime DEFAULT NULL COMMENT '雇佣时间',
`sal` decimal(7,2) DEFAULT NULL COMMENT '工资月薪',
`comm` decimal(7,2) DEFAULT NULL COMMENT '奖金',
`deptno` int(2) unsigned zerofill DEFAULT NULL COMMENT '部门编号'
);
-- 执行存储过程,添加8000000条记录
call insert_emp(100001, 8000000);
到此,已经创建出了海量数据的表了。
这里先查询一下雇员编号为5201314这个雇员的所有信息。结果是查询一条数据平均所需的时间竟然需要7.7秒。并且这还是在本机上进行操作,如果放在公网中被很多人同时访问,估计服务就会挂掉了。
sql
select * from EMP where empno=5201314

上面说到索引可以加快查询速率,这里我给这个表添加一个索引,看看是否真的可以加快查询速率。
sql
alter table EMP add index(empno)

这里我再次查询一下雇员编号为5201314这个雇员的所有信息。这次查询速度确实快了非常多。

二、认识磁盘
2.1 MySQL与存储
MySQL 给用户提供存储服务,而存储的都是数据,数据在磁盘这个外设当中。磁盘是计算机中的一个机械设备,相比于计算机其他电子元件,磁盘效率是比较低的,在加上IO本身的特征,可以知道,如何提交效率,是 MySQL 的一个重要问题。
2.2 磁盘
关于磁盘相关的内容我已经在【Linux】基础IO(磁盘文件)这篇文章中详细讲解过了,这里我就将一些重要的知识展示在这里,有兴趣的可以去阅读一下这篇文章。

一个盘面有很多同心磁道,一个磁道有很多扇区,扇区是磁盘的最小存储单位,通常为512字节。
如果我们想向一个扇区中写入东西,需要通过三个条件进行寻址:
- 选择哪一面 ---- 本质上是选择磁头
- 选择该面上的哪一个磁道
- 选择该磁道上的哪一个扇区
也就是CHS定位法,CHS定位法(Cylinder, Head, Sector)是一种用于定位硬盘上数据的方法。CHS定位法通过三个参数来确定硬盘上的具体位置:柱面(Cylinder)、磁头(Head)和扇区(Sector)。
我们可以向一个扇区中写入,就可以向任意一个/多个连续的扇区中写入,也可以随机写入。
在系统软件上,进行IO交互时,并不是直接按照扇区大小进行交互的,原因如下:
- 如果操作系统直接使用硬件提供的数据大小进行交互,那么系统的IO代码,就和硬件强相关,换言之,如果硬件发生变化,系统必须跟着变化
- 从目前来看,单次IO 512字节,还是太小了。IO单位小,意味着读取同样的数据内容,需要进行多次磁盘访问,会带来效率的降低。
- 之前学习文件系统,就是在磁盘的基本结构下建立的,文件系统读取基本单位,就不是扇区,而是数据块。
故,系统读取磁盘,是以块为单位的,基本单位是 4KB
磁盘随机访问(Random Access)与连续访问(Sequential Access)
- 随机访问:本次IO所给出的扇区地址和上次IO给出扇区地址不连续,这样的话磁头在两次IO操作之间需要作比较大的移动动作才能重新开始读/写数据。
- 连续访问:如果当次IO给出的扇区地址与上次IO结束的扇区地址是连续的,那磁头就能很快的开始这次IO操作,这样的多个IO操作称为连续访问。
因此尽管相邻的两次IO操作在同一时刻发出,但如果它们的请求的扇区地址相差很大的话也只能称为随机访问,而非连续访问。
磁盘是通过机械运动进行寻址的,随机访问不需要过多的定位,故效率比较高。
三、MySQL 与磁盘交互基本单位
MySQL 作为一款应用软件,可以想象成一种特殊的文件系统。它有着更高的IO场景,所以,为了提高基本的IO效率, MySQL 进行IO的基本单位是 16KB 。

也就是说,磁盘这个硬件设备的基本单位是 512 字节,而MySQL InnoDB引擎
使用 16KB 进行IO交互。即, MySQL 和磁盘进行数据交互的基本单位是 16KB ,这个基本数据单元,在 MySQL这里叫做page(注意和系统的page区分)。需要注意的时,MySQL并不能直接与磁盘进行数据交互,而是需要通过操作系统,MySQL 和磁盘进行数据交互的基本单位是 16KB,我们也可以说为MySQL 和操作系统进行数据交互的基本单位是16KB,操作系统与磁盘交互的基本单位是4KB。
四、建立共识
- MySQL 中的数据文件,是以page(16KB)为单位保存在磁盘当中的。
- MySQL 的 CURD 操作,都需要通过计算,找到对应的插入位置,或者找到对应要修改或者查询的数据。
- 而只要涉及计算,就需要CPU参与,而为了便于CPU参与,一定要能够先将数据移动到内存当中。
- 所以在特定时间内,数据一定是磁盘中有,内存中也有。后续操作完内存数据之后,以特定的刷新策略,刷新到磁盘。而这时,就涉及到磁盘和内存的数据交互,也就是IO了。而此时IO的基本单位就是page。
- 为了更好的进行上面的操作, MySQL 服务器在内存中运行的时候,在服务器内部,就申请了被称为
Buffer Pool
的的大内存空间,来进行各种缓存。其实就是很大的内存空间,来和磁盘数据进行IO交互。 - 为了更高的效率,一定要尽可能的减少系统和磁盘IO的次数
五、索引的理解
5.1 测试并简单理解索引
下面我创建一个带主键的表,然后插入一些数据,需要我们并没有按照主键的大小顺序插入哦
sql
--一定要添加主键哦,只有这样才会默认生成主键索引
create table if not exists user (
id int primary key,
age int not null,
name varchar(16) not null
);
insert into user (id, age, name) values(3, 18, '杨过');
insert into user (id, age, name) values(4, 16, '小龙女');
insert into user (id, age, name) values(2, 26, '黄蓉');
insert into user (id, age, name) values(5, 36, '郭靖');
insert into user (id, age, name) values(1, 56, '欧阳锋');
通过查询表中的数据我们可以看到,MySQL将我插入的数据,按照主键从小到大的顺序进行了排序。

- 重谈page
MySQL中一定存在大量的page,也就决定了MySQL需要将这些大量的page管理起来,要管理就要先描述再组织。
所以不要认为page就是一个内存块,实际上page内部必然写入的对应的管理信息。
cpp
struct page
{
struct page* prev;
struct page* next;
char buffer[NUM];
}; // 一共16KB
创建一个page就是new/malloc出了一个page对象,在MySQL中以"链表"的形式将所有的page管理起来。(事实上并不是以链表的方式,这里先使用这样的方式理解一下)
5.2 MySQL选择page方案进行IO交互的原因
MySQL和磁盘进行IO交互的时候,采用的Page的方案进行交互,为什么不用多少就加载多少呢?
假设我们要依次查找数据id=1~5的数据,第一次加载id为1的数据,然后是id为2的数据,以此类推,最终到id为5的数据,这样MySQL就需要进行5次IO交互。
如果说这五条数据都在同一个page中,查找id为1的数据的时候,整个page就会被加载到MySQL中的 Buffer Pool 中,然后继续查找id=2~4的数据的时候,就可以直接在内存中查找了,就不需要再次进行IO交互了。
但是我们并不能保证用户访问的这些数据在同一个page中,但是由于局部性原理的存在,用户访问的下一条数据有很大的可能就在这个page中。这样用户进行IO的次数就减少了。
往往IO效率低下的最主要原因不是IO单次数据量的大小,而是IO的次数。
5.3 理解单个page
MySQL 中要管理很多数据表文件,而要管理好这些文件,就需要 先描述,再组织 ,我们目前可以简单理解成一个个独立文件是有一个或者多个Page构成的。
不同的page,在MySQL中都是16KB,使用 prev 和 next 构成双向链表。
因为有主键的问题, MySQL 会默认按照主键给我们的数据进行排序,从上面的page内数据记录可以看出,数据是有序且彼此关联的。
为什么MySQL中不按用户插入的顺序保存,而是需要将用户插入的数据进行排序呢?
这是因为页内部存储的数据的模块本质上也是一个链表结构,链表的特点就是增删快、查改慢,而用户使用MySQL大多数操作都是查询,所以MySQL需要对查询效率进行优化,插入数据时排序本质上就是优化查询的效率。
5.4 理解多个page
通过上面单个page的理解,我们知道了上面page中,只有一个功能,就是在查询某条数据的时候直接将一整页的数据加载到内存中,以减少硬盘IO次数,从而提高性能。但是,我们也可以看到,现在的page内部,实际上是采用了链表的结构,前一条数据指向后一条数据,本质上还是通过数据的逐条比较来取出特定的数据。
如果有1千万条数据,一定需要多个page来保存1千万条数据,多个page彼此使用双链表链接起来,而且每个page内部的数据也是基于链表的。那么,查找特定一条记录,也一定是线性查找。这样操作下来MySQL的查找效率一定不高。

5.5 引入页目录
我们在看书的时候,如果我们要看这本书的指定一个章节,那么我们找到该章节有两种做法:
- 从头开始找,一页一页的翻,直到找到指定的章节。
- 找到这本书的目录,根据目录查看对应章节的页码数,然后直接翻到指定章节。
在这本书上,本可以没有目录,但是当添加上了目录以后,阅读者就可以更快的找到他想看的内容,本质上书添加了目录就是多花费了纸质,但是却为阅读者查找提高了效率。所以书中添加目录的本质就是"空间换时间"。
5.5.1 对于单个page的情况
书能够引入目录,这里的page同样也能够引入页目录。

在当前这个page中,我们想找到id为4的人的数据,就可以通过目录2找到id为3的数据,再找到id为4的数据,这里需要3步就可以找到想要的数据。如果没有目录则需要从id为1的遍历到4次,才能够找到id为4的数据。很明显引入了页目录以后,查找效率提高。这里就能够回答用户插入数据时,MySQL会根据数据的键值进行排序的原因了,就是为了引入页目录。
5.5.2 对于多个page的情况
MySQL 中每一页的大小只有 16KB ,单个page大小固定,所以随着数据量不断增大, 一个page不可能存下所有的数据,那么必定会有多个页来存储数据。

在单表数据不断被插入的情况下,MySQL会在一个page容量不足的时候,自动开辟新的page来保存新的数据,然后通过指针的方式,将所有的page管理起来。
上面的图,是理想结构,当用户插入数据后,MySQL要保证整体有序,那么新插入的数据就需要被排序,所以新的数据不一定会在新page里面,可能会在其他page里面,这里仅仅做演示。
MySQL将多个page管理起来后,用户就可以通过多个page的遍历和page内部的目录来快速定位数据。
但是这样还是有效率问题,在page之间,MySQL 还是需要遍历多个page的,遍历意味着依旧需要进行大量的IO,将下一个page加载到内存,进行线性检测。所以当数据量太大的时候,只有page内有目录是不够的。
所以,为了解决page之间的变量,只需要给page也带上目录。
使用一个目录项 来指向某一个page,而这个目录项存放的就是将要指向的page中存放的最小数据的键值。和页内目录不同的地方在于,这种目录管理的级别是page(页),而页内目录管理的级别是行。
其中,每个目录项的构成是:键值+指针。(下图中没有画出指针)

这样通过一个目录页来管理页目录,目录页中每一项存放的是当前页的最小数据和指向对应页的指针。在目录页中通过数据的比较就可以快速的找到对应的page了。
目录页的本质也是页,普通页中存的数据是用户数据,而目录页中存的数据是普通页的地址。
若数据量太大了,使得最上层的目录页有多个了,那么MySQL中应该从哪个页开始查找呢?为了解决这个问题,MySQL中会再创建一个新的目录页作为它们的上层,来管理它们。

5.6 深入理解索引
索引的最终形态就是下图这样的,索引就是数据结构中的B+树 ,实际上所以在B+树的基础进行了改进,将同一层的非叶子节点通过指针连接了起来。这样使得索引有了更快的范围查询和增强的遍历能力 ,但是也增强了数据结构的复杂性和额外的内存开销。
MySQL索引中,非叶子节点都是不存数据的,只存储目录项,这样可以使得一个目录页就可以存储更多目录项,也就可以管理更多的page了。这样做最终就会使得这颗树不高 ,找到对应叶子结点(page)所途径的节点就更少 ,IO次数就减少,IO层面上就提高了效率。
MySQL索引中所有的叶子结点都是通过双向链表连接起来的,这就是B+树的特点,正是因为B+树的这个特点,索引才会选择B+树。然后就是通常来说用户喜欢范围查找,当所有的叶子结点都通过双向链表连接起来时,就可以通过一个page直接找到另一个page,就减少了从上往下目录页的IO了,本质上也就是减少了IO次数。
但是,如果说用户创建的表中没有主键,那么MySQL中索引的结构还是如此吗?
即使用户创建的表中没有主键,MySQL中索引的结构还是如此,这是因为用户创建的表中没有主键,MySQL会默认生成一个主键,正是如此,当用户查找数据的时候,MySQL是使用默认的主键进行查找,也就是线性遍历来查找数据的,使得MySQL查找的效率很慢。

5.7 使用其他数据结构建立索引为何不行?
5.7.1 其他数据结构建立索引为何不行?
- 链表:线性遍历
- 二叉搜索树:由于是二叉树,这颗树必定是瘦高的,找到叶子结点所途径的路上节点肯定很多,会增加IO次数,且二叉搜索树面临极端情况,会退化为线性结构
- AVL树/红黑树:虽然它们近似或就是平衡的,但始终是二叉树,必定面临形状是瘦高的,找到叶子结点所途径的路上节点肯定很多,会增加IO次数。
- Hash :官方的索引实现方式中,MySQL是支持HASH的,不过 InnoDB 和 MyISAM 并不支持,Hash根据它的特征,虽然有时候也查找很快为(O(1)),但是在面对范围查找就明显不行

5.7.2 为什么选择B+树,而不选择B树
下面这两张图片引用自博主Dobbin Soong的下面这篇文章。https://blog.csdn.net/u013235478/article/details/50625677
B树
B+树
B+树 VS B树
- B树 :
- B树的路上节点,既存储数据,也存储目录项,所以导致目录页中存储的目录项必定减少,使得整棵树会变高。
- B树的叶子节点没有通过双向链表连接起来,不适合范围查找。
- B+树 :
- B+树的路上节点,只存储了目录项,相比于B树的目录页来说,B+树的目录页存储的目录项更多,使得整棵树的高度降低。
- B+树的叶子结点通过双向链表连接起来,可以更好的进行范围查找。
5.8 聚簇索引 VS 非聚簇索引
聚簇索引 :InnoDB 这种用户数据与索引数据在一起 的索引方案,叫做聚簇索引
非聚簇索引 :MyISAM 这样用户数据与索引数据在分开存放的索引方案,叫做非聚簇索引
MyISAM 引擎同样使用B+树作为索引结果,叶节点的data域存放的是数据记录的地址。下图为 MyISAM表的主索引, Col1 为主键。
其中, MyISAM 最大的特点是,将索引page和数据page分离 ,也就是叶子节点没有数据,只有对应数据的地址。
MySQL 除了默认会建立主键索引外,我们用户也有可能建立按照其他列信息建立的索引,一般这种索引可以叫做普通(辅助)索引。
对于 MyISAM ,建立辅助(普通)索引和主键索引没有差别,无非就是主键不能重复,而非主键可重复。下图就是基于 MyISAM 的 Col2 建立的索引,和主键索引没有差别

同样, InnoDB 除了主键索引,用户也会建立普通(辅助)索引,我们以上表中的 Col3 建立对应的普通索引。
可以看到, InnoDB 的非主键索引中叶子节点仅仅存储的是主键值,而没有其他数据。所以通过普通索引,找到对应数据,需要两遍索引:首先检索辅助索引获得主键 ,然后用主键到主索引中检索获得记录。这种过程,就叫做回表查询。
这里 InnoDB 针对这种普通索引的场景,不给叶子节点也附上数据的原因就是太浪费空间了,因为索引的效率很高,就算进行了两次效率依旧很高。

六、索引操作
6.1 创建索引
6.1.1 创建主键索引
-
在创建表的时候,直接在字段名后指定 primary key
sqlcreate table user1(id int primary key, name varchar(30));
-
在创建表的最后,指定某列或某几列为主键索引
sqlcreate table user2(id int, name varchar(30), primary key(id));
-
创建表以后再添加主键
sqlcreate table user3(id int, name varchar(30)); alter table user3 add primary key(id);
主键索引的特点:
- 一个表中,最多只能有一个主键索引,复合主键也是一个主键
- 主键索引的效率高
- 创建主键索引的列,它的值不能为null,且插入的数据不能重复
- 主键索引的列基本上是整形
6.1.2 创建唯一索引
-
在表定义时,在某列后直接指定unique唯一属性。
sqlcreate table user4(id int primary key, name varchar(30) unique);
-
创建表时,在表的后面指定某列或某几列为unique
sqlcreate table user5(id int primary key, name varchar(30), unique(name));
-
创建表以后再添加唯一键
sqlcreate table user6(id int primary key, name varchar(30)); alter table user6 add unique(name);
唯一索引的特点:
- 一个表中,可以有多个唯一索引
- 唯一索引的效率高
- 如果在某一列建立唯一索引,则这列不能有重复数据
- 如果一个唯一索引上指定not null,等价于主键索引
6.1.3 创建普通索引
-
在表的定义最后,指定某列为普通索引
sqlcreate table user7(id int primary key,name varchar(20),index(name));
-
创建完表以后指定某列为普通索引
sqlcreate table user8(id int primary key, name varchar(20)); alter table user8 add index(name);
-
创建一个索引名为 idx_name 的索引
sqlcreate table user9(id int primary key, name varchar(20)); create index idx_name on user9(name);
普通索引的特点:
- 一个表中可以有多个普通索引
- 如果某列需要创建索引,但是该列有重复的值,那么我们就应该使用普通索引
6.1.4 创建全文索引
当对文章字段或有大量文字的字段进行检索时,会使用到全文索引 。MySQL提供全文索引机制,但是有要求,要求表的存储引擎必须是MyISAM,而且默认的全文索引支持英文。
创建表,然后再插入数据。
sql
CREATE TABLE articles (
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT (title,body)
)engine=MyISAM;
sql
INSERT INTO articles (title,body) VALUES
('MySQL Tutorial','DBMS stands for DataBase ...'),
('How To Use MySQL Well','After you went through a ...'),
('Optimizing MySQL','In this tutorial we will show ...'),
('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'),
('MySQL vs. YourSQL','In the following database comparison ...'),
('MySQL Security','When configured properly, MySQL ...');
查询有没有database数据
explain工具可以查看该查询语句是否使用到索引
sql
select * from articles where body like '%database%';
explain select * from articles where body like '%database%'\G
使用select直接加模糊匹配确实可以查出我想要的结果,但是通过explain工具来看,这条语句并没有使用全文索引。

使用全文索引的方法:
sql
SELECT column_list
FROM table_name
WHERE MATCH (column1, column2) AGAINST ('search_string' [SEARCH_MODE]);
说明:
- column_list:指定了查询结果中应该返回的列
- table_name:指定了查询将要从哪个表中检索数据
- column:column1和column2是用户想要进行全文搜索的列名
- search_string:用户想要搜索的文本或短语。
sql
select * from articles where match(title,body) against ('database');
explain select * from articles where match(title,body) against ('database') \G

6.2 查询索引
-
show keys from 表名
-
show index from 表名
-
desc 表名
6.3 删除索引
6.3.1 删除主键索引
sql
alter table 表名 drop primary key;

6.3.2 删除其他索引(普通索引、唯一索引、全文索引)
-
alter table 表名 drop index 索引名;
说明:
- 索引名就是show keys from 表名中的 Key_name 字段
-
drop index 索引名 on 表名
6.4 索引创建原则
- 比较频繁作为查询条件的字段应该创建索引
- 唯一性太差 的字段不适合单独创建索引,即使频繁作为查询条件
- 更新非常频繁 的字段不适合作创建索引
- 不会出现在查询条件的字段不该创建索引
结尾
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹
