laravel8中怎么優化數據庫查詢?本篇文章給大家整理總結18 個 laravel 8 數據庫查詢優化建議,希望對大家有所幫助!
如果應用運行緩慢或存在大量數據庫查詢,請按照以下性能優化提示來縮短應用的加載時間。
1. 檢索大型數據集
本提示主要側重于提高處理大型數據集時應用的內存使用率。
處理大的集合時,分組檢索結果處理,而不是一次性檢索處理。
如下展示了從 posts 表檢索數據的過程。
$posts?=?Post::all();?//?使用?eloquent $posts?=?DB::table('posts')->get();?//?使用查詢構造器 ?foreach?($posts?as?$post){ ?//?處理?posts?操作 }
上面的例子會從 posts 表檢索所有的記錄并處理。如果這個表達到了 100 多萬行呢?內存將很快被耗盡。
為了避免在處理大型數據集時出現問題,我們可以檢索結果子集并按照下面的方式處理它們。
選項 1:使用 chunk
//?當使用?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 條記錄進行處理。此迭代將繼續,直到處理完所有記錄。
這種方法將創建更多的數據庫查詢,但內存效率會更高。 通常, 大型數據集的處理應該再后臺進行。因此,可以在后臺運行時進行更多查詢,以避免在處理大型數據集時耗盡內存。
選項 2: 使用游標
//?使用?eloquent foreach?(Post::cursor()?as?$post){ ???//?處理單個?post } ?//?使用?query?構建器 foreach?(DB::table('posts')->cursor()?as?$post){ ???//?處理單個?post }
示例進行單個數據庫查詢,檢索表的所有記錄,一個接一個一個處理 Eloquent 模型。這種方式僅查詢一次數據庫,得到全部 posts 。 但使用 php 生成器 優化內存使用。
什么情況使用這個呢?
這能夠在應用層極大地優化內存使用,由于我們檢索表的所有數據,數據庫內存占用任然很高。
在數據庫內存較多,應用內存較少的時候,建議使用游標。然而,如果你的數據庫沒有足夠的內存,最好使用 chunks 。
選項 3:使用 chunkById
//?使用?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 ?
- 當數據庫存在自增 主鍵 的時候使用。
2. 選擇合適的列
通常從數據庫檢索數據時,會像下面這樣做。
$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
3. 當需要數據庫表的一兩個列時
這點主要關注對檢索結果的處理時間。這不影響實際的查詢時間。
如我上面提到的,檢索指定的列,可以這樣做
$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 對象的創建。這將降低查詢結果處理的內存和時間消耗。
建議在新代碼中使用上述方式。個人感覺不值得花時間遵循上面的提示重構代碼。
重構代碼,最好是在要處理大的數據集或者是比較閑的時候
4. 使用查詢代替 collection 來統計行數
統計表的行數,通常這樣做
$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 中計算行數是一個緩慢的過程,當數據庫表中有多行時性能會很差。最好盡量避免計算行數。
5. 通過即時加載關系避免 n + 1查詢
這條建議你可能聽說過無數次了。所以我會盡可能簡短。讓我們假設您有以下場景
class?PostController?extends?Controller { ????public?function?index() ????{ ????????$posts?=?Post::all(); ????????return?view('posts.index',?['posts'?=>?$posts?]); ????} }
//?posts/index.blade.php?文件 ?@foreach($posts?as?$post) ????
{{?$post->title?}}
????????
Author:?{{?$post->author->name?}}
????
@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?}?)
6. 預加載嵌套關系
從上面的例子,考慮作者歸屬于一個組,同時需要顯示組的名字的情況。因此在 blade 文件中,可以按下面這樣做。
@foreach($posts?as?$post) ????
{{?$post->title?}}
????????
Author:?{{?$post->author->name?}}
????????
Author’s?Team:?{{?$post->author->team->name?}}
????
@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。
7. 如果僅需要 id 時,別預加載 belongsTo 關系
想象一下,有 posts 和 authors 兩張表。帖子表有 author_id 列歸屬作者表。
為了得到帖子的作者 id,通常這樣做
$post?=?Post::findOrFail(<post>); $post->author->id;</post>
執行得到兩個查詢。
select?*?from?posts?where?id?=?<post>?limit?1 select?*?from?authors?where?id?=?<post>?limit?1</post></post>
然而,可以直接通過下面方式得到作者 id 。
$post?=?Post::findOrFail(<post>); $post->author_id;?//?帖子表有存放作者?id?的?author_id?列</post>
什么時候采取上面的方式?
采取上的方式,需要確保帖子關聯的作者在作者表始終存在。
8. 避免使用不必要的查詢
很多時候,一些數據庫查詢是不必要的。看看下面的例子。
<?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()?) ????<h2>Private?Posts</h2> ????
- ????????@foreach($private_posts?as?$post) ????????????
- ????????????????
{{?$post->title?}}
????????????????
Published?At:?{{?$post->published_at?}}
????????????
????????@endforeach ????
@endif ?
Posts
- ????@foreach($posts?as?$post) ????????
- ????????????
{{?$post->title?}}
????????????
Published?At:?{{?$post->published_at?}}
????????
????@endforeach
正如你上面看到的,$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(); }
將邏輯更改為上述內容后,我們對管理員用戶進行了兩次查詢,并對其他用戶進行了一次查詢。
9. 合并相似的查詢
我們有時需要進行查詢以同一個表中檢索不同類型的行。
$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 。三個不同的狀態的變量由一個查詢生成。
10. 為常查詢的列添加索引
如果查詢中含有 where 條件作用于 string 類型的 column ,最好給這列添加索引。通過這列的查詢將會快很多。
$posts = Post::where('status','=','published')->get();
上面例子,我們對 status 列添加 where 條件來查詢。可以通過下面這樣的數據庫遷移來優化查詢。
Schema::table('posts',?function?(Blueprint?$table)?{ ???$table->index('status'); });
11. ?使用 simplePaginate 而不是 Paginate
分頁結果時,我們通常會這樣做
$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 |
12. 避免使用前導通配符(LIKE 關鍵字)
當嘗試查詢匹配特性模式的結果時,我們通常會使用
select * from table_name where column like %keyword%
上述查詢導致全表掃描。如果我們知道出現在列值開頭的關鍵字,我們會查詢以下結果。
select * from table_name where column like keyword%
13. 避免 where 子句使用 SQL 函數
最好避免在 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'
14. 避免在表中添加過多的列
最好限制表中列的總數。可以利用像 mysql 這樣的關系數據庫將具有如此多列的表拆分為多個表。可以使用它們的主鍵和外鍵將它們連接在一起。
向表中添加太多列會增加單個記錄的長度,并且會減慢表掃描的速度。在執行 select * 查詢時,最終會檢索到一些實際上并不需要的列。
15. 將帶有文本數據的單獨列輸入到它們自己的表中
這個技巧來自個人經驗,并不是設計數據庫表的標準方法。我建議只有當您的表有太多的記錄或者會快速增長時才遵循這個技巧。
如果一個表有存儲大量數據的列(例如: 數據類型為 TEXT 的列) ,那么最好將它們分離到它們自己的表中,或者分離到一個不經常被詢問的表中。
當表中有包含大量數據的列時,單個記錄的大小會變得非常大。我個人觀察到它影響了我們其中一個項目的查詢時間。
假設您有一個名為 posts 的表,其中包含一列 內容,用于存儲博客文章內容。博客文章的內容將是真正的巨大和經常的時候,你需要這個數據只有當一個人正在查看這個特定的博客文章。
所以,在數據表中有大量文章記錄的時候,將這些長文本字段(大字段)分離到單獨的表中將會徹底的改善查詢性能。
16. 從表中查詢最新記錄的最佳實踐
當需要從一個數據表中查詢最新的記錄行時,通常我們會這么做:
$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
17. 優化 MySQL 的數據插入操作
為了更快地從數據庫查詢數據,我們已經為 select 方法做了很多優化。 大多數情況下,我們只需要為查詢方法進行優化就可以滿足性能要求了。 但是很多時候我們還需要為『插入』和『更新』(insert 和 update)方法進行優化。所以我給大家推薦一篇有趣的文章optimizing mysql inserts,這篇文章將有助于優化緩慢的『插入』和『更新』操作。
18. 檢查和優化查詢方法
在 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 查詢語句。
原文地址:https://laravel-news.com/18-tips-to-optimize-your-laravel-database-queries
【相關推薦:laravel視頻教程】