概述

删除长期未使用的索引 不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 chema_unused_indexes 视图来查询哪些索引从未被使用

避免定义重复的索引

借助 pt-duplicate-key-checker 查找,并生成删除重复索引的 SQL 语句

1
pt-duplicate-key-checker -h <host> -u<user> -p<passwd>

索引好处:

  • 提高访问速度;
  • 优化查询;
  • 将随机 IO 改为顺序 IO

索引缺点:

  • 占用空间,索引建立的越多越占用空间;
  • 更改频繁时,每次修改都需要重建索引;

索引的适用场景

  • 选择性较高的场景;

不适合索引的场景:

  • 数据规模小的情况;
  • 选择性较低的列;

索引的底层实现

  1. Hash 索引

适⽤于只有等值查询的场景,⽐如 Memcached 及其他⼀些 NoSQL 引擎。

(2)) 有序数组

有序数组在等值查询和范围查询场景中的性能就都⾮常优秀, 有序数组索引只适⽤于静态存储引擎。

  1. 二叉树
  2. B+ 树

索引的创建

  • index_type: 可选择 BTREE,HASH
1
2
3
4
5
CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_name
[index_type]
ON tbl_name (key_part,...)
[index_option]
[algorithm_option | lock_option] ...

Functional Key Parts: MySQL8.0+ 支持,不只是列/列特定的前缀,支持

1
2
3
4
5
6
7
8
9
CREATE TABLE t1 (
col1 VARCHAR(10),
col2 VARCHAR(20),
INDEX (col1, col2(10))
);
CREATE TABLE t1 (col1 INT, col2 INT, INDEX func_index ((ABS(col1))));
CREATE INDEX idx1 ON t1 ((col1 + col2));
CREATE INDEX idx2 ON t1 ((col1 + col2), (col1 - col2), col1);
ALTER TABLE t1 ADD INDEX ((col1 * 40) DESC);

JSON 格式的索引

1
2
3
4
5
6
7
8
9
10
CREATE TABLE employees (
data JSON,
INDEX idx ((CAST(data->>"$.name" AS CHAR(30)) COLLATE utf8mb4_bin))
);
INSERT INTO employees VALUES
('{ "name": "james", "salary": 9000 }'),
('{ "name": "James", "salary": 10000 }'),
('{ "name": "Mary", "salary": 12000 }'),
('{ "name": "Peter", "salary": 8000 }');
SELECT * FROM employees WHERE data->>'$.name' = 'James';

索引查看

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 查看指定表的索引
SHOW INDEX FROM <table-name> FROM <database-name>;

-- 查看指定数据库的索引统计
SELECT
TABLE_NAME
,INDEX_NAME
,SEQ_IN_INDEX
,COLUMN_NAME
,CARDINALITY
,INDEX_TYPE
FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema = 'DatabaseName'

重复索引的查看,见 pt-duplicate-key-checker

索引分类

索引分类

  • B-Tree: b 树索引
  • R-tree: 空间索引
  • hash: 散列索引
  • full-text: 全文索引

主键索引(聚集)

作为表的主键,可以有包含多个列,该索引是唯一索引,在 InnoDB 存储引擎中为聚集索引。

主键⻓度越⼩,普通索引的叶⼦节点就越⼩,普通索引占⽤的空间也就越⼩。 所以,从性能和存储空间⽅⾯考量,⾃增主键往往是更合理的选择。

聚集索引和非聚集索引的区别

  • 聚集索引在叶子节点存储的是表中的数据
  • 非聚集索引在叶子节点存储的是主键和索引列

Q:基于主键索引和普通索引的查询有什么区别?

A:如果语句是 select * from table where id=8888,只需要搜索 ID 这棵 B+ 树; 如果语句是 select * from table where key=5,需要先搜索 key 索引树,得到 id 的值为 500,再到 ID 索引树搜索⼀次。这个过程称为 回表。 也就是说,基于⾮主键索引的查询需要多扫描⼀棵索引树。

普通索引

相较于唯一索引,使用了 change buffer

与唯一索引的相比,这两类索引在查询能⼒上是没差别,主要考虑的是对更新性能的影响。

Change buffer 使用场景:

如果所有的更新后⾯,都⻢上伴随着对这个记录的查询,那么应该关闭 change buffer。⽽在其他情况下,change buffer 都能提升更新性能。

唯一索引

唯⼀索引⽤不上 change buffer 的优化机制,因此如果业务可以接受,从性能⻆度出发建议优先考虑⾮唯⼀索引。

对于唯⼀索引来说,由于索引定义了唯⼀性,查找到第⼀个满⾜条件的记录后,就会停⽌继续检索。

覆盖索引

包含了所有查询字段 (where,select,ordery by,group by 包含的字段) 的索引。

对于频繁的查询优先考虑使用覆盖索引。

无需二次扫表,直接扫描索引中的 B+ 树即可获取全部数据;

覆盖索引的好处:

  • 避免 Innodb 表进行索引的二次查询: Innodb 是以聚集索引的顺序来存储的,对于 Innodb 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询,减少了 IO 操作,提升了查询效率。
  • 可以把随机 IO 变成顺序 IO 加快查询效率: 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。

创建的注意事项:

  • 选取字符串的前多少位作为索引项();
    • 阿里巴巴开发手册规定 【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。
  • 注意 最左前缀 的使用,以及基于此衍生出来的联合索引组合;
  • 能够形成覆盖索引,从而避免二次扫表问题。

联合索引

联合索引定义的顺序: 建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。

  • 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)
  • 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)
  • 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)

索引列的选取: 常见索引列建议

  • 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列
  • 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段
  • 并不要将符合 1 和 2 中的字段的列都建立一个索引, 通常将 1、2 中的字段建立联合索引效果更好
  • 多表 join 的关联列

字符串索引

直接创建完整索引,这样可能⽐较占⽤空间;

创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使⽤覆盖索引

倒序存储,再创建前缀索引,⽤于绕过字符串本身前缀的区分度不够的问题;

创建 引,查询性能稳定,有额外的存储和计算消耗,跟第三种⽅式⼀样,都不⽀持 。hash字段索范围扫描

前缀索引

前缀索引限定索引的长度

不指定前缀长度,默认包含整个字符串;

1
2
3
4
ALTER TABLE SUser ADD INDEX idx1(email);

-- only prefix
ALTER TABLE SUser ADD INDEX idx2(email(6));

由于 email(6) 这个索引结构中每个邮箱字段都只取前 6 个字节,所以占⽤的空间会更⼩,这就是使⽤前缀索引的优势。 但可能会增加额外的记录扫描次数。

如果使⽤的是 index1(即 email 整个字符串的索引结构),执⾏顺序是这样的:

  1. 从 index1 索引树找到满⾜索引值是 zhangssxyz@xxx.com ’的这条记录,取得ID2的值;
  2. 到主键上查到主键值是ID2的⾏,判断email的值是正确的,将这⾏记录加⼊结果集;
  3. 取index1索引树上刚刚查到的位置的下⼀条记录,发现已经不满⾜email=’zhangssxyz@xxx.com’的条件了,循环结束。 这个过程中,只需要回主键索引取⼀次数据,所以系统认为只扫描了⼀⾏。

如果使⽤的是index2(即email(6)索引结构),执⾏顺序是这样的:

1 . 从index2索引树找到满⾜索引值是’zhangs’的记录,找到的第⼀个是ID1;

2 . 到主键上查到主键值是ID1的⾏,判断出email的值不是’zhangssxyz@xxx.com’,这⾏记录丢弃;

3 . 取index2上刚刚查到的位置的下⼀条记录,发现仍然是’zhangs’,取出ID2,再到ID索引上取整⾏然后判断,这次值对了,将这⾏记录加⼊结果集;

4 . 重复上⼀步,直到在idxe2上取到的值不是’zhangs’时,循环结束。 在这个过程中,要回主键索引取4次数据,也就是扫描了4⾏。 通过这个对⽐,你很容易就可以发现,使⽤前缀索引后,可能会导致查询语句读数据的次数变多。

使⽤前缀索引,定义好⻓度,就可以做到既节省空间,⼜不⽤额外增加太多的查询成本。

根据区分度来选择前缀的长度;

建立索引列的区分度越⾼越好。区分度越⾼,意味着重复的键值越少。可以通过统计索引上有多少个不同的值来判断要使⽤多⻓的前缀。 可以使⽤下⾯这个语句,算出这个列上有多少个不同的值,95% 作为参考;

1
2
3
4
5
SELECT COUNT(DISTINCT LEFT(email, 4)) AS L4,
COUNT(DISTINCT LEFT(email, 5)) AS L5,
COUNT(DISTINCT LEFT(email, 6)) AS L6,
COUNT(DISTINCT LEFT(email, 7)) AS L7,
FROM SUser;

前缀索引对覆盖索引的影响

如果使⽤ index1(即email整个字符串的索引结构)的话,可以利⽤覆盖索引,从index1查到结果后直接就返回了,不需要回到ID索引再去查⼀次。⽽如果使⽤index2(即email(6)索引结构)的话,就不得不回到ID索引再去判断email字段的值。 即使你将index2的定义修改为email(18)的前缀索引,这时候虽然index2已经包含了所有的信息,但InnoDB还是要回到id索引再查⼀下,因为系统并不确定前缀索引的定义是否截断了完整信息。 也就是说,使⽤前缀索引就⽤不上覆盖索引对查询性能的优化了,这也是你在选择是否使⽤前缀索引时需要考虑的⼀个因素。

1
2
ALTER TABLE SUser ADD INDEX idx_email(eamil(20));
--SELECT id, name, email FROM SUserWHERE email='zhangssxyz@xxx.com';

区分度不足的处理

前缀的区分度不够时如何处理

比如国家的身份证号、电话号码

方式一: 倒序存储

由于身份证号的最后6位没有地址码这样的重复逻辑,所以最后这 6 位很可能就提供了⾜够的区分度,先使⽤ count(distinct) ⽅法做验证

1
2
3
4
5
-- MySQL 函数 revere 操作,插入处理
INSERT INTO T(col.., id_card) VALUES (xxx, revere('input_id_card_string'));

-- 查询的处理
SELECT <field_list> FROM t WHERE id_card=reverse('input_id_card_string');

方式二: 使用 Hash 字段

在表上再创建⼀个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。

之后每次插⼊新记录的时候,都同时⽤ crc32() 函数得到校验码填到这个新字段。由于校验码可能存在冲突,也就是说两个不同的身份证号通过 crc32() 函数得到的结果可能是相同的,所以你的查询语句 where 部分要判断 id_card 的值是否精确相同。

这样,索引的⻓度变成了 4 个字节,⽐原来⼩了很多。

1
2
3
4
5
6
7
8
-- 新增加一列,4byte 存放索引 hash 后的值
ALTER TABLE t ADD id_card_crc INT UNSIGNED, ADD index(id_card_crc);
-- 插入逻辑, crc32 hash 值以及原始的值
INSERT INTO T(co1..., id_card_crc, id_card) VALUES (xxx, crc32('input_card_string'), 'input_id_card_string');

-- INDEX can duplication
-- 对应的查询语句,先对 hash 进行查询后对原始字符串查询
SELECT * FROM t WHERE id_card_crc=crc32('input_id_card_string') AND id_card='input_id_card_string');

倒序存储和 hash 存储两种方式的比较:

相同点是,都不⽀持范围查询。倒序存储的字段上创建的索引是按照倒序字符串的⽅式排序的,已经没有办法利⽤索引⽅式查出身份证号码在[ID_X, ID_Y]的所有市⺠了。同样地,hash 字段的⽅式也只能⽀持等值查询。

它们的区别,主要体现在以下三个⽅⾯:

① 从占⽤的额外空间来看,倒序存储⽅式在主键索引上,不会消耗额外的存储空间,⽽ hash 字段⽅法需要增加⼀个字段。当然,倒序存储⽅式使⽤ 4 个字节的前缀⻓度应该是不够的,如果再⻓⼀点,这个消耗跟额外这个 hash 字段也差不多抵消了。

② 在CPU消耗⽅⾯,倒序⽅式每次写和读的时候,都需要额外调⽤⼀次 reverse 函数,⽽ hash 字段的⽅式需要额外调⽤⼀次 crc32() 函数。如果只从这两个函数的计算复杂度来看的话,reverse 函数额外消耗的 CPU 资源会更⼩些。

③ 从查询效率上看,使⽤ has h字段⽅式的查询性能相对更稳定⼀些。因为 crc32 算出来的值虽然有冲突的概率,但是概率⾮常⼩,可以认为每次查询的平均扫描⾏数接近 1。⽽倒序存储⽅式毕竟还是⽤的前缀索引的⽅式,也就是说还是会增加扫描⾏数。

索引的使用

修改 SQL 语句尽量使其走索引;

对于查询优化器选择索引错误,通过修改 SQL 引导其进行选择正确的索引,如通过 FORCE INDEX() 来指定使用特定的索引进行查询;

为查询较多的字段添加索引,可考虑使用覆盖索引;

要避免编写使索引失效的 SQL 语句;

最左匹配原则

全值匹配我最爱,最左前缀要遵守; 带头大哥不能死,中间兄弟不能断; 原因: B+ 树的索引结构导致

原因: 复合索引(组合),先对最左边的字段进行排序,在第一个字段排序的基础上再对后面的字段排序。

类似 orderby ,只保证第一个字段有序,通常对于第二个字段用不到索引;

B+ 树这种索引结构,可以利⽤索引的“最左前缀”,来定位记录

只要满⾜最左前缀,就可以利⽤索引来加速检索。这个最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。

如何控制索引的顺序:

如果通过调整顺序,可以少维护⼀个索引,那么这个顺序往往就是需要优先考虑采⽤的。 考虑的原则就是空间了。⽐如上⾯这个市⺠表的情况,name 字段是⽐ age 字段⼤的,那我就建议你创建⼀个(name,age)的联合索引和⼀个(age)的单字段索引

http://img.janhen.com/202103072220221551846748988.png

索引失效

索引列上少计算,范围之后全失效; LIKE 百分写最右,覆盖索引不写星; 不等空值还有 OR, 索引失效要少用; VAR 引号不可丢,SQL 高级也不难!

条件字段函数及计算操作

如果对字段做了函数计算,就⽤不上索引了。

SQL语句条件⽤的是 where t_modified=‘2018-7-1’ 的话,B+ 树提供的这个快速定位能⼒,来源于同⼀层兄弟节点的有序性。

显示调用函数,在 t_modified 字段加了 month() 函数操作,导致了全索引扫描

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `tradelog` (
`id` int(11) NOT NULL,
`tradeid` varchar(32) DEFAULT NULL,
`operator` int(11) DEFAULT NULL,
`t_modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `tradeid` (`tradeid`),
KEY `t_modified` (`t_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Query,对索引列使用了函数
SELECT COUNT(*) FROM tradelog WHERE month(t_modified)=7;

按照业务进行修改,使其走索引:

1
2
3
4
5
-- 更改索引列中使用的函数成为索引列的范围比较查询
SELECT COUNT(*) FROM tragelog
WHERE (t_modified >= '2016-7-1' AND t_modified<'2016-8-1') OR
(t_modified >= '2017-7-1' AND t_modified<'2017-8-1') OR
(t_modified >= '2018-7-1' AND t_modified<'2018-8-1');

隐式类型转换

字符串隐式的转换成数字进行操作;

1
2
3
4
5
6
7
select * from tradelog where tradeid=110717;
select10” > 9;
-- 1, String ⇒ number
SELECT * FROM tradelog WHERE CAST(tradid AS signed int)=110717;

-- 修改后的 SQL
SELECT * FROM tradelog WHERE tradeid='110717';

隐式字符编码转换

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `trade_detail` (
`id` int(11) NOT NULL,
`tradeid` varchar(32) DEFAULT NULL,
`trade_step` int(11) DEFAULT NULL COMMENT "操作步骤",
`step_info` varchar(32) DEFAULT NULL COMMENT "步骤信息",
PRIMARY KEY (`id`),
KEY `tradeid` (`tradeid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- log 和 业务的详情表字符集不同
select d.* from tradelog l, trade_detail d
where d.tradeid=l.tradeid and l.id=2; /* 语句 Q1 */

两个字符集不同:

1
2
3
4
select * from trade_detail where tradeid=$L2.tradeid.value;

-- 等同于,对列粒度的字符进行编码转换比较
select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;

字符集 utf8mb4 是 utf8 的超集,所以当这两个类型的字符串在做⽐较的时候,MySQL 内部的操作是,先把 utf8 字符串转成 utf8mb4 字符集,再做⽐较。

方案一: 把 trade_detail 表上的 tradeid字 段的字符集也改成 utf8mb4,这样就没有字符集转换的问题了。

1
ALTER TABLE trade_detail MODIFY tradeid varchar(32) character SET utf8mb4 DEFAULT NULL;

方案二: 在无法修改字符集的情况下,或者表中的数据很多的情况下

1
2
select d.* from tradelog l , trade_detail d 
where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2;

简单的计算

1
SELECT * FROM trade_detail WHERE trade_step=trade_step + 1;

索引下堆(5.6)

// TODO

索引问题

重建索引

@Q: 为什么要重建索引?

索引可能因为删除,或者⻚分裂等原因,导致数据⻚有空洞,重建索引的过程会创建⼀个新的索引,把数据按顺序插⼊,这样⻚⾯的利⽤率最⾼,让索引更紧凑、更省空间。

通过两个 alter 语句重建索引 k,以及通过两个 alter 语句重建主键索引是否合理。

重建索引 k 的做法是合理的,可以达到省空间的⽬的。但是,重建主键的过程不合理。不论是删除主键还是创建主键,都会将整个表重建。所以连着执⾏这两个语句的话,第⼀个语句就⽩做了。这两个语句,可以⽤这个语句代替 : alter table Tengine=InnoDB

记录⽇志⽤的表, 会定期删除过早之前的数据。 最后这个表实际内容的⼤⼩ 10G, 索引却有 30G.. 是 InnoDB 这种引擎导致的,虽然删除了表的部分记录,但是它的索引还在, 并未释放. 只能是重新建表才能重建索引.

索引选择异常

@Q: 索引选择异常和处理? 处理查询优化器选择错误索引?

查询优化器选择的不是最优的索引情况的处理。

⼤多数时候优化器都能找到正确的索引,但偶尔还是会碰到我们上⾯举例的这两种情况:原本可以执⾏得很快的SQL语句,执⾏速度却⽐你预期的慢很多

方式一:采⽤ force index() 强⾏选择⼀个索引

MySQL 会根据词法解析的结果分析出可能可以使⽤的索引作为候选项,然后在候选列表中依次判断每个索引需要扫描多少⾏。如果 force index 指定的索引在候选索引列表中,就直接选择这个索引,不再评估其他索引的执⾏代价。

1
2
3
4
5
-- FORCE INDEX 的使用
SELECT * FROM t FORCE INDEX(a)
WHERE a BETWEEN 1 AND 1000
AND b BETWEEN 50000 AND 100000
ORDER BY bLIMIT 1;

但其实使⽤ force index 最主要的问题还是变更的及时性。因为选错索引的情况还是⽐较少出现的,所以开发的时候通常不会先写上 force index。⽽是等到线上出现问题的时候,你才会再去修改 SQL 语句、加上 force index。但是修改之后还要测试和发布,对于⽣产系统来说,这个过程不够敏捷。

方法二:可以考虑修改语句,引导 MySQL 使⽤期望的索引

⽐如,在这个例⼦⾥,显然把“order by b limit 1” 改成 “order by b,a limit 1” ,语义的逻辑是相同的。

现在 order by b,a 这种写法,要求按照b,a排序,就意味着使⽤这两个索引都需要排序。因此,扫描⾏数成了影响决策的主要条件,于是此时优化器选了只需要扫描 1000 ⾏的索引 a。 当然,这种修改并不是通⽤的优化⼿段,只是刚好在这个语句⾥⾯有 limit 1,因此如果有满⾜条件的记录, order by b limit 1 和 order by b,a limit 1 都会返回 b 是最⼩的那⼀⾏,逻辑上⼀致,才可以这么做。

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * FROM t FORCE INDEX(a)
WHERE a BETWEEN 1 AND 1000
AND b BETWEEN 50000 AND 100000
ORDER BY b,a
-- modifyLIMIT 1;
-- 另一种引导我们⽤limit 100让优化器意识到,使⽤b索引代价是很⾼的。其实是我们根据数据特征诱导了⼀下优化器,也不具备通⽤性。
SELECT * FROM
(SELECT * FROM t WHERE (a BETWEEN 1 AND 1000)
AND (B BETWEEN 50000 AND 100000)
ORDER BY b
LIMIT 100) alias
LIMIT 1;

方法三:在有些场景下,可以新建⼀个更合适的索引,来提供给优化器做选择,或删掉误⽤的索引

这种情况其实⽐较少,尤其是经过 DBA 索引优化过的库,再碰到这个 bug,找到⼀个更合适的索引⼀般⽐较难。 如果我说还有⼀个⽅法是删掉索引 b,你可能会觉得好笑。但实际上我碰到过两次这样的例⼦,最终是 DBA 跟业务开发沟通后,发现这个优化器错误选择的索引其实根本没有必要存在,于是就删掉了这个索引,优化器也就重新选择到了正确的索引。

B 树与索引

B+ 树特点:

  • 关键字分布在整棵树的所有节点。
  • 任何一个关键字,出现且只出现在一个节点中
  • 搜索有可能在非叶子节点结束。
  • 其搜索性能等价于在关键字全集内做一次二分查找。

B+树基本特点

  • 非叶子节点的子树指针与关键字个数相同。
  • 非叶子节点的子树指针 P[i],指向关键字属于 [k[i],K[i+1]) 的子树(注意:区间是前闭后开)。
  • 为所有叶子节点增加一个链指针
  • 所有关键字都在叶子节点出现

B 树与 B+ 树的区别

关键字与孩子节点的个数不同;

B+树的磁盘读写代价更低:B+ 树的内部没有指向关键字具体信息的指针,其内部节点相对 B 树更小,把所有关键字存放在同一块盘中,B+ 树比 B 树所能容纳的关键字数量也越多;

查询的稳定性: B+ 树所欲数据都存放在叶子节点,B+ 树无论如何都要扫表到叶子节点才能返回数据, 所有关键字查询的路径长度相同;B 树可以中途跳出,查询效率不够稳定;

B+ 树适合用于遍历和范围的选择,底层叶子节点是双向链表,只需要去遍历叶子节点就可以实现整棵树的遍历;

MongoDB 的索引为什么选择 B 树,而 MySQL 的索引是 B+树

MongoDB 非传统的 RDBMS,是以 Json 格式作为存储的 NoSQL,目的就是高性能、高可用、易扩展。摆脱了关系模型,所以范围查询和遍历查询的需求就没那么强烈。

Ref

MySQL :: MySQL 8.0 Reference Manual :: C Indexes

MySQL 的索引官方文档

深入理解MySQL索引-InfoQ

深入理解 MySQL 索引