左边的性能问题加入子查询以找出最新日期

performance issue on left join with subquery to find out the latest date

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 table 以找出对每条记录进行的最新 phone 调用。 phone_call table 有 GB 的数据。使用左连接 phone_call,return 数据需要 30 多秒。没有它不到一秒钟。所以 table 是问题所在。是否有更好的方法来实现与上述查询相同的结果?

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

我觉得您表达查询的方式适合 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);

由于 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 是主键并且 table 使用 InnoDB,那么你可以从索引中省略它,因为它将被追加含蓄地。

因为其中一个子查询可能 return 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)
)

我认为您的问题与 问题有关,根据您的分组标准,有多种方法可以获取最新记录。其中之一是使用自连接,您可以将查询重写为

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 table

上添加复合索引
CREATE INDEX index_name ON phone_call(device_user_id,customer_id,call_date);

The query optimizer cannot use the index to perform lookups if the columns do not form a leftmost prefix of the index.

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

Retrieving the last record in each group - MySQL