Search code examples
ruby-on-railsactiverecordarelnamed-scopes

Why are Rails scopes preferable, if messy controllers are faster?


I've been trying to chain Arel queries using scopes, instead of just using some long-winded logic I wrote in the controller. But the scopes are slower than just getting all the records and then sifting through them with some logic. I'm wondering, then, why scopes are better.

Here's what I'm doing:

  • a question has many answers
  • an answer belongs to one question
  • a question has a "question_type" column that I use to sort it

First, the scopes way...

in question.rb:

scope :answered, joins(:answers).order('answers.created_at desc')
scope :dogs, where(:question_type => "dogs")
scope :cats, where(:question_type => "cats")
scope :mermaids, where(:question_type => "mermaids")

in questions_controller.rb:

@dogs_recently_answered = Question.answered.dogs.uniq[0..9]
@cats_recently_answered = Question.answered.cats.uniq[0..9]
@mermaids_recently_answered = Question.answered.mermaids.uniq[0..9]

Then in the view, I cycle through those instance variables (which are now arrays containing at most 10 elements) and display the results.

Here are the times it takes to load the page (five different times):

Completed 200 OK in 535ms (Views: 189.6ms | ActiveRecord: 46.2ms)

Completed 200 OK in 573ms (Views: 186.0ms | ActiveRecord: 46.3ms)

Completed 200 OK in 577ms (Views: 189.0ms | ActiveRecord: 45.6ms)

Completed 200 OK in 532ms (Views: 182.9ms | ActiveRecord: 46.1ms)

Completed 200 OK in 577ms (Views: 186.7ms | ActiveRecord: 46.9ms)

Now, the messy controller way...

@answers = Answer.order("created_at desc")
@all_answered = []
@answers.each {|answer| @all_answered << answer.question}
@recently_answered = @all_answered.uniq
@dogs_all_answered = []
@cats_all_answered = []
@mermaids_all_answered = []
@recently_answered.each do |q|
  if q.question_type == "dogs"
    @dogs_all_answered << q
    @dogs_recently_answered = @dogs_all_answered[0..9]
  elsif q.question_type == "cats"
    @cats_all_answered << q
    @cats_recently_answered = @cats_all_answered[0..9]
  elsif q.question_type == "mermaids"
    @mermaids_all_answered << q
    @mermaids_recently_answered = @mermaids_all_answered[0..9]
  end
end

And here are the times it takes to load the page now (five different times):

Completed 200 OK in 475ms (Views: 196.5ms | ActiveRecord: 34.5ms)

Completed 200 OK in 480ms (Views: 200.4ms | ActiveRecord: 36.4ms)

Completed 200 OK in 434ms (Views: 198.2ms | ActiveRecord: 35.8ms)

Completed 200 OK in 475ms (Views: 194.2ms | ActiveRecord: 36.4ms)

Completed 200 OK in 475ms (Views: 195.0ms | ActiveRecord: 35.4ms)

So...

Aside from readability, what's to be won by honing the query with a scope? Does it eventually become quicker when there are more records?


Solution

  • First, I'm not sure I understand how a question can be other than unique, so I'd look at trying to remove that. I don't know the logic of your data, so that might not be applicable, but it's an extra step that you might be able to avoid.

    Here's how I would approach it:

    scope :answered, joins(:answers).order('answers.created_at desc')
    scope :recent, take(10)
    scope :dogs, where(:question_type => "dogs")
    scope :cats, where(:question_type => "cats")
    scope :mermaids, where(:question_type => "mermaids")
    
    @dogs_recently_answered = Question.answered.dogs.recent
    @cats_recently_answered = Question.answered.dogs.recent
    @mermaids_recently_answered = Question.answered.dogs.recent
    

    This shifts the TOP portion of the query to the database where it belongs rather than fetching all of the rows and then discarding all but 10. Depending on your uniquing criteria, you might also use a scope like

    scope :unique, select('DISTINCT column_name')
    

    and then you can use Question.cats.unique.recent and get it all in one fast query that leverages the relational algebra that database systems are designed for.