10.2.1.2 Range 优化
range
访问方法使用单个索引来检索包含在一个或多个索引值区间内的表行的子集。它可以用于单列或多列索引。以下部分描述了优化器使用范围访问的条件。
单列索引的范围访问方法
对于单列索引,索引值区间可以用WHERE
子句中的相应条件方便地表示,这些条件被称为范围条件,而不是“区间”。
单列索引的范围条件定义如下:
“常量值”在前面的描述中指的是以下之一:
以下是一些使用范围条件的查询示例:
SELECT * FROM t1
WHERE key_col > 1
AND key_col < 10;
SELECT * FROM t1
WHERE key_col = 1
OR key_col IN (15,18,20);
SELECT * FROM t1
WHERE key_col LIKE 'ab%'
OR key_col BETWEEN 'bar' AND 'foo';
一些非常量值可能在优化器常量传播阶段被转换为常量。
MySQL 尝试从 WHERE 子句中提取可能的索引。提取过程中,不能用于构建范围条件的条件将被丢弃,重叠的条件将被组合,空范围的条件将被删除。
考虑以下语句,其中key1
是索引列,nonkey
不是索引:
SELECT * FROM t1 WHERE
(key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
(key1 < 'bar' AND nonkey = 4) OR
(key1 < 'uux' AND key1 > 'z');
对于key1
索引的提取过程如下:
-
从原始 WHERE 子句开始:
(key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR (key1 < 'bar' AND nonkey = 4) OR (key1 < 'uux' AND key1 > 'z')
-
删除
nonkey = 4
和key1 LIKE '%b'
,因为它们不能用于范围扫描。正确的删除方法是将它们替换为TRUE
,以避免在范围扫描时遗漏匹配行。将它们替换为TRUE
后,结果是:(key1 < 'abc' AND (key1 LIKE 'abcde%' OR TRUE)) OR (key1 < 'bar' AND TRUE) OR (key1 < 'uux' AND key1 > 'z')
-
将总是真的或假的条件折叠:
-
(key1 LIKE 'abcde%' OR TRUE)
总是真的 -
(key1 < 'uux' AND key1 > 'z')
总是假的
将这些条件替换为常量后:
(key1 < 'abc' AND TRUE) OR (key1 < 'bar' AND TRUE) OR (FALSE)
删除不必要的
TRUE
和FALSE
常量后:(key1 < 'abc') OR (key1 < 'bar')
-
-
将重叠的时间区间合并到一个中,用于最终的范围扫描:
(key1 < 'bar')
一般来说(如前面的示例所示),用于范围扫描的条件比WHERE子句更不严格。MySQL 还将执行额外的检查,以过滤出满足范围条件但不满足完整WHERE子句的行。
范围条件提取算法可以处理任意深度的嵌套AND
/OR
构造,并且输出不依赖于WHERE子句中的条件顺序。
MySQL 不支持将多个范围合并到range
访问方法中用于空间索引。要绕过这个限制,可以使用UNION
与相同的SELECT
语句,只是将每个空间谓词放在不同的SELECT
语句中。
Range Access Method for Multiple-Part 索引
多个部分索引上的范围条件是单个部分索引上的范围条件的扩展。多个部分索引上的范围条件将索引行限制在一个或多个键元组间。键元组间是使用索引中的键元组顺序定义的。
例如,考虑一个多个部分索引定义为 key1(
,并且以下键元组列表按键顺序列出:key_part1
, key_part2
, key_part3
)
key_part1 key_part2 key_part3
NULL 1 'abc'
NULL 1 'xyz'
NULL 2 'foo'
1 1 'abc'
1 1 'xyz'
1 2 'abc'
2 1 'aaa'
条件
定义了这个间隔:key_part1
= 1
(1,-inf,-inf) <= (key_part1,key_part2,key_part3) < (1,+inf,+inf)
这个间隔涵盖了前面的数据集中第4、5和6个元组,可以被范围访问方法使用。
相比之下,条件
不定义单个间隔且不能被范围访问方法使用。key_part3
= 'abc'
以下描述将对多个部分索引上的范围条件进行更详细的描述。
-
对于
HASH
索引,每个包含相同值的间隔可以被使用。这意味着间隔只能在以下形式的条件中被生产:key_part1 cmp const1 AND key_part2 cmp const2 AND ... AND key_partN cmp constN;
其中
const1
,const2
,… 是常数,cmp
是=
,<=>
或IS NULL
比较操作符,且条件涵盖所有索引部分。 (即存在N
个条件,每个索引部分一个。) 例如,以下是一个三部分HASH
索引的范围条件:key_part1 = 1 AND key_part2 IS NULL AND key_part3 = 'foo'
关于常量的定义,请参见Range Access Method for Single-Part Indexes。
-
对于一个
BTREE
索引,可能可以使用间隔来处理与AND
组合的条件,其中每个条件将一个键部分与常量值进行比较使用=
、<=>
、IS NULL
、>
、<
、>=
、<=
、!=
、<>
、BETWEEN
或LIKE '
(其中pattern
''
不以通配符开头)。可以使用间隔,只要可以确定一个包含所有匹配条件的键元组(或两个间隔如果使用pattern
'<>
或!=
)。优化器尝试使用额外的键部分来确定间隔,直到比较操作符是
=
、<=>
或IS NULL
。如果操作符是>
、<
、>=
、<=
、!=
、<>
、BETWEEN
或LIKE
,优化器使用它,但不考虑更多的键部分。对于以下表达式,优化器使用=
从第一个比较中。它还使用>=
从第二个比较中,但不考虑更多的键部分,不使用第三个比较来构建间隔:key_part1 = 'foo' AND key_part2 >= 10 AND key_part3 > 10
单个间隔是:
('foo',10,-inf) < (key_part1,key_part2,key_part3) < ('foo',+inf,+inf)
可能创建的间隔包含的行数更多于初始条件。例如,前一个间隔包括值
('foo', 11, 0)
,该值不满足原始条件。 -
如果包含行集的条件与
OR
组合,形成的条件包含行集在它们的并集中的行集。如果条件与AND
组合,形成的条件包含行集在它们的交集中的行集。例如,对于这个在两个部分索引上的条件:(key_part1 = 1 AND key_part2 < 2) OR (key_part1 > 5)
间隔是:
(1,-inf) < (key_part1,key_part2) < (1,2) (5,-inf) < (key_part1,key_part2)
在这个示例中,第一个间隔使用一个键部分作为左边界和两个键部分作为右边界。第二个间隔只使用一个键部分。
key_len
列在EXPLAIN
输出中表示键前缀的最大长度。在某些情况下,
key_len
可能表示键部分被使用,但这可能不是你期望的。假设key_part1
和key_part2
可以是NULL
。然后,key_len
列显示两个键部分的长度,以便于以下条件:key_part1 >= 1 AND key_part2 < 2
然而,实际上该条件被转换为:
key_part1 >= 1 AND key_part2 IS NOT NULL
关于如何对单个索引的范围条件进行优化,以组合或消除区间,请见Range Access Method for Single-Part Indexes。类似步骤也用于多个索引的范围条件。
多值比较的等值范围优化
考虑以下表达式,其中col_name
是一个索引列:
col_name IN(val1, ..., valN)
col_name = val1 OR ... OR col_name = valN
每个表达式都是如果col_name
等于多个值之一时为真。这些比较是等值范围比较(其中的““range””是一个单个值)。优化器估算读取符合条件的行的成本,以等值范围比较为:
-
如果对
col_name
有唯一索引,则每个区间的行估算为1,因为最多只有一个行可以具有给定的值。 -
否则,对
col_name
的索引是非唯一的,优化器可以使用索引或索引统计来估算每个区间的行数。
使用索引dives,优化器在每个区间的开始和结束处进行一次探测,并使用区间中的行数作为估算。例如,表达式
有三个等值范围,优化器对每个区间进行两次探测,以生成行估算。每对探测结果都可以生成对给定值的行估算。col_name
IN (10, 20, 30)
索引dives提供了准确的行估算,但随着比较值在表达式中的增加,优化器生成行估算的时间也增加。使用索引统计信息的准确性低于索引dives,但可以快速生成行估算สำหร大值列表。
系统变量eq_range_index_dive_limit
允许您配置优化器在从一个行估算策略切换到另一个策略的阈值。要允许使用索引dives对等值范围比较的比较值数目N
,请将eq_range_index_dive_limit
设置为N
+1。要禁用使用统计信息并总是使用索引dives,无论N
,请将eq_range_index_dive_limit
设置为0。
要更新表索引统计信息以获取最佳估算,请使用ANALYZE TABLE
。
在MySQL 8.4之前,没有方法可以跳过索引dives来估算索引有用性,除了使用eq_range_index_dive_limit
系统变量。在MySQL 8.4中,可以跳过索引dives的查询,以满足以下所有条件:
-
查询是对单个表的查询,不是多个表的连接。
-
单索引
FORCE INDEX
索引提示存在。想法是,如果索引使用被强制执行,那么没有必要再执行索引的深入搜索。 -
索引不是唯一的,也不是
FULLTEXT
索引。 -
没有子查询。
-
没有
DISTINCT
、GROUP BY
或ORDER BY
子句。
对于EXPLAIN FOR CONNECTION
,输出将根据索引深入搜索是否被跳过而变化:
-
对于传统输出,
rows
和filtered
值为NULL
。 -
对于JSON输出,
rows_examined_per_scan
和rows_produced_per_join
不出现,skip_index_dive_due_to_force
为true
,成本计算不准确。
没有FOR CONNECTION
,EXPLAIN
输出不变当索引深入搜索被跳过。
执行完查询后,相应的行在信息_schemaOPTIMIZER_TRACE
表中包含一个index_dives_for_range_access
值为skipped_due_to_force_index
。
考虑以下场景:
CREATE TABLE t1 (f1 INT NOT NULL, f2 INT NOT NULL, PRIMARY KEY(f1, f2));
INSERT INTO t1 VALUES
(1,1), (1,2), (1,3), (1,4), (1,5),
(2,1), (2,2), (2,3), (2,4), (2,5);
INSERT INTO t1 SELECT f1, f2 + 5 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 10 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 20 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 40 FROM t1;
ANALYZE TABLE t1;
EXPLAIN SELECT f1, f2 FROM t1 WHERE f2 > 40;
要执行这个查询,MySQL可以选择索引扫描来获取所有行(索引包含要选择的所有列),然后应用f2 > 40
条件从WHERE子句中来生产最终结果集。
范围扫描更有效,但是不能在这个场景中使用,因为没有对f1
的条件。优化器可以执行多个范围扫描,每个值对f1
使用一个方法称为Skip Scan,类似于Loose Index Scan(见第10.2.1.17节,“GROUP BY 优化”):
-
跳过第一个索引部分的distinct值
f1
(索引前缀)之间。 -
对每个distinct前缀值执行 subrange扫描。
对于之前显示的数据集,算法操作如下:
-
获取第一个distinct值的第一个键部分
f1 = 1
。 -
构建基于第一个和第二个键部分的范围
f1 = 1 AND f2 > 40
。 -
执行范围扫描。
-
获取下一个distinct值的第一个键部分
f1 = 2
。 -
构建基于第一个和第二个键部分的范围
f1 = 2 AND f2 > 40
。 -
执行范围扫描。
使用这个策略可以减少访问行的数量,因为MySQL跳过不符合每个构建的范围的行。这 Skip Scan访问方法适用于以下条件下:
-
表T至少有一个复合索引,其中键部分的形式为([A_1,...,A_
k
,] B_1,...,B_m
, C [, D_1,...,D_n
)。键部分A和D可能为空,但B和C必须非空。 -
查询只引用一个表。
-
查询不使用
GROUP BY
或DISTINCT
。 -
查询只引用索引中的列。
-
对A_1,...,A_
k
的约束必须是等值约束,并且必须是常量。这包括IN()
操作符。 -
查询必须是一个conjunctive查询,即一个
AND
的OR
条件:(
cond1
(key_part1
) ORcond2
(key_part1
)) AND (cond1
(key_part2
) OR ...) AND ... -
必须有C的范围约束。
-
D列的约束允许。D列的约束必须与C的范围约束结合。
使用Skip Scan的指示符出现在EXPLAIN
输出中,如下所示:
-
Using index for skip scan
在Extra
列中指示了 loose index Skip Scan访问方法的使用。 -
如果索引可以用于Skip Scan,索引应该在
possible_keys
列中可见。
使用Skip Scan的指示符出现在优化器跟踪输出中,以以下形式出现:
"skip_scan_range": {
"type": "skip_scan",
"index": index_used_for_skip_scan,
"key_parts_used_for_access": [key_parts_used_for_access],
"range": [range]
}
您可能还会看到一个"best_skip_scan_summary"
元素。如果Skip Scan被选择为最佳范围访问变体,一个"chosen_range_access_summary"
将被写入。如果Skip Scan被选择为最佳访问方法,一个"best_access_path"
元素将被写入。
使用Skip Scan的使用受skip_scan
标志的值影响。见第10.9.2节,“可切换优化”。默认情况下,这个标志是on
。要禁用它,请将skip_scan
设置为off
。
除了使用optimizer_switch
系统变量来控制优化器使用Skip Scan的会话级别外,MySQL还支持optimizer hint来影响优化器的使用。见第10.9.3节,“optimizer hint”。
Row Constructor 表达式的范围优化
优化器可以将查询应用于以下形式的范围扫描访问方法:
SELECT ... FROM t1 WHERE ( col_1, col_2 ) IN (( 'a', 'b' ), ( 'c', 'd' ));
之前,为了使用范围扫描,需要将查询写成:
SELECT ... FROM t1 WHERE ( col_1 = 'a' AND col_2 = 'b' )
OR ( col_1 = 'c' AND col_2 = 'd' );
为了使优化器使用范围扫描,查询必须满足以下条件:
关于优化器和行构造器的更多信息,请见第10.2.1.22节,“Row Constructor Expression Optimization”
限制范围优化的内存使用
要控制范围优化器的内存使用,可以使用range_optimizer_max_mem_size
系统变量:
-
值为0表示“无限制”。
-
值大于0时,优化器将跟踪考虑范围访问方法时消耗的内存。如果指定的限制即将被超过,优化器将放弃范围访问方法,考虑其他方法,包括全表扫描。这可能会导致性能下降。如果发生这种情况,以下警告将出现(其中
N
是当前range_optimizer_max_mem_size
值):Warning 3170 Memory capacity of N bytes for 'range_optimizer_max_mem_size' exceeded. Range optimization was not done for this query.
-
对于
UPDATE
和DELETE
语句,如果优化器退回到全表扫描,并且sql_safe_updates
系统变量启用,则会出现错误,而不是警告,因为实际上没有使用键来确定要修改的行。更多信息,请见使用安全更新模式 (--safe-updates)。
对于单个查询,如果优化器退回到不太优的计划,并且range_optimizer_max_mem_size
值增加可能会改善性能。
要估算处理范围表达式所需的内存,可以使用以下指南:
-
对于简单查询,如以下所示,其中有一个候选键用于范围访问方法,每个谓词组合使用
OR
约占用约230字节:SELECT COUNT(*) FROM t WHERE a=1 OR a=2 OR a=3 OR .. . a=N;
-
类似于以下查询,每个与
AND
组合的谓词大致使用125个字节:SELECT COUNT(*) FROM t WHERE a=1 AND b=1 AND c=1 ... N;
-
对于使用
IN()
谓词的查询:SELECT COUNT(*) FROM t WHERE a IN (1,2, ..., M) AND b IN (1,2, ..., N);
每个
IN()
列表中的每个文字值都被视为与OR
组合的谓词。如果有两个IN()
列表,那么与OR
组合的谓词数量是每个列表中的文字值数量的乘积。因此,在前一个情况下与OR
组合的谓词数量是M
×N
。