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:
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?
Scopes are custom queries that you define inside your Rails models with the scope method. Every scope takes two arguments: A name, which you use to call this scope in your code. A lambda, which implements the query.
You can avoid most n+1 queries in rails by simply eager loading associations. Eager loading allows you to load all of your associations (parent and children) once instead of n+1 times (which often happens with lazy loading, rails' default). As seen above, . includes allows nested association eager loading!
What is scope in Rails? Scopes in Rails are special methods to run SQL queries that you can build in any rails model. A scope will always return an ActiveRecord::Relation object, even if the conditional evaluates to false , whereas a class method, will return nil .
The n+1 query problem is one of the most common scalability bottlenecks. It involves fetching a list of resources from a database that includes other associated resources within them. This means that we might have to query for the associated resources separately.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With