When you’re building a new Rails app, ActiveRecord’s defaults will take you far. Querying with
.where, inserting with
.save — it’s all so easy, and it’s fast enough.
But after a while — when a page of mostly simple content takes a
second or more to come back from the server, when you start seeing 504
Gateway Timeout errors coming back from nginx because it’s taking too
long to process the CSV you uploaded — that’s when you know you’re going
to have to spend some time on performance.
You could solve a lot of these problems with caching. But that adds a
whole extra layer of complication. Between expiration, nesting
partials, and bugs that only reproduce in production, it’s a headache
you don’t need right now.
Instead, you can spend some time fixing the most common performance
problem I’ve seen in Rails apps: hitting your database too much.
Even if you’re running the database on the same machine, there’s a
lot of connection overhead that’ll slow you down. And if your database
is on another machine, fetching data that often will just destroy you.
But you don’t have to go too far from the simplicity of Rails to see drastic improvements in your app’s response time.
You’re trying to find ten restaurants along with with their reviews, and you’re doing eleven SQL calls!
This is called the “N+1 query problem”: for every restaurant, you’re
doing one query for the restaurant data, plus one query for each of
their associated reviews. You can probably imagine how bad it becomes
the deeper you go. Imagine if you also wanted to grab each restaurant’s
address, as well as each address’ phone number.
You’ll run into this problem when you loop over a list of objects and try to query their associations:
app/views/restaurants/index.html.erb
<% @restaurants.each do |restaurant| %>
<tr>
<td><%= restaurant.name %></td>
<td><%= restaurant.review_average %></td>
...
You don’t need to hit the database N+1 times. You want to hit it at
most twice: once for the restaurants you’re trying to find, and once for
all of the reviews associated with
all of those restaurants.
This is called “eager loading,” and you can do it really easily with
.includes:
app/controllers/restaurants_controller.rb
def index
@restaurants = Restaurant.all.includes(:reviews)
end
Or, if you want to do something more complicated, like preload all the addresses and the reviews’ authors:
app/controllers/restaurants_controller.rb
def index
@restaurants = Restaurant.all.includes([{:reviews => author}, :address])
end
Or, if you want to do something more complicated, like preload all the addresses and the reviews’ authors:
app/controllers/restaurants_controller.rb
def index
@restaurants = Restaurant.all.includes([{:reviews => author}, :address])
end
You have to specify the associations you want to preload, using that
array and hash syntax. Rails will do the best it can at consolidating
down those calls:
For more
Click