+2

Tăng tốc độ ActiveRecord trong Rails

Khi xây dựng một ứng dụng Rails, ActiveRecord mặc định sẽ cung cấp cho chúng ta các phương thức để kết nối với database. Truy vấn với .where, thêm mới dữ liệu với .save , tất cả đều dễ dàng và đủ nhanh.

Tuy nhiên, khi một trang cần tài nhiểu dữ liệu từ server , chúng ta có thể bị lỗi 504 nếu thời gian reponse từ server quá lâu và khi đó chúng ta cần phải cài thiện hiệu suất của ứng dụng mà chúng ta đang truy vấn. Chúng ta có thể giải quyết những vấn đề này với bộ nhớ đệm. Nhưng điều đó bổ sung thêm một phức tạp nữa, như việc hết hạn, làm mới, và lỗi sản sinh, và đó là điều không cần thiết lúc này. Thay vào đó, chúng ta có thể sửa một vài vấn đề khi truy vấn để tăng hiệu suất trong Rails.

  • Lấy tất cả dữ liệu cần thiết trong một lần truy vấn với ActiveRecord Giả sử trong DB, chúng ta có 2 bảng dữ liệu restaurants và reviews liên kết theo mỗi quan hệ 1 - n. Trong trang index của restaurants chúng ta cần lấy thông tin trên bảng restaurant và review tương ứng.
<% @restaurants.each do |restaurant| %>
  <tr>
    <td><%= restaurant.name %></td>
    <td><%= restaurant.review_average %></td>
    ...

Khi load trang, trong màn hinh console chúng ta sẽ thấy

Processing by RestaurantsController#index as HTML
  Restaurant Load (1.6ms)  SELECT `restaurants`.* FROM `restaurants`
  Review Load (1.2ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 1
  Review Load (1.2ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 2
  Review Load (1.1ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 3
  Review Load (1.2ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 4
  Review Load (1.2ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 5
  Review Load (1.2ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 6
  Review Load (1.2ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 7
  Review Load (1.0ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 8

Đây là vấn đề truy vấn N +1 chúng ta thường gặp phải khi truy vấn dữ liệu giữa các bảng với nhau, khi lặp các restaurants, với mỗi một restaurant chúng ta lại phải truy vấn thêm một lần vào DB để tìm dữ liêu review tương ứng, chính vấn đề này đã làm giảm hiệu suất của hệ thống, do vậy chúng ta cần phải tránh vấn đề này bằng việc sử dụng cách load tất cả các dữ liệu cần trong một lần, và cách này gọi là eager loading. và ở đây là .includes

#app/controllers/restaurants_controller.rb
def index
  @restaurants = Restaurant.all.includes(:reviews)
end

Khi sử dụng .includes, việc load data như sau:

Restaurant Load (1.1ms)  SELECT `restaurants`.* FROM `restaurants`
Review Load (3.0ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` IN (1, 2, 3, 4, 5, 6, 7, 8)

Trong trường hợp chúng ta không lấy các record cha, chúng ta phải load trước khi sử dụng bằng preload. Cách sử dụng preload cũng đơn giản, chỉ cần liệt kê các bảng cần phải load trước:

#app/controllers/restaurants_controller.rb
def index
  @restaurants = Restaurant.all.preload(:reviews)
end
  • Sử dụng Select trong truy vấn để lấy những trường cần sử dụng.

Trong ví dụ trên, chúng ta cần phải lấy trung bình review của các restauran, nếu lấy theo cách trên chúng ta sẽ phải load toàn bộ các trường của review mà không có sử dụng, do vậy chúng ta nên sử dụng select.

@restaurants = Restaurant.all
  .select("restaurants.*, AVG(reviews.rating) AS review_average")
  .joins(:reviews)
  .group("restaurants.id")

Khi sử dụng AS trong select, Rails sẽ tự ra một thuộc tính mới cho record đang truy vấn `@restaurants.first.review_average # => 2.0'. Trong Rails chúng ta cũng có thể viết như sau:

@restaurants = Restaurant.all
  .select("restaurants.*, COUNT(reviews.id) AS review_count")
  .joins(:reviews)
  .group("restaurants.id")
  .where("reviews.created_at > ?", 1.months.ago)
  .having("COUNT(reviews.id) > 10")

và để có thể truy vấn nhanh hơn, chúng ta dùng find_by_sql

@restaurants = Restaurant.find_by_sql(["
  SELECT  restaurants.*, COUNT(reviews.id) AS review_count
    FROM `restaurants`
      INNER JOIN `reviews` ON `reviews`.`restaurant_id` = `restaurants`.`id`
    WHERE (reviews.created_at > ?)
    GROUP BY restaurants.id
    HAVING COUNT(reviews.id) > 10",
    1.months.ago])

và Rails cũng sẽ tạo các thuộc tính cho tất cả các columns trong select. find_by_sql thường ít khi được sử dụng, nó thường được sử dụng khi cần thực hiện các truy vấn phức tạp mà các phương thức truy vấn nhanh không hỗ trợ được.

  • Sử dụng gem khi import dữ liệu từ file CSV

Sử dụng includes, select,và find_by_sql giúp chúng ta cải thiện hiệu suất khi cần load data cho các page, nhưng khi chúng ta cần import một lượng lớn data từ file csv, chúng ta sẽ gặp một loạt vấn đề khiễn cho việc import trở nên chậm chạp hơn như vấn đề N + 1... Để giải quyết vấn đề này chúng ta phải sử dụng gem activerecord-import.

Giả sử chúng ta cần import 5 record vào một table, theo cách bình thường chúng ta sẽ phải thực hiện 5 lần sql.

pricing_data = [
 ["New York", 130],
 ["Los Angeles", 130],
 ["Chicago", 120],
 ["Miami", 110],
 ["Dallas", 110]]

pricing_data.each do |location, price|
 Inventory.create(location: location, price: price) 
end

Với việc dùng activerecord-import dữ liệu sẽ được thêm vào một mảng và gọi import:

records_to_import = pricing_data.map do |location, price|
 Inventory.new(location: location, price: price) 
end
Inventory.import records_to_import 

Và việc import giờ chỉ cần một lần gọi SQl nên sẽ nhanh hơn nhiều.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí