大家好,我是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 更高。
在 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.