mysql 不在简单的 OR 条件下使用索引
mysql not using index on simple OR condition
我已经 运行 解决了 MySQL 拒绝为看似基本的东西使用索引的古老问题。
有问题的查询:
SELECT c.*
FROM app_comments c
LEFT JOIN app_comments reply_c ON c.reply_to = reply_c.id
WHERE (c.external_id = '840774' AND c.external_context = 'deals')
OR (reply_c.external_id = '840774' AND reply_c.external_context = 'deals')
ORDER BY c.reply_to ASC, c.date ASC
解释:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE c ALL external_context,external_id,idx_app_comments_externals NULL NULL NULL 903507 Using filesort
1 SIMPLE reply_c eq_ref PRIMARY PRIMARY 4 altero_full.c.reply_to 1 Using where
在external_id
和external_context
上分别有索引,我也试过添加复合索引(idx_app_comments_externals
),但一点用都没有。
查询在生产环境中执行时间为 4-6 秒(>1m 条记录),但是删除 WHERE 条件的 OR 部分将其减少到 0.05s(尽管它仍然使用文件排序)。
显然索引在这里不起作用,但我不知道为什么。谁能解释一下?
P.S。我们使用的是 MariaDB 10.3.18,这会不会有问题?
WHERE 子句中 external_id
和 external_context
列的相等谓词,MySQL 可以有效地使用索引...当这些谓词指定行的子集时可能满足查询。
但是将 OR
添加到 WHERE
子句后,现在要从 c
编辑 return 的行 不是 受限于 external_id
和 external_content
值。现在可以对这些列具有 other 值的行进行 returned;这些列的 any 值的行。
这抵消了使用索引范围扫描操作的巨大好处...很快 消除 大量行不被考虑。是的,索引范围扫描用于快速定位行。那是真实的。但问题的实质是范围扫描操作使用索引快速绕过数百万行不可能 returned.
这不是 MariaDB 10.3 特有的行为。我们将在 MariaDB 10.2、MySQL 5.7、MySQL 5.6.
中观察到相同的行为
我在质疑连接操作:当 c
中有多个匹配行时,是否有必要 return 多个 行副本 c
=26=] ?或者规范只是 return 与 c
不同的行?
我们可以将所需的结果集分为两部分。
1) 来自 app_contents
的行在 external_id
和 external_context
上具有相等谓词
SELECT c.*
FROM app_comments c
WHERE c.external_id = '840774'
AND c.external_context = 'deals'
ORDER
BY c.external_id
, c.external_context
, c.reply_to
, c.date
为了获得最佳性能(不考虑覆盖索引,因为 SELECT 列表中的 *
),可以使用这样的索引来满足范围扫描操作和顺序(消除一个Using filesort操作)
... ON app_comments (external_id, external_context, reply_to, date)
2) 结果的第二部分是与匹配行
相关的reply_to
行
SELECT d.*
FROM app_comments d
JOIN app_comments e
ON e.id = d.reply_to
WHERE e.external_id = '840774'
AND e.external_context = 'deals'
ORDER
BY d.reply_to
, d.date
之前推荐的相同索引可用于访问e
中的行(范围扫描操作)。理想情况下,该索引还应包含 id
列。我们最好的选择可能是修改索引以包含 date
之后的 id
列
... ON app_comments (external_id, external_context, reply_to, date, id)
或者,为了获得同等性能,以额外索引为代价,我们可以这样定义索引:
... ON app_comments (external_id, external_context, id)
为了通过范围扫描访问 d
中的行,我们可能需要一个索引:
... ON app_comments (reply_to, date)
我们可以用 UNION ALL
集合运算符将这两个集合结合起来;但是两个查询可能会 return 编辑同一行。 UNION
运算符将强制进行唯一排序以消除重复行。或者我们可以向第二个查询添加条件以消除将被第一个查询return编辑的行。
SELECT d.*
FROM app_comments d
JOIN app_comments e
ON e.id = d.reply_to
WHERE e.external_id = '840774'
AND e.external_context = 'deals'
HAVING NOT ( d.external_id <=> '840774'
AND d.external_context <=> 'deals'
)
ORDER
BY d.reply_to
, d.date
合并这两部分,将每个部分包装在一组括号中,在末尾(括号外)添加 UNION ALL 集合运算符和 ORDER BY 运算符,如下所示:
(
SELECT c.*
FROM app_comments c
WHERE c.external_id = '840774'
AND c.external_context = 'deals'
ORDER
BY c.external_id
, c.external_context
, c.reply_to
, c.date
)
UNION ALL
(
SELECT d.*
FROM app_comments d
JOIN app_comments e
ON e.id = d.reply_to
WHERE e.external_id = '840774'
AND e.external_context = 'deals'
HAVING NOT ( d.external_id <=> '840774'
AND d.external_context <=> 'deals'
)
ORDER
BY d.reply_to
, d.date
)
ORDER BY `reply_to`, `date`
这将需要对组合集进行 "Using filesort" 操作,但现在我们已经非常有机会为每个部分制定良好的执行计划。
当有多个匹配 reply_to 行时,我仍然想知道我们应该 return 多少行。
MySQL(和 MariaDB)无法优化不同列或表上的 OR
条件。请注意,在查询计划的上下文中,c
和 reply_c
被视为不同的表。这些查询通常使用 UNION 语句进行优化 "by hand",其中通常包含大量代码重复。但在你的情况下,使用支持 CTE (Common Table Expressions) 的最新版本,你可以避免其中的大部分:
WITH p AS (
SELECT *
FROM app_comments
WHERE external_id = '840774'
AND external_context = 'deals'
)
SELECT * FROM p
UNION DISTINCT
SELECT c.* FROM p JOIN app_comments c ON c.reply_to = p.id
ORDER BY reply_to ASC, date ASC
此查询的良好索引将是 (external_id, external_context)
上的复合索引(以任何顺序)和 (reply_to)
上的单独索引。
虽然您不会避免 "filesort",但当数据被过滤到一个较小的集合时,这应该不是问题。
但是,名称索引不用于以下查询中的查找:
SELECT * FROM test
WHERE last_name='Jones' OR first_name='John';
我已经 运行 解决了 MySQL 拒绝为看似基本的东西使用索引的古老问题。 有问题的查询:
SELECT c.*
FROM app_comments c
LEFT JOIN app_comments reply_c ON c.reply_to = reply_c.id
WHERE (c.external_id = '840774' AND c.external_context = 'deals')
OR (reply_c.external_id = '840774' AND reply_c.external_context = 'deals')
ORDER BY c.reply_to ASC, c.date ASC
解释:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE c ALL external_context,external_id,idx_app_comments_externals NULL NULL NULL 903507 Using filesort
1 SIMPLE reply_c eq_ref PRIMARY PRIMARY 4 altero_full.c.reply_to 1 Using where
在external_id
和external_context
上分别有索引,我也试过添加复合索引(idx_app_comments_externals
),但一点用都没有。
查询在生产环境中执行时间为 4-6 秒(>1m 条记录),但是删除 WHERE 条件的 OR 部分将其减少到 0.05s(尽管它仍然使用文件排序)。 显然索引在这里不起作用,但我不知道为什么。谁能解释一下?
P.S。我们使用的是 MariaDB 10.3.18,这会不会有问题?
WHERE 子句中 external_id
和 external_context
列的相等谓词,MySQL 可以有效地使用索引...当这些谓词指定行的子集时可能满足查询。
但是将 OR
添加到 WHERE
子句后,现在要从 c
编辑 return 的行 不是 受限于 external_id
和 external_content
值。现在可以对这些列具有 other 值的行进行 returned;这些列的 any 值的行。
这抵消了使用索引范围扫描操作的巨大好处...很快 消除 大量行不被考虑。是的,索引范围扫描用于快速定位行。那是真实的。但问题的实质是范围扫描操作使用索引快速绕过数百万行不可能 returned.
这不是 MariaDB 10.3 特有的行为。我们将在 MariaDB 10.2、MySQL 5.7、MySQL 5.6.
中观察到相同的行为我在质疑连接操作:当 c
中有多个匹配行时,是否有必要 return 多个 行副本 c
=26=] ?或者规范只是 return 与 c
不同的行?
我们可以将所需的结果集分为两部分。
1) 来自 app_contents
的行在 external_id
和 external_context
SELECT c.*
FROM app_comments c
WHERE c.external_id = '840774'
AND c.external_context = 'deals'
ORDER
BY c.external_id
, c.external_context
, c.reply_to
, c.date
为了获得最佳性能(不考虑覆盖索引,因为 SELECT 列表中的 *
),可以使用这样的索引来满足范围扫描操作和顺序(消除一个Using filesort操作)
... ON app_comments (external_id, external_context, reply_to, date)
2) 结果的第二部分是与匹配行
相关的reply_to
行
SELECT d.*
FROM app_comments d
JOIN app_comments e
ON e.id = d.reply_to
WHERE e.external_id = '840774'
AND e.external_context = 'deals'
ORDER
BY d.reply_to
, d.date
之前推荐的相同索引可用于访问e
中的行(范围扫描操作)。理想情况下,该索引还应包含 id
列。我们最好的选择可能是修改索引以包含 date
id
列
... ON app_comments (external_id, external_context, reply_to, date, id)
或者,为了获得同等性能,以额外索引为代价,我们可以这样定义索引:
... ON app_comments (external_id, external_context, id)
为了通过范围扫描访问 d
中的行,我们可能需要一个索引:
... ON app_comments (reply_to, date)
我们可以用 UNION ALL
集合运算符将这两个集合结合起来;但是两个查询可能会 return 编辑同一行。 UNION
运算符将强制进行唯一排序以消除重复行。或者我们可以向第二个查询添加条件以消除将被第一个查询return编辑的行。
SELECT d.*
FROM app_comments d
JOIN app_comments e
ON e.id = d.reply_to
WHERE e.external_id = '840774'
AND e.external_context = 'deals'
HAVING NOT ( d.external_id <=> '840774'
AND d.external_context <=> 'deals'
)
ORDER
BY d.reply_to
, d.date
合并这两部分,将每个部分包装在一组括号中,在末尾(括号外)添加 UNION ALL 集合运算符和 ORDER BY 运算符,如下所示:
(
SELECT c.*
FROM app_comments c
WHERE c.external_id = '840774'
AND c.external_context = 'deals'
ORDER
BY c.external_id
, c.external_context
, c.reply_to
, c.date
)
UNION ALL
(
SELECT d.*
FROM app_comments d
JOIN app_comments e
ON e.id = d.reply_to
WHERE e.external_id = '840774'
AND e.external_context = 'deals'
HAVING NOT ( d.external_id <=> '840774'
AND d.external_context <=> 'deals'
)
ORDER
BY d.reply_to
, d.date
)
ORDER BY `reply_to`, `date`
这将需要对组合集进行 "Using filesort" 操作,但现在我们已经非常有机会为每个部分制定良好的执行计划。
当有多个匹配 reply_to 行时,我仍然想知道我们应该 return 多少行。
MySQL(和 MariaDB)无法优化不同列或表上的 OR
条件。请注意,在查询计划的上下文中,c
和 reply_c
被视为不同的表。这些查询通常使用 UNION 语句进行优化 "by hand",其中通常包含大量代码重复。但在你的情况下,使用支持 CTE (Common Table Expressions) 的最新版本,你可以避免其中的大部分:
WITH p AS (
SELECT *
FROM app_comments
WHERE external_id = '840774'
AND external_context = 'deals'
)
SELECT * FROM p
UNION DISTINCT
SELECT c.* FROM p JOIN app_comments c ON c.reply_to = p.id
ORDER BY reply_to ASC, date ASC
此查询的良好索引将是 (external_id, external_context)
上的复合索引(以任何顺序)和 (reply_to)
上的单独索引。
虽然您不会避免 "filesort",但当数据被过滤到一个较小的集合时,这应该不是问题。
但是,名称索引不用于以下查询中的查找:
SELECT * FROM test
WHERE last_name='Jones' OR first_name='John';