为什么mysql使用错误的索引来按查询排序?


9

这是我的表,具有约10,000,000行数据

CREATE TABLE `votes` (
  `subject_name` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `subject_id` int(11) NOT NULL,
  `voter_id` int(11) NOT NULL,
  `rate` int(11) NOT NULL,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`subject_name`,`subject_id`,`voter_id`),
  KEY `IDX_518B7ACFEBB4B8AD` (`voter_id`),
  KEY `subject_timestamp` (`subject_name`,`subject_id`,`updated_at`),
  KEY `voter_timestamp` (`voter_id`,`updated_at`),
  CONSTRAINT `FK_518B7ACFEBB4B8AD` FOREIGN KEY (`voter_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

这是指数基数

在此处输入图片说明

因此,当我执行此查询时:

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

我期望它使用索引,voter_timestamp 但是mysql选择使用它来代替:

explain select SQL_NO_CACHE * from votes  where subject_name = 'medium' and voter_id = 1001 and rate = 1 order by updated_at desc limit 20 offset 100;`

type:
    index_merge
possible_keys: 
    PRIMARY,IDX_518B7ACFEBB4B8AD,subject_timestamp,voter_timestamp
key:
    IDX_518B7ACFEBB4B8AD,PRIMARY
key_len:
    102,98
ref:
    NULL
rows:
    9255
filtered:
    10.00
Extra:
    Using intersect(IDX_518B7ACFEBB4B8AD,PRIMARY); Using where; Using filesort

而且我得到了200-400ms的查询时间。

如果我强迫它使用正确的索引,例如:

SELECT SQL_NO_CACHE * FROM votes USE INDEX (voter_timestamp) WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

MySQL可以在1-2ms内返回结果

这是解释:

type:
    ref
possible_keys:
    voter_timestamp
key:
    voter_timestamp
key_len:
    4
ref:
    const
rows:
    18714
filtered:
    1.00
Extra:
    Using where

那么mysql为什么不voter_timestamp为我的原始查询选择索引呢?

我曾试图为analyze table votesoptimize table votes,丢弃索引,然后重新添加,但MySQL的仍然使用了错误的指标。不太明白是什么问题。


1
@ypercubeᵀᴹ我认为没有必要对where条件中的所有列建立索引,如您所见,如果我强制使用(voter_id,updated_at)索引,它可以使用它并且非常高效。如果我删除的subject_name = "medium"部分,也可以选择合适的指数,没有必要指标rate
凤凰城

尽管如此,4列索引将比2索引更有效(voter_id, updated_at)。另一个索引将是(voter_id, subject_name, updated_at)(subject_name, voter_id, updated_at)(不包含费率)。
ypercubeᵀᴹ

1
是的,在某些方面,您是对的。您不需要 4列索引。这只是此查询的最佳索引。2列(您认为是正确的)可能适合您当前拥有的数据和分布。如果分配不同,它可能会很可怕。示例:假设99%的行的比率大于1,只有1%的比率= 1。您认为使用2栏索引是否有效?
ypercubeᵀᴹ

它必须遍历索引的很大一部分并在表上进行数千次查找,才发现比率> 1并拒绝行,直到找到120个满足无法由索引判断的标准的行(subject_name='medium' and rate=1
ypercubeᵀᴹ

凤凰城ypercube -除非索引首先满足所有过滤条件LIMITORDER BY否则MySQL不会进入,甚至无法进入。也就是说,如果没有完整的4列,它将收集所有相关行,对所有行进行排序,然后选择LIMIT随着 4列索引,查询可以看完避免排序和停止LIMIT行。
瑞克·詹姆斯

Answers:


5

MySQL使用相对简单(比其他RDBMS简单)的成本模型来计划查询,其中过滤数据集具有很高的优先级。在您使用合并索引的第一个查询中,估计将需要扫描约9000行,而使用索引提示的第二个查询将需要18000行。 。您可以通过打开optimizer_trace,运行查询并评估结果来确认这一点(或找到其他原因)。

set global optimizer_trace='enabled=on';

-- run your query 

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

select * from information_schema.`OPTIMIZER_TRACE`;

关于index_merge以下几点:在大多数情况下,您会发现它非常昂贵。尽管对于OLAP类型的方案非常有用,但可能不适用于OLTP,因为该操作可能会花费大量的查询时间,并且您可以看到有时次佳的执行计划实际上更快。

幸运的是,MySQL为优化器提供了开关,因此您可以根据需要自定义它。

对于所有选项,您都可以运行:

show global variables like 'optimizer_switch';

要更改一个,您无需复制就粘贴整个字符串。它的工作方式如dict.update()python。

 set global optimizer_switch='index_merge=off';

如果可能的话,我还将看一下您的表结构并进行改进。真正建议不要使用〜100字节的主键和许多辅助键。

您有四个辅助键,其中一些是多余的,例如(voter_id)index是(voter_id, updated_at)


MySQL很少使用“索引合并相交”。在所有情况下,拥有更多列的索引要好得多。“索引合并联合”有时很有用;转ORUNION经常是一样好或更好。
瑞克·詹姆斯

5

对于该查询,您需要以下索引:

INDEX(voter_id, rate, subject_name, updated_at)

updated_at必须是最后一个; 其他三个可以任意顺序排列。(ypercube的三列索引不是很有用,因为它们在到达WHERE列之前不会结束ORDER BY列。)

添加此索引时,您可能会摆脱所有其他辅助键:

KEY IDX_518B7ACFEBB4B8ADvoter_id) -的FK可以使用我的索引键subject_timestampsubject_namesubject_idupdated_at) -主要是多余的KEY voter_timestampvoter_idupdated_at), -可能是你的企图

使用4列索引,您就有机会优化“分页”并避免OFFSET请参阅此博客。

关于另一个主题...当我看到X_name和时X_id,我认为“规范化”正在进行中。我希望看到表中的这两列,几乎没有其他内容。我希望在其他表格中看到这两者

(voter_id, updated_at)不会过去,voter_id因为它尚未完成过滤(WHERE)。然后,由于另一个索引较小,因此将其选中。我的3列用于过滤,然后列ORDER BY

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.