优化Laravel ORM 性能使应用程序高可用

UlyssesWallis 发布于14天前

原文链接: https:// learnku.com/laravel/t/4 4270

讨论请前往专业的 Laravel 开发者论坛: https:// learnku.com/Laravel

大家好,我是Valerio,来自意大利的软件工程师,也是Inspector的CTO。

在本文中,我将分享一套我正在几乎所有后端服务中使用的ORM优化策略。

我确信我们每个人都会抱怨机器或应用程序运行缓慢甚至死机,然后花时间在咖啡机上等待长时间运行的查询结果。

我们该如何解决?

开始吧!

数据库是共享资源

为什么数据库会导致如此多的性能问题?

我们经常忘记每个请求都不独立于其他请求。

如果一个请求很慢,似乎会影响其他请求...对吗?

同时在应用程序中运行的所有进程都使用数据库。即使只有一个设计不当的访问也可能会危害整个系统的性能。

因此,请谨慎看待 「不优化代码也是可以的」 。缓慢的数据库访问可能会使数据库紧张,从而给用户带来负面的体验。

N+1 个数据库查询问题

N + 1 问题是什么?

这是使用ORM与数据库进行交互时遇到的一个典型问题。这不是SQL编码问题。

当您使用Eloquent之类的ORM时,它并不总是很清楚将进行什么查询以及何时进行查询。对于这个特定问题,我们谈论 关系 和饥饿加载(预加载)。

任何ORM都允许您声明实体之间的关系,从而提供一个出色的API来导航我们的数据库结构。

「文章和作者」 是一个很好的例子。

/*
 * 每篇文章都属于一个作者
 */
$article = Article::find("1");
echo $article->author->name; 

/*
 * 每个作者有多个文章
 */
foreach (Article::all() as $article)
{
    echo $article->title;
}

但是我们需要谨慎的在循环中使用关联关系。

看下面的例子。

我们要在文章标题旁边添加作者的名字。多亏了ORM,我们可以导航Article与Author之间的一对一关系以获取其名称。

听起来真的很简单:

// 初始查询以获取所有文章
$articles = Article::all();

foreach ($articles as $article)
{
    // 获取作者对象以便于打印作者名字
    echo $article->title . ' by ' . $article->author->name;
}

我们陷入了陷阱。

此循环生成1个初始查询以获取所有文章:

译者注: 原文似乎有表达错误,应该是 「此 函数 生成一个初始查询以获取所有文章」而不是 loop(循环)

SELECT * FROM articles;

然后 N 个查询来获得文章的作者以便打印作者的「名字」字段。如果作者名字是一样的也是如此。

SELECT * FROM author WHERE id = [articles.author_id]

恰好 N+1 个查询。

看起来好像没有这么重要的问题。 十五或二十个问题可能看起来不是一个需要立即解决的问题。 请仔细阅读本文的第一部分:

  • 数据库是所有进程共享的资源。
  • 数据库计算机资源有限,或者如果使用托管服务,则更多的数据库负载可能意味着更多的成本。
  • 如果您的数据库位于单独的计算机上,则所有数据都需要以额外的网络延迟进行传输。

[解决方案]使用预加载

Laravel documentation 所述, 我们很容易陷入 N + 1 的查询问题, 因为在访问 Eloquent 关联作为属性时 ( $article->author ), 关联数据为「延迟加载」. 这意味着关联数据在你第一次访问该属性的时候,才会真正的加载

然而,我们可以用一种简单的方法来加载所有关联数据,所以,当你以属性的方式访问 Eloquent 关联时,它不会运行新的查询,因为 ORM 已经加载了该数据。

这种策略称之为「预加载」,所有的 ORM 都支持此策略

// 作者使用「with」进行预加载.
$articles = Article::with('author')->get();

foreach ($articles as $article)
{
    // 作者不会在每次迭代中运行查询。
    echo $article->author->name;
}

Eloquent 提供了 with() 方法来进行预加载关联。

在这种情况下,只执行两个查询。

首先需要加载所有的文章:

SELECT * FROM articles;

第二种是通过 with() 方法,它将查询所有的作者:

SELECT * FROM authors WHERE id IN (1, 2, 3, 4, ...);

Eloquent 将在内部映射数据以照常使用:

$article->author->name;

优化查询语句

长期以来,我一直认为在 select 查询中显式声明字段数并不会带来明显的性能提升,因此我利用了仅获取查询所有字段的简单性。

此外,对特定 select 的字段列表进行硬编码,这不是一个容易维护的代码语句。

这种说法背后的最大错误是从数据库角度来看这可能是正确的。

但是我们使用的是 ORM ,因此将从数据库中选择的数据加载到 PHP 端的内存中,由 ORM 进行管理。我们获取的字段越多,该过程将占用的内存越多。

Laravel Eloquent 提供了 select 方法来将查询限制为仅我们需要的列:

$articles = Article::query()
    ->select('id', 'title', 'content') // 只获取你需要的字段
    ->latest()
    ->get();

排除字段 PHP 不必处理此数据,因此可以显着减少内存消耗。

不选择所有内容还可以改善排序,分组和连接的性能,因为数据库可以以这种方式节省内存。

使用 MySQL 视图

视图是在其他表的顶部生成并存储在数据库中的 select 查询。

在执行 SELECT 查询的时候,Laravel 框架会将您的查询转换为 SQL 查询,当确认没有错误的时候再执行它。

Mysql 视图是一个预编译过的 SQL 查询,mysql 可以直接运行该查询。

在数据筛选方面,使用 mysql 查询的效率要比 php 更高。

更多 Mysql 的操作请查看: https://www. mysqltutorial.org/

在 Eloquent Model 中添加 Mysql 视图。

Mysql 视图是一个虚拟的表,但是 Eloquent ORM 会以普通表的形式处理他。

这就是为什么我们可以通过 Eloquent ORM 直接操作他。

class ArticleStats extends Model
{
    /**
     * Mysql 视图名称
     */
    protected $table = "article_stats_view";

    /**
     * 如果视图结果中存在 "author_id" 字段
     * 我们可以通过它直接找到 Auth 的数据关联。
     */
    public function author()
    {
        return $this->belongsTo(Author::class);
    }
}

表关联,分页查询都可以像普通 Eloquent ORM 一样操作,并没有什么不同。

总结

先到这里,希望以上文章可以给您的产品开发带来方便或者启发。

我曾经使用 Eloquent ORM 编写过的事例代码,其中的一些 Eloquent ORM 实现方式,同样适用于您的代码。

常言道,工欲善其事,必先利其器。

感谢您的阅读,如果想要了解更多 Inspector信息,请访问 https://www.inspector.dev .

原文链接: https:// learnku.com/laravel/t/4 4270

讨论请前往专业的 Laravel 开发者论坛: https:// learnku.com/Laravel

查看原文: Laravel 性能优化:优化 ORM 性能使应用程序高可用

  • goldendog
  • whiteswan
  • organicmouse