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_idexternal_context上分别有索引,我也试过添加复合索引(idx_app_comments_externals),但一点用都没有。

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

P.S。我们使用的是 MariaDB 10.3.18,这会不会有问题?

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

但是将 OR 添加到 WHERE 子句后,现在要从 c 编辑 return 的行 不是 受限于 external_idexternal_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_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 列表中的 *),可以使用这样的索引来满足范围扫描操作和顺序(消除一个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 条件。请注意,在查询计划的上下文中,creply_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';

enter link description here