使用 LEFT JOIN 和 ORDER BY...LIMIT 查询慢,使用 Filesort

Posted

技术标签:

【中文标题】使用 LEFT JOIN 和 ORDER BY...LIMIT 查询慢,使用 Filesort【英文标题】:query with LEFT JOIN and ORDER BY...LIMIT slow, uses Filesort 【发布时间】:2014-12-05 22:53:04 【问题描述】:

我有以下疑问:

SELECT 
    fruit.date,
    fruit.name,
    fruit.reason,
    fruit.id,
    fruit.notes,
    food.name
FROM
    fruit
 LEFT JOIN
    food_fruits AS ff ON fruit.fruit_id = ff.fruit_id AND ff.type='fruit'
 LEFT JOIN
    food USING (food_id)
 LEFT JOIN
    fruits_sour AS fs ON fruits.id = fs.fruit_id
WHERE
    (fruit.date < DATE_SUB(NOW(), INTERVAL 180 DAY))
        AND (fruit.`status` = 'Rotten')
        AND (fruit.location = 'USA')
        AND (fruit.size = 'medium')
        AND (fs.fruit_id IS NULL)
ORDER BY `food.name` asc
LIMIT 15 OFFSET 0

以及您可能想要的所有索引,包括以下正在使用的索引:

fruit        - fruit_filter (size, status, location, date)
food_fruits  - food_type (type)
food         - food (id)
fruits_sour  - fruit_id (fruit_id)

我什至有一些我认为会更好但没有被使用的索引:

food_fruits  - fruit_key (fruit_id, type)
food         - id_name (food_id, name)

不幸的是,ORDER BY 子句导致使用temporary 表和filesort。没有它,查询将运行 lickety-split。我怎样才能让这个查询不需要filesort?我错过了什么?

编辑:

解释:

【问题讨论】:

你能在这个查询上运行 EXPLAIN 并发布输出吗? @Ashalynd 是的,虽然格式可能有点奇怪。 【参考方案1】:

原因是您的 ORDER BY 子句是在不属于此查询的索引的一部分的字段上完成的。引擎可以使用fruit_filter 索引运行查询,但是它必须在不同的字段上进行排序,这就是filesort 发挥作用的时候(这基本上意味着“不使用索引进行排序”,这要归功于 cmets 中的提醒)。

我不知道你得到的结果是什么时候,但如果差异很大,那么我会创建一个包含中间结果的临时表,然后对其进行排序。

(顺便说一句,我不确定你为什么使用LEFT JOIN 而不是INNER JOIN 以及为什么使用food_fruits - 在 cmets 中回答)

更新。

尝试子查询方法,可能是(未经测试),它将排序与预过滤分开:

SELECT
    fr.date,
    fr.name,
    fr.reason,
    fr.id,
    fr.notes,
    food.name
FROM
  (
  SELECT 
    fruit.date,
    fruit.name,
    fruit.reason,
    fruit.id,
    fruit.notes,
  FROM
    fruit
  LEFT JOIN
    fruits_sour AS fs ON fruit.id = fs.fruit_id
  WHERE
    (fruit.date < DATE_SUB(NOW(), INTERVAL 180 DAY))
        AND (fruit.`status` = 'Rotten')
        AND (fruit.location = 'USA')
        AND (fruit.size = 'medium')
        AND (fs.fruit_id IS NULL)
  ) as fr
LEFT JOIN
    food_fruits AS ff ON fr.fruit_id = ff.fruit_id AND ff.type='fruit'
LEFT JOIN
    food USING (food_id)  
ORDER BY `food.name` asc
LIMIT 15 OFFSET 0

【讨论】:

food_fruits 是水果和食物之间的链接关联表。我使用左连接是因为我想要来自fruits 的所有行,但并非所有fruits 都必须链接到food 子查询似乎带来了大约 7-8% 的效率提升。虽然这很好,我很感激,但如果可以获得更实质性的性能提升,那就太好了。我仍然不完全确定为什么id_name 索引不能用于排序部分... 小心,文件排序并不意味着你认为的那样。 percona.com/blog/2009/03/05/… 对,会解决的。【参考方案2】:

您知道,您的ORDER BY ... LIMIT 子句需要一些排序。优化性能的技巧是ORDER BY ... LIMIT 最小的列集,然后根据所选的 15 行构建完整的结果集。因此,让我们尝试在子查询中使用最小的列集。

     SELECT fruit.id,
            food.name
       FROM fruit
  LEFT JOIN food_fruits AS ff   ON fruit.fruit_id = ff.fruit_id 
                               AND ff.type='fruit'
  LEFT JOIN food USING (food_id)
  LEFT JOIN fruits_sour AS fs ON fruits.id = fs.fruit_id
      WHERE fruit.date < DATE_SUB(NOW(), INTERVAL 180 DAY)
        AND fruit.`status` = 'Rotten'
        AND fruit.location = 'USA'
        AND fruit.size = 'medium'
        AND fs.fruit_id IS NULL
   ORDER BY food.name ASC
      LIMIT 15 OFFSET 0

此查询为您提供了前 15 个*** ID 及其名称。

我会将id 添加到现有fruit_filter 索引的末尾以提供(size, status, location, date, id)。这将使它成为compound covering index,并允许您的过滤查询完全从索引中得到满足。

除此之外,很难使用更多或不同的索引来优化这一点,因为很多查询是由其他因素驱动的,例如您应用的LEFT JOIN ... IS NULL join-fail 标准。

然后您可以将此子查询加入您的 fruits 表以提取完整的结果集。

完成后会是这个样子。

SELECT fruit.date,
       fruit.name,
       fruit.reason,
       fruit.id,
       fruit.notes,
       list.name
  FROM fruit
  JOIN (
               SELECT fruit.id,
                      food.name
                 FROM fruit
            LEFT JOIN food_fruits AS ff    ON fruit.fruit_id = ff.fruit_id
                                          AND ff.type='fruit'
            LEFT JOIN food USING (food_id)
            LEFT JOIN fruits_sour AS fs ON fruits.id = fs.fruit_id
                WHERE fruit.date < DATE_SUB(NOW(), INTERVAL 180 DAY)
                  AND fruit.`status` = 'Rotten'
                  AND fruit.location = 'USA'
                  AND fruit.size = 'medium'
                  AND fs.fruit_id IS NULL
             ORDER BY food.name ASC
                LIMIT 15 OFFSET 0
       ) AS list ON fruit.id = list.id
 ORDER BY list.name

你知道这是怎么回事吗?在子查询中,您只需使用足够的数据来识别您想要检索的行的哪个小子集。然后,将该子查询加入主表以提取所有数据。限制必须排序的内容的行长度有助于提高性能,因为 mysql 可以对其排序缓冲区进行排序,而不必进行更复杂和更慢的排序/合并操作。 (但是,你无法从 EXPLAIN 中判断它是否会这样做。)

【讨论】:

我在写完这篇文章 13 小时后对其进行了编辑,以建议将其添加到索引中。

以上是关于使用 LEFT JOIN 和 ORDER BY...LIMIT 查询慢,使用 Filesort的主要内容,如果未能解决你的问题,请参考以下文章

在 sql 查询中使用 LIMIT 和 ORDER BY 和 LEFT JOIN

使用 LEFT JOIN + ORDER BY 时如何避免 FileSort?

将 ORDER BY 添加到 LEFT OUTER JOIN

Django order_by 导致 LEFT JOIN

ORDER BY date SQL with LEFT JOIN 用于消息传递

mysql 优化慢复杂sql (多个left join 数量过大 order by 巨慢)