数据库快速查询不总是更好
您可能认为快速数据库查询很好。您可能还认为数据库查询速度慢是不好的。除了你对数据库查询的所有这些“想法”之外,还有一种 Laravel 方法,如果你看过,你可能会认为你理解……但你也有可能不理解。
最后一个可能是我的错;对不起。
让我们挑战一下你对查询的想法,看看我们是否不能解决我带来的 Laravel 函数混乱。
慢查询就不好码?
有些查询本身就很慢,并且尽可能地优化。这很好,但是。。。
我想我们可以认同,慢查询通常是不好的。也许“坏”这个词不对。也许“不理想”更好。
数据库慢查询并不理想。是的。这样描述更恰当。
但我们如何定义“慢”呢?
我们可以比较两个查询,例如 50ms 查询和 2s 查询。在这种情况下,2秒的查询速度较慢(不知道查询的任何上下文)。
或者,我们可以通过数据库的慢查询日志功能设置显式阈值,明确定义系统中的“慢”查询,而不是比较两个查询。这使我们能够操作我们认为的慢查询。
我们把想象中的慢查询阈值设置为 1 秒。为本文探讨需要,任何超过 1 秒的事情我们都会被归类为慢查询。
我们的慢查询日志现在将记录 1 秒以上的查询。如果日志已满,我们知道查询速度较慢,可以尝试解决这些问题。
这并不太令人兴奋。让我们先来谈谈数据库快速查询…
快查询好吗?
好吗?是什么让它们变好?我们在什么语境之下谈论?
我有一个猜测,假若你觉得它们很好,因为它们表明你的应用程序很快,至少在查询数据库时是这样。
孤立地说:快速的数据库查询是好的。
但我不会孤立地处理查询。我从事的 web 应用程序具有丰富的交互功能,在单个 HTTP 请求生命周期、队列作业或 artisan 命令中可能会发生多个查询。
如果我们再次使用比较作为衡量标准:查询数据库 5ms 的 HTTP 请求比查询数据库 1.5 秒的HTTP请求要好,对吧?
我认为是的,至少在资源使用方面是这样。
如果我发出 HTTP 请求,而我的慢速查询日志中没有日志,那好吗?
我不这么认为。我不认为这很糟糕,但我也不认为这是“好”的证据。
毕竟,对于一个空的慢查询日志,我们实际上有什么信息?
我们知道 HTTP 请求在 1 秒内没有执行任何查询。这听起来不错,但信息还不够。
如果我们的 HTTP 请求执行了以下代码,该怎么办。原谅这个极端的例子。
use App\Models\User;
$endAt = now()->addSeconds(20);
while ($endAt->isFuture()) {
User::first();
}
上面的代码执行一个非常快速的数据库查询,但是它在 20 秒内不间断地循环执行查询。当然,这是一个人为的例子,但我确实在应用中碰到过,例如,在一个 HTTP 请求中,检查功能标志可以进行数百次,有时甚至数千次的“快速”数据库查询。
如果上面的代码是在对我的应用的 HTTP 请求中执行的,那么会发现一些事实:
- 我是个精神病患者。
- 我的慢查询日志将为空。
这些查询速度很快;这些查询不好。
在这种特定的上下文中,如此快速的查询并不表示“好”。慢查询日志仅限于此。
为了使 HTTP 请求快速,我真正想做的是减少在数据库中处理整个请求的时间。我关心请求的“累积查询时间”。
如果我进行一个 2 秒的查询或四个 0.5 秒的查询,它们都会使我的 HTTP 请求慢 2 秒,而且从最终用户的角度来看,两者的速度都一样慢。
那么我们能做什么呢?好吧,现在我要拿起我的锋利的锤子:Laravel。
用 Laravel 测量查询时间
我要给你说明一件事情。事实并非如此。这是一件需要提及的事,但事实并非如此。所以我会尽量简明扼要。
Laravel 有一种方法可以监听查询并检查其持续时间。例如,我们可以使用以下内容来模拟慢查询日志:
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
Event::listen(QueryExecuted, function (QueryExecuted $event) {
if ($event->time > 1000) {
Log::info('Slow query detected.', [
// ...
]);
}
});
如果 Laravel 进行的任何单个查询超过 1 秒阈值,它将在应用程序日志中记录“检测到慢查询”。如果单个 HTTP 请求中的 5 个查询都占用 1 秒以上,则会将此消息记录 5 次。
这并没有给我们带来任何新的东西,把它搬到 Laravel 是好的,但并不理想。
但了解这项技术有助于我们区分我们要谈论的内容。
用 Laravel 测量累计查询时间
不久前,我为 Laravel 添加了一个方法。你可能已经看到这种方法:
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
DB::whenQueryingForLongerThan(1000, function (Connection $connection) {
Log::info('Queries collectively took longer than 1 second.', [
// ...
]);
});
假设这个处理程序和前面的查询日志模拟示例是相同的,也就是说,如果一个查询的执行时间比调用所提供的闭包的时间长 1 秒以上,那么这是情有可原的。
但它们并不相同。第一个事件监听器示例只单独考虑单个查询,而这个新的处理程序则考虑整个 HTTP 请求的上下文。
为了显示差异,让我们在我们想象的应用服务提供商中注册这两个方法。我们将继续使用 1 秒作为日志记录阈值。
use Illuminate\Database\Connection;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
/*
* Log when any query exceeds one second in duration.
*/
Event::listen(QueryExecuted, function (QueryExecuted $event) {
if ($event->time > 1000) {
Log::info('Query took longer than 1 second.', [
// ...
]);
}
});
/*
* Log when all queries collectivly exceed one second in duration.
*/
DB::whenQueryingForLongerThan(1000, function (Connection $connection) {
Log::info('Queries collectively took longer than 1 second.', [
// ...
]);
});
然后,我们将再次使用糟糕的代码向终端发出 HTTP 请求。我在这里复制了它,以表达你个人的痛苦和折磨,并作为一个友好的提醒。
use App\Models\User;
$endAt = now()->addSeconds(20);
while ($endAt->isFuture()) {
User::first();
}
现在,当该 HTTP 请求完成时,发生了一些有趣的事情:
- 慢查询日志为空。
- 应用日志不包含任何 “Slow query detected" 条目
- 应用日志将包含一条 "Queries collectively took longer than 1 second." 条目
这是因为 whenQueryingForLongerThan
特性将会追踪单个 HTTP 请求生命周期期间的所有查询。如果发起的许多快查询,加总起来时超过 1 秒,闭包将会调用并且只会在每个 HTTP 请求周期内调用一次。
这就允许你在多个快查询导致 HTTP 请求变慢时,进行监测和日志记录 🎉
看!快查询也不一定是好的!
此功能不仅适用于 HTTP 请求。它也适用于队列作业,在队列作业中,每个作业生命周期调用一次闭包。
它非常整洁,但我一直觉得它需要一个更好的名称,以使它更明显地区别于另一个 QueryExecuted
事件监听器设置。
这就是这篇文章的内容。这篇文章整体上就是新方法的名称。
请记住,这些都不能替代数据库的慢查询日志。数据库的慢查询日志是深入了解和学习的重要工具。
有趣的事实:MySQL 慢查询日志的限制是365 天。他们好像知道我可能编造问题 😆