Công việc cải thiện hiệu năng cơ sở dữ liệu thường bắt đầu bằng việc ai đó đề xuất một máy chủ lớn hơn, và thường kết thúc bằng phát hiện rằng chỉ một truy vấn duy nhất đã quét tuần tự qua 4 triệu dòng trong mỗi lần tải trang. Phần cứng chưa bao giờ là giới hạn. Kế hoạch thực thi mới là giới hạn.

Khuôn mẫu này lặp lại đủ đều đặn để đáng được nêu ra như một giả định mặc định. Khi một ứng dụng chạy chậm và cơ sở dữ liệu đang bận, nguyên nhân gần như luôn nằm ở một số ít truy vấn cụ thể chứ không phải ở sự thiếu hụt năng lực chung, và việc nâng cấp máy chỉ che giấu vấn đề đúng bằng khoảng thời gian mà bảng cần để lớn trở lại.

Hãy đo trước khi thay đổi bất cứ điều gì. Tối ưu một truy vấn mà bạn chỉ phỏng đoán chính là cách các đội mất trọn một tuần để thêm những index làm chậm ghi mà không hề làm nhanh đọc. Mọi cơ sở dữ liệu đều có thể cho bạn biết câu lệnh nào tiêu tốn nhiều tổng thời gian nhất. Hãy bắt đầu từ đó, sửa câu lệnh đứng đầu, rồi đo lại. Hai hoặc ba vòng lặp như vậy thường đủ để kết thúc sự cố.


Hiệu năng cơ sở dữ liệu bắt đầu từ việc tìm ra truy vấn

Tổng thời gian quan trọng hơn trường hợp tệ nhất. Một truy vấn mất hai giây và chạy hai lần mỗi ngày là không đáng kể. Một truy vấn mất bốn mươi mili giây nhưng chạy 8.000 lần mỗi phút mới là vấn đề của bạn, và nó sẽ không bao giờ xuất hiện trong nhật ký truy vấn chậm đặt ngưỡng một giây.

Trong Postgres, phần mở rộng pg_stat_statements tổng hợp đúng những con số đó: số lần gọi, tổng thời gian và thời gian trung bình cho mỗi câu lệnh đã chuẩn hóa. Hãy sắp xếp theo tổng thời gian, thủ phạm thường nằm trong ba dòng đầu tiên. MySQL cung cấp cách tổng hợp tương tự thông qua performance schema.

Có hai điều đáng kiểm tra trước khi kết luận rằng bản thân truy vấn có lỗi. Nó chậm mọi lúc, hay chỉ chậm vào một số giờ nhất định, điều vốn chỉ ra sự tranh chấp tài nguyên hơn là kế hoạch thực thi? Và nó chậm khi chạy riêng lẻ, hay chỉ chậm khi có nhiều phiên đồng thời, điều vốn chỉ ra khóa hoặc giới hạn kết nối?

Hãy đọc kế hoạch thay vì đoán

Khi đã có câu lệnh, hãy hỏi cơ sở dữ liệu xem nó định chạy câu lệnh đó ra sao. Postgres phơi bày điều này qua EXPLAIN , và biến thể quan trọng là EXPLAIN ANALYZE, lệnh thực sự chạy truy vấn và báo cáo thời gian thật thay vì con số ước lượng.

Ba điều trong phần kết quả đó mang phần lớn tín hiệu.

Quét tuần tự trên một bảng lớn. Cơ sở dữ liệu đang đọc từng dòng một. Trên bảng nhỏ, điều đó vừa đúng vừa nhanh. Trên bảng lớn, nó có nghĩa là không có index nào dùng được cho điều kiện bạn đã viết, hoặc trình lập kế hoạch đã quyết định rằng index không đáng dùng.

Khoảng cách lớn giữa số dòng ước lượng và số dòng thực tế. Trình lập kế hoạch chọn chiến lược dựa trên thống kê, nên khi ước lượng của nó sai lệch nhiều bậc độ lớn, nó chọn sai vì những lý do chẳng liên quan gì đến truy vấn của bạn. Thống kê cũ là nguyên nhân phổ biến và dễ khắc phục.

Thời gian dồn vào một nút duy nhất. Kế hoạch thực thi là một cây, và việc sửa thuộc về đúng nút đã tiêu tốn thời gian. Tối ưu bất kỳ nút nào khác cũng không thay đổi được gì.

Bản năng thêm ngay một index khi vừa thấy quét tuần tự thường là đúng, nhưng vẫn đáng kìm lại ba mươi giây, bởi lý do khiến index không được dùng đôi khi còn quan trọng hơn cả việc thiếu index.

Vì sao index không giúp được gì

Một index tồn tại chưa phải là một index được dùng.

Điều kiện không sargable. Bọc một cột trong hàm, hoặc thực hiện phép tính trên cột đó, thường ngăn index trên cột ấy được sử dụng, vì index lưu giá trị gốc của cột chứ không lưu giá trị đã biến đổi. Viết lại điều kiện để cột đứng nguyên vẹn thường khôi phục được việc dùng index.

Thứ tự cột trong index tổ hợp bị sai. Một index tổ hợp phục vụ các truy vấn dùng những cột đứng đầu của nó. Index đặt trên một cột rồi tới cột khác sẽ không giúp gì cho truy vấn chỉ lọc theo cột thứ hai, và người ta mắc lỗi này liên tục.

Trình lập kế hoạch cho rằng quét toàn bảng rẻ hơn. Nếu một truy vấn trả về một phần lớn của bảng, đọc tuần tự thực sự nhanh hơn nhảy qua index. Đây là hành vi đúng, và cách sửa là trả về ít dữ liệu hơn.

Thống kê đã cũ. Sau một lần nạp hàng loạt hoặc một lần xóa lớn, bức tranh dữ liệu mà trình lập kế hoạch nắm giữ có thể sai nghiêm trọng cho tới khi thống kê được làm mới.

Và mọi index đều có cái giá của nó. Các thao tác ghi phải duy trì nó, và nó chiếm bộ nhớ vốn có thể dùng để cache dữ liệu. Một bảng có mười lăm index thường chứa vài index chẳng ai cần, mỗi cái lại làm mọi lệnh chèn chậm thêm.

Vấn đề N+1 vẫn là nguyên nhân đơn lẻ lớn nhất

Sự chậm chạp của ứng dụng đến từ đây nhiều hơn từ bất kỳ vấn đề kế hoạch nào, và nó không bao giờ lộ ra dưới dạng truy vấn chậm vì từng truy vấn riêng lẻ đều nhanh.

Hình dạng của nó rất quen thuộc. Lấy về một danh sách 100 bản ghi, rồi lặp qua danh sách và lấy dữ liệu liên quan cho từng bản ghi. Kết quả là 101 lượt đi về trong khi một hoặc hai lượt là đủ. Mỗi truy vấn trả kết quả trong ba mili giây mà trang vẫn mất nửa giây, vì chi phí nằm ở các lượt đi về chứ không nằm ở khối lượng công việc.

Các bộ ánh xạ đối tượng quan hệ khiến lỗi này rất dễ viết ra một cách vô tình, vì việc lấy dữ liệu liên quan trông giống như đọc một thuộc tính chứ không giống một lời gọi cơ sở dữ liệu. Cách sửa là nạp dữ liệu liên quan trong một truy vấn duy nhất cùng với tập dữ liệu cha, điều mà mọi ORM trưởng thành đều hỗ trợ và phần lớn lại không bật sẵn.

Phát hiện nó rất đơn giản: hãy đếm số truy vấn trên mỗi yêu cầu. Nếu một trang phát ra số truy vấn tỉ lệ với số mục hiển thị, bạn đã tìm ra thủ phạm. Đây cũng là việc đáng kiểm tra nhất khi một ứng dụng chạy chậm ở biên, như hướng dẫn Cloudflare Hyperdrive của chúng tôi đã trình bày, vì các lượt đi về tốn kém hơn nhiều khi khoảng cách xa hơn.

Kết nối và tranh chấp

Hai vấn đề trông giống chậm nhưng không phải là chậm.

Cạn kiệt kết nối. Mọi cơ sở dữ liệu đều có trần số kết nối đồng thời, và mỗi kết nối đều tốn bộ nhớ. Khi một ứng dụng mở nhiều kết nối hơn mức pool cho phép, các yêu cầu xếp hàng chờ một kết nối trống và ứng dụng có vẻ chậm trong khi cơ sở dữ liệu ngồi không. Triệu chứng là độ trễ ứng dụng cao đi kèm mức sử dụng CPU của cơ sở dữ liệu thấp, và cách sửa là dùng pool chứ không phải mua máy lớn hơn.

Tranh chấp khóa. Một giao dịch dài giữ khóa sẽ chặn mọi thứ xếp hàng phía sau. Nguyên nhân thường gặp là một giao dịch bị để mở suốt phần công việc vốn không cần đến cơ sở dữ liệu, chẳng hạn một lời gọi HTTP sang dịch vụ khác. Hãy giữ giao dịch ngắn và chỉ bao quanh đúng phần việc trên cơ sở dữ liệu.

Cả hai trường hợp đều đáng được loại trừ sớm, vì cả hai đều dễ bị đọc nhầm thành vấn đề truy vấn và không trường hợp nào được sửa bằng một index.

Nên làm gì, theo thứ tự nào

Hãy tìm những câu lệnh tiêu tốn nhiều tổng thời gian nhất. Chạy EXPLAIN ANALYZE trên câu lệnh tệ nhất và đọc xem thời gian thực sự đi đâu. Kiểm tra số truy vấn trên mỗi yêu cầu để loại trừ N+1 trước khi tối ưu bất cứ thứ gì. Làm mới thống kê trước khi thêm index, vì đôi khi đó đã là toàn bộ cách sửa. Sau đó hãy thêm index hẹp nhất phục vụ được điều kiện, rồi đo lại.

Mecanik làm việc này như một phần trong dịch vụ phát triển phần mềm của chúng tôi, và kết quả gần như luôn giống nhau: hai hoặc ba truy vấn là thủ phạm, cách sửa rất nhỏ, và cỗ máy lớn hơn mà chưa ai kịp mua thì không bao giờ cần đến.


Bài viết liên quan: Phiên bản hóa API: khi nào nên phá vỡ và cách để không phá vỡ , Cách Xây Dựng Ứng Dụng Web Năm 2026 - Hướng Dẫn UK , Lưu trữ mật khẩu: nên dùng gì năm 2026 , Phat trien phan mem tuy chinh tai Vuong quoc Anh .


Câu hỏi thường gặp

Làm sao để biết truy vấn nào đang làm chậm ứng dụng của tôi? Hãy sắp xếp theo tổng thời gian thay vì theo trường hợp tệ nhất. Một truy vấn bốn mươi mili giây chạy 8.000 lần mỗi phút tốn kém hơn nhiều so với một truy vấn hai giây chạy hai lần mỗi ngày, và nó sẽ không bao giờ xuất hiện trong nhật ký truy vấn chậm đặt ngưỡng một giây. Trong Postgres, pg_stat_statements tổng hợp số lần gọi và tổng thời gian cho từng câu lệnh; MySQL cung cấp điều tương tự qua performance schema.

Tôi nên tìm gì trong kết quả EXPLAIN ANALYZE? Ba điều mang phần lớn tín hiệu: quét tuần tự trên một bảng lớn, nghĩa là không có index dùng được; khoảng cách lớn giữa số dòng ước lượng và số dòng thực tế, nghĩa là trình lập kế hoạch đang làm việc với thống kê sai; và thời gian dồn vào một nút của cây kế hoạch, chính là nơi cần sửa. Tối ưu bất kỳ nút nào khác cũng không thay đổi được gì.

Vì sao index của tôi không được dùng? Thường là do một trong bốn lý do. Điều kiện bọc cột trong một hàm hoặc một phép tính, nên index không còn khớp nữa. Index tổ hợp sắp xếp các cột theo thứ tự không phục vụ truy vấn. Truy vấn trả về một phần bảng đủ lớn để quét toàn bảng thực sự rẻ hơn. Hoặc thống kê đã cũ sau một lần nạp hàng loạt hay một lần xóa.

Vấn đề truy vấn N+1 là gì? Là việc lấy về một danh sách bản ghi rồi phát ra một truy vấn riêng cho dữ liệu liên quan của từng bản ghi, tạo ra 101 lượt đi về trong khi một hoặc hai lượt là đủ. Nó không xuất hiện trong nhật ký truy vấn chậm vì mỗi truy vấn đều nhanh; chi phí nằm ở các lượt đi về. Hãy phát hiện nó bằng cách đếm số truy vấn trên mỗi yêu cầu và tìm con số tỉ lệ với số mục hiển thị.

Máy chủ cơ sở dữ liệu lớn hơn có sửa được truy vấn chậm không? Hiếm khi, và chỉ tạm thời. Khi một ứng dụng chạy chậm và cơ sở dữ liệu đang bận, nguyên nhân gần như luôn là một số ít truy vấn cụ thể chứ không phải thiếu năng lực, nên nâng cấp máy chỉ che giấu vấn đề cho tới khi bảng lớn trở lại. Độ trễ ứng dụng cao đi kèm CPU cơ sở dữ liệu thấp thường là dấu hiệu của connection pool cạn kiệt, và thêm phần cứng không giải quyết được điều đó.