【数据库】当面试中被问到Mysql索引优化时......

文章目录

  • 一、前言
  • 二、独立的列
  • 三、前缀索引和索引选择性
    • 3.1 含义
    • 3.2 使用值的列表来确定前缀长度
    • 3.3 使用完整列的选择性来确定前缀长度
    • 3.4 创建前缀索引
  • 四、多列索引
    • 4.1 多列索引的含义
    • 4.2 从多列索引到索引合并
    • 4.3 索引合并说明现有的索引很糟糕
  • 五、选择合适的索引列顺序
    • 5.1 索引列顺序的引入
    • 5.2 索引列顺序的经验法则:将选择性最高的列放到索引最前列
    • 5.3 关于选择性和基数的经验法则的不足
    • 5.4 小结
  • 六、聚簇索引
    • 6.1 含义
    • 6.2 聚簇索引中数据分布
    • 6.3 聚簇索引的优点
    • 6.4 聚簇索引的缺点
    • 6.5 聚簇索引的应用:InnoDB和 MyISAM的数据分布对比
      • 6.5.1 MyISAM的数据分布(不支持聚簇索引)
      • 6.5.2 InnoDB的数据分布(支持聚簇索引)
      • 6.5.3 InnoDB和MyISAM的表存放方式
    • 6.6 聚簇索引的应用之聚集特性:主键单调递增 VS 主键UUID
      • 6.6.1 在 InnoDB表中按主键顺序插入行
      • 6.6.2 在 InnoDB表中按UUID插入行
      • 6.6.3 测试:按主键顺序插入行 VS 按UUID插入行
      • 6.6.4 问题:为什么使用UUID作为主键插入花费的时间长且索引占用空间大?
      • 6.6.5 使用UUID作为主键的缺点
      • 6.6.6 小结
  • 七、覆盖索引
    • 7.1 含义
    • 7.2 覆盖索引的四个优点(即整个查询仅需要扫描索引而无须回表的优点)
    • 7.3 覆盖索引的局限
    • 7.4 覆盖索引的应用
  • 八、使用索引扫描来做排序
    • 8.1 索引排序
    • 8.2 索引既满足排序,又用于查找行
    • 8.3 前导项为常量
    • 8.4 使用索引排序
    • 8.5 未使用索引排序
    • 8.6 索引排序的应用
  • 九、压缩(前缀压缩)索引
  • 十、冗余和重复索引
    • 10.1 重复索引
    • 10.2 冗余索引
    • 10.3 扩展已有的索引 VS 创建新索引
    • 10.4 冗余索引和重复索引解决方法
  • 十一、未使用的索引
  • 十二、索引和锁
  • 十三、索引优化技巧一览
  • 十四、小结

一、前言

索引优化是MySQL查询性能调优的一个重要课题

索引(在 MySQL中也叫做“键(key)")是存储引擎用于快速找到记录的一种数据结构,这是索引的基本功能。索引对于良好的性能非常关键,尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要,在数据量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但当数据量逐渐增大时,性能则会急剧下降。

但是,索引却经常被忽略,有时候甚至被误解,所以在实际案例中经常会遇到由糟糕索引导致的问题。索引优化应该是对查询性能优化最有效的手段了,索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好两个数量级。

正确地创建和使用索引是实现高性能查询的基础,高效地选择和使用索引有很多种方式,使用哪个索引,以及如何评估选择不同索引的性能影响的技巧,则需要持续不断地学习。

二、独立的列

含义:“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。

如果查询中的列不是独立的,则 MySQL就不会使用索引。例如,下面这个查询无法使用索引:

mysql > Select actor_id From sakila.actor WHERE actor_id + 1 =5;

我们凭肉眼很容易看出WHERE子句中的表达式其实等价于 actor_id=4,但是 MySQL无法自动解析这个方程式。这完全是用户行为,我们应该养成简化 WHERE条件的习惯,始终将索引列单独放在比较符号的一侧,一般是左侧(即要使用索引优化查询,索引列要单独放在比较符号的左侧)。

上述SQL语句改为

mysql > Select actor_id From sakila.actor WHERE actor_id = 4;

下面是另一个常见的错误:

mysql > Select ... WHERE TO_DAYS(CURRENT_DAYS) - TO_DAYS(date_col) <= 10

同样的,where子句TO_DAYS(CURRENT_DAYS) - TO_DAYS(date_col) <= 10,没有把索引列单独放在比较符号的一侧。

三、前缀索引和索引选择性

3.1 含义

前缀索引含义:有时候需要索引很长的字符列,这会让索引变得大且慢。一个策略是可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率,但这样也会降低索引的选择性。

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

一般情况下某个列前缀的选择性也是足够高的,足以满足查询性能。对于BLOB、TEXT或 者很长的 VARCHAR类型的列,必须使用前缀索引,因为 MySQL不允许素引这些列的完整长度。

块窍在于前缀长度要适中:要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。换句话说,前缀的“基数”应该接近于完整列的“基数。

3.2 使用值的列表来确定前缀长度

为了决定前缀的合适长度,需要找到最常见的值的列表,然后和最常见的前缀列表进行比较,在示例数据库 Sakila中并没有合适的例子,所以我们从表city中生成一个示例表,这样就有足够的数据进行演示:

create table sakila.city_demo(city Varchar(50) NOT NULL);
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city;
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo;
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo;
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo;
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo;
INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo;
--- Now randomize the distribution (inefficiently but conveniently): 现在随机分配,低效但方便
UPDATE sakila.city_demo SET city = (SELECT city FROM sakila.city ORDER BY RAND() LIMIT 1);

【数据库】当面试中被问到Mysql索引优化时......_第1张图片
【数据库】当面试中被问到Mysql索引优化时......_第2张图片

【数据库】当面试中被问到Mysql索引优化时......_第3张图片

【数据库】当面试中被问到Mysql索引优化时......_第4张图片
现在我们有了示例数据集,数据分布当然不是真实的分布;因为我们使用了RAND(),所以你的结果会与此不同,但对这个练习来说这并不重要。首先,我们找到最常见的城市列表:

select count(*) as cnt,city 
 from sakila.city_demo Group by city Order by cnt Desc Limit 10;

【数据库】当面试中被问到Mysql索引优化时......_第5张图片
注意到,上面每个值都出现了45~61次。现在查找到最频繁出现的域市前缀,先从3个前缀字母开始:

select count(*) as cnt,Left(city,3) as pref 
    from sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;

【数据库】当面试中被问到Mysql索引优化时......_第6张图片

每个前缀都比原来的城市出现的次数更多,因此唯一前缀比唯一城市要少得多,然后我们增加前缀长度,直到这个前缀的选择性接近完整列的选择性,经过实验后发现前缀长度为7时比较合适:

SELECT COUNT(*) As cnt, LEFT(city, 7) AS pref 
       FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;

【数据库】当面试中被问到Mysql索引优化时......_第7张图片

3.3 使用完整列的选择性来确定前缀长度

计算合适的前缀长度的另外一个办法就是计算完整列的选择性,并使前缀的选择性接近于完整列的选择性,下面显示如何计算完整列的选择性:

SELECT COUNT(DISTINCT city)/COUNT(*) FROM sakila.city_demo;

【数据库】当面试中被问到Mysql索引优化时......_第8张图片

通常来说(尽管也有例外情况),这个例子中如果前缀的选择性能够接近0.031,基本上就可用了,可以在一个查询中针对不同前缀长度进行计算,这对于大表非常有用。下面给出了如何在网一个查询中计算不同前缀长度的选择性:

SELECT COUNT(DISTINCT LEFT(city,3))/COUNT(*) AS sel3,
       COUNT(DISTINCT LEFT(city,4))/COUNT(*) AS sel4,
       COUNT(DISTINCT LEFT(city,5))/COUNT(*) AS sel5,
       COUNT(DISTINCT LEFT(city,6))/COUNT(*) AS sel6,
       COUNT(DISTINCT LEFT(city,7))/COUNT(*) AS sel7
   FROM sakila.city_demo;

【数据库】当面试中被问到Mysql索引优化时......_第9张图片

查询显示当前缀长度到达7的时候,再增加前缀长度,选择性提升的幅度已经很小了。

只看平均选择性是不够的,也有例外的情况,需要考虑最坏情况下的选择性。平均选择性会让你认为前缀长度为4或者5的索引已经足够了,但如果数据分布很不均匀,可能就会有陷阱。如果观察前缀为4的最常出现城市的次数,可以看到明显不均匀:

select count(*) as cnt, Left (city,4) as pref 
    from sakila.city_demo Group by pref ORDER BY cnt DESC LIMIT 5;

【数据库】当面试中被问到Mysql索引优化时......_第10张图片

如果前缀是4个字节,则最常出现的前缀的出现次数比最常出现的城市的出现次数要大很多。即这些值的选择性比平均选择性要低,如果有比这个随机生成的示例更真实的数据,就更有可能看到这种现象。例如在真实的城市名上建一个长度为4的前缀索引,对于以"San"和"New"开头的城市的选择性就会非常糟糕,因为很多城市都以这两个词开头。

3.4 创建前缀索引

在上面的示例中,已经找到了合适的前缀长度,下面演示一下如何创建索引:

alter table sakila.city_demo ADD KEY (city(7));

前缀索引是一种能使索引更小、更快的有效办法,但另一方面也有其缺点: MySQL无法使用前缀索引做ORDER BY和 GROUP BY,也无法使用前缀索引做覆盖扫描。

四、多列索引

4.1 多列索引的含义

对于多列索引一个常见的错误就是,为每个列创建独立的索引,或者按照错误的顺序创建多列索引。这是错误的,实际上,多列索引的真正含义是:为特定的列创建索引并给这些列正确排序

4.2 从多列索引到索引合并

先来看第一个问题,为每个列创建独的索引,从 SHOW CREATE TABLE中很容易看到这种情况

CREATE TABLE t{
c1 INT,
c2 INT,
c3 INT,
KEY(c1),
KEY(c2),
KEY(c3)
}

这种索引策略,一般是由于人们听到一些专家诸如“把where条件里面的列都建上索引”这样模糊的建议导致的。实际上这个建议是非常错误的,这样一来最好的情况下也只能是“一星”索引,其性能比起真正最优的索引可能差几个数量级,有时如果无法设计一个“三星索引,那么不如忽略掉 WHERE子句,集中精力优化索引列的顺序,或者创建一个全覆盖索引

在多个列上建立独立的单列索引大部分情况下并不能提高 MySQL的查询性能, MySQL中引入了一种叫"索引合并"( index merge)的策略,一定程度上可以使用表上的多个单列索引来定位指定的行,早期的MySQL只能使用某一个单列索引,然而这种情况下没有哪一个独立的单列索引是非常有效的。例如,表 film_id和 actor id上各有一个单列索引。但对于下面这个查询where条件,这两个单列索引都不是好的选择:

select film_id,actor_id from sakila.film_actor  
       where actor_id = 1 or film_id = 1;

在老的 MySQL版本中, MySQL对这个查询会使用全表扫描。除非改写成如下的两个查询UNION的方式:

SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id=1
UNION ALL
SELECT film_id, actor_id FROM sakila.film_actor WHERE film_id=1
AND actor_id <> 1;

但在 MySQL5.0和更新的版本中,查询能够同时使用这两个单列索引进行扫描,并将结果进行合并,这种算法有三个变种:OR条件的联合( union),AD条件的相交( intersection), 组合前两种情况的联合及相交。下面的查询就是使用了两个索引扫描的联合,通过 EXPLAIN中的 Extra列可以看到这点:

explain select film_id , actor_id  from sakila.film_actor 
        where actor_id = 1 OR film_id = 1;

在这里插入图片描述
MySQL会使用这类技术优化复杂查询,所以在某些语句的Extra列中还可以看到嵌套操作。

4.3 索引合并说明现有的索引很糟糕

索引合并策略有时候是一种优化的结果,但实际上更多时候说明了表上的索引建得很糟糕:

(1)当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。

(2)当服务器需要对多个索引做联合操作时(通常有多个OR条件),通常需要耗费大量CPU和内存资源在算法的缓存,、排序和合并操作上,特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候。

(3)更重要的是,优化器不会把这些计算到“查询成本”中,优化器只关心随机页面读取。这会使得查询的成本被“低估”,导致该执行计划还不如直接走全表扫描。这样做不但会消耗更多的CPU和内存资源,还可能会影响查询的并发性,但如果是单独运行这样的查询则往往会忽略对并发性的影响,通常来说,还不如将查询改写成UNION的方式往往更好。

如果在 EXPLAIN中看到有索引合并,应该好好检查一下查询和表的结构,看是不是已经是最优的。也可以通过参数 optimizer_switch来关闭索引合并功能。也可以使用 IGNORE INDE提示让优化器忽略掉某些索引。

五、选择合适的索引列顺序

5.1 索引列顺序的引入

我们遇到的最容易引起困惑的问题就是索引列的顺序。正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要(顺便说明,本节内容适用于B-Tree索引;哈希或者其他类型的索引并不会像 B-Tree索引一样按顺序存储数据)。

在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以,索引可以按照升序或者降序进行扫描。以满足精确符合列顺序的ORDER BY, GROUP BY和 DISTINCT等子句的查询需求。

所以多列索引的列顺序至关重要。在“三星索引”系统中,列的顺序决定了一个索引是否能够成为一个真正的“三星索引”。

5.2 索引列顺序的经验法则:将选择性最高的列放到索引最前列

对于如何选择索引的列顺序有一个经验法则:将选择性最高的列放到索引最前列,这个建议有用吗?在某些场景可能有帮助,但通常不如避免随机IO和排序那么重要,考虑问题需要更全面(场景不同则选择不同,没有一个放之四海皆准的法则。这里只是说明,这个经验法则可能没有你想象的重要)。

当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的(即采用经验法则),这时候索引的作用只是用于优化 WHERE条件的查找。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在 WHERE子句中只使用了索引部分前缀列的查询来说选择性也更高。

然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关。这和前面介绍的选择前缀的长度需要考虑的地方一样,可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。

5.3 关于选择性和基数的经验法则的不足

以下面这个查询为例:

select * from payment where staff_id = 2 and customer_id = 584;

是应该创建一个( staff_id , customer_id)索引还是应该颠倒一下顺序?可以跑一些查询来确定在这个表中值的分布情况,并确定哪个列的选择性更高,先用下面的查询预测一下,看看各个where条件的分支对应的数据基数有多大:

SELECT sum(staff_id =2),sum(customer_id=584) FROM payment;

【数据库】当面试中被问到Mysql索引优化时......_第11张图片
根据前面的经验法则(即将选择性最高的列放到索引最前列),应该将索引列 customer_id放到前面,因为对应条件值的 customer_id数量更小,我们再来看看对于这个 customer_id的条件值,对应的staff_id列的选择性如何:

SELECT sum(staff_id = 2) from payment where customer_id = 584

【数据库】当面试中被问到Mysql索引优化时......_第12张图片
这样做有一个地方需要注意,查询的结果非常依赖于选定的具体值。如果按上述办法优化,可能对其他一些条件值的查询不公平,服务器的整体性能可能变得更糟,或者其他某些查询的运行变得不如预期。

如果是从诸如 pl-gwery- digest这样的工具的报告中提取“最差”查询,那么再按上述办法选定的索引顺序往往是非常高效的。如果没有类似的具体查询来运行,那么最好还是按经验法则(将选择性最高的列放到索引最前列)来做,因为经验法则考虑的是全局基数和选择性,而不是某个具体查询

select count(distinct staff_id)/count(*) as staff_id_selectivity,
       count(distinct customer_id)/count(*) as staff_id_selectivity,
       count(*)
       from payment;

【数据库】当面试中被问到Mysql索引优化时......_第13张图片
customer_id的选择性更高,所以答案是将其作为索引列的第一列:

ALTER TABLE payment ADD KEY(customer_id, staff_id); 

当使用前缀索引的时候,在某些条件值的基数比正常值高的时候,问题就来了。例如,在某些应用程序中,对于没有登录的用户,都将共用户名记录为“guset”,在记录用户行为的会话( session)表和其他记录用户活动的表中“guest”就成为了一个特殊用户ID,一旦查询涉及这个用户,那么和对于正常用户的查询就大不同了,因为通常有很多会话都是没有登录的。系统账号也会导致类似的问题。一个应用通常都有一个特殊的管理员账号,和普通账号不同,它并不是一个具体的用户,系统中所有的其他用户都是这个用户的好友,所以系统往往通过它向网站的所有用户发送状态通知和其他消息。这个账号的巨大的好友列表很容易导致网站出现服务器性能问题。

这实际上是一个非常典型的向题。任何的异常用户,不仅仅是那些用于管理应用的设计槽糕的账号会有同样的问题,那些拥有大量好友、图片、状态、收藏的用户,也会有前面提到的系统账号同样的问题。

5.4 小结

小结:尽管关于选择性和基数的经验法则值得去研究和分析,但一定要记住别忘了WHERE子句中的排序、分组和范围条件等其他因素,这些因素可能对查询的性能造成非常大的影响。

六、聚簇索引

6.1 含义

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。

当表有聚簇索引时,它的数据行实际上存放在索引的叶子页( leaf page)中,术语“聚簇”表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引(不过,覆盖索引可以模拟多个聚簇索引的情况)。

因为是存储引擎负责实现索引,因此不是所有的存储引擎都支持聚族索引(即其他的存储引擎不一定支持聚簇索引),本节我们主要关注 InnoDB存储引擎,但是这里讨论的原理对于任何支持聚簇索引的存储引擎都是适用的。

6.2 聚簇索引中数据分布

下图展示了聚簇索引中的数据是如何分布的。注意到,叶子页包含了行的全部数据,但是节点页只包含了索引列,在这个案例中,索引列包含的是整数值。
【数据库】当面试中被问到Mysql索引优化时......_第14张图片

一些数据库服务器允许选择哪个索引作为聚簇索引,InnoDB存储引擎通过主键聚集数据,这也就是说上图中的“被索引的列”就是主键列。

如果没有定义主键, InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚族索引, InnoDB只聚集在同一个页面中的记录,包含相邻键值的页面可能会相距甚远。

6.3 聚簇索引的优点

聚簇索引有以下优点:

(1)可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户ID来聚集数据,这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚簇索引,则每封邮件都可能导致一次磁盘I/O。
(2)数据访问更快。聚簇索引将索引和数据保存在同一个B-Tree中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找要快。
(3)使用覆盖索引扫描的查询可以直接使用页节点中的主键值。

如果在设计表和查询时能充分利用上面的优点,那就能极大地提升性能。同时,聚簇索引也有一些缺点。

6.4 聚簇索引的缺点

(1)聚簇数据最大限度地提高了I/O密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势了。
(2)插入速度严重依赖于插入顺序,按照主键的顺序插入是加载数据到 InnoDB表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用OPITMIZE TABLE命令重新组织一下表。
(3)更新聚簇索引列的代价很高,因为会强制InnoDB将每个被更新的行移动到新的位置。
(4)基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临“页分裂( page split)”的问题。当行的主键值要求必须将这一行插入到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的磁盘空间。
(5)聚簇索引可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
(6)二级索引(非聚簇索引)可能比想象的要更大,因为在二级素引的叶子节点包含了引用行的主键列。
(7)二级索引访问需要两次索引查找,而不是一次。

最后一点可能让人有些疑惑,为什么二级索引需要两次索引查找?答案在于二级索引中保存的“行指针”的实质,要记住,二级索引叶子节点保存的不是指向行的物理位置的指针,而是行的主键值。

这意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键值,然后根据这个值去聚簇索引中查找到对应的行。这里做了重复的工作:两次 B-Tree 查找而不是一次。对于 InnoDB,自适应哈希索引能够减少这样的重复工作。

6.5 聚簇索引的应用:InnoDB和 MyISAM的数据分布对比

聚簇索引和非聚簇索引的数据分布有区别,以及对应的主键索引和二级索引的数据分布也有区别,通常会让人感到困扰和意外,来看看 InnoDB和 MyIsAM是如何存储下面这表的:

create table layout_test(
   col1 int NOT NULL,
   col2 int NOT NULL,
   PRIMARY KEY(col1),
   KEY(col2)
);
DROP PROCEDURE IF EXISTS proc_initData1;
CREATE PROCEDURE proc_initData1()
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i<=10000 DO
       INSERT INTO layout_test(col1,col2) VALUES(i,rand()*100);
        SET i = i+1;
    END WHILE;
END 
CALL proc_initData1();

运行结果:
【数据库】当面试中被问到Mysql索引优化时......_第15张图片

假设该表的主键取值为1-10000,按照随机顺序插入并使用OPTIMIZE TABLE命令做 了优化。换句话说,数据在磁盘上的存储方式已经最优,但行的顺序是随机的。列col2的值是从1~100之间随机赋值,所以有很多重复的值。

6.5.1 MyISAM的数据分布(不支持聚簇索引)

MyISAM的数据分布非常简单,所以先介绍它。 MyISAM按照数据插入的顺序存储在磁盘上,如图所示:
【数据库】当面试中被问到Mysql索引优化时......_第16张图片

【数据库】当面试中被问到Mysql索引优化时......_第17张图片
在行的旁边显示了行号,从0开始递增,因为行是定长的,所以 MyISAM可以从表的开头跳过所需的字节找到需要的行( MyIsAM并不总是使用上图中的“行号,而是根据定长还是变长的行使用不同策略)。

这种分布方式很容易创建索引,下面显示的一系列图,隐藏了页的物理细节,只显示索引中的“节点”,索引中的每个叶子节点包含“行号”。

这里忽略了一些细节,例如前一个B-Tree节点有多少个内部节点,不过这并不影响对非聚簇存储引擎的基本数据分布的理解。

那col2列上的索引又会如何呢?有什么特殊的吗?实际上,它和其他索引没有什么区别,下图显示了col2列上的索引。
【数据库】当面试中被问到Mysql索引优化时......_第18张图片

小结:MyISAM中主键索引和其他索引在结构上没有什么不同。主键索引就是一个为PRIMARY的唯一非空索引。

6.5.2 InnoDB的数据分布(支持聚簇索引)

因为支持聚簇索引,所以使用非常不同的方式存储同样的数据。 InnoDB以下图所示的方式存储数据:
【数据库】当面试中被问到Mysql索引优化时......_第19张图片
第一眼看上去,感觉InnoDB数据分布和MyISAM数据分布没有什么不同,但再仔细看细节,会注意到该图显示了整个表,而不是只有索引。因为在 InnoDB中,聚簇索引“就是”表,像MyISAM那样需要独立的行存储

聚簇索引的每一个叶子节点都包含了主键值、事务ID、用于事务和 MVCC的回滚指针以及所有的剩余列(在这个例子中是col2)。如果主键是一个列前缀索引, InnoDB也会包含完整的主键列和剩下的其他列。

还有一点和 MyISAM的不同是, InnoDB的二级索引和聚簇索引很不相同, InnoDB二级索引的叶子节点中存储的不是“行指针”,而是主键值,并以此作为指向行的“指针”。这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。使用主键值当作指针会让二级索引占用更多的空间,换来的好处是, InnoDB在移动行时无须更新二级索引中的这个“指针”。

下图显示了示例表的col2索引。 一个叶子节点都包含了索引列(这里是col2),紧接着是主键值(co1)。该图展示了 B-Tree的叶子节点结构,但我们故意省略了非叶子节点这样的细节。

InnoDB的非叶子节点包含了索引列和一个指向下级节点的指针(下一级节点可以是非叶子节点,也可以是叶子节点),这对聚簇索引和二级索引都适用。
【数据库】当面试中被问到Mysql索引优化时......_第20张图片

6.5.3 InnoDB和MyISAM的表存放方式

下图是描述 InnoDB和MyISAM如何存放表的抽象图。从图中可以很容易看出 InnoDB和 MyISAM保存数据和索引的区别。

【数据库】当面试中被问到Mysql索引优化时......_第21张图片

6.6 聚簇索引的应用之聚集特性:主键单调递增 VS 主键UUID

如果正在使用 InnoDB表并且没有什么数据需要聚集,那么可以定义一个代理键 ( surrogate key)作为主键,这种主键的数据应该和应用无关,最简单的方法是使用 AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入,对于根据主键做关联操作的性能也会更好。

最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于I/O密集型的应用,因为从性能的角度考虑,使用UUID来作为聚簇索引则会很槽糕,它使得聚簇索引的插入变得完全随机,这是最坏的情况,使得数据没有任何聚集特性。

为了演示这一点,我们做如下两个基准测试。

6.6.1 在 InnoDB表中按主键顺序插入行

第一个使用整数ID插入 userinfo表:

create table userinfo (
id         int unsigned not null auto_increment,
name       varchar(255) not null default '',
email      varchar(255) not null default '',
password   varchar(255) not null default '',
dob        date default null,
address    varchar(255) not null default '',
city       varchar(255) not null default '',
state_id   tinyint unsigned not null default '0',
zip   varchar(255) not null default '',
country_id   smallint unsigned not null default '0',
gender   varchar(255) not null default 'M',
account_type      varchar(255) not null default '',
verified     tinyint not null default '0',
allow_mail   tinyint unsigned not null default '0',
parrent_account   int unsigned not null default '0',
closest_airport  varchar(255) not null default '',
primary key (id),
unique key mail(email),
key  country_id (country_id),
key  state_id(state_id),
key  state_id_2(state_id,city,address)
)engine=InnoDB;

运行成功:
【数据库】当面试中被问到Mysql索引优化时......_第22张图片

注意到userinfo使用了自增的整数作为主键。

6.6.2 在 InnoDB表中按UUID插入行

第二个例子是 userinfo_uuid表,除了主键改为UUID,其余和前面的 userinfo表完全相同,如下:

create table userinfo_uuid (
uuid       varchar(255) not null,
name       varchar(255) not null default '',
email      varchar(255) not null default '',
password   varchar(255) not null default '',
dob        date default null,
address    varchar(255) not null default '',
city       varchar(255) not null default '',
state_id   tinyint unsigned not null default '0',
zip   varchar(255) not null default '',
country_id   smallint unsigned not null default '0',
gender   varchar(255) not null default 'M',
account_type      varchar(255) not null default '',
verified     tinyint not null default '0',
allow_mail   tinyint unsigned not null default '0',
parrent_account   int unsigned not null default '0',
closest_airport  varchar(255) not null default '',
primary key (uuid),
unique key mail(email),
key  country_id (country_id),
key  state_id(state_id),
key  state_id_2(state_id,city,address)
)engine=InnoDB;

运行结果:
【数据库】当面试中被问到Mysql索引优化时......_第23张图片

6.6.3 测试:按主键顺序插入行 VS 按UUID插入行

首先,我们在一个有足够内存容纳索引的服务器上向这两个表各插入100万条记录。

DROP PROCEDURE IF EXISTS proc_userinfo;
CREATE PROCEDURE proc_userinfo()
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i<=1000000 DO
       INSERT INTO userinfo(id,name,email) VALUES(i,rand()*1000000,i);
        SET i = i+1;
    END WHILE;
END 
CALL proc_userinfo();

然后向这两个表继续插入300万条记录,使索引的大小超过服务器的内存容量。

得到的结果是,向UUID主键插入行不仅花费的时间更长,而且索引占用的空间也更大,原因一方面是由于主键字段更长,另一方面毫无疑问是由于页分裂和碎片导致的。

6.6.4 问题:为什么使用UUID作为主键插入花费的时间长且索引占用空间大?

当使用递增主键时,主键的值是顺序的,所以 InnoDB把每一条记录都存储在上一条记录的后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的 15/16,留出部分空间用于以后修改),下一条记录就会写入新的页中。一旦数据按这种顺序的方式加载,主键页就会近似于被顺序的记录填满,这也正是所期望的结果(然而,二级索引页可能是不一样的)。

当使用UUID主键时,新行的主键值不一定比之前插入的大,所以 InnoDB无法简单地总是把新行插入到索引的最后,而是需要为新的行寻找合适的位置——通常是已有数据的中间位置——并且分配空间。这会增加很多的额外工作,并导致数据分布不够优化。

6.6.5 使用UUID作为主键的缺点

(1)写入的目标页可能已经刷到磁盘上并从缓存中移除,或者是还没有被加载到缓存中,InnoDB在插入之前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的随机I/O。

(2)因为写入是乱序的, InnoDB不得不频繁地做页分裂操作,以便为新的行分配空间。页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页。

(3)由于频繁的页分裂,页会变得稀疏并被不规则地填充,所以最终数据会有碎片。

在把这些随机值载入到聚簇索引以后,可能需要做一次OPTIMIZE TABLE来重建表并优化页的填充。

6.6.6 小结

小结:使用 InnoDB时应该尽可能地按主键顺序插入数据,并且尽可能地使用单调增加的聚簇键的值来插入新行,这样才能保证较好的聚集效应。

七、覆盖索引

7.1 含义

含义:如果一个索引包含(或者说覆盖)所有需要查询的字段的值,即索引的叶子节点中已经包含要查询的数据, MySQL可以使用索引来直接获取列的数据,而不再需要读取数据行,这样的索引称之为“覆盖索引”。

7.2 覆盖索引的四个优点(即整个查询仅需要扫描索引而无须回表的优点)

(1)索引条目通常远小于数据行大小,所以如果只需要读取索引,那 MySQL就会极大地减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上,覆盖索引对于I/O密集型的应用也有帮助,因为索引比数据更小,更容易全部放入内存中。

(2)因为索引是按照列值顺序存储的(至少在单个页内是如此),所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据的I/O要少得多。

(3)一些存储引擎如 MyIsAM在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统词用,这可能会导致严重的性能问题,尤其是那些系统调用占了数据
访问中的最大开销的场景。

(4)由于InnoDB的聚簇索引,覆盖索引对 InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。

小结:在所有这些场景中,在索引中满足查询的成本一般比查询行要小得多,这全是覆盖索引展现其优势的机会。

7.3 覆盖索引的局限

(1)不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引列的值,但是哈希索引、空间索引和全文索引等都不存储索引列的值,所以 MySQL只能使用B-Tree覆盖索引。

(2)不同的存储引擎实现覆盖索引的方式也不同,而且不是所有支持覆盖索引。

7.4 覆盖索引的应用

当发起一个被索引覆盖的查询(也叫做索引覆盖查询)时,在 EXPLATN的 Extra列可以看到" Using index"的信息,例如,表 sakila.inventory有一个多列索引( store_id ,film_id)。MySQL如果只需访向这两列,就可以使用这个索引做覆盖索引,如下所示:

explain select store_id,film_id from sakila.inventory

在这里插入图片描述

八、使用索引扫描来做排序

8.1 索引排序

MySQL有两种方式可以生成有序的结果:

(1)通过排序操作;
(2)按索引顺序扫描(本节重点)。

如果 EXPLAIN出来的type列的值为" index’,则说明 MySQL使用了索引扫描来做排序(不要和 Extra列的" Using index”搞混淆了)。

扫描索引本身是很快的,因为只需要从一条索引记录移动到紧接着的下一条记录,但如果索引不能覆盖查询所需的全部列,那就不得不每扫描一条索引记录就都回表查询一次对应的行。这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢,尤其是在I/O密集型的工作负载时。

8.2 索引既满足排序,又用于查找行

MySQL可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务,这样是最好的。

只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时, MySQL才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求,否则,MySQL都需要执行排序操作,而无法利用索引排序。

8.3 前导项为常量

有一种情况下 ORDER BY子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候,如果where子句或者Join子句中对这些列指定了常量,就可以“弥补”索引的不足。

例如, Sakila示例数据库的表 rental在列( rental_date, inventory_id, customer_id) 上有名为 rental_date的索引。

【数据库】当面试中被问到Mysql索引优化时......_第24张图片

MySQL可以使用 rental_date索引为下面的查询做排序,从 EXPLAIN中可以看到没有出现文件排序( filesort)操作:

explain select rental_id , staff_id from sakila.rental
    where rental_date = '2005-05-25'
    order by inventory_id,customer_id

【数据库】当面试中被问到Mysql索引优化时......_第25张图片
即使ORDER BY子句不满足索引的最左前缀的要求,也可以用于查询排序,这是因为索引的第一列被指定为一个常数

8.4 使用索引排序

还有更多可以使用索引做排序的查询示例。下面这个查询可以利用索引排序,是因为查询为索引的第一列提供了常量条件,而使用第二列进行排序,将两列组合在一起,就形成了索引的最左前缀:

... where rental_date='2005-05-25' ORDER BY inventory_id DESC;

下面这个查询也没问题,因为 ORDER BY使用的两列就是索引的最左前缀:

where rental date>'2005-o5-25' ORDER BY rental date, inventory_id; 

8.5 未使用索引排序

下面是一些不能使用索引做排序的查询:

下面这个查询使用了两种不同的排序方向,但是索引列都是正序排序的:

where rental_date='2005-05-25' ORDER BY inventory_id DESC, customer_id ASC; 

下面这个查询的ORDER BY子句中引用了一个不在索引中的列:

where rental_date='2005-05-25" ORDER BY inventory_id, staff_id; 

下面这个查询的WHERE和ORDER BY中的列无法组合成索引的最左前缀:

where rental_date='2005-05-25' ORDER BY customer_id;

下面这个查询在索引列的第一列上是范围条件,所以 MySQL无法使用索引的其余列:

where rental_date >'2005-05-25' ORDER BY inventory_id, customer_id;

这个查询在 inventory_id列上有多个等于条件。对于排序来说,这也是一种范围查询:

WHERE rental_date = '2005-05-25' AND inventory_id IN(1, 2) ORDER BY customer_id; 

下面这个例子理论上是可以使用索引进行关联排序的,但由于优化器在优化时将film_actor表当作关联的第二张表,所以实际上无法使用索引:

explain select actor_id , title from sakila.film_actor
       inner join sakila.film using(film_id) order by actor_id;

在这里插入图片描述

8.6 索引排序的应用

使用索引做排序的一个最重要的用法是当SQL查询同时有ORDER BY和LIMIT子句的时候。

九、压缩(前缀压缩)索引

MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中,这在某些情况下能极大地提高性能。默认只压缩字符串,但通过参数设置也可以对整数做压缩。

MyISAM压缩每个索引块的方法是,先完全保存索引块中的第一个值,然后将其他值和第一个值进行比较得到相同前缀的字节数和副余的不同后缀部分,把这部分存储起来即可。例如,索引块中的第一个值是“perform”,第二个值是“performance",那么第二个值的前缀压缩后存储的是类似“7,ance”这样的形式, MyISAM对行指针也采用类似的前缀压缩方式。

压缩块使用更少的空间,代价是某些操作可能更慢。因为每个值的压缩前缀都依赖前面的值,所以 MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。正序的扫描速度还不错,但是如果是倒序扫描(例如ORDER BY DESC)就不是很好了,所有在块中查找某一行的操作平均都需要扫描半个索引块。

测试表明,对于CPU密集型应用,因为扫描需要随机查找,压缩索引使得 MyISAM在索引查找上要慢好几倍。压缩索引的倒序扫描就更慢了。压缩索引需要在CPU内存资源与磁盘之间做权衡。压缩索引可能只需要十分之一大小的磁盘空间,如果是I/O密集型应用,对某些查询带来的好处会比成本多很多。

程序员可以在 CREATE TABLE语句中指定 PACK KEYS参数来控制索引压缩的方式。

十、冗余和重复索引

10.1 重复索引

含义:重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。注意,重复索引的三个相同:所在列相同、排列顺序相同、索引类型相同。

MySQL允许在相同列上创建多个索引,这些创建出来的索引MySQL都需要维护,并且优化器在优化查询的时候也需要逐个地进行考虑。大量冗余和重复的索引会影响性能,应该避免这样创建重复索引,发现以后也应该立即移除。

有时会在不经意间创建了重复索引,例如下面的代码

create table test(
ID INT NOT NULL PRIMARY KEY,
A  INT NOT NULL,
B  INT NOT NULL,
UNIQUE(ID),
INDEX(ID)
)ENGINE=InnoDB;

一个经验不足的用户可能是想创建一个主键,先加上 唯一限制UNIQUE ,然后再加上 索引INDEX 以供查询使用,事实上, MySQL的 唯一限制UNIQUE 和 主键限制PRIMARY KEY 都是通过索引实现的,因此,上面的写法实际上在相同的列上创建了三个重复的索引,我们没有理由这样做,除非是在同 一列上创建不同类型的索引来满足不同的查询需求。

10.2 冗余索引

含义:如果创建了索引(A,B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引。因此索引(A,B)也可以当作索引(A)来使用(这种冗余只是对B-Tree索引来说的)。

但是如果再创建索引(B,A),则不是冗余索引,索引(B) 也不是,因为B不是索引(A,B)的最左前缀列,另外,其他不同类型的索引(例如哈希索引或者全文索引)也不会是B-Tree索引的冗余索引,无论覆盖的索引列是什么,因为索引类型不同了。

10.3 扩展已有的索引 VS 创建新索引

冗余索引通常发生在为表添加新索引的时候。例如,有人可能会增加一个新的索引(A,B)而不是扩展已有的索引(A)。还有一种情况是将一个索引扩展为(A,ID),其中ID是主键,对于 InnoDB来说主键列已经包含在二级索引中了,所以这也是冗余的。

大多数情况下都不需要冗余索引,应该尽量扩展已有的索引而不是创建新索引

但也有时候出于性能方面的考虑需要冗余索引,因为扩展已有的索引会导致其变得太大,从而影响其他使用该索引的查询的性能。

例如,如果在整数列上有一个索引,现在需要额外增加一个很长的 VARCHAR列来扩展该索引,那性能可能会急剧下降,特别是有查询把这个索引当作覆盖索引,或者这是 MyISAM表并且有很多范围查询(由于 MyISAM的前缀压缩)的时候。

考虑一下前面“在 InnoDB中按主键顺序插入行”一节提到的 userinfo表。这个表有1 000 000行,对每个 state_id值大概有20000条记录,在 state_id列有一个索引对下面的查询有用,假设查询名为Q1:

mysql> SELECT count(*) FROM userinfo WHERE state_id=5; 

一个简单的测试表明该查询的执行速度大概是每秒115次(QPS)。还有一个相关查询需要检索几个列的值,而不是只统计行数,假设名为Q2:

mysql> SELECT state_id,city,address FROM userinfo WHERE state_id=5; 

对于这个查询测试结果QPS小于10。提升该查询性能的最简单办法就是扩展索引为 state_id,city, address),让索引能覆盖查询

mysql >ALTER TABLE userinfo drop key state_id, add key 
state_id_2(state_id, cty, address);

索引扩展后,Q2运行得更快了,但是Q1却变慢了,如果我们想让两个查询都变得更快,就需要两个索引,尽管这样一来原来的单列索引是冗余的了。

有两个索引的缺点是索引成本更高,因为表中的索引越多插入速度会越慢。一般来说,增加新索引将会导致 INSERT、UPDATE、DETE等操作的速度变慢,特别是当新增索引后导致达到了内存瓶颈的时候。

10.4 冗余索引和重复索引解决方法

解决冗余索引和重复索引的方法很简单,删除这些索引就可以,但首先要做的是找出这样的索引,可以通过写一些复杂的访问 INFORMATION SCHEMA表的查询来找,

在决定哪些索引可以被删除的时候要非常小心,在前面的 InnoDB的示例表中,因为二级索引的叶子节点包含了主键值,所以在列(A)上的索引就相当于在(A,ID) 上的索引。如果有像where A=50 ORDER BY ID这样的查询,这个索引会很有作用。但如果将索引扩展为(A,B),则实际上就变成了(A,B,ID),那么上面查询的 ORDER BY子句 就无法使用该索引做排序,而只能用文件排序了。

十一、未使用的索引

含义:服务器永远不会使用的数据库的索引,这样索引称为未使用的索引。

未使用的索引是服务器不会用到的索引,是没有任何实际作用,这样的索引完全是累赘。我们的做法是将其删除。

有两个工具可以帮助定位未使用的索引。最简单有效的办法是在 Percona Server或者 MariaDB中先打开 overstates服务器变量(默认是关闭的),然后让服务器正常运行一段时间,再通过查询 INFORMATION_SCHEMA, INDEX_STATISTICS就能查到每个索引的使用频率。

另外,还可以使用 Percona Toolkit 中的 pt-index-usage,该工具可以读取查询日志,并对日志中的每条查询进行 EXPLAIN操作,然后打印出关于索引和查询的报告。这个工具不仅可以找出哪些索引是未使用的,还可以了解查询的执行计划(例如在某些情况,有些类似的查询的执行方式不一样,这可以帮助你定位到那些偶尔服务质量差的查询,优化它们以得到一致的性能表现,该工具也可以将结果写入到 MySQL的表中,方便查询结果)。

十二、索引和锁

索引可以让查询索引更少的行。如果你的查询从不访问那些不需要的行,那么就会锁定更少的行,从两个方面来看这对性能都有好处。

(1)首先,虽然 InnoDB的行锁效率很高,内存使用也很少,因为锁定行的时候仍然会带来额外开销;
(2)其次,锁定超过需要的行会增加锁争用并减少并发性。

InnoDB只有在访问行的时候才会对其加锁。而索引能够减少 InnoDB访问的行数,从而减少锁的数量。但这只有当 InnoDB在存储引擎层能够过滤掉所有不需要的行时才有效。如果索引无法过滤掉无效的行,那么需要在InnoDB检索到数据并返回给服务器层以后, MySQL服务器才能应用where子句。这时已经无法避免锁定行了:InnoDB已经锁住了这些行,到适当的时候才释放

通过下面的例子再次使用数据库sakila很好地解释了这些情况:

mysql> SET AUTOCOMMIT=0;
mysql> BEGIN;
mysql> select actor_id FROM sakila.actor where actor_id < 5
           and actor_id <> 1 FOR UPDATE;

【数据库】当面试中被问到Mysql索引优化时......_第26张图片
这条查询仅仅会返回2-4之间的行,但是实际上获取了1~4之间的行的排他锁。InnoDB会锁住第1行,这是因为 MysQL为该查询选择的执行计划是索引范围扫描:

explain select actor_id from sakila.actor
      where actor_id < 5 and actor_id <> 1 for update;

# 图

换句话说,底层存储引擎的操作是“从索引的开头开始获取满足条件 actor id<5的 记录”,服务器并没有告诉 InnoDB可以过滤第1行的 WHERE条件。注意到 EXPLAIN的 Extra列出现了“ Using where ”,这表示 MySQL服务器是在将存储引擎返回行以后再应用where过滤条件。

下面的第二个查询就能证明第1行确实已经被锁定,保持第一个连接打开,然后开启第二个连接并执行

mysql> SET AUTOCOMMIT=0;
mysql> BEGIN;
mysql> select actor_id FROM sakila.actor where actor_id = 1 for update;

这个查询将会挂起,直到第一个事务释放第1行的锁。所以证明了,之前的第一个查询证明将第1行确实锁定了。

如上所述,即使使用了索引,InnoDB也可能锁住一些不需要的数据。如果不能使用索引查找和锁定行的话问题可能会更糟糕, MySQL会做全表扫描并锁住所有的行,而不管是不是需要。

十三、索引优化技巧一览

常见索引原则有:

  1. 选择唯一性索引:唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。
  2. 为经常需要排序、分组和联合操作的字段建立索引 。
  3. 为常作为查询条件的字段建立 索引 。
  4. 限制索引的数目:越多的索引,会使更新表变得很浪费时间。
  5. 尽量使用数据量少的索引。
  6. 如果索引的值很长,那么查询的速度会受到影响。
  7. 尽量使用前缀来索引:如果索引字段的值很长,最好使用值的前缀来索引。
  8. 删除不再使用或者很少使用的索引
  9. 最左前缀匹配原则,非常重要的原则。
  10. 尽量选择区分度高的列作为索引:区分度的公式是表示字段不重复的比例
  11. 索引列不能参与计算,保持列“干净”:带函数的查询不参与索引。
  12. 尽量的扩展索引,不要新建索引。

十四、小结

本文中介绍MySQL索引优化原则,是MySQL查找性能优化的重要内容之一,希望对读者有用。

天天打码,天天进步!

你可能感兴趣的:(数据库/Linux)