mysql 在简单的 OR 条件下不使用索引

Posted

技术标签:

【中文标题】mysql 在简单的 OR 条件下不使用索引【英文标题】:mysql not using index on simple OR condition 【发布时间】:2020-02-17 18:28:27 【问题描述】:

我遇到了 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_idexternal_context 上分别有索引,我也尝试添加复合索引 (idx_app_comments_externals),但根本没有帮助。

查询在生产环境中执行时间为 4-6 秒(>1m 条记录),但删除 WHERE 条件的 OR 部分会将其减少到 0.05 秒(尽管它仍然使用文件排序)。 显然索引在这里不起作用,但我不知道为什么。谁能解释一下?

附:我们使用的是 MariaDB 10.3.18,这可能有问题吗?

【问题讨论】:

我总是参考mysql.rjweb.org/doc.php/index_cookbook_mysql 来解决索引问题。在页面的中间大约有一个OR 部分,描述了如何使用使用or 的索引。看起来一个解决方案是改用UNION。另请参阅 ***.com/questions/52043444/… 以获取示例。 是的@WOUNDEDStevenJones UNION 可能是一个很好的优化技巧,假设联合数据适合内存并且需要非磁盘 i/o。考虑Internal Temporary Table Use in MySQL OR 一直是优化问题,并且在可预见的未来还会继续存在。一个答案侧重于 UNION 作为一种解决方法;另一个侧重于 CTE,从 MySQL 8.0 和 MariaDB 10.2 开始提供。 【参考方案1】:

但是,名称索引不用于以下查询中的查找:

SELECT * FROM test
WHERE last_name='Jones' OR first_name='John';

enter link description here

【讨论】:

【参考方案2】:

通过 WHERE 子句中 external_idexternal_context 列上的相等谓词,当这些谓词指定可能满足查询的行子集时,MySQL 可以有效地使用索引。

但是将OR 添加到WHERE 子句后,现在要从c 返回的行不受 external_idexternal_content 值的限制。现在可以返回具有这些列的 other 值的行;具有这些列的任何值的行。

这抵消了使用索引范围扫描操作的巨大好处...很快消除大量行被考虑。是的,索引范围扫描用于快速定位行。那是真实的。但问题的实质是范围扫描操作使用索引快速绕过数百万不可能返回的行。


这不是 MariaDB 10.3 特有的行为。我们将在 MariaDB 10.2、MySQL 5.7、MySQL 5.6 中观察到相同的行为。


我在质疑连接操作:当reply_c 有多个匹配行时,是否有必要从c 返回行的多个 副本?还是规范只是从 c 返回不同的行?


我们可以将所需的结果集分为两部分。

1) 来自app_contents 的行在external_idexternal_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 列表中的 * 不考虑覆盖索引),这样的索引可用于同时满足范围扫描操作和 order by(消除 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 集合操作符组合这两个集合;但是两个查询都可能返回同一行。 UNION 运算符将强制进行唯一排序以消除重复行。或者我们可以在第二个查询中添加一个条件来消除第一个查询将返回的行。

  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`

这将需要对组合集进行“使用文件排序”操作,但现在我们可以很好地为每个部分制定良好的执行计划。


当有多个匹配的 reply_to 行时,我们应该返回多少行仍然是我的问题。

【讨论】:

跳过内部ORDER BYs;它们被外层抵消了。 有什么理由使用HAVING而不是将条件折叠成WHERE 我建议唯一有用的索引是(external_id, external_context)(按任意顺序)和(reply_to) @RickJames external_idexternal_content 中的任何一个都可能是前导列。通过包含id 列,可以避免查找底层数据页(如果id 则检索值。如果我们在第一个SELECT 中消除ORDER BY,则没有理由包含reply_todate在第一个建议的索引中。类似地,如果我们在第二个 SELECT 中删除 ORDER BY,则没有理由在索引中包含 date 列。但是允许优化器选择使用索引来避免使用文件排序操作可以提高性能。 是的,继续e.id(即使在 InnoDB 中隐含);这很明显它“覆盖”了e【参考方案3】:

MySQL(和 MariaDB)无法针对不同的列或表优化 OR 条件。请注意,在查询计划的上下文中,creply_c 被视为不同的表。这些查询通常使用 UNION 语句“手动”优化,其中通常包含大量代码重复。但是在您的情况下,并且使用支持 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) 上的单独索引。

虽然不会避免“文件排序”,但当数据被过滤到一个小集合时,这应该不是问题。

【讨论】:

请注意,使用UNION 操作将消除重复行,这可能是实际规范。但这确实与返回重复行的 OP 查询不同,当匹配(连接)到多个 reply_to 行 +10 时,该行的多个副本。 @spencer7593 我假设id 是表中的主键。在原始查询中,右表的主键 (reply_c) 用于 ON 子句。这意味着最多只能有一场比赛。因此,左表 (c) 中的行不会重复。换句话说:一条评论不能是对多个其他评论的回复。 保证id 是主键或唯一键,是的,我遵循。在我的解决方案的行为依赖于关于唯一性的假设的情况下,为了未来读者的利益,我将注明该假设。 看来它可以解决问题,而且是一个值得学习的新东西,谢谢!

以上是关于mysql 在简单的 OR 条件下不使用索引的主要内容,如果未能解决你的问题,请参考以下文章

MySQL 使用 OR 条件导致索引失效

面试官:谈谈 MySQL 联合索引生效失效的条件?

MySQL联合索引

Mysql索引生效条件是啥?

MySQL联合索引生效的条件、索引失效的条件

Mysql系列—— 索引下推优化