Skip to content

fix: eliminate N+1 queries in EventsController#index#2507

Open
mroderick wants to merge 5 commits intocodebar:masterfrom
mroderick:fix/n1-queries-events
Open

fix: eliminate N+1 queries in EventsController#index#2507
mroderick wants to merge 5 commits intocodebar:masterfrom
mroderick:fix/n1-queries-events

Conversation

@mroderick
Copy link
Collaborator

@mroderick mroderick commented Feb 24, 2026

Summary

  • Eliminates N+1 queries in EventsController#index by adding eager loading includes
  • Adds composite database index to optimize host sponsor lookups
  • Adds inverse_of associations for proper Rails caching

Problem Analysis

The events index page was triggering excessive database queries due to several N+1 query issues:

1. Workshop Host (Worst Offender)

  • Before: Each workshop triggered a separate query to find the host sponsor using raw SQL (WorkshopSponsor.hosts.for_workshop(id).first&.sponsor)
  • Impact: With 40 workshops displayed, this caused ~40 additional queries

2. Organisers Association

  • Before: Workshop, Meeting, and Event all use has_many :organisers, through: :permissions
  • Impact: Each event triggered a query to load permissions, then another to load members

3. Meeting Venue (Upcoming Events)

  • Before: Upcoming meetings didn't include venue (Meeting.upcoming.all)
  • Impact: Each meeting triggered a query for the venue

4. Presenter Memoization

  • Before: WorkshopPresenter#venue called model.host without caching
  • Impact: Multiple accesses to venue could trigger redundant calls

Solution

1. Database Index

Added composite index on workshop_sponsors(workshop_id, host) to optimize the host sponsor query:

CREATE INDEX index_workshop_sponsors_on_workshop_id_and_host 
ON workshop_sponsors (workshop_id, host);

2. Workshop Model Associations

Replaced raw SQL host method with eager-loadable associations:

has_one :workshop_host, -> { where(workshop_sponsors: { host: true }) },
        class_name: 'WorkshopSponsor',
        inverse_of: :workshop
has_one :host, through: :workshop_host, source: :sponsor

def host
  workshop_host&.sponsor
end

3. EventsController Includes

Added all necessary includes for eager loading:

# Workshop (past & upcoming)
.includes(:chapter, :sponsors, :host, :permissions)

# Meeting
.includes(:venue, :permissions)

# Event
.includes(:venue, :sponsors, :sponsorships, :permissions)

4. WorkshopPresenter Memoization

Added instance variable caching:

def venue
  @venue ||= model.host
end

Performance Results

Query count reduced from ~47 to 7 for 40 workshops (85% reduction).

Manual Verification

Option 1: Using Bullet

  1. Add Bullet gem to your Gemfile (if not already present):
group :development do
  gem 'bullet'
end
  1. Configure Bullet in config/environments/development.rb:
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bypass = ->(_) { false }
end
  1. Visit /events in your browser
  2. If there are N+1 queries, Bullet will show an alert popup
  3. You should see no alerts after this fix

Option 2: Query Logging

  1. Enable query logging in Rails console:
ActiveRecord::Base.logger = Logger.new(STDOUT)
  1. Visit /events in your browser
  2. Count the queries in the terminal output
  3. With this fix, you should see ~10-15 queries total (not 40+)

Option 3: Bullet Console Log

Add to config/environments/development.rb:

config.after_initialize do
  Bullet.enable = true
  Bullet.logger = true
  Bullet.console = true
end

Then check the server console output after visiting /events.

Test Plan

  • All existing tests pass (124 examples, 0 failures)
  • Manual verification of events index page using Bullet
  • Review query performance in production-like data

This index optimizes the query that finds the host sponsor for a workshop,
improving performance when eager loading the host association.
- Add has_one :workshop_host with inverse_of for proper association caching
- Add has_one :host through :workshop_host for eager loading
- Replace inefficient raw SQL host method with association-based implementation

This eliminates N+1 queries when loading workshop hosts on the events index page.
This enables proper Rails association caching when accessing workshop from
a workshop_sponsor record, reducing redundant queries.
Add :host, :permissions, and :sponsorships to includes for Workshop, Meeting,
and Event queries. This eliminates N+1 queries when rendering the events
index page by loading all associations in a single query.
Use @venue ||= to cache the host sponsor, preventing redundant calls to
model.host which could trigger additional queries in certain contexts.
@mroderick mroderick marked this pull request as ready for review February 24, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant