优化返回行数很大的MySQL查询
Posted
技术标签:
【中文标题】优化返回行数很大的MySQL查询【英文标题】:Optimizing MySQL query where the number of returned rows is very large 【发布时间】:2020-03-20 08:07:02 【问题描述】:上下文: 我们有一个网站,用户(商家)可以将他们的应用程序/网站添加到系统中,并通过 API 向他们的用户付款。现在,当我们必须在他们的仪表板上向商家显示这些交易的列表时,问题就来了。每个商家每秒产生数百笔交易,商家平均每天有大约 200 万笔交易,在仪表板上,我们必须向商家显示今天的统计数据。
主要问题: 我们必须向商家展示今天的交易,单个商家大约有 200 万条记录。 所以像这样的查询,
SELECT * FROM transactions WHERE user_id = 123 LIMIT 0,15
在我们的示例中检查的行数为 200 万,不能以任何方式减少。我认为这个限制在这里没有帮助,因为 mysql 仍然会检查所有行,然后从结果集中选择前 15 行。
我们如何优化这样的查询,我们必须向用户显示数百万条记录(当然还有分页)?
编辑:
解释输出:
查询:
explain select a.id, a.user_app_id, a.created_at, a.type, a.amount, a.currency_id, b.name, b.url from transactions as a left join user_apps as b on a.user_app_id = b.id where a.sender_user_id = ? and a.created_at BETWEEN '2020-03-20' AND '2020-03-21' order by a.created_at desc limit 15 offset 0
详情:
索引sender_user_id_2
是sender_user_id(int)
和created_at(timestamp)
列的复合索引。
此查询需要 5 到 15 秒才能返回 15 行。
如果我对表中只有 24 个事务的 sender_user_id 运行相同的查询,则立即响应。
【问题讨论】:
您需要显示数百万条记录还是需要某种形式的摘要(即每小时/分钟的总数)和搜索特定用户(在某些查询的情况下)。 需要显示所有交易,完整列表,可搜索,可排序。问题是,例如,如果用户只搜索今天的条目,那么结果集将是数百万行,我们可以使用分页,但查询仍然必须遍历所有记录,对吧? 这个要求本来就很慢,所以如果这是他们真正想要的(他们可能会发现现实并不像他们想象的那么有用)那么只要确保你有索引(并使用 ORDER BY 来确保检索行的一致性)。 是的,我已经这样做了,索引使用正确,但查询需要 10 多秒。我正在努力减少时间。 您提供的查询,并假设您在user_id
上有一个索引,将准确读取 15 行并停止。如果这需要 10 秒,那么您要么没有索引,要么这不是您的查询。请提供您的表结构、实际查询和执行计划(在您的查询前面写上explain
)。根据经验:除非您导出数据(包括将所有数据发送到应用程序并让应用程序在其内存中执行所有操作),这是一次性的事情,10 秒就可以了,否则您永远不会返回数百万行.因为没有人有时间查看数百万行。
【参考方案1】:
首先,让我们修复一个可能存在的错误:您在那个“天”中包含了两个午夜。 BETWEEN
是“包容性的”。
AND a.created_at BETWEEN '2020-03-20' AND '2020-03-21'
-->
AND a.created_at >= '2020-03-20'
AND a.created_at < '2020-03-20' + INTERVAL 1 DAY
(没有性能变化,只是消除了明天的午夜。)
在您的简单查询中,由于LIMIT
,只会触及 15 行。但是,对于更复杂的查询,它可能需要收集所有行,对它们进行排序,然后才剥离 15 行。防止这种低效率的技术是这样的:如果可能,设计一个INDEX
来处理所有WHERE
和ORDER BY
。
where a.sender_user_id = ?
AND a.created_at >= '2020-03-20'
AND a.created_at < '2020-03-20' + INTERVAL 1 DAY
order by a.created_at desc
需要INDEX(sender_user_id, created_at)
-- 按此顺序。 (而且,在您的查询中,没有其他任何内容侵犯了这一点。)
通过OFFSET
进行分页引入了另一个性能问题——它必须在获得所需的行之前遍历所有OFFSET
行。这可以通过remembering where you left off 解决。
那么,为什么EXPLAIN
认为它会达到一百万行?因为在处理LIMIT
时,解释是愚蠢的。有一个better way 来估计工作量。如果一切正常,这将显示 15 个,而不是 100 万个。对于LIMIT 150, 15
,它将显示 165。
您说“索引 sender_user_id_2 是 sender_user_id(int) 和 created_at(timestamp) 列的复合索引。”您能否提供SHOW CREATE TABLE
以便我们检查其他微妙的情况?
嗯……不知道
order by a.created_at desc
应该改变以匹配索引:
order by a.sender_user_id DESC, a.created_at desc
(您使用的是什么版本的 MySQL?我做了一些实验,发现没有区别,因为在 `ORDER BY 中有(或没有)sender_user_id
。)
(麻烦——似乎JOIN
阻止了LIMIT
的有效使用。仍在挖掘...)
新建议:
select a.id, a.user_app_id, a.created_at, a.type, a.amount, a.currency_id,
b.name, b.url
from
(
SELECT a1.id
FROM transactions as a1
where a1.sender_user_id = ?
AND a.created_at >= '2020-03-20'
AND a.created_at < '2020-03-20' + INTERVAL 1 DAY
order by a1.created_at desc
limit 15 offset 0
) AS x
JOIN transactions AS a USING(id)
left join user_apps as b ON x.user_app_id = b.id
这使用通用“技巧”将LIMIT
移动到派生表中,而其他东西最少。然后,只有 15 个 id,JOINs
到其他表就会“快速”。
在我的实验中(使用不同的表格对),它只涉及 5*15 行。我检查了多个版本;似乎都需要这种技术。我以前用他Handler_reads
来验证结果。
当我尝试使用 JOIN
而不是派生表时,它触及 2*N 行,其中 N 是没有 LIMIT
的行数。
【讨论】:
@billkarwin - 如果你在看,你说什么? 感谢您的解释。你说的一切,一切都是这样。加入可能会导致问题,所以我正在考虑在没有加入的情况下运行查询,然后使用 select * from apps where id IN() 查询来获取其他数据。我忘了这个方法叫什么,用来解决n+1问题。你怎么看? 您提供的新建议与我在评论中提到的类似技巧,但您的解决方案通过一个查询和更少的代码解决了它。我会试试的。以上是关于优化返回行数很大的MySQL查询的主要内容,如果未能解决你的问题,请参考以下文章