07/09/2018, 15:31

Tối ưu hoá tốc độ truy vấn trong Rails app – phần 1

Khi làm việc với các project nhỏ mình ít quan tâm đến phần tối ưu hóa truy vấn, vì với lượng data ít và quan hệ giữa các table đơn giản nên app nhanh hay chậm có thể khó nhận biết. Nhưng khi phải tính toán metrics, thực hiện các thống kê có dữ liệu từ nhiều bảng và lượng data ở mỗi bảng rất lớn. ...

Khi làm việc với các project nhỏ mình ít quan tâm đến phần tối ưu hóa truy vấn, vì với lượng data ít và quan hệ giữa các table đơn giản nên app nhanh hay chậm có thể khó nhận biết. Nhưng khi phải tính toán metrics, thực hiện các thống kê có dữ liệu từ nhiều bảng và lượng data ở mỗi bảng rất lớn. Mình đã "hoang mang" khi nhìn trang luôn ở trạng thái loading dù ở localhost và màn hình console các câu truy vấn cứ chạy không ngừng. Đến lúc này, mình đã thực cảm nhận được chữ "chậm". Sau khi tự tìm hiểu, cũng như tham khảo ý kiến từ các anh chị đi trước, mình đã có thể làm trang chạy nhanh hơn, dưới đây là một số điều mình thấy thực sự hữu ích nếu muốn cải thiện tốc độ truy vấn:

1. Tránh N + 1 query

N + 1 query là khái niệm được dùng để chỉ việc bạn load hàng loạt các object từ database lên sau đó thực hiện foreach trên các object và thực hiện các câu truy vấn trên các object đó.

Ví dụ như mình có 2 model là Customer và Address, mỗi customer có một address và một address thuộc về một customer.

 # Model
class Customer < ActiveRecord::Base
  has_one :address
end

class Address < ActiveRecord::Base
  belongs_to :customer
end

# Controller
def index
 @customers = Customer.all
end

# View
<% @customers.each do |c| %>
  <%= c.name %>
  <%= c.address %>
<% end %>

Chúng ta hãy chú ý vào phần view, câu lệnh c.name thì hoạt động tốt vì object đã được load lên bộ nhớ từ controller. Tuy nhiên câu lệnh c.address sẽ thực hiện các truy vấn vào database. Tưởng tượng nếu như chúng ta có 100, 1000,... customer như vậy số câu truy vấn sẽ lên đến 100, 1000,...

Cách giải quyết ở đây là sử dụng eager loading, controller sẽ được cập nhật lại thành

# Controller
def index
  @customers = Customer.all.includes(:address)
end

Như vậy address đã được load lên cùng với customer. Một câu query lớn sẽ hiệu quả hơn là chạy rất nhiều câu query.

2. Nên sử dụng các scope cho việc truy vấn để tạo ra ít câu truy vấn nhất có thể có

Các scope được hỗ trợ bởi rails, giúp định nghĩa các điều kiện truy vấn, chúng ta có thể kết nối nhiều scope với nhau mà không tạo ra nhiều câu truy vấn. Các bạn quan tâm có thể tìm hiểu thêm về scope tại đây

3. Chú ý đến thứ tự truy vấn để thu nhỏ dần tập hợp

Về phần này ví dụ như mình có User, Post, Comment, một user có thể có nhiều post, một post có thể có nhiều comment, một comment thuộc về một post, comment có 2 trạng thái là displayed/hidden. Mình muốn hiển thị ra tất cả các comment bị hidden của một post của một user. Tại đây nếu mình tìm user, rồi tìm post, rồi tìm các comment bị hide của post sẽ nhanh hơn là tìm ngược lại.

Những chia sẻ của mình chắc chắn sẽ còn thiếu nhiều cách hay và tốt để tối ưu hoá truy vấn nên rất hi vọng nhận được nhiều sự chia sẻ từ các bạn :)

Nguồn tham khảo: N+1 query

Ruby Nguyen

0