The hidden cost of the invisible queries in Rails
Rails là một framework tuyệt vời được xây dựng trên ngôn ngữ Ruby. Nó cho phép chúng ta tạo ra các ứng dụng web mà không nhất thiết phải hiểu biết hết về các công nghệ cần sử dụng. Để làm được điều đó thì Rails được tối ưu hóa để giảm bớt tối đa khối lượng công việc cho các lập trình viên để họ có ...
Rails là một framework tuyệt vời được xây dựng trên ngôn ngữ Ruby. Nó cho phép chúng ta tạo ra các ứng dụng web mà không nhất thiết phải hiểu biết hết về các công nghệ cần sử dụng. Để làm được điều đó thì Rails được tối ưu hóa để giảm bớt tối đa khối lượng công việc cho các lập trình viên để họ có thêm thời gian tập trung để phát triển những ứng dụng tuyệt vời. Một trong những công nghệ rất hữu ích trong Rails đó là về SQL. Với ActiveRecord, chúng ta không còn phải quá bận tâm đến các câu quy vấn SQL dài dòng và phức tạp nữa. Trong bài viết này, tôi và các bạn sẽ cùng tìm hiểu về một số điểm hạn chế trong ActiveRecord và sự ảnh hưởng của chúng đến các ứng dụng được phát triển trên Rails.
Trong quá trình phát triển ứng dụng với Rails, có bao giờ bạn thắc mắc dòng code này để làm gì, method này có ý nghĩa như thế nào? Không chỉ với riêng Rails và với rất nhiều các framwork khác, câu hỏi muôn thuở được đặt ra là
Do you know exactly what are you doing on each line of your code?
Đây là một câu hỏi thực sự rất quan trọng và nó cần được đặt ra liên tục trong quá trình chúng ta phát triển bất kỳ ứng dụng nào. Chúng ta chỉ có thể trở thành các coder giỏi khi chúng ta không ngừng học hỏi và tìm hiểu về những gì thực sự diễn ra sau những dòng code.
Một điều không thể phủ nhận là Rails giúp chúng ta có những dòng code rất đẹp. Tuy nhiên với những developer ít kinh nghiệm thì họ quên rằng đằng sau những method đẹp đẽ được gọi ấy là những truy vấn tới database, gửi email, lời gọi tới API hay các message đến Redis. Hãy cùng đến với ví dụ sau
# We are doing a query to the table movies # where actor_id == actor.id actor.movies.each do |movie| ... end # Sends an email UserMailer.welcome_email(@user).deliver_now # Here we are doing 3 different API calls to Stripe customer = Stripe::Customer.retrieve("cus_AcnQorgF0BIeBt") card = customer.sources.retrieve("card_1AHXpr2eZvKYlo2CtTq8IsO5") card.name = "Noah Moore" card.save # We are sending a message to a Redis server # with the needed data to perform a background task. # Later a Sidekiq server will read it and it will be executed MyWorker.perform_async(1, 2, 3)
Tất cả những đoạn code trên đều có chung một đặc điểm, đó là nó đều thực hiện công việc với các external processes, có thể là processes trên cùng một máy tính, cũng có thể là các processes trên server khác. Vậy thì tại sao việc thực hiện các công việc với external processes được nêu ra ở đây và các developer cần chú ý? Câu trả lời là việc giao tiếp với các external processes thì thường sẽ chậm, và tất nhiên là không một ai muốn ứng dụng của mình như thế cả.
Một lỗi hết sức phổ biến đối với những developer mới bắt đầu sử dụng Ruby on Rails đó là N+1 queries. Đoạn code sau đây là một ví dụ:
# This is a query actor = Actor.find(actor_id) actor.movies.each do |movie| # for each movie we are doing another query to get the director # so it is not very efficient puts movie.director.name end
Vậy giải pháp ở đây là gì? Chúng ta sẽ chỉ sử dụng 2 queries để làm công việc trên: 1. Lấy ra tất cả những movies từ actor. 2. Lấy ra tất cả các directors thông qua những movies đã được load ra trước đó. Có một method rất tiện rợi cho query thứ 2 đó là sử dụng method includes
# This is a query actor = Actor.find(actor_id) # This makes two queries, one for the movies # and another for all the directors of the movies actor.movies.includes(:director).each do |movie| # No query is performed here puts movie.director.name end
Với những developer mới và ít kinh nghiệm thì không phải lúc nào họ cũng có thể dễ dàng phát hiện ra N+1 queries. Tuy nhiên thì họ cũng không phải quá lo lắng vì vấn đề đó vì chúng ta đã có Gem Bullet, một gem giúp phát hiện dễ dàng N+1 queries. Nếu bạn muốn tìm hiểu thêm về preloading associations trong Rails thì đây là một bài viết rất hữu ích mà bạn nên đọc.
Một method của ActiveRecord có ảnh hưởng không tốt tới preformance đó là count
if Actor.count == 0 puts "No actors" end
Đoạn code này sẽ gọi đến một count query trong SQL server. Tuy nhiên, bạn có thể thấy rằng chúng ta không cần đếm tất cả các actors trong table, điều chúng ta cần là kiểm tra xem có tồn tại bản ghi actor nào trong table không. Một điều cần lưu ý là count query không phải lúc nào cũng nhanh như chúng ta nghĩ. Database sẽ phải duyệt qua tất cả các actors và đếm chúng, sau đó là trả ra kết quả số lượng actors đếm được. Chúng ta hoàn toàn có thể rút ngắn công việc cần làm đi rất nhiều
if Actor.limit(1).count == 0 puts "No actors" end
Với limit(1) thì database sẽ dừng lại ngay khi tìm được bản ghi đầu tiên và chắc chắn nó sẽ có performance tốt hơn hẳn với việc dùng count query. Chúng ta cũng có thể mở rộng ra với việc đếm các số lượng khác
if Actor.limit(2).count > 1 puts "Many actors" end if Actor.limit(101).count > 100 puts "More than 10 pages of 10 actors" end
Some Rails query methods seem not optimized
Một điều khá bất ngờ là method any?, many? và empty? trong Rails vẫn chưa thực sự tối ưu. Ví dụ sau đây là một minh chứng cho điều tôi nói (table Actor có khoảng 9000 records):
pry(main)> Actor.count (3.4ms) SELECT COUNT(*) FROM "actors" => 9211 pry(main)> Actor.all.empty? (3.6ms) SELECT COUNT(*) FROM "actors" => false pry(main)> Actor.any? (3.3ms) SELECT COUNT(*) FROM "actors" => true pry(main)> Actor.many? (3.1ms) SELECT COUNT(*) FROM "actors" => true pry(main)> Actor.all.limit(1).count (0.8ms) SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "actors" LIMIT $1) subquery_for_count [["LIMIT", 1]] => 1 pry(main)> Actor.all.limit(2).count (0.9ms) SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM "actors" LIMIT $1) subquery_for_count [["LIMIT", 2]] => 2
Kiểm tra bằng benchmark:
Benchmark.ips do |x| x.report("any?") { Actor.any? } x.report("limit(1)") { Actor.limit(1).count } x.compare! end Comparison: limit(1): 1213.5 i/s any?: 597.1 i/s - 2.03x slower
Như các bạn đã thấy, chỉ với table có 9000 records thì sự biệt về performance đã khá đáng để chú ý. Với một table có số lượng bản ghi lớn hơn thì chắc chắn chúng ta sẽ phải cân nhắc về việc sử dụng các method trên.
Cảm ơn các bạn đã theo dõi bài viết và hi vọng nó sẽ giúp ích cho các bạn. Reference: https://alexcastano.com/the-hidden-cost-of-the-invisible-queries-in-rails/