左侧的性能问题使用子查询连接以查找最新日期

pxyaymoc  于 2021-08-13  发布在  Java
关注(0)|答案(4)|浏览(330)
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
                    )

上面的问题是使用left join phone\u call表来查找每个记录的最新通话。电话呼叫表有GB的数据。使用left join phone\u call时,返回数据需要30秒以上。不到一秒钟。所以那张table才是问题所在。有没有更好的方法来实现与上述查询相同的结果?

yhived7q

yhived7q1#

由于或条件,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)
)
7y4bm7vi

7y4bm7vi2#

在mysql 5.7中,您对查询的措辞对我来说很好。但是 OR 在子查询中是性能杀手。
我建议使用以下索引,以便快速执行相关子查询:

phone_call(device_user_id, customer_id, id)

您可以尝试切换索引中的前两列,以查看某个版本是否有更好的效果。
您可以尝试的另一件事是将子查询更改为使用sort和row limiting子句,而不是聚合(使用相同的上述索引)。可以保证它会改善情况,但值得一试:

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 需要MySQL8.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 -我更喜欢它,因为 predicate 显式地匹配索引定义,所以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);
qyzbxkaa

qyzbxkaa3#

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

ryevplcw

ryevplcw4#

我相信你的问题与每个组的最大n个问题有关。根据你的分组标准,有几种方法可以获得最新的记录。其中之一是使用自连接,您可以将查询重写为

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);

如果列不构成索引最左侧的前缀,查询优化器将无法使用索引执行查找。
此外,请为您的查询执行explain plan以查看与性能相关的问题,并确保使用了正确的索引。
检索每个组中的最后一条记录-mysql

相关问题