12/08/2018, 15:10

Một số mẹo viết câu truy vấn hiệu quả

Chỉ lấy dữ liệu cần Active record cung cấp interface để tương tác với dữ liệu. Rất dễ dàng tạo mối quan hệ giữa các model và lấy bản ghi, như dưới đây def get_last_company User.find(1).companies.last end Câu lệnh trên sẽ lấy company cuối cùng user đầu tiên. Rất đơn giản! Nhưng bạn đã bao ...

Chỉ lấy dữ liệu cần

Active record cung cấp interface để tương tác với dữ liệu. Rất dễ dàng tạo mối quan hệ giữa các model và lấy bản ghi, như dưới đây

def get_last_company
  User.find(1).companies.last
end

Câu lệnh trên sẽ lấy company cuối cùng user đầu tiên. Rất đơn giản! Nhưng bạn đã bao giờ tự hỏi liệu có cần thiết phải lấy toàn bộ các trường của đối tượng User chỉ để lấy company có liên quan đến user? Chắc chắn là không. Một User có nhiều Companies liên kết với nó, tuy nhiên chỉ cần id của User để lấy được Company được liên kết với nó. Việc lưu trữ các đối tượng trong Ruby cũng tốn bộ nhớ. Ban đầu khi bảng User chỉ có ít trường thì khó mà nhìn thấy sự khác biệt, nhưng khi bảng User phát triển, được thêm nhiều trường, bạn sẽ cảm thấy may mắn vì đã để tâm đến nó ngay từ đầu. Cách cải thiện sẽ là:

def get_last_company
  User.select(:id).find(1).companies.last
end

Một truy vấn kiểu select user.id, user.some_other_required_field from users... luôn tốt hơn select * from users...

Select hay pluck?

select lấy các trường được chọn và trả về một đối tượng model mà bạn có thể gọi đến các hàm trong model đó hoặc các model liên quan. pluck cũng lấy các trường được chọn, nhưng nó chỉ trả về một mảng các giá trị và tốn ít bộ nhớ hơn. Ví dụ, User.select(:id, :company_id) sẽ trả về một đối tượng User cùng với id và copany_id để bạn có thể gọi đến các phương thức như companies. Nhưng nếu bạn dùng User.pluck(:id, :company_id), nó sẽ trả về kết quả lỗi.

Dùng phương thức của Ruby hay phương thức truy vấn?

Một thói quen đơn giản là gọi đến các phương thức của Ruby thay vì phương thức truy vấn có thể làm giảm số lần gọi đến cơ sở dữ liệu không cần thiết. Để đếm tổng số người bạn của một User ta dùng current_user.friends.count, nó sẽ thực thi một câu truy vấn đếm. Liệu có cần thiết phải thực hiện câu truy vấn này sau khi đã lấy tất cả friends? Nếu tất cả friends đã được load, chỉ cần dùng current_user.friends.lenght, cách này sẽ giúp tiết kiệm một lần gọi vào cơ sở dữ liệu. Sự khác biêt là length là phương thức của Ruby, trả về tổng số đối tượng, còn count là một câu lệnh truy vấn select count(*) from table. Hãy cố gắng dùng phương thức của Ruby bất cứ lúc nào có thể để giảm số lần tương tác với cơ sở dữ liệu.

Sử dụng IN

Một trường hợp khá phổ biến là khi muốn truy vấn dữ liệu từ một trường có giá trị được định nghĩa trong enum

class Person < ActiveRecord::Base
  enum preferred_food: [:pizza, :pasta, :noodles, :burgers]
end

Như trong ví dụ trên, để tìm những người thích pasta hoặc noodles, ta viết:

class Person < ActiveRecord::Base
  enum preferred_food: [:pizza, :pasta, :noodles, :burgers]
  
  def self.preferres_pasta_or_noodles
    where(preferred_food: [Person.preferred_foods[:pasta], Person.preferred_foods[:noodles] ])
    # hoặc viết
    # where('preferred_food IN ( ? ) ', [1, 2]) 
  end
end

Tiết kiệm thời gian với Transaction

Để cập nhật nhiều bản ghi với cùng một dữ liệu, ví dụ như chuyển giá trị Boolean sang false, ta viết:

User.where(is_admin: true).update_all(is_admin: false)

Kết quả của nó chỉ là 1 câu truy vấn. Nhưng nếu cập nhật nhiều bản ghi với dữ liệu khác nhau thì một câu truy vấn là không thể, nhưng ta có thể viết nó trong một Transaction để tăng hiệu suất. Cách làm này sẽ không giúp cập nhật tất cả bản ghi trong cùng một lần, nó vẫn sẽ thực hiện từng đấy câu truy vấn, nhưng chỉ commit transaction đúng một lần. Để dễ hiểu, đoạn code dưới đây để update title của nhiều bài post mà không đặt trong transaction:

20.times do |i|
  Post.update(title: "UPDATE_TITLE#{i}")
end

Đây là cách các câu truy vấn chạy: Nhưng khi để trong 1 transaction:

ActiveRecord::Base.transaction do 
  20.times do |i|
    Post.update(title: "UPDATE_TITLE#{i}")
  end
end

Các câu truy vấn sẽ có dạng thế này:

Đừng sợ dùng joins

Tại sao lại dùng joins và khi nào thì nên dùng nó? Sẽ rất đơn giản sau khi bạn hiểu được sự khác biệt giữa các loại joins. Thông thường, chúng ta hay sử dụng inner join và left outer join. User.joins(:post) sẽ cho phép bạn tìm kiếm những user có bài post với tiêu đề nào đó:

User.joins(:posts).where('posts.title = ? ', "SOME TITLE")

joins rất hữu dụng trong nhiều trường hợp và có thể giúp bạn chỉ lấy dữ liệu bạn muốn, giống như trong ví dụ trên. Vì vậy, bất cứ khi nào bạn cần lọc dữ liệu dựa trên bản ghi liên quan, đấy chính là lúc dùng joins. Bạn có thể đọc thêm về joins ở đây.

Dùng includes

Giả sử bạn cần lấy tiêu đề của tất cả bài viết của 10 tác giả đầu tiên. Bạn sẽ nhanh chóng viết:

authors = Author.first(10)
authors.each do |author|
  author.posts.pluck(:title)
end

Tốt, chỉ lấy tiêu đề, nhưng một câu truy vấn để lấy tất cả tác giả, một câu truy vấn để lấy bài viết cho mỗi tác giả, tổng cộng là 11 câu truy vấn. Có cách khác để giải quyết chỉ với 2 câu lệnh

authors = Author.first(10).includes(:posts)
authors.each do |author|
  author.posts.pluck(:title)
end

Chỉ một chút thay đổi, thêm includes(:post) vào câu truy vấn và vấn đề đã được giải quyết.

Nguồn tham khảo: https://blog.codeship.com/writing-efficient-queries/

0