Xin chào các bạn !!!!
Là một Web Developer chắc hẳn mọi người không còn xa lạ với PHP ( ột trong những ngôn ngữ lập trình Web phổ biến nhất) và framework Laravel ( một framework với lượt người dùng lớn nhất của PHP). Tuy nhiên, với mục đích ban đầu là tạo ra một ngôn ngữ lập trình để viết blog web theo hướng process nên vấn đề hiệu suất ( performance ) luôn được coi là nhược điểm của ngôn ngữ này. Cũng tương tự với Laravel, khi framework PHP này có cấu trúc khá đồ sộ khiến performance của 1 web app build bằng Laravel chưa được đánh giá cao bằng một số framework backend khác như NodeJs, Beego ....
Chính vì vậy, việc tối ưu performance cho laravel web app luôn là vấn đề cần được quan tâm với mọi lập trình viên. Có rất nhiều cách để tối ưu hiệu suất của 1 web app : cache, server, tối ưu resource ( image, cache css, database) . Ở khuôn khổ bài viết này, chúng ta sẽ tìm hiểu sâu hơn về Eloquent trong Laravel để tối ưu Query tới database giúp hiệu suất web được cải thiện !!!
Let's gooooo !!!!**
1. Đánh index cho những column cần thiết
Chắc hẳn mọi người đều nghe đến việc sử dụng Index để tối ưu tốc độ tìm kiếm trong database. Về cơ bản, việc đánh Index sẽ cho phép chúng ta tìm kiếm nhị phân thay vì việc tìm kiếm tuần tự.
Ở đây mình có 1 ví dụ với bảng Users đã seed 100,000 record. Sau đó mình tiến hành tìm kiếm với tên của user :
$users = User::where('name', 'Ms. Jammie Miller')->get();
return view('users', compact('users'));
và kết quả cho ra : Vì tìm kiếm tuần tự nên rất có khả năng hệ thống phải đi qua 100,000 record để tìm được bản ghi mình mong muốn và mất tới 39ms. Và giờ mình sẽ tạo 1 migration để thêm index cho trường "name" và thử query lại
Schema::table('users', function (Blueprint $table) { $table->index('name'); });
Và kết quả Sau khi thực hiện tìm kiếm nhị phân với việc đánh index thì thời gian chỉ còn là 1.56ms, nhanh hơn rất nhiều.
Vậy tại sao chúng ta chỉ nên đánh index cho các trường cần thiết mà không phải toàn bộ các trường trong database để tăng tốc độ truy vấn ???? Để tối ưu được tốc độ tìm kiếm bằng index , ta phải đánh đổi 1 vài thứ :
- Resource DB : thêm 1 ô nhớ để ghi lại số index cho mỗi record và thường ta chỉ đánh index trong những table có số lượng record lớn , điều này đồng nghĩa với việc bộ nhớ mà index sử dụng sẽ đáng kể vì tỉ lệ thuận với số lượng record
- Các thao tác thay đổi DB như update/delete/insert sẽ khiến index phải đánh lại từ đầu dẫn đến thời gian của thao tác update/insert/delete sẽ tăng lên vì phải thêm luôn cả việc đánh lại index cho các record => Chúng ta chỉ nên đánh index khi số lượng record lớn, thực hiện đọc dữ liệu nhiều hơn ghi dữ liệu
Một lưu ý nhỏ là mọi người có thể đánh index cho nhiều trường trong 1 table thay vì chỉ 1 trường, mọi người có thể tìm hiểu thêm về cơ chế hoạt động của Index qua bài viết sau
2. Lazy loading và vấn đề N+1 query
Lướt 10 bài viết về performance trong laravel thì có lẽ đến 11 bài viết đều có lazy loading và N+1 query. Tuy nhiên vẫn rất nhiều bạn vẫn còn khá mơ màng về vấn đề này, N+1 query không phải vấn đề của riêng Laravel mà còn của rất nhiều framework, ngôn ngữ khác.
Hiểu nôm na thì N+1 query là vấn đề xảy ra khi một truy vấn không hiệu quả do sử dụng quá nhiều truy vấn không cần thiết. Một ví dụ cơ bản về N+1 query là : ỞI đây mình có model User với quan hệ 1-N với Post :
class User extends Model
{ public function posts(): HasMany { return $this->hasMany(Post::class); }
} class Post extends Model
{ public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); }
}
Ở blade mình muốn in ra danh sách user kèm bài viết của họ
@foreach($users as $user) <tr> <td> {{ $user->name }} </td> <td> {{ $user->email }} </td> </tr> <tr> @foreach ($user->posts as $post) <td> {{ $post->title }} </td> <td> {{ $post->content }} </td> @endforeach </tr> @endforeach
Với 10 user được lấy ra , ta phải mất 11 (10 +1) câu truy vấn để lấy ra danh sách user kèm bài viết của họ Vì với laravel , kiểu lấy dữ liệu mặc định sẽ là Lazy Loading, ta có thể sử dụng kiểu Eager Loading với 2 phương thức chủ yếu là with() và load()
User::limit(10)->with('posts')->get()
Và sẽ chỉ tốn 2 query cho việc lấy ra 10 bản ghi thông tin user kèm danh sách bài viết.
Bên cạnh 2 phương thức được dùng chủ yếu là with() và load() còn khá nhiều phương thức khác loadMissing(), loadMorph() .....
Mọi người có thể tìm hiểu thêm về Eager Loading tại đây
Ngoài ra, mọi người cũng cần để ý khi xử lý query trong vòng lặp, thay vì insert, update từng record thì chúng ta có thể thực hiện bulk update/ insert nhiều record bằng 1 câu query.
Tuy nhiên mọi người cũng không nên quá quan trọng số lượng câu query mà quan trọng là tốc độ và resource xử dụng , với việc sử dụng 1 query với thời gian thực thi lâu và tốn nhiều resource và sử dụng nhiều query nhưng với mỗi query thời gian phản hồi nhanh và tiêu tốn ít resource hơn