您提供的示例假定使用了 InnoDB。假设PRIMARY KEY 就是id。
INDEX(sex, rating)
是“辅助键”。每个辅助键(在 InnoDB 中)都隐含地包含 PK,因此它实际上是 (sex, rating, id) 值的有序列表。为了获取“数据”(<cols>),它使用id 向下钻取PK BTree(也包含数据)以查找记录。
快速案例:因此,
SELECT id FROM profiles
WHERE x.sex='M' ORDER BY rating LIMIT 100000, 10
将对索引中的 100010 个“行”进行“范围扫描”。这对于 I/O 来说非常有效,因为所有信息都是连续的,没有任何浪费。 (不,跳过 100000 行是不够聪明的;那会很混乱,尤其是考虑到 transaction_isolation_mode 时。)这 100010 行可能适合大约 1000 个索引块。然后得到id的10个值。
使用这 10 个 id,它可以进行 10 次连接(“NLJ”=“嵌套循环连接”)。这 10 行很可能分散在桌子周围,可能需要对磁盘进行 10 次点击。
让我们“计算磁盘命中数”(忽略 BTree 中的非叶节点,它们很可能会被缓存):1000 + 10 = 1010。在普通磁盘上,这可能需要 10 秒。
Slow Case:现在让我们看看原始查询 (SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 100000, 10;)。让我们继续假设INDEX(sex, rating) 加上最后隐含的id。
和以前一样,它将索引扫描 100010 行(估计 1000 次磁盘命中)。但事实上,做上面所做的事情太愚蠢了。它将进入数据以获取<cols>。这通常(取决于缓存)需要随机磁盘命中。这可能超过 100010 次磁盘命中(如果表很大并且缓存不是很有用)。
再一次,100000 被抛,10 被交付。总“成本”:100010 次磁盘命中(最坏情况),可能需要 17 分钟。
请记住,高性能 MySQL 有 3 个版本;它们是在过去 13 年左右的时间里写成的。您可能使用的 MySQL 版本比他们介绍的要新得多。我不知道优化器是否在这方面变得更聪明了。这些,如果对你可用,可能会提供线索:
EXPLAIN FORMAT=JSON SELECT ...;
OPTIMIZER TRACE...
我最喜欢的用于研究事物如何工作的“处理程序”技巧可能会有所帮助:
FLUSH STATUS;
SELECT ...
SHOW SESSION STATUS LIKE 'Handler%'.
您可能会看到 100000 和 10 之类的数字,或者是此类数字的小倍数。但是,请记住,索引的快速范围扫描计为每行 1 个,对于大量 <cols> 的慢速随机磁盘命中也是如此。
概述:为了使这项技术发挥作用,子查询需要一个“覆盖”索引,并且列的顺序正确。
“覆盖”表示(sex, rating, id) 包含所有触及的列。 (我们假设<cols> 包含其他列,可能是在INDEX 中无法使用的庞大列。)
列的“正确”排序:列的顺序正好可以通过查询。 (另见my cookbook。)
- 首先将任何
WHERE 列与= 比较到常量。 (sex)
- 然后依次出现整个
ORDER BY。 (rating)
- 最后是“覆盖”。 (
id)