Scroll to top
10 tối ưu đơn giản giúp tăng hiệu suất website Ruby on Rails

10 tối ưu đơn giản giúp tăng hiệu suất website Ruby on Rails

19/12/2025 21:00
12min read

10 mẹo tối ưu Rails dễ áp dụng: sửa N+1, dùng pluck/select, thêm index, batch, bulk update, cache data & view… giúp cải thiện hiệu suất của website Ruby on Rails.

Ứng dụng Ruby on Rails “chậm” gần như luôn xuất phát từ 3 nhóm nguyên nhân:

  1. Query DB quá nhiều (đặc biệt là N+1, COUNT/EXISTS dùng sai, thiếu index).
  2. Load dữ liệu thừa (kéo cả bảng/cả cột “nặng”, hoặc load hết vào RAM).
  3. Render view / callbacks làm việc dư (render nhiều partial, format nặng, chạy callbacks/validations khi không cần).

Bài viết này tổng hợp 10 tối ưu mình dùng nhiều nhấtdễ hiểu, dễ áp dụng, hiệu quả thấy ngay.

1. Giải quyết N+1 Query bằng includes

Vấn đề: Bạn load list posts, trong view lại gọi association như post.user.name hoặc post.comments.size. Nếu có 20 posts, Rails có thể bắn thêm 20 query (hoặc nhiều hơn).

Giải pháp: preload association trước bằng includes.

Trước: Phát sinh N+1 query

ruby
Copy
1
2
3
4
5
@posts = Post.order(created_at: :desc).limit(20)

@posts.each do |post|
  post.user.name # mỗi lần có thể query SELECT users... WHERE id = ?
end

Sau: Đã sử dụng includes

ruby
Copy
1
@posts = Post.includes(:user).order(created_at: :desc).limit(20)

Rails sẽ preload users cho toàn bộ posts trong 1 query (hoặc 1 query bổ sung), tránh loop bắn query.

Dùng bất cứ khi nào bạn render list và gọi association trong loop (post.comments, post.user, order.items…).

Lưu ý quan trọng: includes có 3 dạng (tùy query), Rails có thể chọn cách preload khác nhau.

  • preload: luôn chạy 2 query (1 cho posts, 1 cho users).
  • eager_load: luôn dùng LEFT OUTER JOIN (1 query lớn).
  • includes: Rails tự quyết (có thể preload hoặc join).

2. Chỉ lấy đúng cột cần dùng: select / pluck

Vấn đề: User.all hoặc User.where(...).to_a kéo tất cả cột, kể cả cột “nặng” như bio (text), settings (jsonb), avatar_data, v.v. Trong khi bạn chỉ cần idname.

Giải pháp:

Khi vẫn muốn object ActiveRecord (nhưng tối giản)

Dùng select:

ruby
Copy
1
2
users = User.where(active: true).select(:id, :name)
users.first.name # OK

Khi chỉ cần mảng giá trị (nhanh + ít allocations)

Dùng pluck:

ruby
Copy
1
2
ids   = User.where(active: true).pluck(:id)
pairs = User.where(active: true).pluck(:id, :name) # [[1, "A"], [2, "B"]]

Lợi ích: giảm thời gian DB + giảm dữ liệu trả về + giảm Ruby allocations.

Dùng khi hiển thị trong dropdown/select box vì chỉ cần id, name, hoặc dùng trong các background job chỉ cần id để xử lý.

3. Chỉ cần truy vấn “có tồn tại không?”, hãy dùng exists?

Vấn đề: Nhiều người check tồn tại bằng any?/present? trên relation. Điều này có thể load record không cần thiết, hoặc chạy query không tối ưu.

Giải pháp:

Trước: có thể phát sinh load dữ liệu không cần thiết

ruby
Copy
1
User.where(email: email).any?

Sau: query EXISTS tối ưu hơn

ruby
Copy
1
User.exists?(email: email)

Ví dụ dùng khi check email trùng, check “user có order nào chưa”, check “bản ghi đã tồn tại chưa”…

4. Dùng đúng count, size, length để tránh query thừa

Đây là cái rất hay gây ứng dụng của bạn chậm âm thầm.

Method Có query DB không? Query gì Load record không? Khi nào nên dùng
count Có (luôn luôn) SELECT COUNT(*) Không Khi cần số lượng chính xác trực tiếp từ DB
length Có nếu chưa load SELECT * Có (load toàn bộ) Chỉ dùng khi chắc chắn records đã load sẵn (array)
size Tùy tình trạng COUNT(*) hoặc không Không hoặc có Mặc định tốt nhất cho ActiveRecord và association

Dùng khi bạn không chắc association đã load chưa

ruby
Copy
1
comments_count = post.comments.size

Mẹo: Trong view hiển thị số lượng association thì ưu tiên dùng size (hoặc counter cache nếu dùng nhiều).

5. Thêm Index cho cột hay lọc/sắp xếp

Vấn đề: Query WHERE user_id = ... mà không có index thì DB phải scan cả bảng, có thể khá nặng.

Giải pháp: thêm index cho cột thường dùng trong WHERE, JOIN, ORDER BY

Ví dụ:

ruby
Copy
1
2
3
add_index :orders, :user_id
add_index :users, :email, unique: true
add_index :orders, [:user_id, :created_at]

Nguyên tắc chọn index nhanh:

  • Cột xuất hiện nhiều trong WHERE / JOIN: thêm index.
  • Cột xuất hiện trong ORDER BY + có filter đi kèm: cân nhắc composite index (ví dụ [:user_id, :created_at])`.
  • Cột có giá trị duy nhất (unique field) như email/username: unique: true.

Cách kiểm tra nhanh: Nhìn log xem có query nào chậm không, hoặc chạy EXPLAIN trong DB để xem có dùng index không (đặc biệt với Postgres/MySQL).

Lưu ý: Index giúp đọc nhanh hơn nhưng làm ghi chậm hơn chút.

6. Duyệt dữ liệu lớn bằng batch: find_each / in_batches

Vấn đề: User.where(...).each có thể load hết records vào RAM. Nếu dataset lớn (hàng chục nghìn/hàng triệu), Rails sẽ load nhiều record vào RAM, dễ tốn bộ nhớ, có thể crash worker/job.

Giải pháp: Sử dụng find_each (load theo batch)

find_each dùng pagination theo primary key và chỉ giữ một batch trong memory.

ruby
Copy
1
2
3
User.where(active: true).find_each(batch_size: 1000) do |user|
  # xử lý từng user
end

Nếu cần xử lý theo batch (đặc biệt update hàng loạt), dùng:

Copy
1
2
3
User.where(active: true).in_batches(of: 1000) do |relation|
  relation.update_all(flag: true)
end
Dùng trong rake task, backfill dữ liệu, migrate dữ liệu, chạy job xử lý hàng chục nghìn record.

7. Bulk update/delete: update_all / delete_all (khi không cần callback)

Vấn đề: Vòng lặp each { update } khi có 10k records thì sẽ phát sinh 10k queries và callback/validation. Vừa tốn 10k queries, vừa chạy validations + callbacks (đôi khi là cả email, audit log…).

Giải pháp: Chỉ cần 1 query là xong.

ruby
Copy
1
2
3
4
5
# Khi update hàng loạt
User.where(id: ids).update_all(active: false)

# Khi xóa hàng loạt
Log.where("created_at < ?", 30.days.ago).delete_all

Lưu ý quan trọng: update_all, delete_all bỏ qua validations + callbacks. Do đó chỉ dùng khi bạn chắc chắn không cần logic đó.

Dùng khi muốn đổi flag hàng loạt, ghi dữ liệu đơn giản.

8. Counter cache cho “đếm association” hay hiển thị association

Vấn đề: Trang list hiển thị “số comment” cho mỗi post, nếu bạn gọi post.comments.count nhiều lần thì sẽ khá nặng. Kể cả includes(:comments) cũng có thể nặng vì comments nhiều.

Giải pháp: lưu sẵn số lượng vào cột comments_count.

ruby
Copy
1
2
3
4
5
6
7
# Tạo cột comments_count để lưu
add_column :posts, :comments_count, :integer, default: 0, null: false

# Bật counter cache
class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end

Sau đó dùng: post.comments_count

Cách này rất hữu dụng khi phát triển các trang list, bảng admin, nơi mà “đếm” xuất hiện khắp nơi.

Lưu ý: Counter cache giúp tốc độ đọc cực tốt, đổi lại khi ghi comment sẽ update thêm 1 cột ở post.

9. Cache dữ liệu đắt bằng Rails.cache.fetch (cache kết quả query/tính toán)

Vấn đề: Một đoạn query/tính toán nặng chạy lại cho mọi request (top posts, stats, config…).

Giải pháp: cache có TTL (time to live) và key rõ ràng

ruby
Copy
1
2
3
top_posts = Rails.cache.fetch(["top_posts", Date.current], expires_in: 10.minutes) do
  Post.published.order(score: :desc).limit(20).pluck(:id, :title)
end

Gợi ý đặt cache key: Dùng mảng [feature_name, version, params...] để dễ quản lý. Nên có expires_in để tránh dữ liệu cache “vĩnh viễn” không mong muốn.

10. Cache view (fragment/collection caching) để giảm render time

Vấn đề: DB không chậm lắm nhưng render view chậm vì nhiều partial, nhiều logic format.

Giải pháp 1: fragment cache theo record

ruby
Copy
1
2
3
4
5
<% @posts.each do |post| %>
  <% cache(post) do %>
    <%= render "posts/post", post: post %>
  <% end %>
<% end %>

Rails dùng cache key theo record (có version), khi post thay đổi thì key đổi, tự invalid.

Giải pháp 2: collection caching (gọn + nhanh)

ruby
Copy
1
<%= render partial: "posts/post", collection: @posts, cached: true %>

Lợi ích: giảm thời gian render và giảm CPU cho mỗi request rất rõ, nhất là trang list.


Rails thường chậm không phải vì framework, mà vì code tự làm nó chậm: query DB dư, load dữ liệu không cần thiết, hoặc render view quá nặng. Chỉ cần áp dụng những tối ưu cơ bản ở trên là hiệu năng cải thiện thấy rõ ngay, trong nhiều trường hợp còn nhanh gấp vài lần mà không cần cache phức tạp hay scale server.

Bình luận

Author
hoclaptrinh.io author
Tác giả:Yuto Yasunaga

Mình là một full stack developer, tốt nghiệp và làm việc tại Nhật Bản. Trang web này là nơi mình tổng hợp, đúc kết và lưu trữ lại những kiến thức trong quá trình học và làm việc liên quan đến IT.
Hy vọng những bài viết ở website này sẽ có ích cho bạn.