Prevent N+1 Queries in Rails
Flags Active Record associations read in loops, views, and serializers without includes or preload, and per-row count queries.
What it flags
class ProjectsController < ApplicationController
def index
projects = current_account.projects.order(created_at: :desc).limit(50)
payload = projects.map do |project|
{
id: project.id,
name: project.name,
owner_name: project.owner.name, # one owner query and one COUNT query per project
open_tasks: project.tasks.where(status: "open").count
}
end
render json: payload
end
endclass ProjectsController < ApplicationController
def index
projects = current_account.projects
.includes(:owner)
.order(created_at: :desc)
.limit(50)
.to_a
open_tasks = Task.where(project_id: projects.map(&:id), status: "open")
.group(:project_id)
.count
payload = projects.map do |project|
{
id: project.id,
name: project.name,
owner_name: project.owner.name,
open_tasks: open_tasks.fetch(project.id, 0)
}
end
render json: payload
end
endWhy it matters
An N+1 query passes every test that runs against a handful of fixture rows, then issues hundreds of queries per request once a customer has real data. The two halves usually live in different files: the controller builds the relation, and a partial, jbuilder view, or serializer reads the association. A reviewer who reads only the changed file sees neither half as wrong.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Prevent N+1 Queries in Rails Applies to `**/*.rb`, `**/*.rake`, `**/*.erb`, `**/*.jbuilder`, `**/*.haml`, `**/*.slim`. Review Active Record access in controllers, views, serializers, mailers, and jobs for queries that run once per record. Flag changes that: - Iterate records (`each`, `map`, `find_each`, `render collection:`, `json.array!`) and read a `belongs_to`, `has_one`, or `has_many` association inside the loop when the relation has no `includes`, `preload`, or `eager_load` for it. - Call `count` or `exists?` on an association for each row. `count` always runs a query, even on a preloaded association; use `size` with a preload, a `counter_cache`, or one grouped count keyed by id. - Chain `where`, `order`, `limit`, or `find_by` onto an association inside a loop. Each call builds a new query, so preloading the association does not help; preload a scoped association instead. - Read an association in a serializer, jbuilder view, partial, or helper when the controller that renders it does not preload it. - Preload only the first level, such as `includes(:author)`, when the loop reads a nested association such as `author.company`. - Remove an `includes`, `preload`, or `eager_load` call while rendering code still reads that association. Do not flag: - Code that handles a single record, such as a show action or a job that loads one row. - Relations marked `strict_loading` in apps that keep the default `action_on_strict_loading_violation = :raise`, where a missed preload raises instead of querying. Under `:log`, the lazy load still queries, so this exemption does not apply. - Loops over a small, fixed collection, such as a hard-coded list of a few records.
Checks files matching **/*.rb, **/*.rake, **/*.erb, **/*.jbuilder, **/*.haml, **/*.slim.
More Ruby on Rails agents
See all Ruby on Rails code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.