Các nhà phát triển thường hỏi tôi cùng một câu hỏi: "API của tôi chậm quá. Tôi nên bắt đầu từ đâu?" Phản xạ thông thường là nâng cấp máy chủ hoặc tăng gấp đôi RAM. Việc đó vừa tốn kém vừa hiếm khi giải quyết được nguyên nhân gốc rễ. Trong hầu hết các ứng dụng Laravel, nút thắt cổ chai nằm ở lớp cơ sở dữ liệu. Cú pháp thanh thoát của framework khiến chúng ta dễ quên rằng mỗi lời gọi Eloquent cuối cùng đều trở thành SQL, và chính SQL thường là nơi bắt đầu nảy sinh vấn đề.

Trước khi chạm vào bất kỳ cài đặt máy chủ nào, hãy xem xét các truy vấn của bạn một cách có phương pháp.

Bắt đầu với việc chẩn đoán đúng cách

Đừng tối ưu hóa trong bóng tối. Việc viết lại các truy vấn một cách ngẫu nhiên chỉ là đoán mò, và đoán mò sẽ làm lãng phí hàng giờ đồng hồ.

Bạn cần tìm ra những câu lệnh tiêu tốn nhiều tổng thời gian nhất. Hãy theo dõi ứng dụng của bạn dưới tải thực tế. Laravel Telescope cung cấp cho bạn cái nhìn rõ ràng về mọi truy vấn được thực thi trong một request, kèm theo thời gian thực hiện. Laravel Debugbar hiển thị chúng ngay trên trình duyệt trong quá trình phát triển ở môi trường local để bạn có thể phát hiện các điểm bất thường ngay lập tức. Khi cần bắt lỗi trong môi trường production, hãy bật MySQL Slow Query Log. Nó ghi lại các câu lệnh vượt quá ngưỡng bạn đã thiết lập, điều này rất lý tưởng để tìm ra những vấn đề bất ngờ không xuất hiện trong các tập dữ liệu nhỏ. Nếu bạn vận hành hệ thống lớn hơn, một công cụ Application Performance Monitoring (APM) có thể liên kết các endpoint HTTP chậm với các lời gọi cơ sở dữ liệu cụ thể.

Khi xem xét dữ liệu, hãy chú ý đến hai yếu tố: thời gian thực thi tuyệt đối và tần suất gọi. Một truy vấn mất 40 mili giây nghe có vẻ vô hại cho đến khi bạn nhận ra nó chạy 2.000 lần mỗi phút. Một báo cáo mất 3 giây chạy mỗi giờ một lần có thể ít quan trọng hơn một truy vấn tìm kiếm mất nửa giây chạy trên mọi trang. Hãy ưu tiên xử lý các vấn đề có tác động cao trước.

Đừng yêu cầu lấy mọi thứ

SELECT * rất tiện lợi, nhưng nó cũng rất tốn kém. Khi bạn viết Model::all() hoặc lấy một tập kết quả mà không chỉ định tên cột, MySQL sẽ phải kéo tất cả các trường của mọi hàng khớp với điều kiện. Điều này bao gồm các trường văn bản lớn, các khối JSON và bất kỳ thứ gì khác có trong bảng. Tập kết quả lớn dần, mức sử dụng bộ nhớ tăng lên và thời gian dành cho việc serialize phản hồi cũng tăng theo.

Hãy làm việc một cách tường minh. Nếu controller của bạn chỉ cần các trường id, name, và email, hãy yêu cầu chính xác những trường đó:

User::select('id', 'name', 'email')->get();

Trong query builder, nguyên tắc tương tự cũng được áp dụng. Các payload nhỏ hơn sẽ di chuyển nhanh hơn qua mạng và tiêu tốn ít RAM hơn trên máy chủ ứng dụng của bạn. Đây là một trong những cách tối ưu hóa ít tốn kém nhất nhưng lại dễ bị bỏ qua vì Laravel mặc định sử dụng SELECT *.

Hãy để EXPLAIN dẫn dắt các thay đổi của bạn

Đừng bao giờ tái cấu trúc (refactor) một truy vấn chậm mà không chạy EXPLAIN trước. Trong MySQL, từ khóa EXPLAIN hiển thị kế hoạch thực thi truy vấn. Nó tiết lộ chính xác cách trình tối ưu hóa (optimizer) dự định tìm kiếm dữ liệu của bạn.

Hãy chú ý đến cột type. Nếu bạn thấy ALL, MySQL đang thực hiện quét toàn bộ bảng (full table scan). Điều đó có nghĩa là nó đang đọc mọi hàng để thỏa mãn mệnh đề WHERE của bạn. Hãy nhìn vào cột key để xem liệu trình tối ưu hóa có đang sử dụng index nào không. Sau đó, kiểm tra cột Extra. Nếu bạn thấy Using temporary hoặc Using filesort, MySQL đang xây dựng các bảng trung gian hoặc thực hiện sắp xếp trong bộ nhớ vì cấu trúc hiện tại của bạn không thể đáp ứng truy vấn một cách gọn gàng.

Hãy chạy EXPLAIN trong MySQL client của bạn, hoặc sử dụng một công cụ định dạng kết quả đầu ra cho bạn. Một khi đã thấy kế hoạch thực thi, bạn sẽ biết vấn đề là do thiếu index, một phép join không tốt, hay một vị từ (predicate) mà engine không thể tối ưu hóa. Việc đoán mò sẽ không còn cần thiết nữa.

Đánh chỉ mục (Index) có mục đích

Index là công cụ mạnh mẽ nhất để tăng tốc độ tìm kiếm, nhưng chúng chỉ hiệu quả khi chúng khớp với cách bạn truy vấn. Nếu không có index phù hợp, MySQL sẽ quét từng hàng một. Điều này có thể cảm thấy ổn khi phát triển trên một bảng có một nghìn hàng, nhưng sẽ sụp đổ trong môi trường production trên một bảng có mười triệu hàng.

Hãy bắt đầu với các index đơn cột cho các trường xuất hiện thường xuyên trong mệnh đề WHERE. Nếu bạn liên tục lọc theo status, hãy thêm một index vào status.

Khi một truy vấn lọc trên nhiều cột cùng lúc, hãy chuyển sang sử dụng composite index (index hỗn hợp). Thứ tự các cột trong index rất quan trọng vì MySQL đọc composite index từ trái sang phải. Điều này được gọi là quy tắc tiền tố bên trái nhất (leftmost prefix rule). Nếu truy vấn của bạn tìm kiếm theo user_id và sau đó sắp xếp theo created_at, một composite index trên (user_id, created_at) sẽ giúp ích đáng kể. Nếu đảo ngược thứ tự, trình tối ưu hóa có thể sẽ không sử dụng index cho việc lọc nữa.

Đừng đánh index cho mọi cột. Mỗi index đều làm tăng chi phí (overhead) cho các thao tác insert, update và delete vì MySQL phải duy trì cấu trúc đó. Hãy thêm chúng một cách có tính toán dựa trên các mẫu (patterns) mà bạn tìm thấy trong quá trình đo lường.

Tránh sử dụng hàm trên các cột

Sai lầm này âm thầm làm vô hiệu hóa các chỉ mục. Khi bạn bao bọc một cột trong một hàm bên trong mệnh đề WHERE, MySQL không thể