【问题标题】:MySQL subquery with group by in left join - optimisationMySQL 子查询与左连接中的 group by - 优化
【发布时间】:2012-12-09 21:25:42
【问题描述】:

MySQL 似乎无法使用 GROUP BY 子查询优化选择,并最终导致执行时间过长。这种常见场景必须有已知的优化。

假设我们试图从数据库中返回所有订单,并带有一个标志,表明它是否是客户的第一个订单。

CREATE TABLE orders (order int, customer int, date date);

按客户检索第一批订单的速度非常快。

SELECT customer, min(order) as first_order FROM orders GROUP BY customer;

但是,一旦我们使用子查询将其与完整的订单集连接起来,它就会变得非常慢

SELECT order, first_order FROM orders LEFT JOIN ( 
  SELECT customer, min(order) as first_order FROM orders GROUP BY customer
) AS first_orders ON orders.order=first_orders.first_order;

我希望我们缺少一个简单的技巧,否则它会快大约 1000 倍

CREATE TEMPORARY TABLE tmp_first_order AS 
  SELECT customer, min(order) as first_order FROM orders GROUP BY customer;
CREATE INDEX tmp_boost ON tmp_first_order (first_order)

SELECT order, first_order FROM orders LEFT JOIN tmp_first_order 
  ON orders.order=tmp_first_order.first_order;

编辑
受@ruakh 提出的选项 3 的启发,使用INNER JOINUNION 确实有一个不那么难看的解决方法,它具有可接受的性能但不需要临时表。但是,它对我们的案例有点特殊,我想知道是否存在更通用的优化。

SELECT order, "YES" as first FROM orders INNER JOIN ( 
    SELECT min(order) as first_order FROM orders GROUP BY customer
  ) AS first_orders_1 ON orders.order=first_orders_1.first_order
UNION
SELECT order, "NO" as first FROM orders INNER JOIN ( 
    SELECT customer, min(order) as first_order FROM orders GROUP BY customer
  ) AS first_orders_2 ON first_orders_2.customer = orders.customer 
    AND orders.order > first_orders_2.first_order;

【问题讨论】:

  • 几个思路:分析执行计划(解释查询);索引;子查询而不是左连接。
  • kristox,你检查我的答案了吗?

标签: mysql optimization group-by subquery left-join


【解决方案1】:

我希望在使用变量而不是 LEFT JOIN 时这会更快:

SELECT
  `order`,
  If(@previous_customer<>(@previous_customer:=`customer`),
    `order`,
    NULL
  ) AS first_order
FROM orders
JOIN ( SELECT @previous_customer := -1 ) x
ORDER BY customer, `order`;

这就是我在 SQL Fiddle 上的示例返回的结果:

CUSTOMER    ORDER    FIRST_ORDER
1           1        1
1           2        (null)
1           3        (null)
2           4        4
2           5        (null)
3           6        6
4           7        7

【讨论】:

  • Section 9.4 of the MySQL Reference Manual 建议不要“为用户变量赋值[ing] 并在同一语句中读取[ing] 值”,理由是您不能保证它总是给出你期望的结果(在 MySQL 版本变化、执行计划变化等情况下)。
【解决方案2】:

您可以尝试以下几点:

  1. 从子查询的字段列表中删除 customer,因为它没有做任何事情:

    SELECT order,
           first_order
      FROM orders
      LEFT
      JOIN ( SELECT MIN(order) AS first_order
               FROM orders
              GROUP
                 BY customer
           ) AS first_orders
        ON orders.order = first_orders.first_order
    ;
    
  2. 相反,将customer 添加到ON 子句中,它实际上对您有所帮助:

    SELECT order,
           first_order
      FROM orders
      LEFT
      JOIN ( SELECT customer,
                    MIN(order) AS first_order
               FROM orders
              GROUP
                 BY customer
           ) AS first_orders
        ON orders.customer = first_orders.customer
       AND orders.order = first_orders.first_order
    ;
    
  3. 与之前相同,但使用INNER JOIN 而不是LEFT JOIN,并将原来的ON 子句转换为CASE 表达式:

    SELECT order,
           CASE WHEN first_order = order THEN first_order END AS first_order
      FROM orders
     INNER
      JOIN ( SELECT customer,
                    MIN(order) AS first_order
               FROM orders
              GROUP
                 BY customer
           ) AS first_orders
        ON orders.customer = first_orders.customer
    ;
    
  4. CASE 表达式中不相关的IN-子查询替换整个JOIN 方法:

    SELECT order,
           CASE WHEN order IN
                      ( SELECT MIN(order)
                          FROM orders
                         GROUP
                            BY customer
                      )
                THEN order
            END AS first_order
      FROM orders
    ;
    
  5. CASE 表达式中的相关EXISTS-子查询替换整个JOIN 方法:

    SELECT order,
           CASE WHEN NOT EXISTS
                      ( SELECT 1
                          FROM orders AS o2
                         WHERE o2.customer = o1.customer
                           AND o2.order < o1.order
                      )
                THEN order
            END AS first_order
      FROM orders AS o1
    ;
    

(很可能上面的一些方法实际上会表现更糟,但我认为它们都值得一试。)

【讨论】:

  • 好答案@ruakh。选项 3 很有趣,但是在您的示例中,它只会返回第一个订单。 IE。如果您有 100 个客户和 2000 个订单,那么这只会返回 100 个第一个订单。受到您的建议的启发,我尝试了 UNION 似乎可行的方法。
  • @kristox:回复:“如果您有 100 个客户和 2000 个订单,那么 [选项 3] 将只返回 100 个第一个订单”:那不是真的。您确定您正确复制了ON 子句吗?
  • @ruakh 你是对的,我错了。尝试了选项 3,效果很好。谢谢。
猜你喜欢
  • 1970-01-01
  • 2012-11-26
  • 1970-01-01
  • 1970-01-01
  • 2012-06-12
  • 2017-01-10
  • 2020-02-14
  • 2019-01-15
  • 2017-04-04
相关资源
最近更新 更多