15

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

 3 years ago
source link: https://zhuanlan.zhihu.com/p/140278836
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

摈弃世俗浮躁,追求技术精湛

原文链接:https://learnku.com/laravel/t/44270
讨论请前往专业的 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/44270
讨论请前往专业的 Laravel 开发者论坛:https://learnku.com/Laravel


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK