左连接与子查询的性能问题以找出最新日期

Posted

技术标签:

【中文标题】左连接与子查询的性能问题以找出最新日期【英文标题】:performance issue on left join with subquery to find out the latest date 【发布时间】:2020-09-18 23:28:21 【问题描述】:
SELECT m.*, pc.call_date                     
                    FROM messages m
                    LEFT JOIN customers c ON m.device_user_id = c.device_user_id
                    LEFT JOIN phone_call pc ON pc.id = (
                        SELECT MAX(pc2.id)
                        FROM phone_call pc2
                        WHERE pc2.device_user_id = c.device_user_id OR pc2.customer_id = c.customer_id
                    )

上面的问题是左连接 phone_call 表来找出每条记录的最新电话。 phone_call 表有 GB 的数据。使用 left join phone_call,返回数据需要 30 多秒。没有它不到一秒钟。所以那张桌子就是问题所在。有没有更好的方法来实现与上述查询相同的结果?

【问题讨论】:

代码问题需要minimal reproducible example--包括剪切、粘贴和可运行的最小代码以及以代码形式给出的最小代表性数据。对于包含 DBMS 和 DDL 的 SQL,包括约束、索引和表格初始化。对于包括 EXPLAIN 结果和统计信息的 SQL 性能。请研究和总结。对于包括优化/性能基础的 SQL——立即导致索引、计划、统计和 SARGability。 Tips for asking a good SQL question 在您学习并应用了这些基础知识后,请重新优化。 How to Ask 【参考方案1】:

对于 mysql 5.7,我对查询的表述方式看起来不错。但是子查询中的OR 是性能杀手。

我会推荐以下索引,以便相关子查询快速执行:

phone_call(device_user_id, customer_id, id) 

您可以尝试切换索引中的前两列,看看哪个版本效果更好。

可以尝试的另一件事是更改子查询以使用排序和行限制子句而不是聚合(使用相同的上述索引)。有保证它会改善事情,但值得一试:

LEFT JOIN phone_call pc ON pc.id = (
    SELECT pc2.id
    FROM phone_call pc2
    WHERE 
        pc2.device_user_id = c.device_user_id 
        OR pc2.customer_id = c.customer_id
    ORDER BY pc2.id
    LIMIT 1
)

最后,另一个想法是将子查询分成两部分以避免OR

LEFT JOIN phone_call pc ON pc.id = (
    SELECT MAX(id)
    FROM (
        SELECT MAX(pc2.id)
        FROM phone_call pc2
        WHERE pc2.device_user_id = c.device_user_id 
        UNION ALL
        SELECT MAX(pc3.id)
        FROM phone_call pc3
        WHERE pc3.customer_id = c.customer_id
    ) t
)

或者没有中间聚合:

LEFT JOIN phone_call pc ON pc.id = (
    SELECT MAX(id)
    FROM (
        SELECT pc2.id
        FROM phone_call pc2
        WHERE pc2.device_user_id = c.device_user_id 
        UNION ALL
        SELECT pc3.id
        FROM phone_call pc3
        WHERE pc3.customer_id = c.customer_id
    ) t
)

对于最后两个查询,您需要两个索引:

phone_call(device_user_id, id)
phone_call(customer_id, id)

编辑

上述使用union all 的解决方案需要 MySQL 8.0 - 在早期版本中,它们失败是因为子查询嵌套得太深,无法引用外部查询中的列。所以,另一种选择是IN

LEFT JOIN phone_call pc ON pc.id IN (
    SELECT pc2.id
    FROM phone_call pc2
    WHERE pc2.device_user_id = c.device_user_id 
    UNION ALL
    SELECT pc3.id
    FROM phone_call pc3
    WHERE pc3.customer_id = c.customer_id
)

这也可以与EXISTS 相匹配——我更喜欢它,因为谓词明确匹配索引定义,所以 MySQL 使用它们应该是一个简单的决定:

LEFT JOIN phone_call pc ON EXISTS (
    SELECT 1
    FROM phone_call pc2
    WHERE pc2.device_user_id = c.device_user_id AND pc2.id = pc.id
    UNION ALL
    SELECT 1
    FROM phone_call pc3
    WHERE pc3.customer_id = c.customer_id AND pc3.id = pc.id
)

同样,这在假设您具有以下两个多列索引的情况下有效:

phone_call(device_user_id, id)
phone_call(customer_id, id)

您可以按如下方式创建索引:

create index idx_phone_call_device_user on phone_call(device_user_id, id);
create index idx_phone_call_customer    on phone_call(customer_id, id);

【讨论】:

@SO-user:你在三列上有一个 compound 索引,还是在每一列都有一个索引? 每列的索引。我已经通过 id desc 查询尝试过该订单。没有差异。我会尝试你的其他两个查询。这些是一些不错的想法。 @SO-user:我的意思是多列索引。每列上的索引无济于事。 好的。我会调查一下然后回来。以前没试过。 在最后两个查询中,c.device_user_id 无法识别,因为我们在另一个子查询中使用了它。【参考方案2】:

由于 OR 条件,MAX 子查询无法使用索引。将此子查询拆分为两个 - 每个条件一个 - 并使用 GREATEST() 获取最高结果:

SELECT m.*, pc.call_date                     
FROM messages m
LEFT JOIN customers c ON m.device_user_id = c.device_user_id
LEFT JOIN phone_call pc ON pc.id = GREATEST((
  SELECT MAX(pc2.id)
  FROM phone_call pc2
  WHERE pc2.device_user_id = c.device_user_id
), (
  SELECT MAX(pc2.id)
  FROM phone_call pc2
  WHERE pc2.customer_id = c.customer_id
))

每个子查询都需要它自己的索引——它们是

phone_call(device_user_id, id)
phone_call(customer_id, id)

如果phone_call.id 是主键并且表正在使用InnoDB,那么您可以从索引中对其进行omnit,因为它将被隐式附加。

由于其中一个子查询可能返回NULL,您应该使用COALESCE(),其数字小于任何现有ID。如果idAUTO_INCREMENT 那么0 应该没问题:

SELECT m.*, pc.call_date                     
FROM messages m
LEFT JOIN customers c ON m.device_user_id = c.device_user_id
LEFT JOIN phone_call pc ON pc.id = GREATEST(
  COALESCE((
    SELECT MAX(pc2.id)
    FROM phone_call pc2
    WHERE pc2.device_user_id = c.device_user_id
  ), 0), 
  COALESCE((
    SELECT MAX(pc2.id)
    FROM phone_call pc2
    WHERE pc2.customer_id = c.customer_id
  ), 0)
)

【讨论】:

【参考方案3】:

好吧,你可能不会喜欢这个答案,但是,如果这将是一个重要的数据和一个频繁的查询,我会将last_call_date 作为一个字段放在客户表中。

【讨论】:

很遗憾,我不允许这样做,因为它会影响过去的数据。【参考方案4】:

我相信您的问题与greatest-n-per-group 问题有关,根据您的分组标准,有几种方法可以获取最新记录。其中之一是使用自联接,您可以将查询重写为

SELECT  m.*,
        pc.call_date                     
FROM messages m
LEFT JOIN customers c ON m.device_user_id = c.device_user_id
LEFT JOIN phone_call pc ON pc.device_user_id = c.device_user_id OR pc.customer_id = c.customer_id
LEFT JOIN phone_call pc2 ON (
    (pc.device_user_id = pc2.device_user_id OR pc.customer_id = pc2.customer_id) AND pc1.call_date < pc2.call_date
)
WHERE pc2.call_date IS NULL

在上面的查询中,where子句对于过滤掉日期较旧的行很重要,您还需要在phone_call表上添加复合索引

CREATE INDEX index_name ON phone_call(device_user_id,customer_id,call_date);

如果列未形成索引的leftmost prefix,则查询优化器无法使用索引执行查找。

此外,请执行EXPLAIN PLAN 查询以查看与性能相关的问题并确保使用了正确的索引。

Retrieving the last record in each group - MySQL

【讨论】:

以上是关于左连接与子查询的性能问题以找出最新日期的主要内容,如果未能解决你的问题,请参考以下文章

选择中的左连接与子查询的奇怪问题

UDF 与子查询性能问题

与子查询相比,为啥左外连接查询给出不同的结果?

MySQL 性能:使用左连接的一个查询与多个查询

左连接会导致性能大幅下降。如何修复它

左连接似乎极大地阻碍了 SQL 查询性能