Rails: Tối ưu ActiveRecord queries
This post hasn't been updated for 7 years
ActiveRecord là một trong những điều tuyệt vời khi làm việc với RoR. Nó ngắn gọn và dễ đọc hơn những dòng SQL dài ngoằng khó chịu. Nhưng nếu bạn không cẩn thận thì cũng rất dễ viết ra những ActiveRecord queries tạo ra những câu SQL kém hiệu quả, phí bộ nhớ đặc biệt nếu làm việc với database với nhiều bảng dữ liệu lớn. May thay, ActiveRecord cung cấp cho chúng ta 1 bộ công cụ để viết những câu queries với hiệu suất tốt hơn. Dưới đây mình sẽ list ra những điểm nổi bật của tool này để các bạn tiện theo dõi.
sum(&:amount) vs. sum(:amount)
Một trong 2 phương thức bên dưới nhanh hơn cái còn lại.
Transaction.sum(&:amount)
Transaction.sum(:amount)
Khác nhau rất nhỏ, chỉ 1 dấu &
(xem thêm Symbol#to_proc
) nhưng ở phương thức đầu tiên, tổng được tính bằng Array#sum
, thay vì sử dụng SQL. Điều này nghĩa là Rails phải select tất cả các cột và khởi tạo model cho mỗi dòng để tạo ra 1 mảng rất lớn.
Phương thức thứ 2 chỉ đơn giản để cho db tính tổng - trông có vẻ tốt hơn nhiều so với Ruby, không select những columns không cần thiết hay khởi tạo model nào cả.
SELECT SUM(amount) FROM `transactions`;
Tin tốt là việc gọi ActiveRecord::Calculations#sum
với 1 block đã không còn được chấp nhận ở Rails 4.0, vì thế bạn sẽ nhận được 1 cảnh báo nếu bạn vô tình(hoặc cố ý) add thêm &
.
pluck vs. map
Transaction.all.map(&:user_id)
Transaction.pluck(:user_id)
map
là một phương thức hữu dụng khác dùng cho Array
. Nhưng hắn cũng là vấn đề. Vì phương thức này dùng cho arrays, nên Rails sẽ cần phải select tất cả các cột để tạo ra model cho mỗi dòng, trong khi chúng ta chỉ muốn 1 cột thôi. Ở ví dụ trên, tất cả các cột từ table transactions
sẽ được select ra, dù tất cả những gì chúng ta cần chỉ là user_id
.
Với phương thức pluck
(available từ Rails 3.2.1), thì chỉ 1 cột được select ra, và chẳng có model nào được tạo cả. Vì vậy câu SQL chỉ đơn giản là:
SELECT user_id FROM `transactions`;
Tất nhiên nếu bạn đã có sẵn các array chứa các model dành riêng cho việc tính toán thì việc map các array này lại có vẻ nhanh hơn nhiều việc query xuống db. Trong những trường hợp này, tốt nhất là so sánh cả 2 cách và chọn cái tối ưu nhất.
uniq
Tưởng tượng nếu bạn muốn lấy ra 1 list những user ids duy nhất từ bảng transactions
, thì với việc sử dụng pluck
chúng ta có thể dễ dàng get ra được 1 list. Cộng thêm việc sử dụng uniq
, có ít nhất 2 cách để lọc ra những đối tượng trùng lặp từ list đó, 1 trong 2 cách sau nhanh hơn nhiều so với thằng còn lại:
Transaction.pluck(:user_id).uniq
Transaction.uniq.pluck(:user_id)
Trong 2 phương thức trên không có phương thức nào tạo ra model hay select những trường ko cần thiết. Điểm khác nhau giữa chúng là cách lọc ra những đối tượng bị trùng lặp. Phương thức đầu tiên, pluck
trả về 1 mảng của user_id
s cho mỗi dòng trong bảng transactions
. Thằng nào trùng lặp sẽ bị lọc ra bởi Array#uniq
.
Ở phương thức thứ 2, uniq
chính là thằng ActiveRecord::QueryMethod#uniq
, nó sẽ add thêm keyword DISTINCT
vào câu SQL:
SELECT DISTINCT user_id FROM `transactions`;
Rõ ràng việc query database sẽ nhanh hơn nhiều, đặc biệt nếu user_id
được đánh index.
find_each vs. each
Giả sử bạn cần phải xử lý gì đó trên từng thằng transaction
, thì với 1 mảng nhỏ, việc sử dụng each
cũng ok:
Transaction.where(processed: false).each { |t| ... }
Nhưng nếu cần xử ký cỡ 100,000 records thì thế nào? (boiroi). Với .all.each
thì đống kết quả trả về sẽ phải load vào bộ nhớ và lặp nhiều lần, to quá thì tèo.
Để ngăn chặn điều đó thì ActiveRecord
đã cũng cấp cho chúng ta phương thức find_each
, nhóm 1000 kết quả trả về 1 nhóm, nên toàn bộ kết quả trả về chỉ được load vào bộ nhớ 1 lần. Nhìn sơ qua thì có vẻ cũng giống each
:
Transaction.where(processed: false).find_each { |t| ... }
find_each
là một bao đóng của ActiveRecord::Batches#find_in_batches
, cho phép config được batch size.
join vs. includes
joins
và includes
nếu được lựa chọn đúng cũng có thể tạo ra 1 tác động lớn đến performance.
joins
chỉ đơn giản add JOIN
vào câu SQL, còn nếu muốn access vào association (vd: user.transactions
) thì nên sử dụng includes
. Rails có thể load has_many
association chỉ trong nháy mắt.
Mặt khác, nếu chỉ muốn join các table lại để lọc ra tập kết quả nào đó trong SQL thì việc sử dụng includes
quả là 1 sự phí phạm, vì nó sẽ select 1 đống columns và models không cần thiết.
Wrap up
Nên thận trọng trong việc sử dụng ActiveRecord
queries vì chúng là 1 con dao 2 lưỡi cho performance. Nên để mắt đến development.log
. Cũng có nhiều GEM khá hay dành cho việc quản lý performance:
Bullet
- Giúp kiểm soát N+1 queriesPeek
RailsPanel
Bài viết được dịch lại từ Web ascender
All Rights Reserved