1. Eager loading and the N+1 query problem

Congratulations on coming this far! By now you understand how Ruby on Rails works and you can start making your own applications.

It's time to dive deeper and use some best practices.

In your index.html.erb, we display all of the comments for each post. We have written a loop like this:

<% @posts.each do |post| %>
  ...
<% end %>

Notice how we iterate through each post and for each post, we iterate through each comment in the post. We are also querying for the user of each Post and Comment.

Let's open our Instapost application and trigger the posts#index action (this should be your root_path). In other words, go to the root directory of your application.

Let's look at our Rails server to examine the performance:

image.png

Woah. Look at all of those requests! (As a side note, this request is querying for 13 posts that has 25 comments in total, created by 2 users.)

The application is querying each user for each post, it's querying for all of the comments for each post, and it is querying for each user for each comment. This leads to a long list of queries and is often called the N+1 Problem.

The N+1 problem refers to making a query to fetch the parent, then any number of child queries to fetch the other records.

If the application is still a small application, this won't really affect anything, but once the application starts growing, we need ways to optimize this request.

Eager Loading

One technique to accomplish this is eager loading. Eager loading refers to the practice of fetching all related records at once, instead of fetching them as the program encounters the queries.

In other words, with eager loading, we can fetch all of the records that we need to query all at once. This is far more efficient than trying to fetch all of the records one by one. Fetching 100 records in 1 query is faster than issuing 100 queries to fetch 1 query each.

One way to implement eager loading is by using the includes method. In our posts_controller.rb, let's modify the index method to implement this. First, let's eager load the comments for all of the posts:

def index
  @posts = Post.all.order('created_at DESC').includes(:comments)
end

Save the file and render the index page again. Let's look at the differences in the performance:

image.png

Hmm...this is better, but it is still making a bunch of request. We can do better.

As we can see, the application is still querying for all of the users. This is because we are querying for the user of each post. For example, in this piece of code:

<%= post.user.email.split('@').first %>

Let's also eager load our user for each of our posts:

def index
  @posts = Post.all.order('created_at DESC').includes(:comments, :user)
end

Save the file and refresh the page. Let's see the results:

image.png

Getting better! The requests are getting shorter and shorter. But it is still querying for a bunch of users. This is because each comment is also querying for the user:

<%= comment.user.email.split('@').first %> <%= comment.text %>

Lastly, let's eager load the user for the comments. We can do this by modifying the code like such:

def index
  @posts = Post.all.order('created_at DESC').includes(:user, comments: :user)
end

Save the file and refresh the page.

Let's look at the results:

image.png

Wow! Compare this to what we had initially. As you can see, we clearly have a much smaller request and a better performing application.

Lesson list