在左连接与子查询中查找最新日期时的性能问题

问题描述 投票:0回答:1
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的数据。有了左联接 phone_call,需要30多秒才能返回数据.没有左联接,不到一秒.所以那个表是问题.有没有更好的方法来实现和上面的查询一样的结果?

mysql subquery mysql-5.7
1个回答
1
投票

由于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那么你可以从索引中把它删除,因为它将被隐式添加。

由于其中一个子查询可能返回 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
投票

你对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)

EDIT

以上解法采用 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);

0
投票

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


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.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

在上面的查询中,子句是重要的过滤掉日期较早的记录,你还需要在子句上添加一个复合索引。phone_call 桌子

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

如果列没有形成一个列,查询优化器不能使用索引来执行查找。最左前缀 的索引。

此外,请执行 解释计划 以查看与性能相关的问题,并确保使用了正确的索引。

检索每个组中的最后一条记录-------------------------------MySQL

© www.soinside.com 2019 - 2024. All rights reserved.