MySQL:索引详解

原文地址

  1. MySQL索引原理及慢查询优化: tech.meituan.com/2014/06/30/…
  2. Explain详解: blog.csdn.net/qq_38975553…
  3. BTree和B+Tree详解: blog.csdn.net/weixin_4194…

索引概述

定义索引是存储引擎用于快速找到记录的一种数据结构。举例说明:若是查找一本书中的某个特定主题,通常会先看书的目录(相似索引),找到对应页面。在MySQL,存储引擎采用相似的方法使用索引,高效获取查找的数据。html

索引的分类node

1)从存储结构上来划分mysql

  • Btree 索引(B+tree,B-tree)
  • 哈希索引
  • full-index 全文索引

2)从应用层次上来划分sql

  • 普通索引:即一个索引只包含单个列,一个表能够有多个单列索引。
  • 惟一索引:索引列的值必须惟一,但容许有空值。
  • 复合索引:一个索引包含多个列。

3)从表记录的排列顺序和索引的排列顺序是否一致来划分数据库

  • 汇集索引:表记录的排列顺序和索引的排列顺序一致。
  • 非汇集索引:表记录的排列顺序和索引的排列顺序不一致。

索引底层数据结构

磁盘IO与预读

数据库保存的数据是存储在磁盘上,查找数据时须要将磁盘中的数据加载到内存中,在介绍索引的实现以前,先了解下磁盘IO与预读。bash

磁盘读取数据靠的是机械运动,每次读取数据花费的时间能够分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所须要的时间,主流磁盘通常在5ms如下;旋转延迟就是咱们常常据说的磁盘转速,好比一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4. 17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,通常在零点几毫秒,相对于前两个时间能够忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4. 17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒能够执行5亿条指令,由于指令依靠的是电的性质,换句话说执行一次IO的时间能够执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。数据结构

下图是计算机硬件延迟的对比图,供你们参考:ide

various-system-software-hardware-latencies

考虑到磁盘IO是很是高昂的操做,计算机操做系统作了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,由于局部预读性原理告诉咱们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据咱们称之为一页(page)。具体一页有多大数据跟操做系统有关,通常为4k或8k,也就是咱们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计很是有帮助。函数

B-Tree和B+Tree

B-tree

B-Tree是为磁盘等外存储设备设计的一种平衡查找树。oop

B-Tree结构的数据可让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组[key, data] ,key为记录的键值,对应表中的主键值,data为一行记录中除主键外的数据。对于不一样的记录,key值互不相同。

一棵m阶的B-Tree有以下特性:

  1. 每个节点最多有 m 个子节点
  2. 每个非叶子节点(除根节点)最少有 ⌈m/2⌉ 个子节点
  3. 若是根节点不是叶子节点,那么它至少有两个子节点
  4. k 个子节点的非叶子节点拥有 k − 1 个键
  5. 全部的叶子节点都在同一层

B-Tree中的每一个节点根据实际状况能够包含大量的关键字信息和分支,以下图所示为一个3阶的B-Tree:

B-Tree

每一个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分红的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。

模拟查找关键字29的过程:

  1. 根据根节点找到磁盘块1,读入内存。【磁盘I/O操做第1次】

    比较关键字29在区间(17, 35),找到磁盘块1的指针P2。

  2. 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操做第2次】

    比较关键字29在区间(26, 30),找到磁盘块3的指针P2。

  3. 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操做第3次】

    在磁盘块8中的关键字列表中找到关键字29。

分析上面过程,发现须要3次磁盘I/O操做,和3次内存查找操做。因为内存中的关键字是一个有序表结构,能够利用二分法查找提升效率。而3次磁盘I/O操做是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了做用,从而提升了查询效率。

B+Tree

B+Tree是在B-Tree基础上的一种优化,InnoDB存储引擎就是用B+Tree实现其索引结构。

在B+Tree中,全部数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样能够大大加大每一个节点存储的key值数量,下降B+Tree的高度。

因为B+Tree的非叶子节点只存储键值信息,假设每一个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构以下图所示:

B+Tree

创建索引的几大原则

  1. 最左前缀匹配原则,很是重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就中止匹配,好比a = 1 and b = 2 and c > 3 and d = 4 若是创建(a,b,c,d)顺序的索引,d是用不到索引的,若是创建(a,b,d,c)的索引则均可以用到,a,b,d的顺序能够任意调整。

  2. =和in能够乱序,好比a = 1 and b = 2 and c = 3 创建(a,b,c)索引能够任意顺序,mysql的查询优化器会帮你优化成索引能够识别的形式。

  3. 尽可能选择区分度高的列做为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大咱们扫描的记录数越少,惟一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不一样,这个值也很难肯定,通常须要join的字段咱们都要求是0.1以上,即平均1条扫描10条记录。

  4. 索引列不能参与计算,保持列“干净”,好比from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,缘由很简单,b+树中存的都是数据表中的字段值,但进行检索时,须要把全部元素都应用函数才能比较,显然成本太大。因此语句应该写成create_time = unix_timestamp(’2014-05-29’)。

  5. 尽可能的扩展索引,不要新建索引。好比表中已经有a的索引,如今要加(a,b)的索引,那么只须要修改原来的索引便可。

慢查询优化基本步骤

  1. 先运行看看是否真的很慢,注意设置SQL_NO_CACHE
  2. where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每一个字段分别查询,看哪一个字段的区分度最高
  3. explain查看执行计划,是否与2预期一致(从锁定记录较少的表开始查询)
  4. order by limit 形式的sql语句让排序的表优先查
  5. 了解业务方使用场景
  6. 加索引时参照建索引的几大原则
  7. 观察结果,不符合预期继续从0分析

explain详解

explain为mysql提供语句的执行计划信息。能够应用在select、delete、insert、update和place语句上。explain的执行计划,只是做为语句执行过程的一个参考,实际执行的过程不必定和计划彻底一致,可是执行计划中透露出的信息却能够帮助选择更好的索引和写出更优化的查询语句。

explain输出项

Column JSON Name Meaning
id select_id The SELECT identifier
select_type None The SELECT type
table table_name The table for the output row
partitions partitions The matching partitions
type access_type The join type
possible_keys possible_keys The possible indexes to choose
key key The index actually chosen
key_len key_length The length of the chosen key
ref ref The columns compared to the index
rows rows Estimate of rows to be examined
filtered filtered Percentage of rows filtered by table condition
Extra None Additional information

id

id列的编号是 select 的序列号,有几个 select 就有几个id,而且id的顺序是按 select 出现的顺序增加的。

MySQL将 select 查询分为简单查询(SIMPLE)和复杂查询(PRIMARY)。复杂查询分为三类:简单子查询、派生表(from语句中的子查询)、union 查询。

id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行

select_type

select_type 表示对应行是简单仍是复杂的查询。

table

这一列表示 explain 的一行正在访问哪一个表。

当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,因而先执行 id=N 的查询。

当有 union 时,UNION RESULT 的 table 列的值为<union1, 2>,1和2表示参与 union 的 select 行id。

partitions

type

这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。

依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL

  • NULLmysql可以在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,能够单独查找索引来完成,不须要在执行时访问表
  • const, system:mysql能对查询的某部分进行优化并将其转化成一个常量(能够看show warnings 的结果)。用于 primary key 或 unique key 的全部列与常数比较时,因此表最多有一个匹配行,读取1次,速度比较快。system是const的特例,表里只有一条元组匹配时为system
  • eq_refprimary key 或 unique key 索引的全部部分被链接使用 ,最多只会返回一条符合条件的记录。这多是在 const 以外最好的联接类型了,简单的 select 查询不会出现这种 type。
  • ref相比 eq_ref,不使用惟一索引,而是使用普通索引或者惟一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行。
  • range范围扫描一般出如今 in(), between , > , <, >= 等操做中。使用一个索引来检索给定范围的行。
  • index扫描全表索引,这一般比ALL快一些。(index是从索引中读取的,而all是从硬盘中读取)
  • ALL即全表扫描,意味着mysql须要从头至尾去查找所须要的行。一般状况下这须要增长索引来进行优化了

possible_keys

这一列显示查询可能使用哪些索引来查找。

explain 时可能出现 possible_keys 有列,而 key 显示 NULL 的状况,这种状况是由于表中数据很少,mysql认为索引对此查询帮助不大,选择了全表查询。

若是该列是NULL,则没有相关的索引。在这种状况下,能够经过检查 where 子句看是否能够创造一个适当的索引来提升查询性能,而后用 explain 查看效果。

key

这一列显示mysql实际采用哪一个索引来优化对该表的访问。

若是没有使用索引,则该列是 NULL。若是想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index。

key_len

这一列显示了mysql在索引里使用的字节数,经过这个值能够算出具体使用了索引中的哪些列。

ref

这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film. id)

rows

这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数。

filtered

Extra

  • Using index查询的列被索引覆盖,而且where筛选条件是索引的前导列(最左侧索引),是性能高的表现。通常是使用了覆盖索引(索引包含了全部查询的字段)。对于innodb来讲,若是是辅助索引性能会有很多提升

  • Using where查询的列未被索引覆盖,where筛选条件非索引的前导列

  • Using where Using index查询的列被索引覆盖,而且where筛选条件是索引列之一但不是索引的前导列,意味着没法直接经过索引查找来查询到符合条件的数据, Using index表明select用到了覆盖索引

  • NULL查询的列未被索引覆盖,而且where筛选条件是索引的前导列,意味着用到了索引,可是部分字段未被索引覆盖,必须经过“回表”来实现,不是纯粹地用到了索引,也不是彻底没用到索引

  • Using index condition与Using where相似,查询的列不彻底被索引覆盖,where条件中是一个前导列的范围;

  • Using temporary:mysql须要建立一张临时表来处理查询。出现这种状况通常是要进行优化的,首先是想到用索引来优化。

  • Using filesort:mysql 会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。此时mysql会根据联接类型浏览全部符合条件的记录,并保存排序关键字和行指针,而后排序关键字并按顺序检索行信息。这种状况下通常也是要考虑使用索引来优化的。

案列分析

1. 复杂语句写法

不少状况下,咱们写SQL只是为了实现功能,这只是第一步,不一样的语句书写方式对于效率每每有本质的差异,这要求咱们对mysql的执行计划和索引原则有很是清楚的认识,请看下面的语句:

select
    distinct cert.emp_id
from
    cm_log cl
    inner join (
        select
            emp.id as emp_id,
            emp_cert.id as cert_id
        from
            employee emp
            left join emp_certificate emp_cert on emp.id = emp_cert.emp_id
        where
            emp.is_deleted = 0
    ) cert on (
        cl.ref_table = 'Employee'
        and cl.ref_oid = cert.emp_id
    )
    or (
        cl.ref_table = 'EmpCertificate'
        and cl.ref_oid = cert.cert_id
    )
where
    cl.last_upd_date >= '2013-11-07 15:03:00'
    and cl.last_upd_date <= '2013-11-08 16:00:00';
复制代码
  1. 先运行一下,53条记录 1.87秒,又没有用聚合语句,比较慢
53 rows in set (1.87 sec)
复制代码
  1. explain

简述一下执行计划,首先mysql根据idx_last_upd_date索引扫描cm_log表得到379条记录;而后查表扫描了63727条记录,分为两部分,derived表示构造表,也就是不存在的表,能够简单理解成是一个语句造成的结果集,后面的数字表示语句的ID。derived2表示的是ID = 2的查询构造了虚拟表,而且返回了63727条记录。咱们再来看看ID = 2的语句究竟作了写什么返回了这么大量的数据,首先全表扫描employee表13317条记录,而后根据索引emp_certificate_empid关联emp_certificate表,rows = 1表示,每一个关联都只锁定了一条记录,效率比较高。得到后,再和cm_log的379条记录根据规则关联。从执行过程上能够看出返回了太多的数据,返回的数据绝大部分cm_log都用不到,由于cm_log只锁定了379条记录。

如何优化呢?能够看到咱们在运行完后仍是要和cm_log作join, 那么咱们能不能以前和cm_log作join呢?仔细分析语句不难发现,其基本思想是若是cm_log的ref_table是EmpCertificate就关联emp_certificate表,若是ref_table是Employee就关联employee表,咱们彻底能够拆成两部分,并用union链接起来,注意这里用union,而不用union all是由于原语句有“distinct”来获得惟一的记录,而union刚好具有了这种功能。若是原语句中没有distinct不须要去重,咱们就能够直接使用union all了,由于使用union须要去重的动做,会影响SQL性能。

优化过的语句以下:

select
       emp.id
   from
       cm_log cl
       inner join employee emp on cl.ref_table = 'Employee'
       and cl.ref_oid = emp.id
   where
       cl.last_upd_date >= '2013-11-07 15:03:00'
       and cl.last_upd_date <= '2013-11-08 16:00:00'
       and emp.is_deleted = 0
   union
   select
       emp.id
   from
       cm_log cl
       inner join emp_certificate ec on cl.ref_table = 'EmpCertificate'
       and cl.ref_oid = ec.id
       inner join employee emp on emp.id = ec.emp_id
   where
       cl.last_upd_date >= '2013-11-07 15:03:00'
       and cl.last_upd_date <= '2013-11-08 16:00:00'
       and emp.is_deleted = 0
复制代码
  1. 不须要了解业务场景,只须要改造的语句和改造以前的语句保持结果一致

  2. 现有索引能够知足,不须要建索引

  3. 用改造后的语句实验一下,只须要10ms 下降了近200倍!

2. 明确应用场景

举这个例子的目的在于颠覆咱们对列的区分度的认知,通常上咱们认为区分度越高的列,越容易锁定更少的记录,但在一些特殊的状况下,这种理论是有局限性的。

select
  *
from
  stage_poi sp
where
  sp.accurate_result = 1
  and (
      sp.sync_status = 0
      or sp.sync_status = 2
      or sp.sync_status = 4
  );
复制代码
  1. 先看看运行多长时间,951条数据6.22秒,真的很慢。
951 rows in set (6.22 sec)
复制代码
  1. 先explain,rows达到了361万,type = ALL代表是全表扫描。

  1. 全部字段都应用查询返回记录数,由于是单表查询 0已经作过了951条。

  2. 让explain的rows 尽可能逼近951。

    看一下accurate_result = 1的记录数:

select count(*),accurate_result from stage_poi  group by accurate_result;
+----------+-----------------+
| count(*) | accurate_result |
+----------+-----------------+
|     1023 |              -1 |
|  2114655 |               0 |
|   972815 |               1 |
+----------+-----------------+
复制代码

咱们看到accurate_result这个字段的区分度很是低,整个表只有-1, 0, 1三个值,加上索引也没法锁定特别少许的数据。

再看一下sync_status字段的状况:

select count(*),sync_status from stage_poi  group by sync_status;
+----------+-------------+
| count(*) | sync_status |
+----------+-------------+
|     3080 |           0 |
|  3085413 |           3 |
+----------+-------------+
复制代码

一样的区分度也很低,根据理论,也不适合创建索引。

问题分析到这,好像得出了这个表没法优化的结论,两个列的区分度都很低,即使加上索引也只能适应这种状况,很难作广泛性的优化,好比当sync_status 0、3分布的很平均,那么锁定记录也是百万级别的。

找业务方去沟通,看看使用场景。业务方是这么来使用这个SQL语句的,每隔五分钟会扫描符合条件的数据,处理完成后把sync_status这个字段变成1, 五分钟符合条件的记录数并不会太多,1000个左右。了解了业务方的使用场景后,优化这个SQL就变得简单了,由于业务方保证了数据的不平衡,若是加上索引能够过滤掉绝大部分不须要的数据。

  1. 根据创建索引规则,使用以下语句创建索引
alter table stage_poi add index idx_acc_status(accurate_result,sync_status);
复制代码
  1. 观察预期结果,发现只须要200ms,快了30多倍。
952 rows in set (0.20 sec)
复制代码

咱们再来回顾一下分析问题的过程,单表查询相对来讲比较好优化,大部分时候只须要把where条件里面的字段依照规则加上索引就好,若是只是这种“无脑”优化的话,显然一些区分度很是低的列,不该该加索引的列也会被加上索引,这样会对插入、更新性能形成严重的影响,同时也有可能影响其它的查询语句。因此咱们第4步调差SQL的使用场景很是关键,咱们只有知道这个业务场景,才能更好地辅助咱们更好的分析和优化查询语句。

3. 没法优化的语句

select
   c.id,
   c.name,
   c.position,
   c.sex,
   c.phone,
   c.office_phone,
   c.feature_info,
   c.birthday,
   c.creator_id,
   c.is_keyperson,
   c.giveup_reason,
   c.status,
   c.data_source,
   from_unixtime(c.created_time) as created_time,
   from_unixtime(c.last_modified) as last_modified,
   c.last_modified_user_id
from
   contact c
   inner join contact_branch cb on c.id = cb.contact_id
   inner join branch_user bu on cb.branch_id = bu.branch_id
   and bu.status in (1, 2)
   inner join org_emp_info oei on oei.data_id = bu.user_id
   and oei.node_left >= 2875
   and oei.node_right <= 10802
   and oei.org_category = - 1
order by
   c.created_time desc
limit
   0, 10;
复制代码

仍是几个步骤。

  1. 先看语句运行多长时间,10条记录用了13秒,已经不可忍受。
10 rows in set (13.06 sec)
复制代码
  1. explain

    从执行计划上看,mysql先查org_emp_info表扫描8849记录,再用索引idx_userid_status关联branch_user表,再用索引idx_branch_id关联contact_branch表,最后主键关联contact表。

    rows返回的都很是少,看不到有什么异常状况。咱们在看一下语句,发现后面有order by + limit组合,会不会是排序量太大搞的?因而咱们简化SQL,去掉后面的order by 和 limit,看看到底用了多少记录来排序。

select
 count(*)
from
  contact c  
inner join
  contact_branch cb 
     on  c.id = cb.contact_id  
inner join
  branch_user bu 
     on  cb.branch_id = bu.branch_id 
     and bu.status in (
        1,
     2)  
  inner join
     org_emp_info oei 
        on  oei.data_id = bu.user_id 
        and oei.node_left >= 2875 
        and oei.node_right <= 10802 
        and oei.org_category = - 1  

+----------+
| count(*) |
+----------+
|   778878 |
+----------+
1 row in set (5.19 sec)
复制代码

发现排序以前竟然锁定了778878条记录,若是针对70万的结果集排序,将是灾难性的,怪不得这么慢,那咱们能不能换个思路,先根据contact的created_time排序,再来join会不会比较快呢?

因而改形成下面的语句,也能够用straight_join来优化:

select
   c.id,
   c.name,
   c.position,
   c.sex,
   c.phone,
   c.office_phone,
   c.feature_info,
   c.birthday,
   c.creator_id,
   c.is_keyperson,
   c.giveup_reason,
   c.status,
   c.data_source,
   from_unixtime(c.created_time) as created_time,
   from_unixtime(c.last_modified) as last_modified,
   c.last_modified_user_id
from
   contact c
where
   exists (
       select
           1
       from
           contact_branch cb
           inner join branch_user bu on cb.branch_id = bu.branch_id
           and bu.status in (1, 2)
           inner join org_emp_info oei on oei.data_id = bu.user_id
           and oei.node_left >= 2875
           and oei.node_right <= 10802
           and oei.org_category = - 1
       where
           c.id = cb.contact_id
   )
order by
   c.created_time desc
limit
   0, 10;
复制代码

验证一下效果 预计在1ms内,提高了13000多倍!

10 rows in set (0.00 sec)
复制代码

本觉得至此大工告成,但咱们在前面的分析中漏了一个细节,先排序再join和先join再排序理论上开销是同样的,为什么提高这么可能是由于有一个limit!大体执行过程是:mysql先按索引排序获得前10条记录,而后再去join过滤,当发现不够10条的时候,再次去10条,再次join,这显然在内层join过滤的数据很是多的时候,将是灾难的,极端状况,内层一条数据都找不到,mysql还傻乎乎的每次取10条,几乎遍历了这个数据表!

​ 用不一样参数的SQL试验下:

select
  sql_no_cache c.id,
  c.name,
  c.position,
  c.sex,
  c.phone,
  c.office_phone,
  c.feature_info,
  c.birthday,
  c.creator_id,
  c.is_keyperson,
  c.giveup_reason,
  c.status,
  c.data_source,
  from_unixtime(c.created_time) as created_time,
  from_unixtime(c.last_modified) as last_modified,
  c.last_modified_user_id
from
  contact c
where
  exists (
      select
          1
      from
          contact_branch cb
          inner join branch_user bu on cb.branch_id = bu.branch_id
          and bu.status in (1, 2)
          inner join org_emp_info oei on oei.data_id = bu.user_id
          and oei.node_left >= 2875
          and oei.node_right <= 2875
          and oei.org_category = - 1
      where
          c.id = cb.contact_id
  )
order by
  c.created_time desc
limit
  0, 10;

Empty set (2 min 18.99 sec)
复制代码

2 min 18. 99 sec!比以前的状况还糟糕不少。因为mysql的nested loop机制,遇到这种状况,基本是没法优化的。这条语句最终也只能交给应用系统去优化本身的逻辑了。

经过这个例子咱们能够看到,并非全部语句都能优化,而每每咱们优化时,因为SQL用例回归时落掉一些极端状况,会形成比原来还严重的后果。因此,第一:不要期望全部语句都能经过SQL优化,第二:不要过于自信,只针对具体case来优化,而忽略了更复杂的状况。

慢查询的案例就分析到这儿,以上只是一些比较典型的案例。咱们在优化过程当中遇到过超过1000行,涉及到16个表join的“垃圾SQL”,也遇到过线上线下数据库差别致使应用直接被慢查询拖死,也遇到过varchar等值比较没有写单引号,还遇到过笛卡尔积查询直接把从库搞死。再多的案例其实也只是一些经验的积累,若是咱们熟悉查询优化器、索引的内部原理,那么分析这些案例就变得特别简单了。

相关文章
相关标签/搜索