您好,登錄后才能下訂單哦!
本篇內容介紹了“Laravel8優化數據庫查詢的技巧有哪些”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
本提示主要側重于提高處理大型數據集時應用的內存使用率。
處理大的集合時,分組檢索結果處理,而不是一次性檢索處理。
如下展示了從 posts
表檢索數據的過程。
$posts = Post::all(); // 使用 eloquent
$posts = DB::table('posts')->get(); // 使用查詢構造器
foreach ($posts as $post){
// 處理 posts 操作
}
上面的例子會從 posts 表檢索所有的記錄并處理。如果這個表達到了 100 多萬行呢?內存將很快被耗盡。
為了避免在處理大型數據集時出現問題,我們可以檢索結果子集并按照下面的方式處理它們。
// 當使用 eloquent 時
$posts = Post::chunk(100, function($posts){
foreach ($posts as $post){
// Process posts
}
});
// 當使用查詢構造器時
$posts = DB::table('posts')->chunk(100, function ($posts){
foreach ($posts as $post){
// Process posts
}
});
以上例子從 posts 表中檢索 100 條記錄對其進行處理,另外再檢索 100 條記錄進行處理。此迭代將繼續,直到處理完所有記錄。
這種方法將創建更多的數據庫查詢,但內存效率會更高。 通常, 大型數據集的處理應該再后臺進行。因此,可以在后臺運行時進行更多查詢,以避免在處理大型數據集時耗盡內存。
// 使用 eloquent
foreach (Post::cursor() as $post){
// 處理單個 post
}
// 使用 query 構建器
foreach (DB::table('posts')->cursor() as $post){
// 處理單個 post
}
示例進行單個數據庫查詢,檢索表的所有記錄,一個接一個一個處理 Eloquent 模型。這種方式僅查詢一次數據庫,得到全部 posts 。 但使用 php 生成器 優化內存使用。
什么情況使用這個呢?
這能夠在應用層極大地優化內存使用,由于我們檢索表的所有數據,數據庫內存占用任然很高。
在數據庫內存較多,應用內存較少的時候,建議使用游標。然而,如果你的數據庫沒有足夠的內存,最好使用 chunks 。
// 使用 eloquent
$posts = Post::chunkById(100, function($posts){
foreach ($posts as $post){
// 處理 posts
}
});
// 使用 query 構造器
$posts = DB::table('posts')->chunkById(100, function ($posts){
foreach ($posts as $post){
// 處理 posts
}
});
chunk
和 chunkById
最大的區別是 chunk 通過offset
和 limit
檢索數據。然而chunkById
通過id
字段檢索結構。id 字段通常是整型字段,而且它也是自增字段。
chunk
和 chunkById
的查詢如下。
chunk
select * from posts offset 0 limit 100
select * from posts offset 101 limit 100
chunkById
select * from posts order by id asc limit 100
select * from posts where id > 100 order by id asc limit 100
通常,查詢使用 limit 和 offset 是較慢的,盡量避免使用。本文 詳細介紹使用 offset 的問題。
chunkById 使用 id 整型字段,通過 where clause
查詢,這樣會更快。
什么時候使用 chunkById ?
當數據庫存在自增 主鍵
的時候使用。
通常從數據庫檢索數據時,會像下面這樣做。
$posts = Post::find(1); // 使用 eloquent
$posts = DB::table('posts')->where('id','=',1)->first(); // 使用 query 構建器
上面的代碼會得到如下的查詢
select * from posts where id = 1 limit 1
select *
表示從表中查出所有列。
當需要所有列時,這沒有問題。
然而,僅需要指定的列(id,title)時,只需要像下面這樣檢索那些列。
$posts = Post::select(['id','title'])->find(1); // 使用 eloquent
$posts = DB::table('posts')->where('id','=',1)->select(['id','title'])->first(); // 使用 query 構建器
上面代碼得到如下查詢
select id,title from posts where id = 1 limit 1
這點主要關注對檢索結果的處理時間。這不影響實際的查詢時間。
如我上面提到的,檢索指定的列,可以這樣做
$posts = Post::select(['title','slug'])->get(); // 使用 eloquent
$posts = DB::table('posts')->select(['title','slug'])->get(); // 使用 query 構建器
執行上面的代碼,它會在幕后執行以下操作。
執行 select title, slug from posts
查詢
檢索出的每一行對應一個 Post
模型對象(對 PHP 對象)(query 構建器得到標準的 PHP 對象)
為 Post
模型生成 collection
返回 collection
訪問數據
foreach ($posts as $post){
// $post 是 Post 模型或 php 標準對象
$post->title;
$post->slug;
}
上面的方式有額外的開銷,為每一行創建 Post
模型,并為這些對象創建一個集合。如果的確需要 Post
模型實例而不是數據,這是最正確的做法。
但如果您只需要兩個值時,則可以執行以下操作:
$posts = Post::pluck('title', 'slug'); // 使用 eloquent 時
$posts = DB::table('posts')->pluck('title','slug'); // 使用查詢構造器時
當上面代碼被執行時,它在幕后會執行以下操作。
對數據庫執行 select title, slug from posts
查詢
創建一個數組,其中會以 title
作為 數組值
,slug
作為 數組鍵
返回數組 ( 數組格式:[ slug => title, slug => title ]
)
要訪問結果,我們可以這么做
foreach ($posts as $slug => $title){
// $title 是 post 的 title
// $slug 是 post 的 slug
}
如果您想檢索一列,您可以這么做
$posts = Post::pluck('title'); // 使用 eloquent 時
$posts = DB::table('posts')->pluck('title'); // 使用查詢構造器時
foreach ($posts as $title){
// $title 是 post 的 title
}
上面的方式消除了每一行 Post
對象的創建。這將降低查詢結果處理的內存和時間消耗。
建議在新代碼中使用上述方式。個人感覺不值得花時間遵循上面的提示重構代碼。
重構代碼,最好是在要處理大的數據集或者是比較閑的時候
統計表的行數,通常這樣做
$posts = Post::all()->count(); // 使用 eloquent
$posts = DB::table('posts')->get()->count(); // 使用查詢構造器
這將生成以下查詢
select * from posts
上述方法將從表中檢索所有行。將它們加載到 collection
對象中并計算結果。當數據表中的行較少時,這可以正常工作。但隨著表的增長,內存很快就會耗盡。
與上述方法不同,我們可以直接計算數據庫本身的總行數。
$posts = Post::count(); // 使用 eloquent 時
$posts = DB::table('posts')->count(); // 使用查詢構造器時
這將生成以下查詢
select count(*) from posts
在 sql 中計算行數是一個緩慢的過程,當數據庫表中有多行時性能會很差。最好盡量避免計算行數。
這條建議你可能聽說過無數次了。所以我會盡可能簡短。讓我們假設您有以下場景
class PostController extends Controller
{
public function index()
{
$posts = Post::all();
return view('posts.index', ['posts' => $posts ]);
}
}
// posts/index.blade.php 文件
@foreach($posts as $post)
<li>
<h4>{{ $post->title }}</h4>
<p>Author: {{ $post->author->name }}</p>
</li>
@endforeach
上面的代碼是檢索所有的帖子,并在網頁上顯示帖子標題和作者,假設帖子模型關聯作者
。
執行以上代碼將導致運行以下查詢。
select * from posts // 假設返回5條數據
select * from authors where id = { post1.author_id }
select * from authors where id = { post2.author_id }
select * from authors where id = { post3.author_id }
select * from authors where id = { post4.author_id }
select * from authors where id = { post5.author_id }
如上,1 條查詢來檢索帖子,5 條查詢來檢索帖子的作者(假設有 5 篇帖子)。因此對于每篇帖子,都會進行一個單獨的查詢來檢索它的作者。
所以如果有 N 篇帖子,將會產生 N+1 條查詢(1 條查詢檢索帖子,N 條查詢檢索每篇帖子的作者)。這常被稱作 N+1 查詢問題。
避免這個問題,可以像下面這樣預加載帖子的作者。
$posts = Post::all(); // Avoid doing this
$posts = Post::with(['author'])->get(); // Do this instead
執行上面的代碼得到下面的查詢:
select * from posts // Assume this query returned 5 posts
select * from authors where id in( { post1.author_id }, { post2.author_id }, { post3.author_id }, { post4.author_id }, { post5.author_id } )
從上面的例子,考慮作者歸屬于一個組,同時需要顯示組的名字的情況。因此在 blade 文件中,可以按下面這樣做。
@foreach($posts as $post)
<li>
<h4>{{ $post->title }}</h4>
<p>Author: {{ $post->author->name }}</p>
<p>Author's Team: {{ $post->author->team->name }}</p>
</li>
@endforeach
接著
$posts = Post::with(['author'])->get();
得到下面的查詢:
select * from posts // Assume this query returned 5 posts
select * from authors where id in( { post1.author_id }, { post2.author_id }, { post3.author_id }, { post4.author_id }, { post5.author_id } )
select * from teams where id = { author1.team_id }
select * from teams where id = { author2.team_id }
select * from teams where id = { author3.team_id }
select * from teams where id = { author4.team_id }
select * from teams where id = { author5.team_id }
如上,盡管預加載了 authors
關系,仍然產生了大量的查詢。這是因為沒有預加載 authors
上的 team
關系。
通過下面這樣來解決這個它。
$posts = Post::with(['author.team'])->get();
執行得到下面的查詢。
select * from posts // Assume this query returned 5 posts
select * from authors where id in( { post1.author_id }, { post2.author_id }, { post3.author_id }, { post4.author_id }, { post5.author_id } )
select * from teams where id in( { author1.team_id }, { author2.team_id }, { author3.team_id }, { author4.team_id }, { author5.team_id } )
通過預加載嵌套關系,可以將查詢數從 11 減到 3。
想象一下,有 posts
和 authors
兩張表。帖子表有 author_id
列歸屬作者表。
為了得到帖子的作者 id,通常這樣做
$post = Post::findOrFail(<post id>);
$post->author->id;
執行得到兩個查詢。
select * from posts where id = <post id> limit 1
select * from authors where id = <post author id> limit 1
然而,可以直接通過下面方式得到作者 id 。
$post = Post::findOrFail(<post id>);
$post->author_id; // 帖子表有存放作者 id 的 author_id 列
什么時候采取上面的方式?
采取上的方式,需要確保帖子關聯的作者在作者表始終存在。
很多時候,一些數據庫查詢是不必要的。看看下面的例子。
<?php
class PostController extends Controller
{
public function index()
{
$posts = Post::all();
$private_posts = PrivatePost::all();
return view('posts.index', ['posts' => $posts, 'private_posts' => $private_posts ]);
}
}
上面代碼是從兩張不同的表(posts
, private_posts
)檢索數據,然后傳到視圖中。
視圖文件如下。
// posts/index.blade.php
@if( request()->user()->isAdmin() )
<h3>Private Posts</h3>
<ul>
@foreach($private_posts as $post)
<li>
<h4>{{ $post->title }}</h4>
<p>Published At: {{ $post->published_at }}</p>
</li>
@endforeach
</ul>
@endif
<h3>Posts</h3>
<ul>
@foreach($posts as $post)
<li>
<h4>{{ $post->title }}</h4>
<p>Published At: {{ $post->published_at }}</p>
</li>
@endforeach
</ul>
正如你上面看到的,$private_posts
僅對 管理員
用戶可見,其他用戶都無法看到這些帖子。
問題是,當我們在做
$posts = Post::all();
$private_posts = PrivatePost::all();
我們進行兩次查詢。一次從 posts
表獲取記錄,另一次從 private_posts
表獲取記錄。
private_posts
表的記錄僅 管理員用戶
可見。但我們仍在查詢以檢索所有用戶記錄,即使它們不可見。
我們可以調整邏輯,避免額外的查詢。
$posts = Post::all();
$private_posts = collect();
if( request()->user()->isAdmin() ){
$private_posts = PrivatePost::all();
}
將邏輯更改為上述內容后,我們對管理員用戶進行了兩次查詢,并對其他用戶進行了一次查詢。
我們有時需要進行查詢以同一個表中檢索不同類型的行。
$published_posts = Post::where('status','=','published')->get();
$featured_posts = Post::where('status','=','featured')->get();
$scheduled_posts = Post::where('status','=','scheduled')->get();
上述代碼正從同一個表檢索狀態不同的行。代碼將進行以下查詢。
select * from posts where status = 'published'
select * from posts where status = 'featured'
select * from posts where status = 'scheduled'
如您所見,它正在對同一個表進行三次不同的查詢以檢索記錄。我們可以重構此代碼以僅進行一次數據庫查詢。
$posts = Post::whereIn('status',['published', 'featured', 'scheduled'])->get();
$published_posts = $posts->where('status','=','published');
$featured_posts = $posts->where('status','=','featured');
$scheduled_posts = $posts->where('status','=','scheduled');
select * from posts where status in ( 'published', 'featured', 'scheduled' )
上面的代碼生成一個查詢來檢索全部特定狀態的帖子,通過狀態為返回的帖子創建不同的 collections 。三個不同的狀態的變量由一個查詢生成。
如果查詢中含有 where
條件作用于 string
類型的 column
,最好給這列添加索引。通過這列的查詢將會快很多。
$posts = Post::where('status','=','published')->get();
上面例子,我們對 status
列添加 where 條件來查詢。可以通過下面這樣的數據庫遷移來優化查詢。
Schema::table('posts', function (Blueprint $table) {
$table->index('status');
});
分頁結果時,我們通常會這樣做
$posts = Post::paginate(20);
這將進行兩次查詢,第一次檢索分頁結果,第二次表中計算表中的總行數。對表中的行數進行計數是一個緩慢的操作,會對查詢性能產生負面影響。
那么為什么 laravel 會計算總行數呢?
為了生成分頁連接,Laravel 會計算總行數。因此,當生成分頁連接時,您可以預先知道會有多少頁,以及過去的頁碼是多少。
另一方面,執行 simplePaginate
不會計算總行數,查詢會比 paginate
方法快得多。但您將無法知道最后一個頁碼并無法跳轉到不同的頁面。
如果您的數據庫表有很多行,最好避免使用 paginate
,而是使用 simplePaginate
。
$posts = Post::paginate(20); // 為所有頁面生成分頁鏈接
$posts = Post::simplePaginate(20); // 僅生成上一頁和下一頁的分頁鏈接
什么時候使用分頁和簡單分頁
查看下面的比較表,確定是分頁還是簡單分頁適合您
paginate / simplePaginate | |
---|---|
數據庫表只有很少行,并且不會變大 | paginate / simplePaginate |
數據庫表有很多行,并且增長很快 | simplePaginate |
必須提供用戶選項以跳轉到特定頁面 | paginate |
必須向用戶顯示結果總數 | paginate |
不主動使用分頁鏈接 | simplePaginate |
UI/UX 不會影響從切換編號分頁鏈接到下一個/上一個分頁鏈接 | simplePaginate |
使用“加載更多”按鈕或“無限滾動”分頁 | simplePaginate |
當嘗試查詢匹配特性模式的結果時,我們通常會使用
select * from table_name where column like %keyword%
上述查詢導致全表掃描。如果我們知道出現在列值開頭的關鍵字,我們會查詢以下結果。
select * from table_name where column like keyword%
最好避免在 where 子句中使用 SQL 函數,因為它們會導致全表掃描。 讓我們看下面的例子。要根據特定的時間查詢結果,我們通常會這樣做
$posts = POST::whereDate('created_at', '>=', now() )->get();
這將導致類似的于下面的查詢
select * from posts where date(created_at) >= 'timestamp-here'
上面的查詢將導致全表掃描,因為在計算日期
函數之前,不會應用 where 條件。
我們可以重構這個函數,以避免使用如下的 date
sql 函數
$posts = Post::where('created_at', '>=', now() )->get();
select * from posts where created_at >= 'timestamp-here'
最好限制表中列的總數。可以利用像 mysql 這樣的關系數據庫將具有如此多列的表拆分為多個表。可以使用它們的主鍵和外鍵將它們連接在一起。
向表中添加太多列會增加單個記錄的長度,并且會減慢表掃描的速度。在執行 select *
查詢時,最終會檢索到一些實際上并不需要的列。
這個技巧來自個人經驗,并不是設計數據庫表的標準方法。我建議只有當您的表有太多的記錄或者會快速增長時才遵循這個技巧。
如果一個表有存儲大量數據的列(例如: 數據類型為 TEXT 的列) ,那么最好將它們分離到它們自己的表中,或者分離到一個不經常被詢問的表中。
當表中有包含大量數據的列時,單個記錄的大小會變得非常大。我個人觀察到它影響了我們其中一個項目的查詢時間。
假設您有一個名為 posts
的表,其中包含一列 內容
,用于存儲博客文章內容。博客文章的內容將是真正的巨大和經常的時候,你需要這個數據只有當一個人正在查看這個特定的博客文章。
所以,在數據表中有大量文章記錄的時候,將這些長文本字段(大字段)分離到單獨的表中將會徹底的改善查詢性能。
當需要從一個數據表中查詢最新的記錄行時,通常我們會這么做:
$posts = Post::latest()->get();
// or $posts = Post::orderBy('created_at', 'desc')->get();
上面的查詢方式將會產生如下 sql 語句:
select * from posts order by created_at desc
這種查詢方式基本上都是按照 created_at
字段做降序排列來給查詢結果排序的。由于 created_at
字段是字符串類型的數據,所以用這種方式對查詢結果進行排序通常會更慢。(譯者注:MySQL 的 TIMESTAMP 類型字段是以 UTC 格式存儲數據的,形如 20210607T152000Z,所以 created_at 字段確實是字符串類型的數據)。
如果你的數據表中使用了自增長的 id
字段作為主鍵,那么大多數情況下,最新的數據記錄行的 id
字段值也是最大的。因為 id
字段不僅是一個整形數據的字段,而且也是一個主鍵字段,所以基于 id
字段對查詢結果進行排序會更快。所以查詢最新記錄的最佳實踐如下:
$posts = Post::latest('id')->get();
// or $posts = Post::orderBy('id', 'desc')->get();
該方法會產生如下 sql 語句
select * from posts order by id desc
為了更快地從數據庫查詢數據,我們已經為 select
方法做了很多優化。 大多數情況下,我們只需要為查詢方法進行優化就可以滿足性能要求了。 但是很多時候我們還需要為『插入』和『更新』(insert
和 update
)方法進行優化。所以我給大家推薦一篇有趣的文章optimizing mysql inserts,這篇文章將有助于優化緩慢的『插入』和『更新』操作。
在 Laravel 框架中,優化數據查詢并沒有完全通用的辦法。你只能盡量搞清楚下面這些問題:你的程序是如何運行的、進行了多少個數據庫查詢操作、有多少查詢操作是真正必要的。所以請檢查你的應用產生的查詢操作,這將有助于你確定并減少數據查詢操作的總量。
有很多工具可以輔助你檢查每個頁面產生的查詢方法:
注意: 不推薦在生產環境下使用這些工具。在生產環境使用這些工具將會降低你的應用性能,并且會讓未經授權的用戶獲取到程序的敏感信息。
Laravel Debugbar - Laravel Debugbar 有一個 database
選項卡,點擊該選項卡將會展示你打開一個頁面時應用程序執行的所有查詢語句。你可以瀏覽應用的每個頁面并查看每個頁面用到的查詢語句。
Clockwork - Clockwork 與 Laravel Debugbar 一樣,只不過 Clockwork 不會在你的網站上注入一個工具欄,你可以在『開發者工具窗口』( developer tools window
),或者通過打開 url /yourappurl/clockwork
進入一個單獨的頁面來查看應用的調試信息。
Laravel Telescope - Laravel Telescope 是一個專為開發 Laravel 應用而提供的十分優秀的調試工具。一旦你安裝了 Laravel Telescope,便可以通過訪問 yourappurl/telescope
地址進入它的儀表盤頁面。在 telescope 的儀表盤界面,點擊打開 queries
標簽頁,這個頁面將會展示你的應用執行過的所有 MySQL 查詢語句。
“Laravel8優化數據庫查詢的技巧有哪些”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。