左边的性能问题加入子查询以找出最新日期
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 的数字。如果 id
是 AUTO_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)
)
我认为您的问题与 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
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
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 的数字。如果 id
是 AUTO_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)
)
我认为您的问题与 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
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