编程

数据库快速查询不总是更好

779 2023-12-02 18:47:00

您可能认为快速数据库查询很好。您可能还认为数据库查询速度慢是不好的。除了你对数据库查询的所有这些“想法”之外,还有一种 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 请求中执行的,那么会发现一些事实:

  1. 我是个精神病患者。
  2. 我的慢查询日志将为空。

这些查询速度很快;这些查询不好。

在这种特定的上下文中,如此快速的查询并不表示“好”。慢查询日志仅限于此。

为了使 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 请求完成时,发生了一些有趣的事情:

  1. 慢查询日志为空。
  2. 应用日志不包含任何 “Slow query detected" 条目
  3. 应用日志将包含一条 "Queries collectively took longer than 1 second." 条目

这是因为 whenQueryingForLongerThan 特性将会追踪单个 HTTP 请求生命周期期间的所有查询。如果发起的许多快查询,加总起来时超过 1 秒,闭包将会调用并且只会在每个 HTTP  请求周期内调用一次。

这就允许你在多个快查询导致 HTTP 请求变慢时,进行监测和日志记录 🎉

看!快查询也不一定是好的!

此功能不仅适用于 HTTP 请求。它也适用于队列作业,在队列作业中,每个作业生命周期调用一次闭包。

它非常整洁,但我一直觉得它需要一个更好的名称,以使它更明显地区别于另一个 QueryExecuted 事件监听器设置。

这就是这篇文章的内容。这篇文章整体上就是新方法的名称。

请记住,这些都不能替代数据库的慢查询日志。数据库的慢查询日志是深入了解和学习的重要工具。

有趣的事实:MySQL 慢查询日志的限制是365 天。他们好像知道我可能编造问题 😆