【问题标题】:DISTINCT causing full table scanDISTINCT 导致全表扫描
【发布时间】:2014-05-07 23:31:40
【问题描述】:

我在 MySQL (5.5.31) 中有一个大约 20M 行的表。以下查询:

SELECT DISTINCT mytable.name name FROM mytable 
LEFT JOIN mytable_c ON mytable_c.id_c = mytable.id
WHERE mytable.deleted = 0  ORDER BY mytable.date_modified DESC  LIMIT 0,21

导致全表扫描,解释说typeALL,额外信息是Using where; Using temporary; Using filesort。解释结果:

  id  select_type table           type    possible_keys   key     key_len ref         rows        Extra
  1   SIMPLE      mytable         ALL     NULL            NULL    NULL    NULL        19001156    Using where; Using temporary; Using filesort
  1   SIMPLE      mytable_c       eq_ref  PRIMARY         PRIMARY 108     mytable.id  1           Using index

没有加入说明如下:

  id  select_type table           type    possible_keys   key         key_len ref         rows        Extra
  1   SIMPLE      mytable         index   NULL            mytablemod  9       NULL        21          Using where; Using temporary

id_cmytable_c 的主键,mytable_cmytable 中的每一行不超过一行。 date_modified 已编入索引。但看起来 MySQL 不明白这一点。如果我删除 DISTINCT 子句,那么explain 使用索引并且只触及 21 行,正如预期的那样。如果我删除加入,它也会这样做。有没有办法让它在没有连接的全表扫描的情况下工作? explain 表明 mysql 知道它只需要来自 mytable_c 的一行并且它正在使用主键,但仍然对 mytable 进行完整扫描。

存在 DISTINCT 的原因是查询是由 ORM 系统生成的,在这种情况下,JOIN 可能会生成多行,但 SELECT 字段的值将始终是唯一的(即,如果 JOIN 反对多行值链接只有在每个连接行中都相同的字段才会出现在 SELECT 中)。

【问题讨论】:

  • 您在一个表上有一个外部联接,您没有从中选择任何列。我不明白。
  • @Strawberry 这个查询有点简化,但它仍然会导致全表扫描。这是奇怪的部分 - 无论我是否包含其他表中的字段,都会进行全表扫描。
  • 试图在没有看到 a) 解释和 b) 正确 DDL 的情况下回答这类问题是愚蠢的。 (并不是说即使有了这些信息,我也能做得更好!)
  • @Strawberry 我已经添加了解释结果。我不认为 table defs 在这里很重要 - 任何类型的字段都会发生这种情况,但假设所有字段都是 int 或 varchars 并且 id、id_c 和 date_modified 都有索引。
  • 这意味着这些列都不是PRIMARY?

标签: mysql sql select left-join distinct


【解决方案1】:

这些只是通用的 cmets,不是特定于 mysql 的。

要从mytable 中找到所有可能的name 值,需要对表或索引进行全面扫描。可能的选择:

  • 全表扫描
  • 对以deleted 开头的索引进行全索引扫描(利用过滤器)
  • 对以name 开头的索引进行全索引扫描(仅关注输出列)

如果deleted 上有索引,则服务器可以找到所有deleted = 0 索引条目,然后从表中查找对应的name 值。但是如果deleted 的基数低或者统计数据没有不同的说法,那么首先对索引进行双重读取然后是相应的数据项可能会更昂贵。在这种情况下,只需扫描表即可。

如果name 上有索引,则索引扫描可能就足够了,但是需要检查表中的过滤器。再次频繁地从索引跳到表。

连接列也需要以类似的方式考虑。

如果您忘记了连接部分并且在列 namedeleted 上有一个多部分索引,那么可能会发生索引扫描。

更新

对我来说,DISTINCTORDER BY 部分有点令人困惑。其中name记录是用于排序的date_modified?我认为这样的事情会更清楚一点:

SELECT mytable.name name --, MIN(mytable.date_modified)
  FROM mytable 
  LEFT JOIN mytable_c ON mytable_c.id_c = mytable.id
  WHERE mytable.deleted = 0
  GROUP BY mytable.name
  ORDER BY MIN(mytable.date_modified) DESC  LIMIT 0,21

无论哪种方式,一旦ORDER BY 发挥作用,就需要进行全面扫描才能找到订单。如果没有 ORDER BY,找到的前 21 个就足够了。

【讨论】:

  • deleted 确实往往具有非常低的选择性,它只有两个值(0 和 1),几乎所有行都是 0。但它不需要找到 name 的所有值,只有 21 个不同的,是吗?事实上,这就是没有加入的情况。但是通过加入,它会进行完整扫描。为什么?
  • @StasM 查看此内容,了解不同和订购方式:dev.mysql.com/doc/refman/5.7/en/distinct-optimization.html
  • 但这不是正在发生的事情 - 即使有订单,除非有连接,否则不会发生完整扫描。它也不应该发生,因为 date_modified 索引中的前 21 条记录完成了查询(无论如何,所有名称都是不同的)并且不需要其他记录。刚刚注意到我忘了明确提及 - date_modified 有一个索引。
【解决方案2】:

您为什么不尝试将条件 mytable.deleted = 0 从 WHERE 移动到 JOIN ON ?你也可以试试 FORCE INDEX (mytablemod)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-11-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多