Tối Ưu Queries Database Và Tăng Hiệu Suất

Tối Ưu Queries Database Và Tăng Hiệu Suất
Một trang web toàn vẹn không những có đủ các chức năng chạy đúng mà còn phải chạy nhanh để mang đến cho người dùng trải nghiệm tốt nhất.

Vì sao trang web của bạn chạy chậm như rùa? Có rất nhiều nguyên nhân gây ra như vậy. Ví dụ trang web có dung lượng hình ảnh khổng lồ (khiến trang web bị nặng), bạn quên chưa tối ưu cache, bạn chưa tối ưu phần code js và css. Thậm chí là các thành phần tracking log hoặc mailer chưa được mang vào background job. Có nhiều nguyên nhân, nhưng lý do phổ biến nhất vẫn là database. Database luôn là nơi mà các bạn cần lấy để xử lý code. 

 

Vì sao cần tập trung vào vấn đề tối ưu hóa hiệu suất làm việc trong Laravel?

 

Cấu trúc của framework và các thư viện (có liên quan đến framework) có thể đảm bảo cho các bạn lập trình web tạo code với effort tối thiểu. Tuy nhiên, quá trình code vẫn cần phải tối ưu hóa để sử dụng và điều chỉnh hiệu suất làm việc trên Laravel. Sau khi hoàn thành phần code, câu chuyện triển khai cũng cần phải quan tâm đến kỹ lưỡng. 

 

Thông thường, hiệu năng và tối ưu hóa là 2 yếu tố thiết yếu để quyết định sự thành công của các trang web. Đảm bảo hiệu năng là 1 tính năng quan trọng mà các bạn nên chú ý đến. Là lập trình viên, bạn cần cung cấp các thông tin cần thiết cho khách hàng hoặc người sử dụng trực tiếp. 

 

Vì Laravel thường được dùng để xây dựng các hệ thống thông tin trong kinh doanh. Hiệu năng của các ứng dụng được cung cấp từ Laravel mang ý nghĩa nhất định với sự thành công của 1 doanh nghiệp. Các hệ thống quản lý thông tin hỗ trợ vấn đề ra quyết định cho cấp quản lý vì họ phải thao tác nhanh và hiệu suất luôn phải cao. 

 

Truy xuất các dữ liệu lớn 

 

Ví dụ các bạn có 1 bảng posts chứa rất nhiều record (data record post của 1 mạng xã hội,…), để lấy ra tất cả bản ghi trong bảng posts đó thì bạn phải làm như thế nào?

 

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

 

Đây là dòng code các bạn sẽ thường xuyên thấy nếu muốn lấy ra tất cả data của posts. Sau đó, bạn sẽ xử lý thêm dữ liệu trong mỗi data đó. Nếu bảng của bạn có giới hạn, thì nó không thành vấn đề. Nếu bảng của bạn có hàng triệu record, thì bộ nhớ sẽ xảy ra tình trạng quá tải. Để tránh trường hợp dữ liệu lớn ảnh hưởng đến hiệu suất, thì bạn sẽ có 3 cách thực hiện.

 

Sử dụng hàm chunk và chia nhỏ

 

Thay vì bạn phải thao tác trên hàng triệu bản ghi thì bạn chỉ cần chia bản ghi là 1 triệu chia 100 cụm. Ở đây, bạn có 1 triệu chia 100 lần query. 

 

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

 

Hàm chunk sẽ tự động chia array thành nhiều cụm. Mỗi cụm sẽ có tối đa là 100 phần tử. Song song cùng với đó, bạn sẽ thấy có tham số closure function xuất hiện để thao tác trên mỗi cụm. 

 

Sử dụng con trỏ và hàm cursor 

 

Khi kết hợp con trỏ và hàm cursor, bạn đang giảm tải lượng bộ nhớ cần thiết trong trang web. Do đó, trang web sẽ tăng hiệu năng làm việc. Lưu ý là khi dùng hàm cursor, nó giúp giảm lượng bộ nhớ nhưng không đảm bảo bộ nhớ của trang web không xảy ra tình trạng “tràn đầy”. Nếu trang web của bạn cần phải lưu lượng thông tin khổng lồ, thì cách này sẽ không hiệu quả về lâu dài. Bạn nên ưu tiên dùng hàm chunk để tối ưu hóa toàn bộ. 

 

Tối ưu câu truy vấn cần thiết

 

Tùy theo câu query mà các bạn chuẩn bị các dòng lệnh linh hoạt. 

 

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

 

Phân tích truy vấn thì như thế này:

 

-- khi dùng chunk
select * from posts offset 0 limit 100
select * from posts offset 101 limit 100
-- khi dùng chunkById
select * from posts order by id asc limit 100
select * from posts where id > 100 order by id asc limit 100

 

chunkById đang sử dụng trường id để tạo mệnh đề where nên truy vấn sẽ nhanh chóng. Khi bảng cơ sở dữ liệu của bạn có cột id (là khóa chính) tăng tự động, bạn nên dùng hàm chunkById.

 

Sử dụng limit và offset cũng không sai. Tuy nhiên, thời gian để 2 hàm này chạy sẽ chậm hơn. Do đó, các bạn muốn tiết kiệm thời gian thì tránh dùng nó quá nhiều. Offset thường được dùng để phân trang hoặc query toàn bộ các bản ghi trong bảng có giới hạn và trật tự rõ ràng. 

 

Theo lý thuyết, offset sẽ ra lệnh cho DB phải bỏ N kết quả đầu tiên. Tuy nhiên, DB vẫn phải đọc và sắp xếp toàn bộ bản ghi. Nếu có quá nhiều dữ liệu khiến tham số N của offset tăng, thì DB phải tìm toàn bộ các bản ghi thích hợp để sắp xếp lại. Nó sẽ xóa bỏ N bản ghi. Điều này sẽ ảnh hưởng đến hiệu suất của hệ thống.

 

Chọn lọc và chỉ định chính xác field 

 

Giả sử 1 câu SQL thông thường mà bạn chỉ có lấy tiêu đề nhưng trong bảng đó lấy tất cả text. Text lại có rất nhiều câu lệnh thì tràn bộ nhớ là chuyện hiển nhiên. Thậm chí trang web sẽ bị chậm khi bạn query sql. Do đó, bạn nên chọn và chỉ định field nào sẽ tham gia vào cuộc chơi.

 

// không nên
$posts = Post::find(1); //When using eloquent
$posts = DB::table('posts')->where('id','=',1)->first(); //When using query builder
// => sql sẽ ra thế này: select * from posts where id = 1 limit 1
////////////////////////////////////////////////////////////////////
// nên: 
$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 
/// sql sẽ thế này: select id,title from posts where id = 1 limit 1

 

Pluck để lấy dữ liệu là array

 

Thông thường, các bạn sẽ có xu hướng chọn collection trong Laravel để làm việc vì thuận tiện để xử lý dữ liệu. Tuy nhiên, điều này sẽ ảnh hưởng đến hiệu suất khi trang web phải tạo nhiều object model. Do đó, ta nên ưu tiên sử dụng pluck nếu chỉ muốn lấy 1-2 field. Ví dụ:

 

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

 

Khi cho chạy dòng lệnh trên, nó sẽ thực hiện như sau:

 

  • Excutes select title và slug từ truy vấn của bảng posts trên database
  • Tạo 1 đối tượng post mới hàng tháng khi nó đã truy xuất
  • Tạo 1 collection mới với các post model
  • Quay trở lại với collection

 

Nếu bạn cần output chỉ là 2 giá trị (title và slug), thì hãy làm như vậy: 

 

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

 

Diễn giải đơn giản thì như thế này:

 

  • Excutes select title và slug từ truy vấn của bảng posts trên database
  • Tạo 1 mảng với title là value và slug là key
  • Quay trở lại với array

 

Dùng count(id) để đếm số bản ghi 

 

Trong trường hợp bạn cần đếm số lượng bản ghi, đừng nên đếm thủ công mà hãy thực hiện như thế này:

 

$posts = Post::count(); //When using eloquent
$posts = DB::table('posts')->count(); //When using query builder 
// truy vấn sẽ là : select count(*) from posts 
// Thay vì vậy, bạn nên truyền thêm id vào hàm count 
$posts = Post::count('id'); //When using eloquent
$posts = DB::table('posts')->count('id'); //When using query builder 
// khi đó query bạn sẽ thu được kết quả tốt hơn: select count(id) from posts

 

Tránh N+1 query bằng eager loading

 

Nhiều bạn làm dự án sẽ gặp tình huống này nhiều nhất. Tình huống này tương tự như việc áp dụng eloquent relationship không đúng chỗ và sai thời điểm. Giải pháp sẽ như sau: 

 

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::all();
        return view('posts.index', ['posts' => $posts ]);
    }
}
// khi đó query sẽ như này:
// select * from posts // giả sử có 4 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 }
Thay vì phải query rất nhiều lần author, các bạn chỉ cần query duy nhất 1 lần. Song song với đó, bạn cần dùng thêm id in (1,2,3,4). Bạn sẽ có toán tử with như sau: 
$posts = Post::with(['author'])->get(); 
// câu query thực sự còn 2 câu thôi 
// 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 } )
//

 

Tối ưu theo hướng tránh query thừa 

 

Khi bạn truy vấn data trong action của controller nhưng ngoài view vì có 1 rule nào đó mà bạn không thể dùng đến. Ví dụ khi bạn vào trang chủ, request và đã login thì có hiện lên các post bạn đã like. 

 

<?php
class PostController extends Controller
{
    public function index()
    {
        $auth = Auth::user();
        $userId = $auth ? $auth->id : 0;
        $posts = Post::all();
        $private_posts = PostLike::where('user', $userId)->get();
        return view('posts.index', ['posts' => $posts, 'private_posts' => $private_posts ]);
    }
}

 

Với trường hợp user không login thì lấy mặc định là 0 thì vẫn luôn query vào DB. Nếu user id = 0 thì đã bị dư query. Khi đó, bạn nên tối ưu bằng cách if else thì sẽ ổn hơn.

 

Hồ Hữu Hiền

Mình là developer nên đôi khi viết bài không hay lắm mong các bạn thông cảm. Nếu muốn biết thêm thông tin về mình thì vui lòng vào website này để biết. https://huuhienqt.dev/

Bình luận (0)