如何在 MySQL 中使用前缀索引进行仅索引扫描
How to use a prefix index for an index-only scan in MySQL
想象一下我有一个(巨大的)table 像这样:
category type
-------- ----
foo EC22
foo EC00
bar EC00
bar EDC0
... ...
type
中的前两个字符具有特殊含义,我只对那些用于 SELECT
目的的字符感兴趣。我想在 type
上使用带有前缀的复合索引,如下所示:category, type(2)
现在当我这样做时:
EXPLAIN SELECT category, type FROM table
WHERE category = 'foo'
AND LEFT(type,2) = 'EC'
... 它告诉我 MySQL 是 Using index condition;
(意味着读取行以双重检查索引)。
我想使用索引值为 EC
的所有内容,并继续进行其余的仅索引扫描。例如。 EXPLAIN
告诉我 Using index;
(没有 condition
)。无需仔细检查该字段的实际值,因为我只查看前两个字符。是否有可能实现或强制执行此操作?
更新
我可以 SET optimizer_switch='index_condition_pushdown=off';
,然后 EXPLAIN
从 Using index lookup;
更改为 Using where;
,速度提高了大约 15%。我想我不完全确定这里发生了什么以及我如何看到我的查询是单独使用索引完成的。
当EXPLAIN
显示"Using index"时,表示该索引为覆盖索引查询.也就是说,查询可以完全从索引块中得到满足,而不需要在底层 table 块中查找任何行。
再看看你的查询。请注意,它返回 type
列(SELECT 列表中的表达式。)这是整个列。整个列在索引中不可用。
因此该索引不能是查询的 覆盖 索引,因此 MySQL 永远不会在 EXPLAIN 中显示 'Using index'输出(带有那个查询和那个索引。)
由于它不是查询的覆盖索引,MySQL 将不得不查找基础数据页以获取列的值,以便返回它。
现在至于索引是否被用于检查条件 LEFT(type,2) = 'EC'
,我们需要检查 EXPLAIN 输出中的 key_len
。
当 type
列上没有条件时,我们可以比较 EXPLAIN 中的 key_len
和有条件时。我还会使用 type LIKE 'EC%'
.
等条件进行测试
我将从解释中比较 key_len
所有这些:
SELECT category, type FROM huge_table WHERE category = 'bar' ;
SELECT category, type FROM huge_table WHERE category = 'bar' AND type LIKE 'E%' ;
SELECT category, type FROM huge_table WHERE category = 'bar' AND type LIKE 'EC%' ;
SELECT category, type FROM huge_table WHERE category = 'bar' AND LEFT(type,1) = 'E' ;
SELECT category, type FROM huge_table WHERE category = 'bar' AND LEFT(type,2) = 'EC' ;
如果 key_len
在所有这些情况下都相同(即只有 category
列的长度),那么这表明 MySQL 没有使用索引来检查 LEFT(type,2) =
条件。
你是对的。 MySQL 在检查条件之前正在访问基础数据页。
但是如果 key_len
在某些情况下更长,这表明 MySQL 在查找行之前正在检查索引中的条件。
对于不包含 SELECT 列表中的 type
列的查询,您可能还会得到 EXPLAIN。
(我同意斯宾塞的回答;这个回答增加了更多内容。)
"I am only allowed to create indices" -- 如果那条法令来自管理层,我建议你热身一下你的简历。
INDEX(category, type)
并改变
AND LEFT(type,2) = 'EC'
至
AND type LIKE 'EC%'
是一级优化。现在它将使用 INDEX
中的 both 个字段。并且,假设查询完全如声明的那样,索引将是 "covering",这意味着它不需要在索引和数据之间反弹,而是可以在索引 BTree 中进行整个查询。
第二层优化是看type
是否可以是一个ENUM
,只有1个字节。这使得 table 和索引每一个几十亿字节更小。 (这个建议可能不切实际,因为你的 "type" 不是典型的,只有几个不同的值,没有 "prefixing"。)
至于为什么 "using Where" 快了 15%... 可能 如下:
- 优化器看到
WHERE
和 INDEX
并说 "hot-digitty; this is a pretty good index; let me use it!"。然后花了很多时间在索引和数据之间跳来跳去。在 "using Where" 中,它在索引上进行操作并简单地扫描数据——可以跳过更多行,但不会来回跳动。 (优化器没有足够的 good 统计数据来始终在两者之间进行选择。在您的示例中,微不足道的统计数据误导了它。)或,一些的数据 and/or 索引 BTree 当时被(或没有)缓存。 运行 再次计时;你可能会得到不同的结论。 (典型范围:2x。)
"Using index condition"(又名,ICP = Index Condition Pushdown)也意味着引擎 (InnoDB) 获取行并测试 LEFT(type, 2) = 'EC'
。在旧版本中(在 ICP 之前),InnoDB 获取行,但必须将它 "up" 发送到 "handler" 以执行测试。旧方法使速度减慢了约 2 倍。但是,正如您所说,必须获取该行。获取行是效率低下的最重要部分。
对于 1.2B 行,缓存 (innodb_buffer_pool_size
) 中是否有空间容纳所有数据和所有索引?如果数据是 400GB,则可能不会。你有多少内存? buffer_pool 是该设置的大约 70% 吗?
至于 "prefix" 索引 (type(2)
) -- 它们实际上是无用的;您的代码就是原因的示例。我告诉人们要避开他们。
如果您的 types
总是 4 个英语 digits/letters,那么从索引中删除 (2)
只需花费 2.4GB。 这可能是您问题的最佳答案。
另一个想法...MySQL 5.7 和 MariaDB 有 "generated/virtual columns"。您可以为 LEFT(type,2)
创建这样的索引并将其编入索引。您需要更改查询以引用该新列。该列(如果不是 'persisted')将不会在 table 中占用 space;索引将使用新列并且不会比现有的 (category, type(2))
大。因此,如果我在本段中所说的一切都奏效,您将获得所需的速度,而无需消耗额外的磁盘 space!
想象一下我有一个(巨大的)table 像这样:
category type
-------- ----
foo EC22
foo EC00
bar EC00
bar EDC0
... ...
type
中的前两个字符具有特殊含义,我只对那些用于 SELECT
目的的字符感兴趣。我想在 type
上使用带有前缀的复合索引,如下所示:category, type(2)
现在当我这样做时:
EXPLAIN SELECT category, type FROM table
WHERE category = 'foo'
AND LEFT(type,2) = 'EC'
... 它告诉我 MySQL 是 Using index condition;
(意味着读取行以双重检查索引)。
我想使用索引值为 EC
的所有内容,并继续进行其余的仅索引扫描。例如。 EXPLAIN
告诉我 Using index;
(没有 condition
)。无需仔细检查该字段的实际值,因为我只查看前两个字符。是否有可能实现或强制执行此操作?
更新
我可以 SET optimizer_switch='index_condition_pushdown=off';
,然后 EXPLAIN
从 Using index lookup;
更改为 Using where;
,速度提高了大约 15%。我想我不完全确定这里发生了什么以及我如何看到我的查询是单独使用索引完成的。
当EXPLAIN
显示"Using index"时,表示该索引为覆盖索引查询.也就是说,查询可以完全从索引块中得到满足,而不需要在底层 table 块中查找任何行。
再看看你的查询。请注意,它返回 type
列(SELECT 列表中的表达式。)这是整个列。整个列在索引中不可用。
因此该索引不能是查询的 覆盖 索引,因此 MySQL 永远不会在 EXPLAIN 中显示 'Using index'输出(带有那个查询和那个索引。)
由于它不是查询的覆盖索引,MySQL 将不得不查找基础数据页以获取列的值,以便返回它。
现在至于索引是否被用于检查条件 LEFT(type,2) = 'EC'
,我们需要检查 EXPLAIN 输出中的 key_len
。
当 type
列上没有条件时,我们可以比较 EXPLAIN 中的 key_len
和有条件时。我还会使用 type LIKE 'EC%'
.
我将从解释中比较 key_len
所有这些:
SELECT category, type FROM huge_table WHERE category = 'bar' ;
SELECT category, type FROM huge_table WHERE category = 'bar' AND type LIKE 'E%' ;
SELECT category, type FROM huge_table WHERE category = 'bar' AND type LIKE 'EC%' ;
SELECT category, type FROM huge_table WHERE category = 'bar' AND LEFT(type,1) = 'E' ;
SELECT category, type FROM huge_table WHERE category = 'bar' AND LEFT(type,2) = 'EC' ;
如果 key_len
在所有这些情况下都相同(即只有 category
列的长度),那么这表明 MySQL 没有使用索引来检查 LEFT(type,2) =
条件。
你是对的。 MySQL 在检查条件之前正在访问基础数据页。
但是如果 key_len
在某些情况下更长,这表明 MySQL 在查找行之前正在检查索引中的条件。
对于不包含 SELECT 列表中的 type
列的查询,您可能还会得到 EXPLAIN。
(我同意斯宾塞的回答;这个回答增加了更多内容。)
"I am only allowed to create indices" -- 如果那条法令来自管理层,我建议你热身一下你的简历。
INDEX(category, type)
并改变
AND LEFT(type,2) = 'EC'
至
AND type LIKE 'EC%'
是一级优化。现在它将使用 INDEX
中的 both 个字段。并且,假设查询完全如声明的那样,索引将是 "covering",这意味着它不需要在索引和数据之间反弹,而是可以在索引 BTree 中进行整个查询。
第二层优化是看type
是否可以是一个ENUM
,只有1个字节。这使得 table 和索引每一个几十亿字节更小。 (这个建议可能不切实际,因为你的 "type" 不是典型的,只有几个不同的值,没有 "prefixing"。)
至于为什么 "using Where" 快了 15%... 可能 如下:
- 优化器看到
WHERE
和INDEX
并说 "hot-digitty; this is a pretty good index; let me use it!"。然后花了很多时间在索引和数据之间跳来跳去。在 "using Where" 中,它在索引上进行操作并简单地扫描数据——可以跳过更多行,但不会来回跳动。 (优化器没有足够的 good 统计数据来始终在两者之间进行选择。在您的示例中,微不足道的统计数据误导了它。)或,一些的数据 and/or 索引 BTree 当时被(或没有)缓存。 运行 再次计时;你可能会得到不同的结论。 (典型范围:2x。)
"Using index condition"(又名,ICP = Index Condition Pushdown)也意味着引擎 (InnoDB) 获取行并测试 LEFT(type, 2) = 'EC'
。在旧版本中(在 ICP 之前),InnoDB 获取行,但必须将它 "up" 发送到 "handler" 以执行测试。旧方法使速度减慢了约 2 倍。但是,正如您所说,必须获取该行。获取行是效率低下的最重要部分。
对于 1.2B 行,缓存 (innodb_buffer_pool_size
) 中是否有空间容纳所有数据和所有索引?如果数据是 400GB,则可能不会。你有多少内存? buffer_pool 是该设置的大约 70% 吗?
至于 "prefix" 索引 (type(2)
) -- 它们实际上是无用的;您的代码就是原因的示例。我告诉人们要避开他们。
如果您的 types
总是 4 个英语 digits/letters,那么从索引中删除 (2)
只需花费 2.4GB。 这可能是您问题的最佳答案。
另一个想法...MySQL 5.7 和 MariaDB 有 "generated/virtual columns"。您可以为 LEFT(type,2)
创建这样的索引并将其编入索引。您需要更改查询以引用该新列。该列(如果不是 'persisted')将不会在 table 中占用 space;索引将使用新列并且不会比现有的 (category, type(2))
大。因此,如果我在本段中所说的一切都奏效,您将获得所需的速度,而无需消耗额外的磁盘 space!