Strict Loading and N+1

Strict loading is a feature introduced in Rails 6.1 to help prevent N+1 query issues by raising an error when careless engineers like myself attempt to load associations that haven’t been preloaded. This helps to identify places in the code where you might face an N+1 situation. In an N+1 query scenario, instead of using a single query to retrieve all related data, the application retrieves the primary records (N records) and then issues separate queries for each associated record (1 query for each primary record). This results in N+1 total queries, causing performance issues due to the increased number of round trips between the application and the database.

Enable strict loading

You may enable strict loading in three different ways:

  1. …for a specific association. You can enable strict loading for a specific association using the :strict_loading option on the belongs_to, has_one, or has_many associations.
      class User < ApplicationRecord
         has_many :posts, strict_loading: true
      end
    
  2. …for a specific model. To enable strict loading for all associations in a specific model, use the strict_loading_by_default class method in the model definition:
      class User < ApplicationRecord
         strict_loading_by_default
    
         has_many :posts
      end
    
  3. …for the entire application. To enable strict loading for all models and associations in your entire application, add the following configuration to your config/application.rb file:
      config.active_record.strict_loading_by_default = true
    

After enabling strict loading, if you try to access an association that hasn’t been preloaded, Rails will raise a StrictLoadingViolationError. To avoid this error, you must preload the association using one of the following methods:

  • includes: Preloads the specified associations using eager loading.
  • preload: Preloads the specified associations, but performs the loading in separate queries.
  • eager_load: Preloads the specified associations using a LEFT OUTER JOIN. Here’s an example of how to preload associations:
# Preload posts for a user using includes
@user = User.includes(:posts).find(params[:id])

# Preload posts for multiple users using preload
@users = User.preload(:posts).all

# Preload posts for multiple users using eager_load
@users = User.eager_load(:posts).all

By preloading associations, you can minimize the occurrence of N+1 query issues and greatly improve performance.

Which one to use?

  • includes: The includes method is the most flexible of the three. It tries to choose the most efficient loading strategy depending on the query you are running. It may use either eager loading (performing a single query with a JOIN) or separate queries for each association, similar to preload. Rails decides the strategy based on its internal heuristics.
@user = User.includes(:posts).find(params[:id])
  • preload: The preload method preloads the specified associations using separate queries for each association. It first fetches the main records (in this case, users) and then fetches the associated records (posts) in a separate query. It does not use JOINs, which can result in more queries but can be more efficient when loading a large number of records.
@users = User.preload(:posts).all
  • eager_load: The eager_load method preloads the specified associations using a single query with a LEFT OUTER JOIN. This can be more efficient in some cases because it retrieves all the data in a single query. However, it might not always be the best choice, especially when loading a large number of records or when the associated records are not used in every case.
@users = User.eager_load(:posts).all

Summary

  • includes: Automatically picks the best loading strategy for you - either eager loading or separate queries - based on internal heuristics.
  • preload: Always uses separate queries for each association.
  • eager_load: Always uses a single query with a LEFT OUTER JOIN to load the main records and their associations. In general, you can use includes for most cases, as Rails will try to determine the most efficient strategy. However, if you know the specific strategy you want to use for preloading associations, you can use preload or eager_load.

Join the newsletter

Subscribe for goodies on tech straight from my reading list.

Once a week. Maybe Wednesday 🤷

    No spam I promise 🤝

    Bonus SQL join types meme

    best join types meme

    Picture worth a thousand words.