0

尽管查询很简单,但下面的执行计划似乎令人失望和次优。

我正在使用 MySQL 5.7。这是小提琴(虽然它只提供 5.6)。

CREATE TABLE `event` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(63) CHARSET ASCII COLLATE ASCII_BIN NOT NULL,
  `is_sequenced` TINYINT(3) UNSIGNED NOT NULL,
  `sequence_number` BIGINT(20) UNSIGNED DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `Name-SequenceNumber` (`name`,`sequence_number`),
  KEY `Name-IsSequenced` (`name`,`is_sequenced`,`id`)
) ENGINE=INNODB
;

INSERT INTO `event`
(id, `name`, is_sequenced, sequence_number)
VALUES
(NULL, 'OrderCreated', 0, NULL),
(NULL, 'OrderCreated', 0, NULL),
(NULL, 'OrderCreated', 0, NULL),
(NULL, 'OrderCreated', 0, NULL),
(NULL, 'OrderCreated', 0, NULL),
(NULL, 'OrderCreated', 0, NULL),
(NULL, 'OrderCreated', 0, NULL),
(NULL, 'OrderCreated', 0, NULL),
(NULL, 'OrderCreated', 0, NULL)
;

我们将使用Name-IsSequenced二级索引。让我们尝试以下EXPLAIN. (查询在小提琴中。打开“查看执行计划”以查看其EXPLAIN结果。)

EXPLAIN
SELECT * -- This part needs the PK
FROM `event` e
WHERE e.name = 'OrderCreated'
AND e.is_sequenced = 0
AND e.id <= 3
;

到目前为止,一切都很好。Using index condition有道理:整个条件可以在预期的索引上解决Name-IsSequenced,然后需要PK来获取剩余的数据SELECT *

Using index如果我们只选择二级索引的一部分,我们应该能够改进它,对吧?(请注意,PK 始终是任何二级索引的一部分,但我们甚至可以通过id在二级索引的末尾添加来确保这一点。结果应该是一样的。)

EXPLAIN
SELECT id
FROM `event` e
WHERE e.name = 'OrderCreated'
AND e.is_sequenced = 0
AND e.id <= 3
;

现在,结果是Using where; Using index。等等,这……更糟?!我们减少了它的工作量,而计划表明它正在更加努力地工作。

Using index应该是可以实现的。找到范围 where name=OrderCreated,然后在其中找到子范围 where is_sequenced=0,然后在里面找到子范围 where id<=3

奇怪的是,我还有其他实验(有更多数据),我可以Using index通过更改id<=3来获得id=3(结合FORCE INDEX以防止它更喜欢 PK)。我看不出差异的原因。(如果我们用 Fiddle 试试这个,它保持不变 - 可能是因为数据集小。)

谁能解释为什么执行计划没有表明二级索引的预期有效使用?有没有办法把它弄直?

4

1 回答 1

1
WHERE e.name = 'OrderCreated'
  AND e.is_sequenced = 0
  AND e.id <= 3

规则很简单:先做=列,以任何顺序。然后你会在一个“范围”上得到一个裂缝。

INDEX(name, is_sequenced, -- in either order
      id)                 -- last

不要听老太婆根据基数排序的故事。

使用SELECT id,该索引包含所有需要的列,因此它是“覆盖”,如EXPLAIN's“使用索引”所示。

随着SELECT *索引的缺失sequence_number。因此,它有两种执行方式:

方案A:使用索引;对于索引的 BTree 中的每一行相关行,伸入数据的 BTree(通过id)以找到丢失的列。

方案 B:避开索引并简单地扫描由PRIMARY KEY(id). 不过你看,id < 3其实是一个相当不错的PK使用。可能会EXPLAINPRIMARYand Range

优化器将在计划之间做出半智能选择,通常会选择更好的。

计划 C:计划 A 可以改进。添加sequence_number最后)使INDEX(name, is_sequenced, id, sequence_number). 现在你得到“覆盖”(“使用索引”)和最快的索引。

更多讨论:http: //mysql.rjweb.org/doc.php/index_cookbook_mysql

大约 5.6 / 5.7 / 8.0,优化器进行了很多改进。它转向“基于成本的模型”,在该模型中使用索引统计等来计算每个可能的执行计划的成本估计。这项更改大肆宣传,但对查询计划的净影响很小。没有模型做得好的一个领域是,如果WHERE子句在JOIN. ORDER BY和/或LIMIT将额外的活动扳手投入战斗。

ANALYZE TABLE曾经对“修复”统计数据很重要;5.6在这方面做了根本的改进。尽管如此,“统计数据”并不完美。

id=3- 好吧,您要求所有列,并且使用 PK 具有所有列,那么为什么还要考虑一些二级索引。(PK 与数据“聚集”在一起。)即使有一个同样好的索引,数据也更有可能缓存在 RAM 中。(成本模型 [尚未] 考虑缓存或 SSD 与 HDD。)

作为经验法则(根据经验确定),如果需要超过 20% 的二级索引,则将避开二级索引。假定辅助 BTree 和 Data BTree 之间的来回跳动比简单地扫描数据成本更高。在您的表中,需要 30% 的索引。QED。(实际上,这是优化器有时“弄错”的另一个灰色区域。)

于 2020-01-15T04:15:02.770 回答