面试之Mysql篇

Mysql篇

写在前面:今天2021年3月10日,携程一面挂了,把自己在准备面试的时候写的和整理的一些复习资料挂上来吧

B树和B+树的区别,为什么Innodb用B树作为索引的数据结构

B树

  1. B树的阶:结点的最多子结点个数,比如2-3树的阶就是3,2-3-4树的阶是4
  2. 关键字集合分布在整棵树中,即叶子结点和非叶子节点都存放数据
  3. 搜索有可能在非叶子结点中结束

B树的图示

image-20210301100055974

B+树

  1. B+树的搜索和B树也基本相同,区别是B+树只有到达叶子节点才命中(B树可以在非叶子结点命中)
  2. B+树的性能也等价于在关键字全集做一次二分查找
  3. 所有的关键字都出现在叶子结点的链表中(即数据只能在叶子结点中【也叫做稠密索引】),且链表关键字(数据)恰好是有序的

B+树的图示

image-20210301100424388

B树和B+树的区别

  • ==总结:B+树的数据只存放在叶子节点中,这样其他结点就可以放下更多的key,从而使得树变矮,降低磁盘IO的次数,提高索引查找的速度==
  • 每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为null,而B+树的数据只能存放在叶子结点中
  • B+树上增加了顺序访问指针,也就是每个叶子结点增加一个指向相邻叶子结点的指针,相对于B树的数据可能存在不同的层级中,B+树的数据只存在于叶子结点中,数据的查找效率更高
  • 这样B+树的非叶子结点就可以存放更多的key,可以使得树更矮
  • 由于索引一般以索引文件的形式存储在磁盘上,索引查找的时候产生IO,相对于内存读取,IO耗时更长
  • 所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的时间复杂度。树高度越小,I/O次数越少。

Mysql索引采用的数据结构

在谈及Mysql索引采用的数据结构之前,我们先来认识下B树以及B+树

B树

  1. B树的度:结点所能挂载的最多子结点数(比如2-3树的度就是3,2-3-4树的度是4)
  2. 叶子结点和非叶子节点都存放数据

B树的图示

image-20210301100055974

B+树

  1. B+树的搜索和B树也基本相同,区别是B+树只能在叶子结点找到数据
  2. 所有的关键字都出现在叶子结点的链表中,且链表数据恰好是有序的,这是因为每个叶子结点都有指向下一个叶子结点的指针

B+树的图示

image-20210301100424388

大概了解了B树和B+树之后,我们来看一下B树和B+树的区别

B树的优势

  • B+树的数据只能存放在叶子结点中
  • B+树上增加了顺序访问指针,也就是每个叶子结点都有一个指向相邻叶子结点的指针 ==> 相对于B树的数据可能存在不同的层级中,B+树的数据只存在于叶子结点中,数据的查找效率更高
  • B+树的非叶子结点就可以存放更多的索引,可以使得树更矮,从而减少查询的次数,进一步的就是减少IO的次数

总结

​ B+树的数据只存放在叶子节点中,这样其他结点就可以放下更多的key,从而使得树变矮,降低磁盘IO的次数

聚簇索引和非聚簇索引

聚簇索引与非聚簇索引的定义与特征

聚簇索引的定义

​ 聚簇索引是根据码值找到数据的物理存储位置,从而达到快速检索数据的目的。聚簇索引的顺序就是数据的物理存储顺序,叶节点就是数据节点

聚簇索引的特征

  1. 聚簇索引具有唯一性,由于聚簇索引是将数据跟索引结构放到一块,因此一个表仅有一个聚簇索引
  2. 表中行的物理顺序和索引中行的物理顺序是相同的在创建任何非聚簇索引之前创建聚簇索引,这是因为聚簇索引改变了表中行的物理顺序,数据行按照一定的顺序排列,并且自动维护这个顺序
  3. 叶子节点处储存的是整行数据(这也是为何用主键(聚簇索引)进行查询时,查询速度会较快的原因(减少回表查询))

聚簇索引需要注意的地方

先给出结论:

  1. 尽量不用uuid
  2. 尽量用int
  3. 尽量自增

下面是说明:

  1. uuid的值离散程度高,不适合排序,有可能插入在索引树的中间位置,导致索引树调整复杂度变大,消耗更多的时间和资源
  2. 使用int类型的自增,有以下原因:(1)方便排序;(2)默认会在索引树的末尾增加主键值,对索引树的结构影响最小
  3. 如果使用uuid,主键值占用的存储空间变大,辅助索引中保存的主键值也会跟着变大,占用存储空间,也会影响到IO操作读取到的数据量
聚簇索引的数据的物理存放顺序与索引顺序是一致的,即: 只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的。如果主键不是自增id,那么可以想象,它会干些什么,不断地调整数据的物理地址、分页,当然也有其他一些措施来减少这些操作,但却无法彻底避免。但,如果是自增的,那就简单了,它只需要一 页一页地写,索引结构相对紧凑,磁盘碎片少,效率也高。

索引的建立与优化

如果是 随机主键或者频繁更新主键的话,就会存在以下问题:(1)数据页频繁断裂;(2)B + 树不饱

这是因为聚簇索引是按顺序进行排序的,而如果设置主键是自增,那么每一次都是在聚集索引的最后增加,当一页写满,就会自动开辟一个新页,不会出现页分类的现象,效率比随机主键高。这也是建表规范中要求主键自增的原因

非聚簇索引的定义

​ 非聚簇索引的顺序与数据物理排列顺序无关,叶节点仍然是索引节点,只不过有一个指针指向对应的数据块

非聚簇索引的特征

Innodb和MyISAM

Innodb和MyISAM的定义与特征

InnoDB

在Innodb中,真实的数据存放在主键索引的叶子结点中

MyISAM

​ data存的是数据地址,其叶子节点中保存的实际上是指向存放数据的物理块的指针
从MYISAM存储的物理文件我们能看出,MYISAM引擎的索引文件(.MYI)和数据文件(.MYD)是相互独立的

Innodb和MyISAM的区别

Innodb

  • Innodb的主键索引是聚簇索引
  • 主键索引的叶节点中存放着真实数据
  • 辅助索引存放的是主键值

image-20210306103341299

下面是示例:

总结:通过辅助索引需找到主键索引才能找到真实数据

  1. InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。
  2. 若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树中再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。(重点在于通过其他键需要建立辅助索引)

MyISAM

总结:通过辅助索引可以定位到硬盘上的真实数据

  1. MyISAM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键
  2. 表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树

image-20210306103733752

使用聚簇索引的优势:

每次使用辅助索引检索都要经过两次B+树查找,看上去聚簇索引的效率明显要低于非聚簇索引,这不是多此一举吗?聚簇索引的优势在哪?

  1. 由于行数据和聚簇索引的叶子节点存储在一起,同一页中会有多条行数据,访问同一数据页不同行记录时,已经把页加载到了Buffer中(缓存器),再次访问时,会在内存中完成访问,不必访问磁盘。这样主键和行数据是一起被载入内存的,找到叶子节点就可以立刻将行数据返回了,如果按照主键Id来组织数据,获得数据更快。
  2. 辅助索引的叶子节点,存储主键值,而不是数据的存放地址。好处是当行数据放生变化时,索引树的节点也需要分裂变化;或者是我们需要查找的数据,在上一次IO读写的缓存中没有,需要发生一次新的IO操作时,可以避免对辅助索引的维护工作,只需要维护聚簇索引树就好了。另一个好处是,因为辅助索引存放的是主键值,减少了辅助索引占用的存储空间大小。
注:我们知道一次io读写,可以获取到16K大小的资源,我们称之为读取到的数据区域为Page。而我们的B树,B+树的索引结构,叶子节点上存放好多个关键字(索引值)和对应的数据,都会在一次IO操作中被读取到缓存中,所以在访问同一个页中的不同记录时,会在内存里操作,而不用再次进行IO操作了。除非发生了页的分裂,即要查询的行数据不在上次IO操作的换村里,才会触发新的IO操作。
  1. 因为MyISAM的主索引并非聚簇索引,那么他的数据的物理地址必然是凌乱的,拿到这些物理地址,按照合适的算法进行I/O读取,于是开始不停的寻道不停的旋转。聚簇索引则只需一次I/O。(强烈的对比)
  2. 不过,如果涉及到大数据量的排序、全表扫描、count之类的操作的话,还是MyISAM占优势些,因为索引所占空间小,这些操作是需要在内存中完成的。

其他区别

  1. MyISAM是非事务安全的,而InnoDB是事务安全的
  2. MyISAM锁的粒度是表级的,而InnoDB支持行级锁
  3. MyISAM支持全文类型索引,而InnoDB不支持全文索引
  4. MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM
  5. MyISAM表保存成文件形式,跨平台使用更加方便
  6. MyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果在应用中执行大量select操作可选择
  7. InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,可选择
  8. Innodb中的主键索引是聚簇索引,辅助索引是非聚簇索引;MyISAM中的主键索引是非聚簇索引,辅佐索引也是非聚簇索引

索引的建立与优化

需要建立索引的情况

  1. 主键自动建立唯一索引
  2. 频繁作为查询条件的字段应该创建索引 (经常做查询的字段)
  3. 查询中与其他表关联的字段,外键关系建立索引 (关联的字段)
  4. 频繁更新的字段不适合创建索引
  5. where条件里用不到的字段不创建索引d (where用不到的不做索引)
  6. 单键/组合索引的选择问题 (在高并发的情况下倾向于建立组合索引)
  7. 查询中排序的字段,排序字段若通过索引去访问将会大大提高排序速度 (order 后的字段)
  8. 查询中统计或者分组字段

索引设计的原则

1)适合索引的列是出现在where子句中的列,或者连接子句中指定的列;

2)基数较小的类,索引效果较差,没有必要在此列建立索引;

3)使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间;

4)==不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可==

image-20210127154538222

mysql中管理索引的语法

一、使用ALTER TABLE语句创建索引。

语法如下:

#1 PRIMARY KEY(主键索引)
    mysql>ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` ) 
#2.UNIQUE(唯一索引)
     mysql>ALTER TABLE `table_name` ADD UNIQUE (`column` ) 
#3.INDEX(普通索引)
    mysql>ALTER TABLE `table_name` ADD INDEX index_name ( `column` )
#4.FULLTEXT(全文索引)
    mysql>ALTER TABLE `table_name` ADD FULLTEXT ( `column` )
#5.多列索引
    mysql>ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` )

二、使用CREATE INDEX语句对表增加索引。

能够增加普通索引、也可增加普通多列索引以及UNIQUE索引两种

create index idx_name on table_name (column_list) ;
create unique index idx_name on table_name (column_list) ;
  • CREATE INDEX语句不能创建PRIMARY KEY索引(即:唯一索引)

三、删除索引。
删除索引可以使用ALTER TABLE或DROP INDEX语句来实现

DROP INDEX可以在ALTER TABLE内部作为一条语句处理,其格式如下:

drop index index_name on table_name ;
alter table table_name drop index index_name ;
alter table table_name drop primary key ;

其中,在前面的两条语句中,都删除了table_name中的索引index_name。而在最后一条语句中,只在删除PRIMARY KEY索引中使用,因为一个表只可能有一个PRIMARY KEY索引,因此不需要指定索引名。如果没有创建PRIMARY KEY索引,但表具有一个或多个UNIQUE索引,则MySQL将删除第一个UNIQUE索引。
如果从表中删除某列,则索引会受影响。对于多列组合的索引,如果删除其中的某列,则该列也会从索引中删除。如果删除组成索引的所有列,则整个索引将被删除。

索引优化实战

进行实验的表:datahandler_acce_daily

查看表数据结构

DESCRIBE `tablename`

有些时候虽然数据库有索引,但是并不被优化器选择使用。我们可以通过SHOW STATUS LIKE 'Handler_read%';查看索引的使用情况:

SHOW STATUS LIKE 'Handler_read%'

image-20210305220305938

Handler_read_key:如果索引正在工作,Handler_read_key的值将很高。

Handler_read_rnd_next:数据文件中读取下一行的请求数,如果正在进行大量的表扫描,值将较高,则说明索引利用不理想。

索引优化规则

  1. 如果MySQL估计使用索引比全表扫描还慢,则不会使用索引
返回数据的比例是重要的指标,比例越低越容易命中索引。记住这个范围值——30%,后面所讲的内容都是建立在返回数据的比例在30%以内的基础上。
  1. 前导模糊查询不能命中索引(即字符串前面有%等通配符的,不能命中索引)

(1)为字段stu_name 创建普通索引

#`datahandler_acce_daily` `stu_name`
create index idx_acce_daily_stuname on `datahandler_acce_daily` (`stu_name`)

(2)前导模糊查询语句

SELECT * FROM `datahandler_acce_daily`
WHERE `stu_name` LIKE '%张%'

(3)用Explain关键字进行分析

Explain SELECT * FROM `datahandler_acce_daily`
WHERE `stu_name` LIKE '%张%'

image-20210305222815630

(4)非前导模糊查询则可以使用索引,可优化为使用非前导模糊查询:

explain select * from `datahandler_acce_daily`
where `stu_name` like '张%'

image-20210305223030031

  1. 数据类型出现隐式转换的时候不会命中索引,特别是当字段类型是字符串,一定要将字符常量值用引号引起来
  • 之前看技术博客,大家都强调where用到的字段需要建立索引,但是我每次根据where条件查询的字段建立索引都不起作用

根据investigate_month 以及 invesigate_month创建索引

create index idx_acce_daily_iy_im on `datahandler_acce_daily`(investigate_year,investigate_month)

查询语句如下

EXPLAIN SELECT * 
FROM `datahandler_acce_daily`
WHERE investigate_year = 2020
AND investigate_month = 12

其中字段investigate_year 是varchar类型的

image-20210305223510844

我们可以发现,上述查询语句并没有命中索引

当我们为将2020加上单引号后

EXPLAIN SELECT * 
FROM `datahandler_acce_daily`
WHERE investigate_year = '2020'
AND investigate_month = '12'

image-20210305224024136

发生这种转变的原因是:==数据类型出现隐式转换的时候不会命中索引==(即从数值类型隐式转换为字符串类型)

  1. 复合索引的情况下,查询条件不包含索引列最左边部分==(不满足最左原则)==,不会命中复合索引

根据investigate_month 以及 invesigate_month创建复合索引

create index idx_acce_daily_iy_im on `datahandler_acce_daily`(investigate_year,investigate_month)

datahandler_acce_daily表索引详情:

SHOW INDEX FROM `datahandler_acce_daily`

image-20210305225928146

根据最左原则,可以命中复合索引index_name:

EXPLAIN SELECT * 
FROM `datahandler_acce_daily`
WHERE `investigate_year` = '2020'
AND `investigate_month` = '12'

==注意,最左原则并不是说是查询条件的顺序==

EXPLAIN SELECT * 
FROM `datahandler_acce_daily`
WHERE investigate_month = '12'
AND investigate_year = '2020'

而是查询条件中是否包含索引最左列字段:

  1. union、in、or都能够命中索引,建议使用in

根据字段investigate_month 建立普通索引

create index idx_acce_daily_im on `datahandler_acce_daily`(investigate_month)

Union语句

explain select * 
from `datahandler_acce_daily`
where investigate_month = '10'
union all
SELECT * 
FROM `datahandler_acce_daily`
WHERE investigate_month = '12'

image-20210305230845588

==查询的CPU消耗:or>in>union==

  1. 用or分割开的条件,如果or前的条件中列有索引,而后面的列中没有索引,那么涉及到的索引都不会被用到

EXPLAIN SELECT * FROM payment WHERE customer_id = 203 OR amount = 3.96;

因为or后面的条件列中没有索引,那么后面的查询肯定要走全表扫描,在存在全表扫描的情况下,就没有必要多一次索引扫描增加IO访问。

  1. 负向条件查询不能使用索引,可以优化为in查询
  2. 范围条件查询可以命中索引。范围条件有:<、<=、>、>=、between等
  3. 数据库执行计算不会命中索引
  4. 利用覆盖索引进行查询,避免回表
  5. 建立索引的列,不允许为null

总结

a. 更新十分频繁的字段上不宜建立索引:因为更新操作会变更B+树,重建索引。这个过程是十分消耗数据库性能的。

b. 区分度不大的字段上不宜建立索引:类似于性别这种区分度不大的字段,建立索引的意义不大。因为不能有效过滤数据,性能和全表扫描相当。另外返回数据的比例在30%以外的情况下,优化器不会选择使用索引。

c. 业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。虽然唯一索引会影响insert速度,但是对于查询的速度提升是非常明显的。另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,在并发的情况下,依然有脏数据产生。

d. 多表关联时,要保证关联字段上一定有索引。

e. 创建索引时避免以下错误观念:索引越多越好,认为一个查询就需要建一个索引;宁缺勿滥,认为索引会消耗空间、严重拖慢更新和新增速度;抵制唯一索引,认为业务的唯一性一律需要在应用层通过“先查后插”方式解决;过早优化,在不了解系统的情况下就开始优化。

感悟

对于自己编写的SQL查询语句,要尽量使用EXPLAIN命令分析一下,做一个对SQL性能有追求的程序员。衡量一个程序员是否靠谱,SQL能力是一个重要的指标。作为后端程序员,深以为然。

数据库的事务以及事务的隔离级别

什么是事务

事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。事务的正确执行使得数据库从一种状态转换为另一种状态

事务需要服从的ACID原则

事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)的缩写,这四种状态的意思是:

ACID

==当事务处理系统创建事务时,将确保事务有某些特性。组件的开发者们假设事务的特性应该是一些不需要他们亲自管理的特性。这些特性称为ACID特性==

1、原子性

以前的物理认为原子就是最小不可再分的了,所以大概是因为这个原因起了这个名字吧。

要么一起完成,要么回滚。

为了提供回滚或者撤销未提交的变化的能力,许多数据源采用日志机制。例如,SQLServer使用一个预写事务日志,在将数据应用于(或提交到)实际数据页面前,先卸载事务日志上。但是,其他一些数据源不是关系型数据库管理系统(RDBMS),他们管理未提交事务的方式完全不同。只要事务回滚,数据源可以撤销所有未提交的改变,那么这种技术应该可以用于管理事务

2、一致性

事务的一致性表示事务的运行不改变数据库的一致性。例如完整性约束了a+b=10,一个事务改变了a,那么b也应该随之改变

3、隔离性

​ 隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

 即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

4、持久性

事务的持久性是指事务运行成功之后,系统的更新是永久的,不会无缘无故的回滚

事务的作用

事务管理对于企业级应用而言至关重要,它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性。就像银行的自动提款机ATM,通常ATM都可以正常为客户服务,但是也难免遇到操作过程中及其突然出故障的情况,此时,事务就必须确保出故障前对账户的操作不生效,就像用户刚才完全没有使用过ATM机一样,以保证用户和银行的利益都不受损失。

并发下事务会产生的问题

举个例子,事务A和事务B操纵的是同一个资源,事务A有若干个子事务,事务B也有若干个子事务,事务A和事务B在高并发的情况下,会出现各种各样的问题。"各种各样的问题",总结一下主要就是五种:第一类丢失更新、第二类丢失更新、脏读、不可重复读、幻读。五种之中,第一类丢失更新、第二类丢失更新不重要,不讲了,讲一下脏读、不可重复读和幻读。

1、脏读

所谓脏读,就是指`事务A读到了事务B还没有提交的数据,比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务-->取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。

2、不可重复读(另一个线程执行了update)

所谓不可重复读,就是指在一个事务里面读取了两次某个数据,读出来的数据不一致。还是以银行取钱为例,事务A开启事务-->查出银行卡余额为1000元,此时切换到事务B事务B开启事务-->事务B取走100元-->提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。

3、幻读(另一个线程执行了insert)

所谓幻读,就是指在一个事务里面的操作中发现了未被操作的数据。比如学生信息,事务A开启事务-->修改所有学生当天签到状况为false,此时切换到事务B,事务B开启事务-->事务B插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。

事务隔离级别

事务隔离级别,就是为了解决上面几种问题而诞生的。为什么要有事务隔离级别,因为事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大,因此很多时候必须在并发性和性能之间做一个权衡。所以设立了几种事务隔离级别,以便让不同的项目可以根据自己项目的并发情况选择合适的事务隔离级别,对于在事务隔离级别之外会产生的并发问题,在代码中做补偿。

  1. read_uncommited(可以读到未提交的数据 )

这个不扯淡吗???比如事务A可以读取到事务B未提交的数据,如果事务B回滚了,那你事务A读个卵蛋蛋,就是脏读了。

  1. read_commited(可以读到已经提交的数据)

这个就可以避免脏读 ,但是避免不了不可重复读

  1. repeatable_read(前后读取到的数据是一致的)(mysql默认的事务隔离级别)

解决了不可重复读的问题

重复读取,就是在数据读出来之后加锁,类似“select * from XXX for update”,明确数据读取出来就是为了更新用的,所以需要加一把锁,防止别人修改它。repeatable_read的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可更改这条记录,这样解决了脏读、不可重复读的问题,但是幻读的问题还是没有解决。

  1. Serializable 串行化

最高的事务隔离级别,不管多少事务,挨个运行完一个事务之后的所有子数据才可以执行另一个事务中的所有子事务,这样就避免了脏读、不可重复读和幻读的问题了

隔离级别 脏读可能性 不可重复读可能性 幻读可能性
READ_UNCOMMITED
READ_COMMITED ×
REPEATABLE_READ × ×
SERIALIZABLE × × ×
参考文献: https://www.cnblogs.com/xrq73...

你可能感兴趣的:(mysql)