07 | 数据库索引:索引并不是万能药

你好,我是朱晔。今天,我要和你分享的主题是,数据库的索引并不是万能药。

几乎所有的业务项目都会涉及数据存储,虽然当前各种NoSQL和文件系统大行其道,但MySQL等关系型数据库因为满足ACID、可靠性高、对开发友好等特点,仍然最常被用于存储重要数据。在关系型数据库中,索引是优化查询性能的重要手段。

为此,我经常看到一些同学一遇到查询性能问题,就盲目要求运维或DBA给数据表相关字段创建大量索引。显然,这种想法是错误的。今天,我们就以MySQL为例来深入理解下索引的原理,以及相关误区。

InnoDB是如何存储数据的?

MySQL把数据存储和查询操作抽象成了存储引擎,不同的存储引擎,对数据的存储和读取方式各不相同。MySQL支持多种存储引擎,并且可以以表为粒度设置存储引擎。因为支持事务,我们最常使用的是InnoDB。为方便理解下面的内容,我先和你简单说说InnoDB是如何存储数据的。

虽然数据保存在磁盘中,但其处理是在内存中进行的。为了减少磁盘随机读取次数,InnoDB采用页而不是行的粒度来保存数据,即数据被分成若干页,以页为单位保存在磁盘中。InnoDB的页大小,一般是16KB。

各个数据页组成一个双向链表,每个数据页中的记录按照主键顺序组成单向链表;每一个数据页中有一个页目录,方便按照主键查询记录。数据页的结构如下:

页目录通过槽把记录分成不同的小组,每个小组有若干条记录。如图所示,记录中最前面的小方块中的数字,代表的是当前分组的记录条数,最小和最大的槽指向2个特殊的伪记录。有了槽之后,我们按照主键搜索页中记录时,就可以采用二分法快速搜索,无需从最小记录开始遍历整个页中的记录链表。

举一个例子,如果要搜索主键(PK)=15的记录:

  • 先二分得出槽中间位是(0+6)/2=3,看到其指向的记录是12<15,所以需要从#3槽后继续搜索记录;
  • 再使用二分搜索出#3槽和#6槽的中间位是(3+6)/2=4.5取整4,#4槽对应的记录是16>15,所以记录一定在#4槽中;
  • 再从#3槽指向的12号记录开始向下搜索3次,定位到15号记录。

理解了InnoDB存储数据的原理后,我们就可以继续学习MySQL索引相关的原理和坑了。

聚簇索引和二级索引

说到索引,页目录就是最简单的索引,是通过对记录进行一级分组来降低搜索的时间复杂度。但,这样能够降低的时间复杂度数量级,非常有限。当有无数个数据页来存储表数据的时候,我们就需要考虑如何建立合适的索引,才能方便定位记录所在的页。

为了解决这个问题,InnoDB引入了B+树。如下图所示,B+树是一棵倒过来的树:

B+树的特点包括:

  • 最底层的节点叫作叶子节点,用来存放数据;
  • 其他上层节点叫作非叶子节点,仅用来存放目录项,作为索引;
  • 非叶子节点分为不同层次,通过分层来降低每一层的搜索量;
  • 所有节点按照索引键大小排序,构成一个双向链表,加速范围查找。

因此,InnoDB使用B+树,既可以保存实际数据,也可以加速数据搜索,这就是聚簇索引。如果把上图叶子节点下面方块中的省略号看作实际数据的话,那么它就是聚簇索引的示意图。由于数据在物理上只会保存一份,所以包含实际数据的聚簇索引只能有一个

InnoDB会自动使用主键(唯一定义一条记录的单个或多个字段)作为聚簇索引的索引键(如果没有主键,就选择第一个不包含NULL值的唯一列)。上图方框中的数字代表了索引键的值,对聚簇索引而言一般就是主键。

我们再看看B+树如何实现快速查找主键。比如,我们要搜索PK=4的数据,通过根节点中的索引可以知道数据在第一个记录指向的2号页中,通过2号页的索引又可以知道数据在5号页,5号页就是实际的数据页,然后再通过二分法查找页目录马上可以找到记录的指针。

为了实现非主键字段的快速搜索,就引出了二级索引,也叫作非聚簇索引、辅助索引。二级索引,也是利用的B+树的数据结构,如下图所示:

这次二级索引的叶子节点中保存的不是实际数据,而是主键,获得主键值后去聚簇索引中获得数据行。这个过程就叫作回表。

举个例子,有个索引是针对用户名字段创建的,索引记录上面方块中的字母是用户名,按照顺序形成链表。如果我们要搜索用户名为b的数据,经过两次定位可以得出在#5数据页中,查出所有的主键为7和6,再拿着这两个主键继续使用聚簇索引进行两次回表得到完整数据。

考虑额外创建二级索引的代价

创建二级索引的代价,主要表现在维护代价、空间代价和回表代价三个方面。接下来,我就与你仔细分析下吧。

首先是维护代价。创建N个二级索引,就需要再创建N棵B+树,新增数据时不仅要修改聚簇索引,还需要修改这N个二级索引。

我们通过实验测试一下创建索引的代价。假设有一个person表,有主键ID,以及name、score、create_time三个字段:

CREATE TABLE `person` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `score` int(11) NOT NULL,
  `create_time` timestamp NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

通过下面的存储过程循环创建10万条测试数据,我的机器的耗时是140秒(本文的例子均在MySQL 5.7.26中执行):

CREATE DEFINER=`root`@`%` PROCEDURE `insert_person`()
begin
    declare c_id integer default 1;
    while c_id<=100000 do
    insert into person values(c_id, concat('name',c_id), c_id+100, date_sub(NOW(), interval c_id second));
    set c_id=c_id+1;
    end while;
end

如果再创建两个索引,一个是name和score构成的联合索引,另一个是单一列create_time的索引,那么创建10万条记录的耗时提高到154秒:

KEY `name_score` (`name`,`score`) USING BTREE,
KEY `create_time` (`create_time`) USING BTREE

这里,我再额外提一下,页中的记录都是按照索引值从小到大的顺序存放的,新增记录就需要往页中插入数据,现有的页满了就需要新创建一个页,把现有页的部分数据移过去,这就是页分裂;如果删除了许多数据使得页比较空闲,还需要进行页合并。页分裂和合并,都会有IO代价,并且可能在操作过程中产生死锁。

你可以查看这个文档,以进一步了解如何设置合理的合并阈值,来平衡页的空闲率和因为再次页分裂产生的代价。

其次是空间代价。虽然二级索引不保存原始数据,但要保存索引列的数据,所以会占用更多的空间。比如,person表创建了两个索引后,使用下面的SQL查看数据和索引占用的磁盘:

SELECT DATA_LENGTH, INDEX_LENGTH FROM information_schema.TABLES WHERE TABLE_NAME='person'

结果显示,数据本身只占用了4.7M,而索引占用了8.4M。

最后是回表的代价。二级索引不保存原始数据,通过索引找到主键后需要再查询聚簇索引,才能得到我们要的数据。比如,使用SELECT * 按照name字段查询用户,使用EXPLAIN查看执行计划:

EXPLAIN SELECT * FROM person WHERE NAME='name1'

执行计划如下,可以发现:

  • key字段代表实际走的是哪个索引,其值是name_score,说明走的是name_score这个索引。
  • type字段代表了访问表的方式,其值ref说明是二级索引等值匹配,符合我们的查询。

把SQL中的*修改为NAME和SCORE,也就是SELECT name_score联合索引包含的两列:

EXPLAIN SELECT NAME,SCORE FROM person WHERE NAME='name1'

再来看看执行计划:

可以看到,Extra列多了一行Using index的提示,证明这次查询直接查的是二级索引,免去了回表。

原因很简单,联合索引中其实保存了多个索引列的值,对于页中的记录先按照字段1排序,如果相同再按照字段2排序,如图所示:

图中,叶子节点每一条记录的第一和第二个方块是索引列的数据,第三个方块是记录的主键。如果我们需要查询的是索引列索引或联合索引能覆盖的数据,那么查询索引本身已经“覆盖”了需要的数据,不再需要回表查询。因此,这种情况也叫作索引覆盖。我会在最后一小节介绍如何查看不同查询的成本,和你一起看看索引覆盖和索引查询后回表的代价差异。

最后,我和你总结下关于索引开销的最佳实践吧。

第一,无需一开始就建立索引,可以等到业务场景明确后,或者是数据量超过1万、查询变慢后,再针对需要查询、排序或分组的字段创建索引。创建索引后可以使用EXPLAIN命令,确认查询是否可以使用索引。我会在下一小节展开说明。

第二,尽量索引轻量级的字段,比如能索引int字段就不要索引varchar字段。索引字段也可以是部分前缀,在创建的时候指定字段索引长度。针对长文本的搜索,可以考虑使用Elasticsearch等专门用于文本搜索的索引数据库。

第三,尽量不要在SQL语句中SELECT *,而是SELECT必要的字段,甚至可以考虑使用联合索引来包含我们要搜索的字段,既能实现索引加速,又可以避免回表的开销。

不是所有针对索引列的查询都能用上索引

在上一个案例中,我创建了一个name+score的联合索引,仅搜索name时就能够用上这个联合索引。这就引出两个问题:

  • 是不是建了索引一定可以用上?
  • 怎么选择创建联合索引还是多个独立索引?

首先,我们通过几个案例来分析一下索引失效的情况。

第一,索引只能匹配列前缀。比如下面的LIKE语句,搜索name后缀为name123的用户无法走索引,执行计划的type=ALL代表了全表扫描:

EXPLAIN SELECT * FROM person WHERE NAME LIKE '%name123' LIMIT 100

把百分号放到后面走前缀匹配,type=range表示走索引扫描,key=name_score看到实际走了name_score索引:

EXPLAIN SELECT * FROM person WHERE NAME LIKE 'name123%' LIMIT 100

原因很简单,索引B+树中行数据按照索引值排序,只能根据前缀进行比较。如果要按照后缀搜索也希望走索引的话,并且永远只是按照后缀搜索的话,可以把数据反过来存,用的时候再倒过来。

第二,条件涉及函数操作无法走索引。比如搜索条件用到了LENGTH函数,肯定无法走索引:

EXPLAIN SELECT * FROM person WHERE LENGTH(NAME)=7

同样的原因,索引保存的是索引列的原始值,而不是经过函数计算后的值。如果需要针对函数调用走数据库索引的话,只能保存一份函数变换后的值,然后重新针对这个计算列做索引。

第三,联合索引只能匹配左边的列。也就是说,虽然对name和score建了联合索引,但是仅按照score列搜索无法走索引:

EXPLAIN SELECT * FROM person WHERE SCORE>45678

原因也很简单,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。尝试把搜索条件加入name列,可以看到走了name_score索引:

EXPLAIN SELECT * FROM person WHERE SCORE>45678 AND NAME LIKE 'NAME45%'

需要注意的是,因为有查询优化器,所以name作为WHERE子句的第几个条件并不是很重要。

现在回到最开始的两个问题。

  • 是不是建了索引一定可以用上?并不是,只有当查询能符合索引存储的实际结构时,才能用上。这里,我只给出了三个肯定用不上索引的反例。其实,有的时候即使可以走索引,MySQL也不一定会选择使用索引。我会在下一小节展开这一点。
  • 怎么选择建联合索引还是多个独立索引?如果你的搜索条件经常会使用多个字段进行搜索,那么可以考虑针对这几个字段建联合索引;同时,针对多字段建立联合索引,使用索引覆盖的可能更大。如果只会查询单个字段,可以考虑建单独的索引,毕竟联合索引保存了不必要字段也有成本。

数据库基于成本决定是否走索引

通过前面的案例,我们可以看到,查询数据可以直接在聚簇索引上进行全表扫描,也可以走二级索引扫描后到聚簇索引回表。看到这里,你不禁要问了,MySQL到底是怎么确定走哪种方案的呢。

其实,MySQL在查询数据之前,会先对可能的方案做执行计划,然后依据成本决定走哪个执行计划。

这里的成本,包括IO成本和CPU成本:

  • IO成本,是从磁盘把数据加载到内存的成本。默认情况下,读取数据页的IO成本常数是1(也就是读取1个页成本是1)。
  • CPU成本,是检测数据是否满足条件和排序等CPU操作的成本。默认情况下,检测记录的成本是0.2。

基于此,我们分析下全表扫描的成本。

全表扫描,就是把聚簇索引中的记录依次和给定的搜索条件做比较,把符合搜索条件的记录加入结果集的过程。那么,要计算全表扫描的代价需要两个信息:

  • 聚簇索引占用的页面数,用来计算读取数据的IO成本;
  • 表中的记录数,用来计算搜索的CPU成本。

那么,MySQL是实时统计这些信息的吗?其实并不是,MySQL维护了表的统计信息,可以使用下面的命令查看:

SHOW TABLE STATUS LIKE 'person'

输出如下:

可以看到:

  • 总行数是100086行(之前EXPLAIN时,也看到rows为100086)。你可能说,person表不是有10万行记录吗,为什么这里多了86行?其实,MySQL的统计信息是一个估算,其统计方式比较复杂我就不再展开了。但不妨碍我们根据这个值估算CPU成本,是100086*0.2=20017左右。
  • 数据长度是4734976字节。对于InnoDB来说,这就是聚簇索引占用的空间,等于聚簇索引的页面数量*每个页面的大小。InnoDB每个页面的大小是16KB,大概计算出页面数量是289,因此IO成本是289左右。

所以,全表扫描的总成本是20306左右。

接下来,我还是用person表这个例子,和你分析下MySQL如何基于成本来制定执行计划。现在,我要用下面的SQL查询name>‘name84059’ AND create_time>‘2020-01-24 05:00:00’

EXPLAIN SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00'

其执行计划是全表扫描:

只要把create_time条件中的5点改为6点就变为走索引了,并且走的是create_time索引而不是name_score联合索引:

我们可以得到两个结论:

  • MySQL选择索引,并不是按照WHERE条件中列的顺序进行的;
  • 即便列有索引,甚至有多个可能的索引方案,MySQL也可能不走索引。

其原因就是,MySQL并不是猜拳决定是否走索引的,而是根据成本来判断的。虽然表的统计信息不完全准确,但足够用于策略的判断了。

不过,有时会因为统计信息的不准确或成本估算的问题,实际开销会和MySQL统计出来的差距较大,导致MySQL选择错误的索引或是直接选择走全表扫描,这个时候就需要人工干预,使用强制索引了。比如,像这样强制走name_score索引:

EXPLAIN SELECT * FROM person FORCE INDEX(name_score) WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00' 

我们介绍了MySQL会根据成本选择执行计划,也通过EXPLAIN知道了优化器最终会选择怎样的执行计划,但MySQL如何制定执行计划始终是一个黑盒。那么,有没有什么办法可以了解各种执行计划的成本,以及MySQL做出选择的依据呢?

在MySQL 5.6及之后的版本中,我们可以使用optimizer trace功能查看优化器生成执行计划的整个过程。有了这个功能,我们不仅可以了解优化器的选择过程,更可以了解每一个执行环节的成本,然后依靠这些信息进一步优化查询。

如下代码所示,打开optimizer_trace后,再执行SQL就可以查询information_schema.OPTIMIZER_TRACE表查看执行计划了,最后可以关闭optimizer_trace功能:

SET optimizer_trace="enabled=on";
SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";

对于按照create_time>'2020-01-24 05:00:00’条件走全表扫描的SQL,我从OPTIMIZER_TRACE的执行结果中,摘出了几个重要片段来重点分析:

  • 使用name_score对name84059<name条件进行索引扫描需要扫描25362行,成本是30435,因此最终没有选择这个方案。这里的30435是查询二级索引的IO成本和CPU成本之和,再加上回表查询聚簇索引的IO成本和CPU成本之和,我就不再具体分析了:
{
	"index": "name_score",
	"ranges": [
		"name84059 < name"
	],
	"rows": 25362,
	"cost": 30435,
	"chosen": false,
	"cause": "cost"
},
  • 使用create_time进行索引扫描需要扫描23758行,成本是28511,同样因为成本原因没有选择这个方案:
{
	"index": "create_time",
	"ranges": [
		"0x5e2a79d0 < create_time"
	],
	"rows": 23758,
	"cost": 28511,
	"chosen": false,
	"cause": "cost"
}
  • 最终选择了全表扫描方式作为执行计划。可以看到,全表扫描100086条记录的成本是20306,和我们之前计算的一致,显然是小于其他两个方案的28511和30435:
{
	"considered_execution_plans": [{
		"table": "`person`",
		"best_access_path": {
			"considered_access_paths": [{
				"rows_to_scan": 100086,
				"access_type": "scan",
				"resulting_rows": 100086,
				"cost": 20306,
				"chosen": true
			}]
		},
		"rows_for_plan": 100086,
		"cost_for_plan": 20306,
		"chosen": true
	}]
},

把SQL中的create_time条件从05:00改为06:00,再次分析OPTIMIZER_TRACE可以看到,这次执行计划选择的是走create_time索引。因为是查询更晚时间的数据,走create_time索引需要扫描的行数从23758减少到了16588。这次走这个索引的成本19907小于全表扫描的20306,更小于走name_score索引的30435:

{
	"index": "create_time",
	"ranges": [
		"0x5e2a87e0 < create_time"
	],
	"rows": 16588,
	"cost": 19907,
	"chosen": true
}

有关optimizer trace的更多信息,你可以参考MySQL的文档

重点回顾

今天,我先和你分析了MySQL InnoDB存储引擎页、聚簇索引和二级索引的结构,然后分析了关于索引的两个误区。

第一个误区是,考虑到索引的维护代价、空间占用和查询时回表的代价,不能认为索引越多越好。索引一定是按需创建的,并且要尽可能确保足够轻量。一旦创建了多字段的联合索引,我们要考虑尽可能利用索引本身完成数据查询,减少回表的成本。

第二个误区是,不能认为建了索引就一定有效,对于后缀的匹配查询、查询中不包含联合索引的第一列、查询条件涉及函数计算等情况无法使用索引。此外,即使SQL本身符合索引的使用条件,MySQL也会通过评估各种查询方式的代价,来决定是否走索引,以及走哪个索引。

因此,在尝试通过索引进行SQL性能优化的时候,务必通过执行计划或实际的效果来确认索引是否能有效改善性能问题,否则增加了索引不但没解决性能问题,还增加了数据库增删改的负担。如果对EXPLAIN给出的执行计划有疑问的话,你还可以利用optimizer_trace查看详细的执行计划做进一步分析。

今天用到的代码,我都放在了GitHub上,你可以点击这个链接查看。

思考与讨论

  1. 在介绍二级索引代价时,我们通过EXPLAIN命令看到了索引覆盖和回表的两种情况。你能用optimizer trace来分析一下这两种情况的成本差异吗?
  2. 索引除了可以用于加速搜索外,还可以在排序时发挥作用,你能通过EXPLAIN来证明吗?你知道,在什么情况下针对排序索引会失效吗?

针对数据库索引,你还有什么心得吗?我是朱晔,欢迎在评论区与我留言分享,也欢迎你把这篇文章分享给你的朋友或同事,一起交流。

精选留言

  • Darren

    2020-03-24 14:21:49

    第一个问题:
    覆盖索引的cost是1.21而回表的是2.21
    覆盖索引:
    analyzing_range_alternatives": {
    "range_scan_alternatives": [
    {
    "index": "name_score",
    "ranges": [
    "name1 <= name <= name1"
    ] /* ranges */,
    "index_dives_for_eq_ranges": true,
    "rowid_ordered": false,
    "using_mrr": false,
    "index_only": true,
    "rows": 1,
    "cost": 1.21,
    "chosen": true
    }
    ]
    回表:
    "range_scan_alternatives": [
    {
    "index": "name_score",
    "ranges": [
    "name1 <= name <= name1"
    ] /* ranges */,
    "index_dives_for_eq_ranges": true,
    "rowid_ordered": false,
    "using_mrr": false,
    "index_only": false,
    "rows": 1,
    "cost": 2.21,
    "chosen": true
    }
    ]

    第二个问题:
    SQL中带order by且执行计划中Extra 这个字段中有"Using index"或者"Using index condition"表示用到索引,并且不用专门排序,因为索引本身就是有序的;
    如果Extra有“Using filesort”表示的就是需要排序;

    排序时:MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。sort_buffer_size(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
    上述排序中,只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 和临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。所以如果单行很大,这个方法效率不够好。max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。称为 rowid 排序;
    rowid排序简单的描述就是先取出ID和排序字段进行排序,排序结束后,用ID回表去查询select中出现的其他字段,多了一次回表操作,
    对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。
    作者回复

    👍🏻

    2020-03-24 15:16:37

  • Darren

    2020-03-24 15:11:04

    另外分享下之前在公司分享的MySQL相关的PPT,主要从MySQL整体架构,引擎、索引、锁和MVCC这及部分分享的,感兴趣的同学可以看看,然后大家交流交流。

    https://github.com/y645194203/geektime-java-100/blob/master/MySQL-%E5%88%86%E4%BA%AB%E4%BA%A4%E6%B5%81%E7%9A%84%E5%89%AF%E6%9C%AC.pptx
    作者回复

    感谢分享

    2020-03-24 15:39:19

  • 袁帅

    2020-03-25 13:35:37

    老师的文章真的是太用心了,讲的太好了,赞👍
    作者回复

    觉得好可以多分享

    2020-03-25 13:50:46

  • 👽

    2020-03-24 15:38:22

    过早的优化,是万恶之源。不需要提前考虑建立索引。等性能出现问题,真正的需求降临的时候再考虑优化。
    建立索引之前,需要考虑索引带来的副作用:维护成本,空间成本,回表成本。
    更重要的是还要考虑,你的查询是否能用到索引。如果花费大量成本建立的索引,最后还用不上。那就赔了夫人又折兵了。
    索引又牵扯到了很多注意事项,例如:尽量使用前缀匹配,而避免使用后缀匹配。因为后缀匹配会使得索引失效,走全表匹配。
    作者回复

    总结的不错

    2020-03-24 15:40:37

  • 马以

    2020-06-02 13:05:11

    老师这里讲的查询优化器选择流程详细一些,可否加个餐,说一说普通索引和唯一索引在什么业务场景下使用?
    作者回复

    可以进一步阅读MySQL实战45讲专栏来了解相关内容,比如:
    09 | 普通索引和唯一索引,应该怎么选择?
    10 | MySQL为什么有时候会选错索引?

    2020-06-02 13:20:28

  • 汝林外史

    2020-03-24 16:28:35

    如果没有主键,就选择第一个不包含 NULL 值的唯一列。
    没有主键的话,mysql不是会为每行数据默认生成一个rowid字段作为主键吗??

    排序索引失效应该也是对索引做了一些函数操作。

    研读过mysql的专栏,所以这一节感觉的知识感觉都了解,不过这种感觉还是很爽的,期待老师更多的干货。
    作者回复

    聚簇索引的规则是:

    When you define a PRIMARY KEY on your table, InnoDB uses it as the clustered index. Define a primary key for each table that you create. If there is no logical unique and non-null column or set of columns, add a new auto-increment column, whose values are filled in automatically.

    If you do not define a PRIMARY KEY for your table, MySQL locates the first UNIQUE index where all the key columns are NOT NULL and InnoDB uses it as the clustered index.

    If the table has no PRIMARY KEY or suitable UNIQUE index, InnoDB internally generates a hidden clustered index named GEN_CLUST_INDEX on a synthetic column containing row ID values. The rows are ordered by the ID that InnoDB assigns to the rows in such a table. The row ID is a 6-byte field that increases monotonically as new rows are inserted. Thus, the rows ordered by the row ID are physically in insertion order.

    因为这些太细节的东西和坑点没有太多关系,为了节省篇幅就不展开说了。

    是的,当你读一些文章的时候发现99%的知识点自己都知道,完全是复习一遍的感觉,这种契合的感觉很舒服

    2020-03-24 17:27:20

  • 6点无痛早起学习的和尚

    2020-04-13 19:10:59

    这一篇文章,我感觉哪里都是重点,哈哈哈哈
  • pedro

    2020-03-24 09:57:00

    第二个问题,我使用 EXPLAIN SELECT create_time from person ORDER BY create_time LIMIT 10; 语句,分析该 SQL,结果果然是 type = index,Extra = using index,证明排序走了索引并且是索引覆盖没有回表;我本以为加上函数就可以使索引失效,于是分别使用了 ORDER BY YEAR(create_time) 和 ORDER BY substring(create_time,1,3),发现 type 还是 index,我的数据库版本是8.0,请问老师,有什么方式可以让索引失效呢?
    作者回复

    select *,你select仅仅是索引列,那么走索引更划算了

    2020-03-24 10:58:44

  • 海战

    2020-03-25 08:39:56

    利用jpa 完成查询,是不是都会发生回表呀,不知道会不会产生影响
    作者回复

    文中回表是指二级索引查询后回聚簇索引,这个和jpa没什么关系

    2020-03-25 10:27:49

  • 三分之二

    2021-01-30 16:56:08

    再使用二分搜索出 #3 槽和 #6 槽的中间位是 (3+6)/2=4.5 取整 4,#4 槽对应的记录是 16>15,所以记录一定在 #4 槽中;
    【所以记录一定在 #4 槽中;】这里应该是#3槽。
  • Geek_3b1096

    2020-03-26 10:27:35

    讲解非常细致谢谢老师
  • Geek_gthxw2

    2020-08-28 08:10:09

    尽量不要在 SQL 语句中 SELECT *,而是 SELECT 必要的字段,实际使用中能做到实在太少了
  • LiG❄️

    2020-04-27 23:00:12

    老师好优秀,讲的非常透彻~自己还要要实操,加深理解!
    作者回复

    :)

    2020-04-28 09:50:12

  • hellojd

    2020-03-28 15:57:57

    有时候一个表索引太多,也会带来成本
    作者回复

    是的

    2020-03-28 17:52:07

  • pedro

    2020-03-24 10:15:15

    第一个问题,我仔细对比了 optimizer trace 下的索引覆盖和回表,发现索引覆盖的如下项:
    "range_scan_alternatives": [
    {
    "index": "name_score",
    "ranges": [
    "name1 <= name <= name1"
    ],
    "index_dives_for_eq_ranges": true,
    "rowid_ordered": false,
    "using_mrr": false,
    "index_only": true,
    "rows": 1,
    "cost": 1.1056,
    "chosen": true
    }
    我猜测是 index_only =true 告诉我们这是索引覆盖而不需回表,可以索引覆盖的 cost_for_plan 为 1.1056 而回表的 cost_for_plan 为 0.61,这是不是证明索引覆盖所付出的损耗还要高于回表了?
    作者回复

    我这边做实验的结果在源码的coveringindex.jpg中可以看一下

    2020-03-24 11:00:44

  • joyboy

    2024-05-22 17:23:57

    学习这篇文章的收获:
    1.使用索引来加快性能,是以空间换时间,越多索引以为着越多空间的浪费,越多的维护代价,所以建立索引要确保查询时能够用上索引,不然就纯坏处没好处了。
    2.为了能够确保用上索引,可以通过explain查看执行计划,但是要注意,某个查询用不上索引不代表一直用不上索引,可以使用optimizer trace来跟踪更细节的执行计划。
    3.确保用上索引之后,要看看能不能更高效的使用索引,比如使用多列索引来替换单列索引,加大查询索引覆盖的几率的同时还可以减少索引数量。
    4.使用索引之前,最好将索引相关的知识都了解清楚,这样才能方便用好索引。
  • fusion

    2023-09-11 13:42:23

    ”使用 name_score 对 name84059条件进行索引扫描需要扫描 25362 行,成本是 30435,因此最终没有选择这个方案。这里的 30435 是查询二级索引的 IO 成本和 CPU 成本之和,再加上回表查询聚簇索引的 IO 成本和 CPU 成本之和,我就不再具体分析了“

    老师,能否分析下,如何得出cost 30435?你上面只讲了全表扫描的cost计算方法。
  • Ewin

    2023-03-21 11:30:36

    有一个疑问没想明白,非聚集索引,比如条件 age = 18, 在表中对应了1万条数据,在B+ 树的叶子节点上是怎么存储,age 与对应主键的关系的? 是下面哪种形式,或者都不是?
    1、age = 18 -> 1, age = 18 -> 2, age = 18 -> 3, ......
    2、age = 18 -> 对应主键的一个范围,比如形如这样, age = 18 -> (1, 10000)
  • 夏天#风之友

    2022-04-12 16:45:13

    老师,你好,看到文章很受启发。我这里有个问题请教一下,项目经常遇到多条件查询,例如,记录表数量级大,想着添加多字段的二级索引(比如 index_test(a,b,c))来提升效率,但由于查询条件字段是动态拼接,数量不定(比如:abc,ac,bc,a,b,c)。这样会导致无法每次查询都会命中索引。 像这样的情况,老师有什么更好的查询方案?感谢
    作者回复

    数量大,字段多,这种搜索就用ES好了

    2022-04-13 09:39:04

  • SharpBB

    2022-03-13 15:00:20

    秀啊 特别是不走索引的情况 原来mysql会自行判断成本 难怪dba是一个单独的职位 数据库方面的学问很深啊