Question

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?

Was it helpful?

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.

OTHER TIPS

I think the reason the scopes are slower in this case is because they result in 3 separate database queries, whereas the other approach uses the knowledge that all three results can be satisfied by the single query you use.

Assuming that is the case, it is not surprising the scopes are doing 3 separate queries since the system doesn't know when you call the first one that you're going to call the others right afterwards. Maybe there is an optimisation strategy that would be sensible for this scenario, but I don't know that ActiveRecord implements it.

Anyway, this is one disadvantage of the scope in this particular case. I like scopes because they are clean/clear, flexible and encapsulated a named abstraction for a query. AFAICT, in many scenarios they are not appreciably slower than the equivalent direct query.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top