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

Pho*_*nix 10 mysql index order-by mysql-5.7

这是我的表,其中包含约 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;
Run Code Online (Sandbox Code Playgroud)

这是索引基数

在此处输入图片说明

所以当我做这个查询时:

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;
Run Code Online (Sandbox Code Playgroud)

我原以为它使用索引,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
Run Code Online (Sandbox Code Playgroud)

我得到了 200-400 毫秒的查询时间。

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

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;
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

我尝试过的是analyze table votes, optimize table votes, 删除该索引并再次添加它,但 mysql 仍然使用错误的索引。不太明白是什么问题。

Kár*_*agy 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`;
Run Code Online (Sandbox Code Playgroud)

一句话评论index_merge:在大多数情况下,您会发现它非常昂贵。尽管对于 OLAP 类型的场景非常有用,但它可能不太适合 OLTP,因为该操作可能会占用查询的大量时间,并且正如您所看到的,有时次优执行计划实际上更快。

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

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

show global variables like 'optimizer_switch';
Run Code Online (Sandbox Code Playgroud)

要更改一个,您不必复制粘贴整个字符串。它的工作原理类似于dict.update()python。

 set global optimizer_switch='index_merge=off';
Run Code Online (Sandbox Code Playgroud)

如果可能的话,我也会看看你的表结构并改进。真的不建议拥有一个约 100 字节的主键和许多辅助键。

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


Ric*_*mes 5

对于那个查询,你需要这个索引:

INDEX(voter_id, rate, subject_name, updated_at)
Run Code Online (Sandbox Code Playgroud)

updated_at必须是最后一个; 其他三个可以按任何顺序排列。(ypercube 的 3 列索引不是很有用,因为它们WHERE在击中列之前没有完成ORDER BY列。)

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

KEY IDX_518B7ACFEBB4B8AD( voter_id), -- FK 可以使用我的索引 KEY subject_timestamp( subject_name, subject_id, updated_at), -- 主要是多余的 KEY voter_timestamp( voter_id, updated_at), -- 可能是你的尝试

使用 4 列索引,您有机会优化“分页”并避免OFFSET. 看到这个博客。

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

(voter_id, updated_at)不会过去,voter_id因为它还没有完成过滤 (the WHERE)。然后,由于其他索引较小,因此将其选中。我的有 3 列来处理过滤,然后是ORDER BY.