Sau đây, mình xin hướng dẫn các bạn một vài típ có thể tối ưu hóa query khi sử dụng Active Record nhé
1. Sử dụng Eager Loading với includes
để tránh n+1 query
1 2 3 4 | <span class="token keyword">class</span> <span class="token class-name">Post</span> has_many <span class="token symbol">:comments</span> <span class="token keyword">end</span> |
- Ví dụ, nếu bạn có
n
rows của bảngPost
nếu gọi như bình thường sẽ khi gọi tất cả bản ghicomments
củaPost
ra thì nó sẽ tạo ra các câu query như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | posts <span class="token operator">=</span> <span class="token constant">Post</span><span class="token punctuation">.</span>all posts<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>post<span class="token operator">|</span> post<span class="token punctuation">.</span>comments <span class="token keyword">end</span> <span class="token comment"># Nó sẽ tạo ra N+1 query như thế này:</span> <span class="token comment"># SELECT * FROM posts;</span> <span class="token comment"># SELECT * FROM comments WHERE comments.post_id = 1;</span> <span class="token comment"># SELECT * FROM comments WHERE comments.post_id = 2;</span> <span class="token comment"># SELECT * FROM comments WHERE comments.post_id = 3;</span> <span class="token comment"># .....</span> <span class="token comment"># SELECT * FROM comments WHERE comments.post_id = n;</span> <span class="token comment">#-> Điều này là sẽ ảnh hưởng đến performance của hệ thống</span> |
- Tuy nhiên, nếu ở đây ta sử dụng
includes
thì chúng ta chỉ mất 2 query là có thể lấy ra dữ liệu mà chúng ta cần:
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token comment"># Sử dụng Eager-load</span> posts <span class="token operator">=</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token symbol">:comments</span><span class="token punctuation">)</span> posts<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>post<span class="token operator">|</span> post<span class="token punctuation">.</span>comments <span class="token keyword">end</span> <span class="token comment"># Sẽ tạo ra 2 query:</span> <span class="token comment"># SELECT * FROM posts;</span> <span class="token comment"># SELECT * FROM comments WHERE comments.post_id IN (1,2,3,4,....,n);</span> <span class="token comment">#-> Như vậy, performance của bạn có thể cải thiện hơn nhiều rồi :)</span> |
2. Sử dụng find_each
khi load một lượng lớn các bản ghi
- Nếu sử dụng cách này nó sẽ khiến ta sẽ tăng số lượng truy vấn được thực hiện, nhưng nó sẽ giảm lượng bộ nhớ mà ta sử dụng khi tải một lượng lớn bản ghi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <span class="token comment"># 1. Sử dụng truy vấn bình thường</span> <span class="token constant">Post</span><span class="token punctuation">.</span>all<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>post<span class="token operator">|</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">end</span> <span class="token comment"># -> Sẽ sinh ra câu query như sau:</span> <span class="token comment"># SELECT * FROM posts;</span> <span class="token comment"># 2. Sử dụng find_each sẽ query theo batch của size </span> <span class="token comment"># với tùy chọn :batch_size option (default là 1000)</span> <span class="token constant">Post</span><span class="token punctuation">.</span>all<span class="token punctuation">.</span><span class="token function">find_each</span><span class="token punctuation">(</span>batch_size<span class="token punctuation">:</span> <span class="token number">5000</span><span class="token punctuation">)</span> <span class="token keyword">do</span> <span class="token operator">|</span>post<span class="token operator">|</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">end</span> <span class="token comment"># -> Sẽ sinh ra câu query như sau:</span> <span class="token comment"># SELECT * FROM posts ORDER BY posts.id ASC LIMIT 5000;</span> <span class="token comment"># SELECT * FROM posts WHERE posts.id > 5000 ORDER BY posts.id ASC LIMIT 5000;</span> <span class="token comment"># SELECT * FROM posts WHERE posts.id > 10000 ORDER BY posts.id ASC LIMIT 5000;</span> <span class="token comment"># ....</span> |
- Ở đây, thay vì tải một lượng lớn các bản ghi vào bộ nhớ để xử lý (điều này, có thể làm sập máy chủ nếu có quá nhiều bản ghi) thì
find_each
tải từng batch một lần và sẽ thu thập chúng lại sau khi sử dụng.
3. Sử dụng Select
và Pluck
cho attributes
- Đôi khi, chúng ta không cần lấy tất cả các thuộc tính của model, mà chỉ lấy một vài thuộc tính của model đó thôi thì ta có thể sử dụng
pluck
để truy vấn các thuộc tính cần thiết và trả về chúng dưới dạng mộtArray
thay vìActiveRecord_Relation
. - Tuy nhiên, nếu ta cần trả về dưới dạng
ActiveRecord_Relation
, ta có thể sử dụngselect
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token comment"># 1. Ví dụ ta muốn lấy tất cả các bản ghi Post</span> post_names <span class="token operator">=</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">where</span><span class="token punctuation">(</span>type<span class="token punctuation">:</span> <span class="token string">"js"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token operator">&</span><span class="token symbol">:name</span><span class="token punctuation">)</span> <span class="token comment"># SELECT * FROM posts;</span> <span class="token comment"># -> Như thế ta sẽ phải load tất cả thuộc tính từ CSDL và tải tất cả chúng vào trong bộ nhớ.</span> <span class="token comment"># 2. Thay vì thế, chúng ta có thể sử dụng pluck để truy vấn với</span> <span class="token comment"># các thuộc tính mà chúng ta cần. Nó cũng trả về các thuộc tính dưới dạng một Array</span> <span class="token comment"># thay vì các ActiveRecord_Relation Post. Điều này sẽ giảm việc sử dụng bộ nhớ lại</span> post_names <span class="token operator">=</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">where</span><span class="token punctuation">(</span>type<span class="token punctuation">:</span> <span class="token string">"js"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">pluck</span><span class="token punctuation">(</span><span class="token symbol">:name</span><span class="token punctuation">)</span> <span class="token comment"># Select posts.name FROM posts;</span> <span class="token comment"># 3. Tương tự như pluck nhưng nếu ta muốn trả về </span> <span class="token comment"># theo dạng ActiveRecord_Relation thì ta có thể sử dụng select:</span> post_names <span class="token operator">=</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">where</span><span class="token punctuation">(</span>type<span class="token punctuation">:</span> <span class="token string">"js"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">select</span><span class="token punctuation">(</span><span class="token symbol">:name</span><span class="token punctuation">)</span> <span class="token comment"># Select posts.name FROM posts;</span> |
4. Sử dụng exists?
để kiểm tra sự tồn tại của bản ghi
- Đôi khi, chúng ta chỉ muốn biết bản ghi có tồn tại hay không, mà không cần phải làm gì với bản ghi đó. Ta có thể sử dụng
exists?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <span class="token comment"># Nếu chúng ta chỉ muốn biết một bản ghi tồn tại hay ko mà không thực hiện gì với bản ghi đó như này:</span> <span class="token keyword">if</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">where</span><span class="token punctuation">(</span>type<span class="token punctuation">:</span> <span class="token string">"ruby"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>present<span class="token operator">?</span> puts <span class="token string">"Good"</span> <span class="token keyword">else</span> puts <span class="token string">"Bad"</span> <span class="token keyword">end</span> <span class="token comment"># -> Như vậy, nó sẽ lại giống với việc ta sử dụng ở phần 3.</span> <span class="token comment"># Đó là, ta sẽ phải load tất cả thuộc tính từ CSDL và tải tất cả chúng vào trong bộ nhớ.</span> <span class="token comment"># Nếu sử dụng exists?</span> <span class="token keyword">if</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">where</span><span class="token punctuation">(</span>type<span class="token punctuation">:</span> <span class="token string">"ruby"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>exists<span class="token operator">?</span> puts <span class="token string">"Good"</span> <span class="token keyword">else</span> puts <span class="token string">"Bad"</span> <span class="token keyword">end</span> <span class="token comment"># Như vậy nó sẽ giới hạn truy vấn tương ứng với việc chỉ có 1 bản ghi được truy vấn</span> <span class="token comment"># và nó không select bất kỳ atrribute nào.</span> <span class="token comment"># SELECT 1 FROM posts WHERE type = "ruby" LIMIT 1;</span> |
5. Sử dụng ActiveRecord::Relation#size
thay vì ActiveRecord::Calculations#count
- Ví dụ ta có
ActiveRecord :: Relation Post
, khi sử dụngposts.count
vàposts.size
thì nó đều trả về số lượngposts
. Tuy nhiên,posts.count
sẽ luôn chạy truy vấn nhưSELECT COUNT(*) FROM posts WHERE ...
ngay cả khi các mối quan hệ đã được tải. - Còn
size
sẽ thông minh hơn ở chỗ nó sẽ gọilength
nếu mối quan hệ nếu nó đã được tải nhưng nó sẽ gọicount
nếu mối quan hệ chưa được tải.
1 2 3 4 5 | <span class="token comment"># File activerecord/lib/active_record/relation.rb, line 210</span> <span class="token keyword">def</span> size loaded<span class="token operator">?</span> <span class="token operator">?</span> <span class="token variable">@records</span><span class="token punctuation">.</span>length <span class="token punctuation">:</span> <span class="token function">count</span><span class="token punctuation">(</span><span class="token symbol">:all</span><span class="token punctuation">)</span> <span class="token keyword">end</span> |
6. Sử dụng delete_all
số lượng lớn các bản ghi
- Khi muốn xóa một số lượng lớn các bản ghi không require
Active Record callback
, hãy sử dụngdelete_all
hoặc thay vì gọidestroy
cho các đối tượng riêng lẻ.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token comment"># 1. Xóa một lượng lớn các bản ghi sử dụng destroy</span> <span class="token comment"># -> dẫn đến việc nó sẽ sinh ra N query như này:</span> destroy_posts <span class="token operator">=</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">where</span><span class="token punctuation">(</span>type<span class="token punctuation">:</span> <span class="token string">"js"</span><span class="token punctuation">)</span> destroy_posts<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>dp<span class="token operator">|</span> dp<span class="token punctuation">.</span>destroy <span class="token keyword">end</span> <span class="token comment"># DELETE FROM posts WHERE id = 6;</span> <span class="token comment"># DELETE FROM posts WHERE id = 10;</span> <span class="token comment"># DELETE FROM posts WHERE id = 23;</span> <span class="token comment"># DELETE FROM posts WHERE id = 50;</span> <span class="token comment"># ...</span> <span class="token comment"># 2. Sử dụng delete_all để xóa hàng loạt các bản ghi này bằng một truy vấn:</span> destroy_posts<span class="token punctuation">.</span>delete_all <span class="token comment"># DELETE FROM posts WHERE posts.type = "js";</span> |
7. Sử dụng create
cho array chứa hash
ActiveRecord::Base#create
có thể chấp nhận array chứa các hash bên trong để tạo các bản ghi. Nó sẽ chạy một query thay vì N query như bình thường. Tuy nhiên, điều kiện ở đây là CSDL của chúng ta phải hỗ trỡ việc insert hàng loạt như thế này. Ví dụ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <span class="token comment"># Ta có một array chứa hash</span> new_posts <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token punctuation">{</span>name<span class="token punctuation">:</span> <span class="token string">"Ruby"</span><span class="token punctuation">,</span> type<span class="token punctuation">:</span> <span class="token string">"ruby"</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>name<span class="token punctuation">:</span> <span class="token string">"Python"</span><span class="token punctuation">,</span> type<span class="token punctuation">:</span> <span class="token string">"pyp"</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>name<span class="token punctuation">:</span> <span class="token string">"JS"</span><span class="token punctuation">,</span> type<span class="token punctuation">:</span> <span class="token string">"js"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token punctuation">{</span>name<span class="token punctuation">:</span> <span class="token string">"Java"</span><span class="token punctuation">,</span> type<span class="token punctuation">:</span> <span class="token string">"java"</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token comment"># 1. Đối việc việc insert vào DB như bình thường như thế này nó sẽ sinh ra N query:</span> new_posts<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>post<span class="token operator">|</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>post<span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token comment"># INSERT INTO posts (name, type) VALUES ("Ruby", "ruby)</span> <span class="token comment"># INSERT INTO posts (name, type) VALUES ("Python", "pyp")</span> <span class="token comment"># INSERT INTO posts (name, type) VALUES ("JS", "js")</span> <span class="token comment"># ...</span> <span class="token comment"># INSERT INTO posts (name, type) VALUES ("Java, "java")</span> <span class="token comment"># 2. Thay vào đó, ta sẽ truyền mảng vào create, nó sẽ tạo</span> <span class="token comment"># tất cả các bản ghi trong một truy vấn nếu CSDL hỗ trợ tính năng này:</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>new_posts<span class="token punctuation">)</span> <span class="token comment"># INSERT INTO posts (name, type) </span> <span class="token comment"># VALUES </span> <span class="token comment"># ("Ruby", "ruby),</span> <span class="token comment"># ("Python", "pyp"),</span> <span class="token comment"># ("JS", "js"),</span> <span class="token comment"># ....</span> <span class="token comment"># ("Java, "java");</span> |
8. Sử dụng update
cho nhiều bản ghi cùng lúc
- Tương tự, cũng có thể
update
nhiều bản ghi trong một truy vấn
1 2 3 4 5 6 7 8 9 10 | posts <span class="token operator">=</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">where</span><span class="token punctuation">(</span>category_id<span class="token punctuation">:</span> <span class="token number">2</span><span class="token punctuation">)</span> <span class="token comment"># Sử dụng update bình thường:</span> posts<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>post<span class="token operator">|</span> post<span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span>status<span class="token punctuation">:</span> <span class="token number">3</span><span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token comment"># Sử dụng update tất cả trong một query</span> <span class="token constant">Post</span><span class="token punctuation">.</span><span class="token function">where</span><span class="token punctuation">(</span>category_id<span class="token punctuation">:</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">update_all</span><span class="token punctuation">(</span>status<span class="token punctuation">:</span> <span class="token number">3</span><span class="token punctuation">)</span> |