如何在 PHP 中检测 N+1 查询
发布日期:作者: Sarah Morgan
什么是 N+1 查询问题?
N+1 查询问题是软件开发中常见的性能问题。N+1 查询会导致许多不必要的数据库调用。这会导致您的应用程序运行速度缓慢,尤其是在数据量增长的情况下。因此,您必须了解并解决 N+1 查询,以确保您的应用程序高效、响应迅速且可扩展。
当应用程序发出一个数据库查询来检索一个对象,然后为检索到的每个对象发出额外的查询来获取相关对象时,就会出现 N+1 查询。这会导致总共执行 N+1 个数据库查询来获取 N 个对象,这会严重降低应用程序的效率和性能,尤其是在处理大型数据集时。
本文将探讨如何使用 应用程序性能监控 (APM) 工具 快速检测和解决 N+1 查询。
N+1 查询示例
让我们考虑一个需要显示作者及其书籍列表的书店应用程序。应用程序可能首先查询数据库以检索所有作者(1 个查询)。然后,为检索到的每个作者,它发出另一个查询来获取他们各自的书籍。如果有 100 个作者,则这将导致 1(初始查询)+ 100(每个作者一个)= 101 个查询。
这是非常低效的,并且会严重降低应用程序的性能。
为了避免 N+1 查询问题,开发人员通常使用诸如 预加载 之类的技术,在初始数据库查询中加载相关数据,或者使用 批量获取,在其中 批量获取 多个对象的关联数据。
如何检测 N+1 查询?
如果您有一个非平凡的应用程序,您可能会有 N+1 查询。如果您的应用程序使用 Laravel 或 Symfony 等 Web 框架构建,并使用 ORM,您肯定会遇到许多 N+1 查询。这是因为许多现代 Web 框架的 ORM 层默认情况下会延迟加载记录。
N+1 查询问题可能会在您的开发和测试环境中被忽视。但是,当部署到生产环境时,它们可能会突然破坏应用程序的性能,因为数据库中的行数要多得多。
应用程序存在 N+1 查询的明显迹象是执行的数据库查询数量异常多。
这些查询通常是顺序的,并且没有重叠。
这很好,但是您可能想知道,“好吧,但是我怎么知道执行了多少查询以及它们是否‘顺序且没有重叠’?” 这是一个好问题!这就是应用程序性能监控 (APM) 工具发挥作用的地方。
使用 APM 工具查找 N+1 查询
顾名思义,应用程序性能监控工具是您可以用来监控和诊断应用程序问题的应用程序。这些工具可以帮助您监控各种性能指标,包括数据库查询。
有许多不同的 APM 工具可用。在本示例中,我将使用 Scout APM。
Scout APM 使查找 N+1 查询变得非常简单,因为它具有专门的 N+1 洞察力选项卡。N+1 洞察力选项卡显示应用程序中所有端点的列表(以红色突出显示),并且对于每个端点,您可以看到运行了多少个查询以及它们花费了多长时间。单击单个端点将显示更深入的信息。
在端点详细信息页面上,您可以获得一个非常有用的时间线视图,您可以在其中看到 N+1 何时出现,例如,在部署更新之后。上面的屏幕截图描绘了显示对 N+1 查询发生情况的简化洞察力的浓缩视图。单击此处突出显示的蓝色 SQL 按钮,Scout 将显示有问题的 SQL 查询。
回溯
在端点详细信息页面上,您可以单击回溯按钮来查找导致 N+1 查询的代码的确切行。
您可以在上面的图像中看到,Scout 将有问题的代码追溯到导致问题的代码行。代码片段显示出来了,因为我使用的是 Scout 的 GitHub 集成。但是,即使您没有使用 GitHub 集成,Scout 仍然会报告有问题的代码文件和行号。
现在我们知道了如何使用 APM 查找 N+1 查询,让我们继续解决它们。
解决 N+1 查询
为了说明如何在使用 ORM(如 Laravel 的 Eloquent 或 Doctrine)的 PHP 应用程序中解决 N+1 查询问题,我们将重新审视之前提到的基于书店应用程序场景的示例。
场景
您有一个具有两个表的数据库:authors和 books。每个作者可以拥有多本书。让我们首先看看导致 N+1 查询问题的代码,然后我将展示如何解决它。
具有 N+1 查询问题的代码
// Retrieve all authors$authors = Author::all(); foreach($authors as $author) { // For each author, retrieve their books$books = $author->books()->get(); // This causes an additional query for each author // Process the books...}
在此代码中,第一个查询检索所有作者,然后为每个作者执行一个新的查询来获取他们的书籍,从而导致 N+1 查询。
N+1 查询解决方案 1:预加载
解决 N+1 查询问题的一种方法是使用称为预加载的策略。使用预加载,您将在初始查询中加载相关模型(在本例中为 books)。
// Eager load books with authors in one query$authors = Author::with('books')->get(); foreach($authors as $author) { // Now, no additional query is made here $books = $author->books; // Process the books...}
N+1 查询解决方案 2:联接查询
有时,您可能希望使用 JOIN 语句 在单个查询中获取所有内容。您可以使用框架提供的原始查询或查询构建器来执行此操作。
原始查询示例(使用 PDO)
$sql = "SELECT * FROM authors JOIN books ON authors.id = books.author_id";$stmt = $pdo->query($sql);$authorsAndBooks = $stmt->fetchAll(PDO::FETCH_ASSOC);// Process the books accordingly
查询构建器示例(在 Laravel 中)
$authorsAndBooks = DB::table('authors') ->join('books', 'authors.id', '=', 'books.author_id') ->get(); // Process the results accordingly
使用原始查询或查询构建器使您可以编写更优化的 SQL 查询,以通过对数据库的单个请求来获取所需的数据。
N+1 查询解决方案 3:缓存
另一种解决 N+1 查询问题的方法是使用缓存。缓存为 N+1 查询问题提供了一种策略性解决方案,尤其是在数据不经常更改的情况下。通过将数据库查询结果存储在缓存中,后续请求可以从该缓存中检索数据,而不是再次访问数据库。这显着减少了对数据库发出的查询次数,尤其是对于重复请求。
请注意,您可以将缓存与之前任何一种解决方案一起使用。
PHP 中的 N+1 查询:结论
了解并解决 N+1 查询对于优化 PHP 应用程序至关重要。我们已经探讨了什么是 N+1 查询,它们如何悄无声息地降低应用程序性能以及如何使用 Scout APM 等工具来检测 N+1 问题。
解决 N+1 查询不仅仅是提高速度,它还关乎编写更智能、更高效的代码。通过应用这些见解,您可以为用户提供更流畅、更快的体验。
Sarah 在软件行业拥有超过 18 年的经验。除了目前担任 Scout 的高级产品经理外,她还是 Product School 的特邀演讲嘉宾,以及 Tapple.io 的产品策略顾问。