+1

18 tips tối ưu hóa database query trong Laravel

Dịch từ: https://laravel-news.com/18-tips-to-optimize-your-laravel-database-queries

Truy xuất lượng dữ liệu lớn

Mẹo này chủ yếu tập trung vào việc cải thiện việc sử dụng bộ nhớ của ứng dụng của bạn khi xử lý lượng dữ liệu lớn.

Để lấy ra nhiều kết quả từ một bảng posts, chúng ta thường làm như dưới đây.

$posts = Post::all(); // when using eloquent
$posts = DB::table('posts')->get(); // when using query builder
foreach ($posts as $post){
 // Process posts
}

Cách làm trên sẽ lấy tất cả các bản ghi từ bảng bài viết và xử lý chúng. Điều gì sẽ xảy ra nếu bảng này có 1 triệu record? Chúng ta sẽ nhanh chóng hết bộ nhớ.

Để tránh các vấn đề khi xử lý các tập dữ liệu lớn, chúng ta có thể truy xuất một tập hợp con các kết quả và xử lý chúng như bên dưới.

Option 1: sử dụng chunk

// when using eloquent
$posts = Post::chunk(100, function($posts){
    foreach ($posts as $post){
     // Process posts
    }
});
// when using query builder
$posts = DB::table('posts')->chunk(100, function ($posts){
    foreach ($posts as $post){
     // Process posts
    }
});

Ví dụ trên lấy 100 bản ghi từ bảng posts, xử lý chúng, truy xuất 100 bản ghi khác và xử lý chúng. Việc lặp lại này sẽ tiếp tục cho đến khi tất cả các bản ghi được xử lý.

Cách tiếp cận này sẽ tạo ra nhiều truy vấn cơ sở dữ liệu hơn nhưng tiết kiệm bộ nhớ hơn. Thông thường, việc xử lý các tập dữ liệu lớn nên được làm trong nền. Vì vậy, có thể thực hiện nhiều truy vấn hơn khi chạy nền để tránh hết bộ nhớ khi xử lý các tập dữ liệu lớn.

Option 2: Sử dụng cursor

// when using eloquent
foreach (Post::cursor() as $post){
   // Process a single post
}
// when using query builder
foreach (DB::table('posts')->cursor() as $post){
   // Process a single post
}

Ví dụ trên sẽ thực hiện một truy vấn cơ sở dữ liệu duy nhất, truy xuất tất cả các bản ghi từ bảng và điều chỉnh từng Eloquent model. Cách tiếp cận này sẽ chỉ thực hiện một truy vấn cơ sở dữ liệu để truy xuất tất cả các posts. Nhưng sử dụng php generator để tối ưu hóa việc sử dụng bộ nhớ.

Khi nào bạn có thể sử dụng cái này?

Mặc dù điều này tối ưu hóa đáng kể việc sử dụng bộ nhớ ở cấp ứng dụng, Vì chúng ta đang truy xuất tất cả các mục nhập từ một bảng, việc sử dụng bộ nhớ trên cơ sở dữ liệu sẽ vẫn cao hơn.

Tốt hơn là sử dụng cursor nếu ứng dụng web đang chạy ứng dụng của bạn có ít bộ nhớ hơn và phiên bản cơ sở dữ liệu có nhiều bộ nhớ hơn. Tuy nhiên, nếu phiên bản cơ sở dữ liệu của bạn không có đủ bộ nhớ, tốt hơn là bạn nên chia nhỏ ra để xử lý.

Option 3: sử dụng chunkById

// when using eloquent
$posts = Post::chunkById(100, function($posts){
    foreach ($posts as $post){
     // Process posts
    }
});
// when using query builder
$posts = DB::table('posts')->chunkById(100, function ($posts){
    foreach ($posts as $post){
     // Process posts
    }
});

Sự khác biệt chính giữa chunk và chunkById là chunk truy xuất dựa trên offset và limit. Trong khi chunkById truy xuất kết quả cơ sở dữ liệu dựa trên trường id. Trường id này thường là một trường số nguyên và trong hầu hết các trường hợp, nó sẽ là trường tự động tăng dần.

Các truy vấn được thực hiện bởi chunk và chunkById như sau.

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

Nói chung, sử dụng limit và offset sẽ chậm hơn và chúng ta nên cố gắng tránh sử dụng nó. Bài viết này giải thích chi tiết vấn đề với việc sử dụng offset.

chunkById đang sử dụng trường id là một số nguyên và truy vấn đang sử dụng mệnh đề where, nên truy vấn sẽ nhanh hơn nhiều.

Khi nào chúng ta có thể sử dụng chunkById

Nếu bảng có cột autoincrement primary key

2. Chỉ select column cần thiết

Thông thường để lấy kết quả từ một bảng, chúng ta sẽ làm như sau.

$posts = Post::find(1); //When using eloquent
$posts = DB::table('posts')->where('id','=',1)->first(); //When using query builder

Query của đoạn code bên trên như sau:

select * from posts where id = 1 limit 1

Query sử dụng select *, có nghĩa là nó sẽ lấy về tất cả column từ table, nó ổn nếu bạn cần tất cả những column đó.

Tuy nhiên, nếu chúng ta chỉ cần các cột cụ thể (id, title), chúng ta có thể chỉ lấy các cột đó như bên dưới.

$posts = Post::select(['id','title'])->find(1); //When using eloquent
$posts = DB::table('posts')->where('id','=',1)->select(['id','title'])->first(); //When using query builder

Query của đoạn code bên trên sẽ là

select id,title from posts where id = 1 limit 1

3. Sử dụng pluck nếu bạn cần chính xác 1 hoặc 2 column

Mẹo này tập trung nhiều hơn vào thời gian sau khi kết quả được truy xuất từ cơ sở dữ liệu. Điều này không ảnh hưởng đến thời gian truy vấn thực tế.

Như đã đề cập bên trên, để chỉ lấy những column cần thiết chung ta sẽ code như sau

$posts = Post::select(['title','slug'])->get(); //When using eloquent
$posts = DB::table('posts')->select(['title','slug'])->get(); //When using query builder

Khi đoạn mã trên được thực thi, nó sẽ thực hiện những điều sau đây.

  • Thực hiện query select title, slug from posts
  • Tạo Post model object cho mỗi row nhận được từ DB (PHP standard object nếu sử dụng query builder)
  • Tạo collection với model Post
  • trả về collection

Để truy cập vào kết quả chúng ta có thể:

foreach ($posts as $post){
    // $post is a Post model or php standard object
    $post->title;
    $post->slug;
}

Cách trên sẽ tạo object model cho mỗi row nhận được từ DB và tạo collect cho chúng, nếu bạn cần instance model Post để xử lý thì các này ok, còn nếu chỉ cần data thì có thể thử cách bên dưới.

$posts = Post::pluck('title', 'slug'); //When using eloquent
$posts = DB::table('posts')->pluck('title','slug'); //When using query builder

Khi đoạn mã trên được thực thi, nó sẽ thực hiện những điều sau đây.

  • Thực hiện query select title, slug from posts
  • Tạo array với key là slug và value là title
  • Trả về array với dạng [ slug => title, slug => title ]

Để truy cập vào kết quả trả về, chúng ta có thể:

foreach ($posts as $slug => $title){
    // $title is the title of a post
    // $slug is the slug of a post
}

Nếu bạn chỉ muốn truy xuất một cột, bạn có thể làm như sau:

$posts = Post::pluck('title'); //When using eloquent
$posts = DB::table('posts')->pluck('title'); //When using query builder
foreach ($posts as  $title){
    // $title is the title of a post
}

Cách này loại bỏ bước tạo object Post cho các row được trả về, từ đó giảm bộ nhớ sử dụng cũng như thời gian xử lý kết quả.

4. Count sử dụng query thay vì collection

Khi đếm số lượng record của 1 bảng chúng ta thường dùng:

$posts = Post::all()->count(); //When using eloquent
$posts = DB::table('posts')->get()->count(); //When using query builder

Query của đoạn code trên:

select * from posts

Code bên trên sẽ get tất cả record từ table, đẩy vào collection và đếm số record. Cách này ổn nếu table có số lượng record nhỏ, nhưng sẽ nhanh chóng hết bộ nhớ khi table to lên. Thay vì như trên, chúng ta có thể count record bằng query như sau:

$posts = Post::count(); //When using eloquent
$posts = DB::table('posts')->count(); //When using query builder

Đoạn code trên sẽ thực thi đoạn code sau:

select count(*) from posts

Count trong sql là một quá trình chậm và hoạt động kém khi database có rất nhiều rows. Tốt hơn là nên tránh count càng nhiều càng tốt.

5. Tránh N+1 queries bằng eager loading relationship

Có lẻ bạn đã nghe đến tips này rất nhiều lần rồi

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::all();
        return view('posts.index', ['posts' => $posts ]);
    }
}
// posts/index.blade.php file
@foreach($posts as $post)
    <li>
        <h3>{{ $post->title }}</h3>
        <p>Author: {{ $post->author->name }}</p>
    </li>
@endforeach

Đoạn code trên get toàn bộ code và hiển thị titleauthor của nó lên web. Và nó sẽ thực thị những query sau:

select * from posts // Assume this query returned 5 posts
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 }

Như bên trên, chúng ta đã thực thi 1 query để get post và 5 query để get author của 5 post đó. Nên nếu có N posts, nó sẽ thực hiện N+1 query (1 query để get post và N query để get author).

Để tránh trường hợp trên, chúng ta có thể sử dụng eager loading như sau:

$posts = Post::all(); // Avoid doing this
$posts = Post::with(['author'])->get(); // Do this instead

Đoạn code trên sẽ thực hiện query như sau:

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

UPDATING..


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí