Laravel8中怎麼優化資料庫查詢?18 個優化建議分享

2022-03-07 22:00:32

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
    }
});

chunkchunkById 最大的區別是 chunk 通過offsetlimit 檢索資料。然而
chunkById 通過id 欄位檢索結構。id 欄位通常是整型欄位,而且它也是自增欄位。

chunkchunkById 的查詢如下。

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)
    <li>
        <h3>{{ $post->title }}</h3>
        <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 } )

6. 預載入巢狀關係

從上面的例子,考慮作者歸屬於一個組,同時需要顯示組的名字的情況。因此在 blade 檔案中,可以按下面這樣做。

@foreach($posts as $post)
    <li>
        <h3>{{ $post->title }}</h3>
        <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。

7. 如果僅需要 id 時,別預載入 belongsTo 關係

想象一下,有 postsauthors 兩張表。貼文表有 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 列

什麼時候採取上面的方式?

採取上的方式,需要確保貼文關聯的作者在作者表始終存在。

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 ]);
    }
}

上面程式碼是從兩張不同的表(postsprivate_posts)檢索資料,然後傳到檢視中。
檢視檔案如下。

// posts/index.blade.php
 @if( request()->user()->isAdmin() )
    <h2>Private Posts</h2>
    <ul>
        @foreach($private_posts as $post)
            <li>
                <h3>{{ $post->title }}</h3>
                <p>Published At: {{ $post->published_at }}</p>
            </li>
        @endforeach
    </ul>
@endif
 <h2>Posts</h2>
<ul>
    @foreach($posts as $post)
        <li>
            <h3>{{ $post->title }}</h3>
            <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();
}

將邏輯更改為上述內容後,我們對管理員使用者進行了兩次查詢,並對其他使用者進行了一次查詢。

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 方法做了很多優化。 大多數情況下,我們只需要為查詢方法進行優化就可以滿足效能要求了。 但是很多時候我們還需要為『插入』和『更新』(insertupdate)方法進行優化。所以我給大家推薦一篇有趣的文章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

【相關推薦:】

以上就是Laravel8中怎麼優化資料庫查詢?18 個優化建議分享的詳細內容,更多請關注TW511.COM其它相關文章!