Java八股文——MySQL「索引篇」
文章目录
- 索引是什么?有什么好处?
- 讲讲索引的分类是什么?
- MySQL 聚簇索引和非聚簇索引的区别是什么?
- 如果聚簇索引的数据更新,它的存储要不要变化?
- 什么字段适合当做主键?
- 性别字段能加索引么?为啥?
- 表中十个字段,你主键用自增ID还是UUID,为什么?
- MySQL 中的索引是怎么实现的?
- 查询数据时,到了B+树的叶子节点,之后的查找数据是如何做?
- 说说 B+ 树和 B 树的区别
- 为什么 B+ 树每一层代表一次 I/O 操作
- 为什么 MySQL 不用跳表?
- 联合索引的实现原理是什么?
- 创建联合索引时需要注意什么?
- 联合索引(A, B, C),现在有个执行语句是A = XXX and C < XXX,索引怎么走
- 联合索引(A, B, C),查询条件 where B > xxx and A = x 会生效吗
- 索引失效有哪些?
- 什么是覆盖索引?
- 如果一个列即是单列索引,又是联合索引,单独查它的话先走哪个?
- 索引已经建好了,那我再插入一条数据,索引会有哪些变化?
- 索引字段是不是建的越多越好?
- 详细讲讲什么是索引优化
- 了解过前缀索引吗?
索引是什么?有什么好处?
面试官您好,数据库索引是提升查询性能最核心、最有效的手段。
1. 索引是什么?—— 一本书的目录
理解索引最好的方式,就是把它比作一本厚书的目录。
- 没有索引:就像我们要在一部没有目录的《新华字典》里查找一个字。我们唯一的办法就是从第一页开始,一页一页地向后翻,直到找到为止。这种方式,我们称之为 “全表扫描”(Full Table Scan)。当数据量非常大时,这会极其缓慢。
- 有了索引:就像我们翻开字典的目录(比如按拼音或部首)。我们可以快速地在目录中定位到要找的字在哪一页,然后直接翻到那一页。这个“目录”,就是数据库的索引。
从技术上讲,索引是数据库中一种独立于表数据、用于快速定位数据行的、特殊的数据结构。在MySQL的InnoDB引擎中,这种数据结构最常用的就是B+树。
2. 索引的好处:为什么能提效?
索引的核心好处,就是极大地减少了查询时需要扫描的数据量,从而提高了查询效率。
-
从算法复杂度的角度看:
- 全表扫描的时间复杂度是O(N),其中N是表的总行数。数据量越大,查询时间就越长。
- 而通过B+树索引进行查找,其时间复杂度大约是O(log N)。这意味着,即使数据量增长一个数量级(比如从100万行到1000万行),查询的耗时也仅仅是增加了一点点,性能非常稳定。
-
除了提升查询(
SELECT)速度,索引还能:- 加速排序(
ORDER BY):如果排序的字段正好是索引,那么数据库可以直接按照索引的顺序来读取数据,而无需再进行额外的排序操作。 - 加速分组(
GROUP BY):与排序类似,可以利用索引来优化分组操作。 - 保证数据的唯一性:通过创建唯一索引(UNIQUE INDEX),可以由数据库层面来强制保证某一列(或几列组合)的值是唯一的。
- 加速排序(
3. 索引的缺点:它是一把“双刃剑”
虽然索引能极大地提升查询性能,但它并不是“银弹”,创建和维护索引也是有成本的:
- 空间成本:索引本身也是要占用磁盘空间的。一个表的索引越多,它占用的存储空间就越大。
- 时间成本(写操作的性能损耗):这是最主要的成本。当我们对表中的数据进行增、删、改(
INSERT,DELETE,UPDATE)操作时,数据库不仅要修改数据本身,还必须同步地去维护和更新这张表上所有相关的索引。如果一张表索引过多,它的写入性能就会急剧下降。
4. 实践中的思考:什么情况下应该创建索引?
基于索引的优缺点,我在实践中会遵循以下原则来创建索引:
- 为频繁作为
WHERE查询条件的字段创建索引。这是最核心的原则。 - 为频繁作为
ORDER BY或GROUP BY排序/分组依据的字段创建索引。 - 为需要保证唯一性的字段创建唯一索引。
- 优先考虑为区分度高(Cardinality高) 的列创建索引。比如,“身份证号”字段的区分度就远高于“性别”字段。
- 在多表
JOIN操作中,为连接字段(通常是外键)创建索引。 - 尽量设计覆盖索引,让查询只需要扫描索引就能拿到所有需要的数据,而无需“回表”查询原始数据行。
- 避免创建过多冗余的索引,以降低写操作的维护成本。
总结一下,索引是一种用空间和写性能,来换取巨大查询性能提升的典型技术。它的使用是一门权衡的艺术,我们需要根据业务的实际查询场景,来审慎地、有针对性地进行设计。
讲讲索引的分类是什么?
面试官您好,MySQL的索引种类繁多,可以从以下四个不同的维度来进行分类。
第一维度:按底层「数据结构」分类
这决定了索引是如何组织和查找数据的。
-
B+树索引 (B+ Tree Index)
- 这是MySQL中最常用、也是InnoDB和MyISAM存储引擎的默认索引类型。
- 特点:它是一种平衡多路查找树。数据都存储在叶子节点上,并且所有叶子节点通过一个双向链表连接,非常适合进行范围查询。无论是单点查询还是范围查询,性能都非常稳定。
-
哈希索引 (Hash Index)
- 特点:基于哈希表实现。
- 优点:在进行等值查询时(比如
WHERE name = 'Alice'),它的速度极快,理论上时间复杂度是O(1)。 - 缺点:不支持范围查询(比如
WHERE age > 30),因为哈希后的值是无序的。 - 应用:Memory存储引擎支持哈希索引。InnoDB有一个“自适应哈希索引”的功能,它会在内部对一些热点数据自动建立哈希索引来加速等值查询。
-
全文索引 (Full-text Index)
- 特点:专门用于在大段文本(如文章内容)中,进行关键词搜索。它不像
LIKE '%keyword%'那样进行全表扫描,而是像搜索引擎一样,通过分词和倒排索引来快速定位记录。
- 特点:专门用于在大段文本(如文章内容)中,进行关键词搜索。它不像
第二维度:按「物理存储」方式分类 (InnoDB引擎特定)
这描述了索引与数据行的物理关系。
-
聚簇索引 (Clustered Index)
- 特点:索引的叶子节点,直接存储了完整的行数据。可以理解为,数据文件本身就是一棵按主键组织起来的B+树。
- 规则:每张InnoDB表有且只有一个聚簇索引,通常就是主键索引。
-
二级索引 (Secondary Index) / 非聚簇索引
- 特点:除了聚簇索引之外的所有其他索引,都叫二级索引。
- 结构:二级索引的叶子节点,存储的不是行数据的物理地址,而是该行数据对应的主键值。
- 查询过程(回表):通过二级索引查找数据,通常需要两步:先在二级索引中找到对应的主键值,再用这个主键值去聚簇索引中找到完整的行数据。这个过程,我们称之为“回表”。
第三维度:按「字段特性」或功能分类
这是我们创建索引时,从逻辑功能上最常接触的分类。
- 主键索引 (Primary Key):一种特殊的唯一索引,不允许有空值,一张表只能有一个。
- 唯一索引 (Unique Index):要求索引列的值必须唯一,但允许有空值。
- 普通索引 (Normal Index):最基本的索引类型,没有任何限制,仅仅是为了加速查询。
- 前缀索引 (Prefix Index):只对字符串类型字段的前N个字符创建索引。这对于很长的文本字段(如
VARCHAR(255)或TEXT)非常有用,可以大大节省索引空间。
第四维度:按「字段个数」分类
- 单列索引 (Single-column Index):一个索引只包含表中的一个列。
- 联合索引 / 复合索引 (Composite Index):一个索引同时包含表中的多个列。
- 重要原则:在使用联合索引时,需要遵循 “最左前缀原则”。查询条件必须从索引的最左边的列开始,并且不能跳过中间的列,索引才会生效。比如,对
(a, b, c)创建了联合索引,那么查询条件为(a)、(a, b)、(a, b, c)时,索引都会生效。
- 重要原则:在使用联合索引时,需要遵循 “最左前缀原则”。查询条件必须从索引的最左边的列开始,并且不能跳过中间的列,索引才会生效。比如,对
总结一下,这四个维度是从不同角度对索引进行观察和划分。在实际设计中,我们会综合考虑这些因素:比如,我们会为某个字段创建一个普通的、单列的、基于B+树的二级索引。理解这些分类,能帮助我们更精确地设计出满足业务需求、且性能最优的索引方案。
MySQL 聚簇索引和非聚簇索引的区别是什么?
面试官您好,关键在于索引与数据行的物理存储关系。在MySQL中,我们最常用的InnoDB存储引擎,其数据本身就是以聚簇索引的形式来组织的。
我可以用一个“查字典”的比喻来解释它们:
- 聚簇索引 (Clustered Index):就像一本新华字典的“正文”部分,按拼音排序。
- 非聚簇索引 (Non-clustered Index):就像是这本字典末尾的 “偏旁部首检字表”。
1. 数据存储方式与索引结构的区别
-
聚簇索引 (字典正文)
- 索引即数据:它的叶子节点直接存储了完整的、实际的数据行。整个表的数据,就是按照聚簇索引的键值(通常是主键)顺序,物理地存放在磁盘上的。
-
非聚簇索引 (偏旁部首检字表)
- 索引与数据分离:它的叶子节点不存储完整的数据行。它存储的是索引键的值,以及一个指向该数据行实际位置的 “书签”。在InnoDB中,这个“书签”就是该行数据对应的聚簇索引键(即主键)的值。
2. 查询过程与“回表”
-
使用聚簇索引查询
- 一步到位:当我们通过主键进行查询时,InnoDB会直接在聚簇索引这棵B+树上进行查找,一旦在叶子节点找到对应的主键,也就同时找到了完整的数据行。效率非常高。
-
使用非聚簇索引查询
- 需要两步,可能涉及“回表”:
- 第一步:查非聚簇索引。比如,我们按一个建立了索引的
name字段来查询。InnoDB会先在name字段的索引树上找到对应的name值。 - 第二步:获取主键,回表查询。从
name索引的叶子节点中,获取到对应行的主键值。然后,拿着这个主键值,再回到聚簇索引树上,进行一次新的查找,最终定位到完整的数据行。这个“拿着主键再查一次”的过程,就是 “回表” (Back to Table)。
- 第一步:查非聚簇索引。比如,我们按一个建立了索引的
- 需要两步,可能涉及“回表”:
3. 数量与唯一性
- 聚簇索引:一张表只能有一个。因为数据行的物理存储顺序只能有一种。在InnoDB中,它会优先使用主键作为聚簇索引。
- 非聚簇索引:一张表可以有多个。我们可以为不同的列创建多个非聚簇索引。
4. 性能影响与优化
- 聚簇索引的优势:
- 范围查询非常高效。因为数据是按主键顺序物理存储的,所以查询一个范围的数据,只需要定位到范围的起点,然后向后顺序读取即可,I/O是连续的。
- 非聚簇索引的优化:覆盖索引 (Covering Index)
- 如果我们的查询语句(
SELECT子句)所需要的所有列,恰好都在这个非聚簇索引的叶子节点中包含了(比如,我SELECT name, id FROM users WHERE name = 'Alice',而name索引的叶子节点本身就存了name和主键id),那么数据库就不需要再进行回表操作了。它直接从索引中就能拿到所有需要的数据。这种情况下,查询效率会非常高。
- 如果我们的查询语句(
总结
| 特性 | 聚簇索引 (Clustered Index) | 非聚簇索引 (Non-clustered Index) |
|---|---|---|
| 与数据关系 | 索引即数据 | 索引与数据分离 |
| 叶子节点 | 存储完整数据行 | 存储索引键 + 主键值 |
| 数量 | 每张表仅一个 | 每张表可有多个 |
| 查询性能 | 主键查询/范围查询极快 | 等值查询快,但可能需要回表 |
| 存储引擎 | InnoDB使用 | InnoDB(二级索引), MyISAM(主/辅索引)都使用 |
在InnoDB中,聚簇索引是“数据本身”,是表的根基。而非聚簇索引则是为了加速特定列查询而建立的“辅助通道”,它通过牺牲一部分空间和维护成本,并可能引入“回表”开销,来换取查询性能的提升。
如果聚簇索引的数据更新,它的存储要不要变化?
面试官您好,更新非索引数据,存储结构大概率不变;而更新索引数据,特别是聚簇索引,存储结构则必然发生改变。
情况一:更新的列是“非索引列”
-
理想情况:原地更新 (In-Place Update)
- 在这种情况下,InnoDB会尝试进行 “原地更新”。它会直接定位到数据所在的那个数据页(Page),找到对应的行记录,然后直接修改那个非索引列的值。
- 存储结构变化吗? 在这种理想情况下,行的物理位置没有改变,B+树的结构也没有任何变化。所以,我们可以认为存储结构是不变的。这是最高效的更新方式。
-
特殊情况:页分裂 (Page Split)
- 这里有一个需要注意的特例。如果被更新的列是一个变长字段(比如
VARCHAR),并且更新后的值比原来的值长很多,导致当前数据页剩余的空间不足以容纳这个变大后的行记录。 - 这时,InnoDB就不得不执行一个成本更高的操作:页分裂。它会新申请一个数据页,并将当前页的部分行记录移动到新页上,以腾出空间。
- 在这种特殊情况下,即使我们只更新了非索引列,也可能导致数据行的物理位置发生改变,从而引起了存储结构的变化。但这相对少见。
- 这里有一个需要注意的特例。如果被更新的列是一个变长字段(比如
情况二:更新的列是“聚簇索引列”(通常是主键)
这是会导致剧烈变化的情况。
-
核心原理:InnoDB是索引组织表。这意味着,数据行的物理存储顺序,就是由聚簇索引的键值顺序决定的。
-
更新过程的本质:并非“更新”,而是“删除+插入”
- 当我们执行
UPDATE my_table SET id = 200 WHERE id = 100;时,InnoDB在底层执行的逻辑,并不是简单地把100改成200。 - 它的实际操作是:
- 删除 (Delete):首先,根据旧值
id = 100,在B+树中定位到该行记录,并将其删除(或标记为删除)。 - 插入 (Insert):然后,根据新值
id = 200,在B+树中找到新的、正确的位置,并将整行数据重新插入进去。
- 删除 (Delete):首先,根据旧值
- 当我们执行
-
存储结构的变化:
- 数据行物理迁移:这个“删除+插入”的过程,必然导致数据行从一个数据页移动到了另一个数据页。这是非常大的存储结构变化,涉及到大量的I/O操作。
- B+树结构调整:这次删除和插入,可能会导致旧数据页的合并或新数据页的分裂,需要维护B+树的平衡。
- 所有二级索引的全面更新(这是开销最大的部分):
- 在InnoDB中,所有的二级索引(非聚簇索引)的叶子节点,存储的并不是数据的物理地址,而是对应行的聚簇索引键(即主键值)。
- 当我们更新了主键值时(比如从100变为200),就意味着,这张表上所有二级索引中,凡是包含了这条记录的条目,都必须进行更新,把它们存储的旧主键值
100,全部改成新主键值200。 - 如果一张表有5个二级索引,那么一次主键的更新,可能会导致6次索引的写操作(1次聚簇索引的“删除+插入”,5次二级索引的更新),这是一个巨大的性能开销。
总结与最佳实践
| 更新类型 | 存储结构变化 | 核心操作 | 二级索引影响 | 性能 |
|---|---|---|---|---|
| 非索引列 | 大概率不变(除非页分裂) | 原地更新 | 无 | 高 |
| 聚簇索引列 | 必然改变 | 删除 + 插入 | 全部更新 | 极低 |
结论与最佳实践:
- 我们应该极力避免更新聚簇索引(主键)的列。
- 在设计表结构时,聚簇索引键应该选择绝对稳定、永不改变的列。这就是为什么无业务含义的、单调递增的
AUTO_INCREMENT整型ID,是作为主键的最佳选择。而像身份证号、手机号、用户名这类虽然唯一但可能发生变更的业务字段,更适合作为唯一二级索引,而不是主键。
什么字段适合当做主键?
面试官您好,选择哪个字段作为主键关系到数据库的性能、可维护性和未来的扩展性。
在InnoDB存储引擎下,因为主键就是聚簇索引,所以主键的选择尤其重要。我通常会遵循以下几个层层递进的原则来选择主键:
原则一:满足主键的基本约束 (这是底线)
- 唯一性 (Unique):主键值在整张表中必须是唯一的,不能有任何重复。
- 非空性 (Not Null):主键列绝对不允许有
NULL值。
这是数据库对主键的强制要求,是选型的第一道门槛。
原则二:从性能角度考量 (这是关键)
这是InnoDB下选择主键最重要的考量。
1. 必须是趋势递增的 (Monotonically Increasing)
- 为什么? 这主要是为了避免B+树索引的页分裂。InnoDB的数据是按主键顺序物理存储的。如果主键是单调递增的(比如自增ID),那么新的数据行总是会被追加到最后一个数据页的末尾。当一个页写满后,会平滑地创建一个新页继续写入。
- 反例:如果使用像UUID或者身份证号这样无序的、随机的值作为主键,那么每次插入新数据,都可能需要插入到B+树中间的某个位置。这极易导致目标数据页空间不足,从而引发页分裂——一个页被拆成两个,并可能导致后续一系列的节点调整。页分裂是一个非常耗费I/O和CPU的昂贵操作,会严重影响插入性能。
2. 字段长度尽可能短 (Short as Possible)
- 为什么? 因为在InnoDB中,所有的二级索引的叶子节点,存储的都是主键的值。
- 如果主键很长(比如一个很长的字符串),那么每一个二级索引的体积都会相应地变得非常庞大。这不仅会浪费大量的磁盘空间,更重要的是,当查询需要走二级索引时,需要加载到内存中的索引数据也会更多,导致查询性能下降。
- 因此,使用像
INT(4字节)或BIGINT(8字节)这样的整型,通常是比使用长字符串更优的选择。
原则三:从业务解耦角度考量 (这是最佳实践)
- 强烈不建议使用有业务含义的字段作为主键。
- 为什么? 业务是会变的。今天看似唯一的会员卡号、订单号、身份证号,都不能100%保证在未来的业务迭代中,不会出现需要变更、作废、甚至“一号多用”的情况。
- 一旦业务主键需要被修改,那将是一场灾难。因为修改主键,意味着需要更新聚簇索引本身,以及所有二级索引中对这个主键的引用,成本极高。
- 将主键与业务逻辑解耦,可以让我们的系统更健壮,更能适应未来的变化。
最终的理想选择
综合以上所有原则,一个理想的主键应该具备以下特质:
无业务含义、单调递增、短小精悍、唯一非空。
这完美地指向了我们的最佳选择:
- 单机环境下:使用数据库自带的
AUTO_INCREMENT自增ID(通常是BIGINT类型),这是最简单、最高效的方案。 - 分布式环境下:
AUTO_INCREMENT会因为多节点写入而产生冲突。此时,我们就需要采用分布式ID生成方案。- 常见的方案包括:
- 雪花算法 (Snowflake):Twitter开源的方案,生成的ID是64位的长整型,并且是趋势递增的,非常适合做分布式主键。
- 数据库号段模式 (Segment)。
- UUID:虽然简单,但因其无序且太长,不适合做主键,可以作为业务ID。
总结一下,在设计主键时,我会优先选择一个与业务无关的、单调递增的整型ID。在分布式系统中,则会采用类似雪花算法的方案来生成这样的ID。而那些有业务含义的唯一字段,我会为它们建立唯一二级索引,而不是把它们用作主键。
性别字段能加索引么?为啥?
面试官您好,技术上当然可以为性别字段加索引,但从性能和成本的角度看,绝大多数情况下,这都是一个非常糟糕的设计,我们应该极力避免。
1. 为什么不建议?—— 两个核心原因
这背后的原因主要有两点:
a. 索引的“区分度”或“选择性”太低 (Low Cardinality/Selectivity)
-
什么是区分度? 简单来说,就是一个字段的值,能把数据筛选到多“精细”的程度。
-
对于性别字段,它的值通常只有’男’、‘女’(或者1、0)几种。在一个拥有100万条记录的用户表中,
WHERE sex = '男'可能会筛选出大约50万条记录。 -
MySQL的查询优化器非常智能,当它发现某个索引的区分度太低,通过这个索引筛选出的数据量仍然非常大时(比如超过了全表的20%-30%),它就会认为走这个索引的成本,可能比直接全表扫描还要高,于是它会主动放弃使用这个索引。
b. 巨大的回表成本 (High Cost of Key Lookups)
这是更深层次、更致命的原因。假设优化器真的决定使用性别索引,来执行SELECT * FROM users WHERE sex = '男'。
- 工作流程:
- MySQL会先在性别索引(这是一个二级索引)的B+树上,找到所有值为“男”的条目。因为数据量大,这可能涉及到扫描大量的索引页。
- 对于找到的每一条记录(可能是50万条),二级索引的叶子节点只存储了主键ID。
- 为了获取完整的用户信息(
SELECT *),MySQL必须拿着这50万个主键ID,一次又一次地回到聚簇索引中去查找完整的数据行。这个过程就叫 “回表”。
- 成本分析:这可能意味着50万次随机I/O!这种大量的、离散的磁盘读写,其性能开销是极其巨大的,远远超过了直接进行一次全表扫描(通常是顺序I/O)的成本。
2. 创建这个索引的负面影响
所以,为性别字段创建索引,不仅在查询时很可能不会被使用,反而还会带来负面影响:
- 占用磁盘空间:每一个索引都需要额外的磁盘空间来存储。
- 降低写性能:每当我们对表进行
INSERT,UPDATE,DELETE操作时,不仅要维护聚簇索引,还需要额外地维护这个性别索引,这会降低写的性能。
3. 特例:什么时候可以考虑?—— 覆盖索引
这里有一个例外情况。如果我们的查询,只涉及到索引本身包含的列,那么即使是性别字段,加索引也可能是有益的。
- 场景:假设我们有一个查询
SELECT id, sex FROM users WHERE sex = '男'。 - 如果我们建立一个联合索引
idx_sex_id(sex, id):- 此时,查询需要的所有信息(
id和sex)都已经存在于这个联合索引的叶子节点上了。 - MySQL在扫描完这个联合索引后,无需再进行回表操作,可以直接返回结果。这种情况就叫 “覆盖索引”(Covering Index)。
- 在这种特定情况下,由于避免了大量的回表I/O,即使区分度低,走索引也可能比全表扫描要快。
- 此时,查询需要的所有信息(
总结
除了极少数能通过“覆盖索引”来优化的特定查询场景外,为像性别、状态、类型等区分度极低的字段创建索引,是一个投入远大于产出、弊大于利的设计。它不仅无法有效提升查询性能,反而会浪费空间并拖慢写操作。
表中十个字段,你主键用自增ID还是UUID,为什么?
面试官您好,我会毫不犹豫地选择使用自增ID作为主键。
虽然UUID在某些场景下(比如需要一个全局唯一且不连续的标识符)有其用武之地,但如果把它用作InnoDB表的主键,那将是一场性能灾难。
要理解为什么,我们需要深入到InnoDB的聚簇索引存储模型。
1. 自增ID为什么是最佳选择?—— 顺序写入的巨大优势
- 在InnoDB中,主键就是聚簇索引,它决定了数据行在磁盘上的物理存储顺序。
- 当我们使用自增ID作为主键时,新的数据行总是被顺序地追加(Append)到表(B+树)的最后一个数据页的末尾。
- 这种写入方式带来了巨大的好处:
- 顺序I/O:写入操作是连续的,性能极高。
- 页填充率高:数据页会被紧凑地填满,空间利用率高。
- 避免页分裂:只有当最后一个数据页被写满时,才会平滑地创建一个新页继续写入,几乎不会发生代价高昂的“页分裂”操作。
2. UUID为什么是灾难性的选择?—— 随机写入的“三宗罪”
UUID的核心问题在于它的无序性和随机性。当它作为主键时,会导致以下三大性能问题:
-
第一宗罪:大量的随机I/O
- 因为UUID是随机的,所以新插入的数据行的主键值,可能会落在B+树的任何一个位置。
- 这意味着,InnoDB为了找到这个“合适的位置”,很可能需要从磁盘中随机地读取一个或多个数据页到内存中。这个过程充满了随机I/O,其性能远低于自增ID带来的顺序I/O。
-
第二宗罪:频繁的页分裂 (Page Split)
- 这是最致命的问题。当InnoDB好不容易找到了要插入的数据页,却发现这个页的空间已经满了,无法再容纳新的数据行。
- 此时,InnoDB就必须执行页分裂:
- 创建一个新的数据页。
- 将原数据页中的一部分行记录,移动到这个新页中。
- 更新B+树的索引指针。
- 页分裂是一个非常昂贵的操作,它涉及到大量的数据移动和索引维护,会严重拖慢插入性能,并可能导致长时间的锁等待。
-
第三宗罪:数据碎片与空间浪费
- 频繁的页分裂,会导致数据页的填充率非常低。比如,一个16KB的页,可能只存了很少的数据就被拆分了。
- 这导致整张表在物理上变得稀疏、不紧凑,充满了大量的内部碎片,极大地浪费了磁盘空间。
补充一点:对二级索引的影响
- UUID通常是36个字符的字符串,而自增ID是4字节的
INT或8字节的BIGINT。 - 在InnoDB中,所有二级索引的叶子节点都存储了主键的值。
- 如果使用长UUID做主键,会导致每一个二级索引都变得异常臃肿,不仅浪费了更多的磁盘空间,还降低了二级索引的查询效率。
结论与分布式场景考量
- 单机环境:永远选择自增ID。
- 分布式环境:
- 在分布式系统中,普通的自增ID会因为节点间的冲突而失效。
- 此时,我们需要的不是UUID,而是一个 “分布式环境下的、趋势递增的、全局唯一的ID”。
- 这就是为什么业界会采用雪花算法(Snowflake)或类似的方案。雪花算法生成的64位ID,既能保证全局唯一,又因为其高位是时间戳,从而保证了整体的趋势递增,完美地契合了InnoDB对主键的要求。
所以,最终的结论是:无论在什么环境下,InnoDB的主键都应该追求趋势递增。在单机下用AUTO_INCREMENT,在分布式下用雪花算法这类方案,而UUID则应该被用作一个普通的、具有业务唯一性的非主键字段。
MySQL 中的索引是怎么实现的?
面试官您好,MySQL的索引实现,其核心是选择了一种非常适合磁盘存储的数据结构。在最常用的InnoDB存储引擎中,索引就是通过B+树来实现的。
要理解它的实现,我们不仅要看B+树是什么,更要明白为什么是它。
1. B+树的结构特征
B+树的核心结构特征包括:
- 它是一种多路平衡查找树:每个节点可以拥有多个子节点,这使得树的高度非常低。
- 非叶子节点只存索引键,不存数据:这是它与B树的一个关键区别。非叶子节点只作为索引的索引,这使得它们可以存储更多的键值,从而进一步降低树的高度。
- 所有的数据都存储在叶子节点:在InnoDB中,聚簇索引的叶子节点存储的是完整的行数据,二级索引的叶子节点存储的是主键值。
- 所有叶子节点形成一个有序的双向链表:所有叶子节点通过指针相互连接。
2. 为什么数据库索引偏爱B+树?—— 核心是减少磁盘I/O
数据库的数据和索引,绝大部分都存储在磁盘上。而磁盘I/O的成本,相比内存操作,要高出好几个数量级。因此,索引设计的核心目标,就是尽可能地减少磁盘I/O的次数。
B+树的结构,正是为了这个目标而精心设计的。我们可以通过与其他数据结构的对比来看它的优势:
-
为什么不用二叉查找树(或红黑树)?
- 在数据量大时,二叉树的高度会非常深。查找一个数据,可能需要进行很多次磁盘I/O(每访问一个节点,都可能是一次I/O),性能无法接受。
-
为什么不用哈希表?
- 哈希表在进行等值查询时(比如
WHERE name = 'Alice'),速度极快,理论上时间复杂度是O(1)。 - 但它的致命缺点是无法进行范围查询。因为哈希后的数据是无序的,你无法高效地查找
id > 100这样的数据。而范围查询在数据库中是极其常见的需求。
- 哈希表在进行等值查询时(比如
-
B+树的巨大优势:
- 极低的高度,极少的I/O:由于B+树是“矮胖”的(多叉),并且非叶子节点不存数据,使得一个节点(通常对应一个磁盘页,如16KB)可以容纳成百上千个索引键。对于一个千万级别数据的表,其B+树的高度通常也只有3到4层。这意味着,查找任何一条数据,最多只需要3到4次磁盘I/O,性能非常高。
- 完美支持范围查询:这是B+树的另一个杀手锏。由于叶子节点本身是有序的,并且通过双向链表连接,使得进行范围查询变得极其高效。比如查找
id BETWEEN 100 AND 200,只需要在B+树中定位到id=100的叶子节点,然后沿着这个双向链表向后遍历,直到id > 200为止,无需再回溯树的上层结构。 - 更稳定的查询性能:由于所有数据都存放在叶子节点,所以任何一次查询,其I/O路径的长度都是基本相同的(都需要从根走到叶),这使得查询性能非常稳定。
总结
MySQL中的索引,特别是InnoDB引擎,是通过B+树这种数据结构来实现的。它通过保持树的低高度来最小化磁盘I/O次数,同时利用叶子节点的有序链表结构来高效地支持范围查询,是兼顾了等值查询和范围查询性能的最佳选择。
查询数据时,到了B+树的叶子节点,之后的查找数据是如何做?
面试官您好,通过B+树的层层索引,我们最终能快速定位到数据所在的叶子节点。但这个叶子节点,并不是一条数据记录,而是一个数据页(Page),在InnoDB中通常大小为16KB。
所以,当查询定位到这个目标数据页,并将其加载到内存后,接下来的查找过程,是在这个16KB的内存块内部进行的。这个页内部的查找,同样是一个非常高效的过程。
1. 数据页的内部结构
一个InnoDB的数据页,为了能快速地在内部查找数据,它被设计成了一个非常有条理的结构,主要包含以下几个部分:
-
行记录 (User Records):
- 我们真正的数据行,就是存放在这个区域。
- 重要的是,在一个数据页内部,这些行记录是按照主键的顺序,以单向链表的形式串联起来的。
-
页目录 (Page Directory) / 槽 (Slots):
- 这是实现页内快速查找的关键。
- InnoDB会把页内的所有行记录,分成若干个组。每个组的第一条记录会被特殊标记。
- 页目录就是由这些“组长”记录的地址偏移量组成的,它像一个稀疏的索引。这些地址偏移量(槽)在页目录中是有序的。
-
其他部分:还包括文件头、页头(包含了指向页目录的指针等)、文件尾等元数据信息。
2. 页内查找的“二分+遍历”过程
当我们需要在已经加载到内存的这个数据页里,查找一个特定的主键值时(比如id=50),其过程大致如下:
-
通过页目录进行二分查找:
- 首先,InnoDB会利用页目录这个有序的“稀疏索引”,进行一次二分查找。
- 比如,页目录里有槽
[slot1: id=10, slot2: id=40, slot3: id=80]。我们要找id=50,二分查找会快速定位到,50应该在slot2(起始id=40)和slot3(起始id=80)之间。因此,它确定了目标记录一定在slot2所代表的那个记录组里。
-
在记录组内进行遍历查找:
- 定位到具体的记录组之后(比如
slot2指向的那个以id=40开头的记录链表),由于一个组内的记录数量通常很少(InnoDB控制在4到8条左右),此时就不需要再用复杂的算法了。 - InnoDB会直接从这个组的“组长”记录开始,顺着行记录之间的单向链表,逐个遍历,直到找到主键值匹配的那一条记录。
- 定位到具体的记录组之后(比如
总结一下,从B+树的叶子节点(数据页)内部查找数据的过程,是一个 “二分查找 + 遍历” 的组合拳:
- 先利用页目录(Page Directory)进行高效的二分查找,快速锁定目标数据所在的大致范围(记录组)。
- 再在这个很小的记录组内,通过遍历链表的方式,精准地找到目标行。
这个设计,保证了即使在单个数据页内存储了多条记录,查找过程依然是非常高效的。
说说 B+ 树和 B 树的区别
面试官您好,B树和B+树都是非常优秀的多路平衡查找树,它们的核心区别,主要体现现在以下三个方面:
1. 数据存储位置不同
- B树:它的设计更像是“遍地开花”。每个节点(无论是叶子节点还是非叶子节点)都既可以存储索引键,也可以存储数据。
- B+树:它的设计则是“泾渭分明”。只有叶子节点才存储真正的数据(或指向数据的指针)。所有的非叶子节点都只存储索引键,它们纯粹是作为“路标”存在的,用于引导查询。
2. 叶子节点的连接方式不同
- B树:各个叶子节点之间是相互独立的,没有指针连接。
- B+树:所有的叶子节点通过双向指针相互连接,形成一个有序的双向链表。
3. 查询性能的稳定性与效率不同
- B树:由于数据可能存在于任何一个节点,所以查询的性能是不稳定的。运气好的话,可能在根节点或上层非叶子节点就直接命中了数据,查询路径很短;运气不好的话,则需要一直查到最底层的叶子节点。
- B+树:由于所有数据都必须在叶子节点才能找到,所以任何一次数据查询,其IO路径的长度都是固定的(都需要从根走到叶),这使得查询性能非常稳定和可预测。
这些区别带来了什么关键影响?—— 为什么数据库更爱B+树
这些设计上的差异,导致了B+树在数据库索引这个特定场景下,具有B树无法比拟的优势:
-
优势一:更低的树高,更少的磁盘I/O
- 由于B+树的非叶子节点不存储数据,只存储索引键,这意味着在同样大小的一个磁盘页(比如16KB)中,B+树的非叶子节点可以容纳更多的键值和指针。
- 更多的键值意味着更大的 “扇出”(fan-out),也就是一个节点能拥有的子节点更多。
- 更大的扇出,直接导致了B+树的高度比B树更低、更“矮胖”。对于一个千万级数据的表,B+树的高度通常只有3-4层。
- 在数据库中,每一次节点访问都可能对应一次磁盘I/O。更低的树高,就意味着更少的磁盘I/O次数,这是B+树性能胜出的最核心原因。
-
优势二:对范围查询的完美支持
- 这是B+树的另一个“杀手锏”。由于它的叶子节点形成了一个有序的双向链表,所以进行范围查询(比如
WHERE id > 100)变得极其高效。 - 数据库只需要在B+树中定位到第一个满足条件的叶子节点,然后就可以沿着这个链表顺序地向后遍历,直到范围结束。
- 而B树要实现范围查询,则需要进行复杂的中序遍历,可能需要频繁地在不同层级的节点之间来回跳转,效率远低于B+树。
- 这是B+树的另一个“杀手锏”。由于它的叶子节点形成了一个有序的双向链表,所以进行范围查询(比如
总结
| 特性 | B树 (B-Tree) | B+树 (B+ Tree) |
|---|---|---|
| 数据存储 | 所有节点都存Key+Data | 只有叶子节点存Data,非叶子节点只存Key |
| 叶子节点 | 相互独立 | 形成有序双向链表 |
| 查询性能 | 不稳定,命中即返回 | 稳定,必须查到叶子节点 |
| 磁盘I/O | 相对较高 (树更高) | 相对较低 (树更矮胖) |
| 范围查询 | 差 (需中序遍历) | 极佳 (可利用叶子链表) |
虽然B树在单次“命中即走”的查询上可能更快,但B+树通过牺牲非叶子节点的存储能力,换来了更低的树高和更强大的范围查询能力。这种设计,完美地契合了数据库 “减少磁盘I/O” 和 “高效处理范围扫描” 这两大核心需求,因此成为了数据库索引技术的事实标准。
为什么 B+ 树每一层代表一次 I/O 操作
面试官您好,这是一个在分析数据库索引性能时非常核心且合理的简化模型,可以从数据存储的位置和I/O操作的本质这两个角度来看。
1. 数据存储在哪里?—— 绝大部分在磁盘
- 首先,对于一个大型数据库来说,无论是索引本身还是表数据,其体积都远远超过了计算机的物理内存大小。因此,B+树的绝大部分节点,在任意时刻,都是存储在磁盘上的。
- 内存(比如InnoDB的Buffer Pool)的作用,是作为磁盘数据的一个高速缓存。它会缓存那些被频繁访问的B+树节点(数据页),以避免每次都从磁盘读取。
2. I/O操作的本质是什么?
- 当我们需要从B+树中查找一个数据时,我们的查询过程是从根节点开始,逐层向下访问,直到找到叶子节点。
- 核心问题在于:在遍历树的每一层,当我们准备从一个父节点,跳转到它的某一个子节点时,这个子节点的数据,大概率是不在内存缓存中的。
- 为什么不在内存中? 因为B+树的“扇出”非常大,一个父节点可能关联着成百上千个子节点。除非这个父节点及其所有子节点都恰好是热点数据,否则内存缓存(大小是有限的)不可能同时缓存住这么多的节点。
- 因此,每当我们需要访问树的一个新层级的节点时,就极有可能需要从磁盘把它加载到内存中。这个“从磁盘加载数据到内存”的操作,就是一次I/O操作。
3. 将两者等价起来
基于以上两点,我们的推论就变得很自然了:
- B+树的查询,是一个从根节点到叶子节点的、逐层向下的访问过程。
- 树的每一层的节点,都存储在不同的磁盘块上。
- 在理想的最坏情况下(即每次要访问的节点都不在内存缓存中),访问树的一层,就需要进行一次磁盘I/O,来加载该层的目标节点。
- 所以,一个高度为
h的B+树,在最坏情况下,就需要进行h次磁盘I/O才能定位到最终的叶子节点。
因此,“B+树的一层代表一次I/O操作”,这个说法是对B+树查询过程中I/O开销的一个非常直观且合理的近似估算。
总结
B+树之所以性能高,就是因为它通过“矮胖”的结构,使得树的高度极低(通常只有3-4层)。
这意味着,在绝大多数情况下,无论我们的表有多大(千万甚至上亿级别),定位到任何一条数据,都只需要3到4次磁盘I/O操作。这就是B+树能够高效地从海量数据中进行快速查找的核心原理。
为什么 MySQL 不用跳表?
面试官您好,根据数据结构必须与其所服务的存储介质相匹配,B+树是为磁盘等慢速存储设备而生的,而跳表则是为内存等快速存储设备而设计的。
下面我来详细解释一下,为什么在MySQL InnoDB这种以磁盘为主要存储的场景下,B+树是更优的选择。
1. 核心差异:I/O效率与“高度”控制
对于磁盘存储来说,减少磁盘I/O次数是性能优化的第一要务。而I/O次数,直接取决于索引结构的高度或查找路径的长度。
-
B+树:为“矮胖”而生,扇出巨大
- B+树的一个核心设计是它的节点(通常是一个磁盘页,如16KB)可以存储成百上千个索引键。这意味着它的 “扇出(fan-out)” 非常大。
- 巨大的扇出,使得B+树的高度极低。一个存储了千万甚至上亿条记录的B+树,其高度通常也只有3到4层。这意味着,查找任何一条数据,最多只需要3到4次磁盘I/o。
-
跳表:为“轻快”而生,本质是链表
- 跳表的基础结构是一个有序链表。它的多层索引,是通过在链表节点上“搭建立交桥”来实现的。
- 问题来了:在磁盘上,一个链表节点的物理存储可能是高度离散的。从一个节点跳到下一个节点,很可能就是一次随机的磁盘I/O。
- 为了在千万级数据中找到一个值,跳表需要“跳跃”的次数(即I/O次数)会远超B+树的3-4次。它的“查找路径长度”,相比B+树要大得多。
2. 范围查询的效率
- B+树:它的叶子节点形成了一个有序的双向链表,并且这些叶子节点在物理存储上通常是相对连续的。这使得范围查询可以利用磁盘的顺序I/O,效率极高。
- 跳表:虽然跳表也可以进行范围查询(在最底层链表上遍历),但由于节点物理存储的离散性,这个遍历过程可能会退化成大量的随机I/O,性能远不如B+树。
3. 跳表的“主场”在哪里?—— 内存数据库
说了这么多,并不是说跳表不好,它只是“选错了赛道”。跳表在内存数据库或内存缓存的场景中,大放异彩。
- 典型代表:Redis
- Redis的有序集合(Sorted Set)的底层实现之一就是跳表。
- 为什么在内存中跳表很优秀?
- 在内存中,节点的随机访问成本极低,跳表“指针跳跃”的劣势不复存在。
- 相比于在内存中实现复杂、需要频繁进行平衡操作的平衡二叉树(如红黑树),跳表的实现更简单、代码更清晰。
- 跳表的插入和删除操作,只涉及到局部节点的修改,无需像平衡树那样进行全局的旋转调整,因此在并发场景下,其锁的粒度可以更小,并发性能更好。
总结
| 对比维度 | B+树 | 跳表 |
|---|---|---|
| 设计目标 | 最小化磁盘I/O | 在内存中实现高效查找与并发 |
| I/O模型 | 利用磁盘页,扇出大,顺序I/O友好 | 节点离散,更适应随机内存访问,磁盘上是随机I/O |
| 最佳舞台 | 磁盘数据库索引 (如MySQL) | 内存数据库/缓存 (如Redis) |
这两种数据结构,是“在不同的约束条件下,寻找最优解”的典范。MySQL选择B+树,是因为它能通过巨大的节点扇出,构建一个极低高度的索引,从而最大程度地减少了昂贵的磁盘I/O次数,并且高效地支持范围查询。
联合索引的实现原理是什么?
面试官您好,联合索引(也叫复合索引),它的底层实现原理,是B+树。
它的特别之处,不在于数据结构本身,而在于B+树中索引键的排序和比较方式。
1. 核心原理:多字段的“字典序”排序
我们可以把联合索引想象成查英文字典的过程,字典的排序规则就是一种典型的“联合索引”排序。
-
假设我们有一个联合索引
idx_name_age,它包含了(name, age)这两个字段。 -
B+树的排序规则:
- 在构建B+树时,它会首先按照第一个字段
name进行字典序排序。 - 在
name字段相同的情况下,它会再按照第二个字段age进行排序。 - 最终,所有的索引条目在B+树的叶子节点上,会形成一个严格遵循
(name, age)这个整体顺序的有序链表。
- 在构建B+树时,它会首先按照第一个字段
2. 一个生动的例子
假设我们有以下数据:
| name | age |
|---|---|
| Alice | 25 |
| Bob | 30 |
| Alice | 30 |
| Bob | 28 |
那么,在idx_name_age这个联合索引的B+树叶子节点上,它们的存储顺序会是:
(Alice, 25)(Alice, 30)(name相同,按age=25, 30排序)(Bob, 28)(name='Bob’排在’Alice’之后)(Bob, 30)(name相同,按age=28, 30排序)
3. 这个原理如何引出“最左前缀原则”?
理解了这种“字典序”的排序原理,我们就能非常自然地推导出联合索引最重要的使用规则——最左前缀原则 (Leftmost Prefix Rule)。
-
为什么查询必须从“最左边”开始?
- 因为整个索引是先按
name排序的。如果你直接用WHERE age = 30来查询,MySQL根本无法利用索引快速定位。 - 所以,查询条件必须包含联合索引的最左边的列(即
name),索引才可能被高效使用。
- 因为整个索引是先按
-
为什么不能“跳过”中间的列?
- 如果我们有一个
(name, age, position)的索引,但查询条件是WHERE name = 'Alice' AND position = 'Manager'。 - MySQL只能利用到
name这一部分索引。 - 因为在
name='Alice’的这个“大分组”里,数据是先按age排序的,此时position是相对无序的。MySQL无法在age条件未知的情况下,再去快速定位position。它只能找到所有name='Alice’的记录,然后再逐条地去过滤position=‘Manager’。
- 如果我们有一个
-
哪些查询可以用到
(name, age, position)这个索引?WHERE name = ?-> 可以用 (name)WHERE name = ? AND age = ?-> 可以用 (name, age)WHERE name = ? AND age = ? AND position = ?-> 可以用 (name, age, position)WHERE age = ?-> 不可以用WHERE position = ?-> 不可以用WHERE name = ? AND position = ?-> 只能用上name部分
总结一下,联合索引的实现原理,就是在B+树中,按照创建索引时指定的列的顺序,进行多重、逐级的字典序排序。这个排序规则,决定了我们必须遵循“最左前缀原则”来进行查询,才能最大限度地发挥出联合索引的威力。
创建联合索引时需要注意什么?
面试官您好,联合索引的好坏会直接影响到SQL的查询性能。在设计联合索引时,我通常会遵循以下三个核心原则来决定字段的顺序:
原则一:区分度(或称选择性)最高的字段放最左边
这是最重要、最基本的原则。
- 为什么?
- 区分度高,意味着这个字段的值重复率低。把高区分度的字段放在最前面,可以使得索引在第一层筛选时,就能迅速地排除掉大量无关的数据行,让查询范围快速收窄。
- 如何判断区分度?
- 我们可以通过
SELECT COUNT(DISTINCT column_name) / COUNT(*) FROM table_name;来计算一个字段的区分度,这个值越接近1,说明区分度越高。
- 我们可以通过
原则二:查询最频繁,且将范围查询字段放后面
在满足区分度原则的基础上,我们还需要考虑业务的查询模式。
- 最左前缀原则:
- 这源于联合索引的“最左前缀原则”。查询必须从索引的最左边的列开始,并且不能跳过中间的列,索引才能被充分利用。
- 因此,我们应该把那些在
WHERE子句中最常被用作查询条件的字段,放在联合索引的更靠前的位置。
- 范围查询的考量:
- 一个联合索引中,最多只有一个字段能有效地使用范围查询(如
>,<,BETWEEN,LIKE '...%')。一旦某个字段使用了范围查询,它右边的所有字段就都无法再利用索引进行快速定位了。 - 因此,我们应该把需要进行范围查询的字段,尽可能地放在联合索引的末尾,而把用于等值查询的字段放在前面。
- 一个联合索引中,最多只有一个字段能有效地使用范围查询(如
原则三:字段长度尽可能短
这是一个从空间和性能成本角度的考量。
- 为什么?
- 一个联合索引的所有字段,都会被存储在B+树的索引页中。字段越短,一个索引页能容纳的索引条目就越多,这能让索引树的高度更低,从而减少磁盘I/O。
一个综合决策案例
假设我们有一个订单表orders,最常见的查询是“查询某个客户最近一周内,已支付的订单”。
涉及的字段是 customer_id (客户ID, BIGINT), create_time (创建时间, DATETIME), order_status (订单状态, TINYINT)。
- 分析:
- 区分度:
customer_id的区分度最高,create_time次之,order_status最低。 - 查询模式:
customer_id和order_status是等值查询,create_time是范围查询。
- 区分度:
- 最佳索引顺序:
- 综合考虑,最佳的联合索引顺序是
(customer_id, order_status, create_time)。 - 理由:
- 将区分度最高、且是等值查询的
customer_id放在最左边。 - 将区分度较低、但也是等值查询的
order_status放在中间。 - 将范围查询的
create_time放在最后,以确保前面的等值查询能最大限度地利用索引。
- 将区分度最高、且是等值查询的
- 综合考虑,最佳的联合索引顺序是
通过遵循这三大原则,我们就能设计出最高效、最合理的联合索引。
联合索引(A, B, C),现在有个执行语句是A = XXX and C < XXX,索引怎么走
面试官您好,对于一个在(A, B, C)三个字段上建立的联合索引,当执行WHERE A = xxx AND C < xxx这个查询时,MySQL只会利用到该联合索引的A这一部分。
原因分析:最左前缀原则
这背后的根本原因,就是联合索引的最左前缀原则。
- 索引的排序方式:联合索引
(A, B, C)在B+树中的排序规则是:- 首先,严格按照字段A的值进行排序。
- 在字段A的值相等的情况下,再按照字段B的值进行排序。
- 在字段A和B的值都相等的情况下,最后才按照字段C的值进行排序。
- 查询过程的匹配:
- 当我们的查询条件是
A = xxx时,MySQL可以利用索引,快速地定位到所有A等于xxx的索引记录。这就像在字典里,快速翻到“B”字母开头的部分。 - 但是,我们的查询条件跳过了中间的字段B,直接对字段C进行了范围查询 (
C < xxx)。 - 在所有
A等于xxx的这个“大分组”内部,数据是先按照B来排序的,此时,C字段在这个范围内是相对无序的。 - 因此,MySQL无法在B值未知的情况下,继续利用索引来快速地查找满足
C < xxx的记录。
- 当我们的查询条件是
MySQL的实际执行过程
- 索引查找 (Index Seek):MySQL会利用联合索引,高效地找到所有
A = xxx的记录。这个过程速度很快。 - 回表与过滤 (Table Fetch & Filtering):
- 对于找到的每一条
A = xxx的索引记录,MySQL会拿到它的主键ID,然后回表查询完整的数据行。 - 在拿到完整数据行后,再在内存中对
C字段的值进行过滤,判断其是否满足C < xxx这个条件。
- 对于找到的每一条
总结
| 查询条件 | 索引利用情况 |
|---|---|
A = ? | 利用 A |
A = ? AND B = ? | 利用 A, B |
A = ? AND B = ? AND C = ? | 利用 A, B, C |
A = ? AND C = ? | 只利用 A |
B = ? AND C = ? | 完全无法利用索引 |
因此,对于WHERE A = xxx AND C < xxx这个查询,联合索引(A, B, C)只发挥了一部分作用。为了让这个查询更高效,我们应该考虑建立一个新的联合索引 (A, C)。在这个新索引下,查询就可以完整地利用到A和C两部分,甚至可能实现索引覆盖,从而避免回表,性能会得到巨大提升。
联合索引(A, B, C),查询条件 where B > xxx and A = x 会生效吗
面试官您好,对于一个在(A, B, C)三个字段上建立的联合索引,当执行WHERE B > xxx AND A = xxx这个查询时,这个联合索引是会生效的。
原因分析:MySQL查询优化器的功劳
虽然我们的WHERE子句中,B写在了A的前面,看起来似乎违反了“最左前缀原则”,但现代的MySQL查询优化器非常智能,它不会机械地按照我们书写的顺序来执行。
-
查询重写 (Query Rewriting):
- 在SQL解析和生成执行计划的阶段,查询优化器会分析
WHERE子句中的所有条件。 - 它会发现,
AND连接的条件,其执行顺序是可以任意调换的,最终的过滤结果完全一样。 - 优化器会进一步检查可用的索引。它发现存在一个
(A, B, C)的联合索引。为了能最大限度地利用这个索引,它会自动地、在逻辑上将我们的查询条件重写为:WHERE A = xxx AND B > xxx
- 在SQL解析和生成执行计划的阶段,查询优化器会分析
-
索引的实际利用情况:
- 经过优化器重写后,查询就完美地符合了最左前缀原则。
- MySQL会利用这个联合索引:
- 首先,通过
A = xxx这个等值查询,快速地在B+树中定位到一个起始范围。 - 然后,在这个已经大大缩小的范围内,继续利用索引的有序性,去查找所有满足
B > xxx的记录。这是一个范围查询。
- 首先,通过
一个重要的细节:范围查询的影响
- 虽然索引对
A和B都生效了,但需要注意的是,一旦联合索引中的某个字段被用于范围查询(如>、<、BETWEEN、LIKE '...%'),那么它右边的所有字段就无法再利用索引进行快速定位了。 - 在这个例子中,因为
B字段进行了范围查询 (B > xxx),所以联合索引的第三个字段C,就无法再被这个查询所利用了。
总结
| 原始查询条件 | 优化器重写后的逻辑条件 | 索引利用情况 |
|---|---|---|
B > xxx AND A = xxx | A = xxx AND B > xxx | 生效。利用了索引的 A部分(用于等值定位)和 B部分(用于范围扫描)。C部分不生效。 |
因此,结论是:得益于MySQL查询优化器的智能重排,即使WHERE子句中的字段顺序与联合索引的顺序不一致,只要满足最左前缀原则的核心字段都存在,索引依然会生效。但我们需要注意范围查询对后续索引字段的“截断效应”。
索引失效有哪些?
面试官您好,其根本原因大多是 “查询条件破坏了B+树的有序性,使得优化器无法高效地利用索引进行查找”。
1. 违反最左前缀原则
- 情况:这是针对联合索引最常见的失效场景。查询没有从联合索引的最左边的列开始,或者跳过了中间的列。
- 示例:对于联合索引
(a, b, c),WHERE b = ?或WHERE a = ? AND c = ?都会导致索引部分或完全失效。 - 为什么失效? 因为联合索引在B+树中是按
(a, b, c)的顺序进行“字典序”排序的。不从a开始,或者跳过了b,就破坏了这个有序结构,MySQL无法进行快速定位。
2. 在索引列上进行计算、函数或类型转换
这是另一个非常大的类别,本质上都是对索引列进行了“加工”,导致其无法直接与B+树中的原始值进行比较。
-
情况一:对索引列使用函数
- 示例:
WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2023-10-27' - 为什么失效? 索引中存储的是完整的
create_time值,而不是函数计算后的结果。MySQL无法预知所有create_time经过函数计算后的值是什么,因此只能放弃索引,进行全表扫描。
- 示例:
-
情况二:对索引列进行表达式计算
- 示例:
WHERE age - 1 = 20 - 为什么失效? 同理,索引中存的是
age的原始值。正确的、能走索引的写法应该是将计算移到查询条件的值上:WHERE age = 21。
- 示例:
-
情况三:隐式类型转换
- 示例:假设
phone字段是VARCHAR类型并建立了索引,查询时写成WHERE phone = 12345678901(数字类型)。 - 为什么失效? MySQL为了进行比较,会自动地将索引列
phone的每一行值,都通过CAST()函数转换为数字类型,再与查询条件比较。这等同于在索引列上使用了函数,导致索引失效。正确的写法是WHERE phone = '12345678901'。
- 示例:假设
3. 使用LIKE进行左模糊或全模糊查询
- 情况:
WHERE name LIKE '%Li'或WHERE name LIKE '%Li%'。 - 为什么失效? B+树的有序性是从左到右的。当查询条件以通配符
%开头时,MySQL不知道索引的前缀是什么,就无法利用B+树进行快速定位,只能退化为全表扫描。 - 如何优化? 应该尽可能地使用右模糊查询
WHERE name LIKE 'Li%',这种写法可以有效地利用到索引。
4. OR连接的条件中包含非索引列
- 情况:
WHERE indexed_col = ? OR unindexed_col = ?。 - 为什么失效? MySQL优化器会认为,既然
OR条件的一部分无论如何都需要进行全表扫描,那么为了避免两次查询(一次索引查找,一次全表扫描)再合并结果的复杂性,还不如直接对整个查询进行一次全表扫描来得简单高效。
5. 其他情况
IS NULL和IS NOT NULL:在早期的MySQL版本中,IS NULL通常不走索引。但在现代版本中,优化器已经能很好地处理IS NULL,通常可以走索引。而IS NOT NULL通常还是不走索引,因为它筛选出的数据量可能太大。!=或<>:不等于操作符通常也无法使用索引,因为它不符合B+树“查找某个范围”的模式,筛选的记录数可能过多。
总结一下,要保证索引有效,核心就是要让查询条件能够直接、干净地利用上B+树的有序性。任何对索引列的“加工处理”,或者破坏了最左前缀原则的查询,都可能导致优化器放弃使用索引,从而引发性能问题。
什么是覆盖索引?
面试官您好,覆盖索引(Covering Index)并不是一种特定的索引类型,比如像主键索引、唯一索引那样的分类。它更多的是一种查询优化的状态或一种理想的查询场景。
1. 核心定义
一句话概括:当一个查询语句,它所需要查询的所有字段,都能从一个二级索引的B+树中直接获取,而无需再回到聚簇索引中去查找完整的行数据时,我们就称这个查询命中了“覆盖索引”。
简单来说,就是“索引覆盖了所有查询的字段”。
2. 如何实现覆盖索引?
覆盖索引的实现,依赖于我们精心设计的联合索引。
-
场景:假设我们有一张用户表
users,经常需要根据用户名name来查询用户的年龄age。SELECT age FROM users WHERE name = 'Alice'; -
没有优化的情况:如果我们只在
name字段上有一个单列索引idx_name。- MySQL会走
idx_name索引,找到name='Alice'的记录。 - 从索引中,它只能拿到
name和主键id。 - 为了获取
age字段,它必须拿着id去回表,查询聚簇索引。
- MySQL会走
-
实现覆盖索引的优化:我们创建一个联合索引
idx_name_age(name, age)。ALTER TABLE users ADD INDEX idx_name_age (name, age);现在,再执行同样的查询
SELECT age, name FROM users WHERE name = 'Alice';:- MySQL同样会走
idx_name_age这个联合索引。 - 在索引的B+树叶子节点上,它不仅能找到
name,还能直接找到age这个字段的值。 - 查询需要的所有信息都已经满足,MySQL无需再进行回表操作,可以直接返回结果。
- MySQL同样会走
4. 覆盖索引的好处
- 极大减少I/O:这是最核心的好处。它避免了大量的回表操作,特别是当
WHERE条件筛选出的结果集很大时,可以减少成千上万次的随机I/O,性能提升是巨大的。 - 二级索引通常更小:二级索引的B+树通常比聚簇索引的B+树要小得多。只扫描二级索引,意味着需要加载到内存中的数据量更少,效率更高。
如何判断是否命中了覆盖索引?
我们可以通过EXPLAIN命令来分析SQL的执行计划。如果Extra列中显示为 Using index,就明确地表示这个查询成功地命中了覆盖索引,它只访问了索引树就获取了所有需要的数据,性能是最佳的。
总结一下,覆盖索引是MySQL中一种极其重要的查询优化手段。通过合理地设计联合索引,让它能够“覆盖”掉我们常用查询所需的所有字段,就可以有效地避免回表,从而大幅提升查询性能。
如果一个列即是单列索引,又是联合索引,单独查它的话先走哪个?
MySQL优化器会估算走每个索引的成本,并最终选择那个它认为成本最低的索引。在绝大多数情况下,如果联合索引能够覆盖查询或者更精确地缩小扫描范围,优化器会优先选择联合索引。
我们来分两种典型的查询场景来分析:
场景一:查询条件只涉及这个公共列
-
索引情况:单列索引
idx_a (a),联合索引idx_abc (a, b, c)。 -
查询SQL:
SELECT * FROM my_table WHERE a = 'some_value'; -
优化器的思考过程:
- 可用索引:优化器发现,
idx_a和idx_abc这两个索引,都可以用来服务WHERE a = 'some_value'这个条件。 - 成本估算:
idx_a:是一个只包含a列和主键的索引,相对较小。idx_abc:包含了a,b,c三列和主键,索引体积更大。
- 选择:在这种情况下,两个索引都能定位到相同的记录集,但
idx_a这个单列索引更小、更“专一”。扫描一个更小的索引,其I/O成本和CPU成本通常会更低。因此,在这种简单的等值查询下,优化器大概率会选择单列索引idx_a。
我们可以通过
EXPLAIN的key_len字段来验证,如果选择了idx_a,key_len会等于a列的长度;如果选择了idx_abc,key_len同样也只会是a列的长度,因为它只用到了最左前缀。 - 可用索引:优化器发现,
场景二:查询涉及联合索引的其他列,且能形成“覆盖索引”
-
索引情况:单列索引
idx_a (a),联合索引idx_ab (a, b)。 -
查询SQL:
SELECT a, b FROM my_table WHERE a = ? AND b = ?; -
优化器的思考过程:
- 可用索引:
idx_a和idx_ab都能处理WHERE a = ?的部分。 - 成本估算:
- 如果走
idx_a:
a. 通过idx_a找到所有满足a=?的记录的主键ID。
b. 对每一条找到的记录,都进行回表,去聚簇索引中拉取完整的行数据。
c. 在Server层,对这些完整的行数据,再进行b=?的过滤。
d. 这个过程包含了大量的回表I/O,成本非常高。 - 如果走
idx_ab:
a. 通过idx_ab,利用最左前缀原则,直接定位到同时满足a=?和b=?的索引记录。
b. 关键点:查询所需要的所有列(a和b)都已经包含在这个联合索引中了。
c. 这就触发了 “覆盖索引”(Covering Index) 优化。MySQL完全不需要进行任何回表操作,可以直接从索引中获取所有需要的数据并返回。
d. 这个过程的I/O成本极低。
- 如果走
- 选择:毫无疑问,走
idx_ab联合索引的成本,远低于走idx_a再回表的成本。因此,优化器会果断选择联合索引idx_ab。
- 可用索引:
总结
所以,当一个列同时是单列索引和联合索引的成员时,MySQL优化器会:
- 评估所有可用的索引路径。
- 计算每条路径的成本,主要考量因素包括:
- 需要扫描的索引范围大小。
- 是否需要回表,以及预估的回表次数。
- 是否能触发覆盖索引。
- 最终,选择那个预估成本最低的执行计划。
在大多数情况下,如果联合索引能提供更多的过滤信息或能实现覆盖索引,它都会是被优先选择的对象。我们可以通过EXPLAIN命令,来实际地查看优化器最终做出的选择。
索引已经建好了,那我再插入一条数据,索引会有哪些变化?
面试官您好,当一张已经建好索引的表中插入一条新数据时,为了维护B+树的有序性和平衡性,数据库必须对相关的索引进行更新。这个“变化”的剧烈程度,主要取决于插入的主键值是顺序的还是乱序的。
我们来分两种情况讨论:
情况一:插入的是“顺序主键”(例如,自增ID)—— 最理想的情况
这是我们最推荐的、对索引影响最小的插入方式。
-
1. 对聚簇索引(主键索引)的变化:
- 定位:由于主键是单调递增的,InnoDB永远知道这条新记录应该放在物理存储的最后。它会直接定位到B+树的最后一个叶子节点(数据页)。
- 插入:将新的行数据追加到这个数据页的末尾。
- 页分裂:
- 绝大多数情况下,不会发生页分裂。
- 只有当这个最后的数据页恰好被写满时,才会发生一次非常“平滑”的页分裂:InnoDB会简单地创建一个新的空数据页,然后让后续的插入继续在新页上进行。这个过程开销很小,因为不需要移动旧数据。
-
2. 对二级索引的变化:
- 对于表上的每一个二级索引,都需要进行一次插入操作。
- InnoDB会根据二级索引的键值,在对应的B+树中找到合适的位置,插入新的索引记录(包含索引键和新的主键ID)。
- 这个插入过程,因为二级索引的键值不一定是顺序的,所以可能会发生随机I/O和页分裂。
总结:顺序主键插入,对聚簇索引的影响极小,性能最高。但仍然需要更新所有的二级索引。
情况二:插入的是“乱序主键”(例如,UUID)—— 性能杀手
这是我们应该极力避免的情况。
-
1. 对聚簇索引(主键索引)的变化:
- 定位:由于主键是无序的,新记录可能会被插入到B+树的任何一个位置。InnoDB必须从根节点开始,进行一次随机的查找,才能定位到目标数据页。这个过程就可能涉及到多次随机I/O。
- 页分裂(高概率发生):当InnoDB找到目标数据页后,这个页很可能已经满了,或者没有足够的连续空间。此时,就必须进行一次代价高昂的页分裂:
- 创建一个新的数据页。
- 将原数据页中的一部分数据行,移动到这个新页中,以腾出空间。
- 将新记录插入到正确的位置。
- 更新B+树上层节点的指针。
- 性能影响:页分裂涉及到大量的数据移动和索引维护,是非常耗费性能的操作,会严重影响插入速度。并且,它会导致数据页的填充率降低,产生内部碎片。
-
2. 对二级索引的变化:
- 与情况一类似,所有的二级索引也都需要进行插入操作。
总结
所以,当一条新数据被插入时,索引会发生以下变化:
- 所有索引都需要更新:无论是聚簇索引还是二级索引,都必须插入新的条目来反映这条新数据。
- 聚簇索引的变化是关键:
- 如果主键是顺序的,聚簇索引的变化是高效的追加操作,性能影响小。
- 如果主键是乱序的,聚簇索引的变化则是一系列低效的随机I/O和高成本的页分裂操作,性能会急剧下降。
这就是为什么我们总是强调,InnoDB表的主键,应该选择单调递增的、与业务无关的ID,以保证高效的写入性能和存储的紧凑性。
索引字段是不是建的越多越好?
面试官您好,绝对不是。索引就像一把“双刃剑”,它在提升查询性能的同时,也带来了不可忽视的成本。因此,索引的创建必须是克制的、有针对性的,绝非越多越好。
我们可以从索引的“利”与“弊”两方面来看这个问题。
一、索引的“利”:大幅提升查询(SELECT)性能
- 这是我们创建索引的唯一目的。一个设计良好的索引,可以通过B+树结构,将查询的复杂度从全表扫描的O(N),降低到对数级别的O(logN),极大地减少了磁盘I/O,使得查询速度可以提升几个数量级。
二、索引的“弊”:过多的索引带来的三大成本
当我们无节制地创建索引时,就会付出沉重的代价。主要体现在以下几个方面:
1. 空间成本:占用大量磁盘空间
- 索引本身并不是虚无的,它需要被物化存储在磁盘上。每一个索引,都是一棵独立的B+树。
- 一张表的索引越多,其占用的磁盘空间就越大。在数据量庞大的情况下,所有索引占用的空间,甚至可能会超过表数据本身占用的空间。
2. 时间成本:严重拖慢写操作(INSERT, UPDATE, DELETE)的性能
- 这是最核心、最直接的性能影响。当您对表中的数据进行写操作时,数据库不仅要更新表数据本身,还必须同步地更新这张表上的每一个索引。
INSERT:每插入一条新数据,就必须向这张表的所有索引的B+树中,都插入一条新的索引记录。DELETE:每删除一条数据,就必须从所有索引的B+树中,都删除掉对应的索引记录。UPDATE:如果更新的字段包含了索引列,那么就需要先删除旧的索引记录,再插入新的索引记录。- 结论:您表上的索引越多,一次写操作需要维护的B+树就越多,带来的I/O和CPU开销就越大,写的性能就越差。
3. 维护成本:可能“迷惑”查询优化器
- MySQL的查询优化器在执行一条SQL时,需要从所有可用的索引中,选择一个它认为成本最低的来使用。
- 如果一张表上有大量冗余或不合理的索引,这会增加优化器制定执行计划的时间。
- 在极少数复杂的情况下,过多的索引甚至可能会“迷惑”优化器,导致它选错了索引,反而走了性能更差的执行路径。
总结与我的实践原则
所以,创建索引的本质,是一个在“查询收益”和“空间与写入成本”之间的权衡(Trade-off)。
我的实践原则是:
- 按需创建:只为那些在
WHERE,JOIN,ORDER BY子句中频繁使用的列创建索引。 - 优先考虑联合索引:对于多条件的查询,优先创建联合索引,而不是为每个字段都创建单独的索引,这样可以更好地利用覆盖索引等优化。
- 定期审查与清理:定期地审查数据库中索引的使用情况(比如通过
performance_schema),找出并删除那些长期不被使用的“僵尸”索引,为系统减负。
最终的目标,是用最少的、最高效的索引,来满足我们核心的查询需求。
详细讲讲什么是索引优化
面试官您好,索引优化是数据库性能调优中至关重要的一环,可以从 “索引的规划与设计” 和 “索引的正确使用” 这两个大的维度来进行。
第一维度:索引的规划与设计 (How to Build)
这是优化的基础,一个好的索引设计能事半功倍。
-
选择合适的主键:永远优先使用自增ID
- 原理与优势:在InnoDB中,使用单调递增的主键,可以保证新数据总是顺序追加。这带来了极高的写入性能,避免了代价高昂的页分裂,并且使得数据页的填充率最高,存储最紧凑。
- 反例:使用UUID等无序值做主键,会导致大量的随机I/O和页分裂,严重影响性能。
-
设计高效的联合索引:遵循“区分度优先”与“范围查询靠后”原则
- 在创建联合索引时,字段的顺序至关重要。我会将区分度最高(选择性最好) 的字段放在最左边,这样可以使索引在筛选时最快地排除大量数据。
- 同时,我会将需要进行范围查询的字段,尽可能地放在联合索引的末尾,因为一旦某个字段使用了范围查询,它右边的所有字段就都无法再利用索引了。
-
利用覆盖索引,避免回表
- 核心思想:这是我进行SQL优化时的一个核心目标。通过精心设计联合索引,使得一个查询所需要的所有字段(
SELECT和WHERE中涉及的),都能直接从这个二级索引中获取到。 - 好处:这样就完全避免了“回表”——即再根据主键去聚簇索引中查找数据的额外步骤。对于需要返回大量数据的查询,覆盖索引带来的性能提升是巨大的。
- 核心思想:这是我进行SQL优化时的一个核心目标。通过精心设计联合索引,使得一个查询所需要的所有字段(
-
对长字符串使用前缀索引
- 场景:当需要在很长的字符串字段(如URL、文章标题)上建立索引时,如果对整个字段建立索引,会导致索引文件非常庞大,性能不佳。
- 做法:此时,我会使用前缀索引,只对字符串的前N个字符建立索引(
CREATE INDEX ... ON my_table(url(50));)。 - 权衡:选择合适的前缀长度是一个关键,需要通过计算区分度来找到一个既能保证足够筛选能力,又能有效减小索引体积的平衡点。
第二维度:索引的正确使用 (How to Use)
索引建好了,但如果SQL写得不对,同样会前功尽弃。我会时刻注意避免以下几种导致索引失效的“坑”。
-
遵循最左前缀原则
- 对于联合索引
(a, b, c),查询必须从a开始,且不能跳过中间的b,索引才能被最大程度地利用。
- 对于联合索引
-
不在索引列上做任何“手脚”
- 核心原则:保证索引列在
WHERE子句中是 “干净” 的。 - 具体表现:
- 不使用函数:
WHERE DATE(create_time) = ...会失效;应改为WHERE create_time BETWEEN ... AND ...。 - 不进行表达式计算:
WHERE age - 1 = 20会失效;应改为WHERE age = 21。 - 避免隐式类型转换:如果
phone是字符串类型,WHERE phone = 123456会导致索引失效;应改为WHERE phone = '123456'。
- 不使用函数:
- 核心原则:保证索引列在
-
谨慎使用
LIKE和ORLIKE:只有 右模糊匹配LIKE 'abc%'才能有效利用索引。左模糊LIKE '%abc'和全模糊LIKE '%abc%'都会导致索引失效。OR:当OR连接的条件中,有一方不是索引列时,整个查询的索引都会失效。
-
使用
EXPLAIN进行分析- 最后,也是最重要的一步,对于任何复杂的、性能敏感的SQL,我都会在执行前,先使用
EXPLAIN命令来分析它的执行计划。 - 通过查看
type,possible_keys,key,key_len,rows,Extra等字段,我可以清晰地知道这条SQL是否命中了索引、命中了哪个索引、索引的使用情况如何,以及是否存在回表、文件排序等性能瓶颈,从而进行针对性的优化。
- 最后,也是最重要的一步,对于任何复杂的、性能敏感的SQL,我都会在执行前,先使用
通过在设计和使用这两个阶段都遵循这些最佳实践,就能最大化地发挥出索引的威力,保证数据库的高性能。
了解过前缀索引吗?
面试官您好,它是一种针对字符串类型字段的、非常实用的索引优化技巧。
1. 前缀索引是用来解决什么问题的?
- 核心问题:当我们需要在一些很长的字符串字段上建立索引时,比如
VARCHAR(255)类型的URL、文章标题等,如果对整个字段建立索引,会导致索引文件变得异常庞大。 - 带来的弊端:
- 占用大量磁盘空间。
- 降低查询性能:因为索引变大,一个磁盘页(Page)能容纳的索引条目就变少,这可能导致B+树的层高增加,从而需要更多的磁盘I/O。
- 影响写性能:维护一个巨大的索引,其成本也更高。
前缀索引,就是为了解决这个问题而生的。它的核心思想是:不索引整个字符串,而只索引字符串的前N个字符。
2. 如何使用前缀索引?
- 语法示例:
-- 假设有一个url字段,类型为VARCHAR(255) -- 我们只对它前50个字符建立索引 ALTER TABLE my_table ADD INDEX idx_url_prefix (url(50)); - 带来的好处:通过只索引部分前缀,我们可以极大地减小索引的体积,从而让一个索引页能存储更多的索引项,有效降低B+树的高度,提升查询性能。
3. 如何选择合适的前缀长度?—— 核心是“区分度”
选择一个合适的“前缀长度”是使用前缀索引的关键所在,也是一个权衡的过程。
-
长度太短:如果前缀太短,那么索引的区分度(选择性)就会很差。比如,对于一堆URL,如果都只取前10个字符(可能都是
http://www...),那这个索引就几乎没什么筛选能力,优化器可能根本不会用它。 -
长度太长:如果前缀太长,那就失去了使用前缀索引来“减小体积”的意义。
-
我的选择方法:
我会通过计算不同前缀长度下的区分度,来找到一个最佳的平衡点。- 首先,计算整个字段的区分度:
SELECT COUNT(DISTINCT url) / COUNT(*) FROM my_table; - 然后,尝试不同前缀长度的区分度:
SELECT COUNT(DISTINCT LEFT(url, 30)) / COUNT(*) FROM my_table; SELECT COUNT(DISTINCT LEFT(url, 40)) / COUNT(*) FROM my_table; SELECT COUNT(DISTINCT LEFT(url, 50)) / COUNT(*) FROM my_table; - 我会选择一个使得区分度最接近完整字段区分度,同时长度又尽可能短的那个值。比如,发现当长度为50时,区分度已经达到了完整字段的99%,那么50就是一个非常好的选择。
- 首先,计算整个字段的区分度:
4. 前缀索引的缺点与限制
在使用前缀索引时,我们必须清楚它带来的两个重要限制:
- 无法用于
ORDER BY和GROUP BY:因为前缀索引只包含了字符串的一部分,所以MySQL无法使用它来进行完整的排序或分组操作。 - 无法实现“覆盖索引”:这是最关键的限制。一个查询如果走了前缀索引,它只是利用这个前缀进行了快速定位。但如果查询需要获取完整的字符串值,它必然需要进行一次回表操作,去聚簇索引中获取。因为它无法从前缀索引中得到完整的字段值。
总结一下,前缀索引是一种用 “部分信息” 来换取 “更高效率” 的空间和性能优化手段。它在处理长字符串索引时非常有效,但我们需要通过计算区分度来科学地选择前缀长度,并且要清楚地认识到它在排序和覆盖索引方面的局限性。
更多推荐



所有评论(0)