Speed up members newsletter list by paginating MailDelivery loads - #175
Merged
Merged
Conversation
thibaudgg
commented
Sep 7, 2026
thibaudgg
left a comment
Member
Author
There was a problem hiding this comment.
The page-size load is the actual fix. Newsletter.preload_sources! has no remaining app caller — includes(:source_newsletter) already does that job. I'd drop the method and the test that only exists for it.
Two assertions in the controller test can't fail: Extra 24 is never inserted (PER_PAGE extras only), and the content-omit check still passes on SELECT *.
| .order(created_at: :desc) | ||
| end | ||
|
|
||
| def preload_sources!(deliveries) |
Member
Author
There was a problem hiding this comment.
No app caller left for this. Admin still inlines its own preload. Safe to delete with the test below.
|
|
||
| assert_response :success | ||
| assert_includes response.body, "Subject John Doe" | ||
| assert_not_includes response.body, "Extra 24" |
Member
Author
There was a problem hiding this comment.
This insert only creates Extra 0–19, so Extra 24 can never show up. Page 2 is already covered by Subject John Doe appearing.
Members::NewsletterDeliveriesController#index loaded every processed newsletter delivery, including HTML content, before applying the page limit, then queried Newsletter once per row. Load one page without content, includes(:source_newsletter), and add (member_id, mailable_type, created_at) so the list query can seek and sort. Drop unused Newsletter.preload_sources!. Tests keep the query count bounded, reject SELECT *, and cover offset pagination. Co-authored-by: Thibaud Guillaume-Gentil <thibaud@thibaud.gg>
cursor
Bot
force-pushed
the
cursor/speed-up-members-newsletter-index-22c4
branch
from
September 7, 2026 15:36
1b80e1b to
b70d683
Compare
thibaudgg
marked this pull request as ready for review
September 7, 2026 15:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
AppSignal production traces for
Members::NewsletterDeliveriesController#index(/newsletters) show a p95 regression (~217ms → ~363ms) with slow samples of 0.8–1.4s across tenants.On a 1447ms sample (stadsgroenteboer, rev
0760de0…), the 1313.7ms hotspot SQL is:No
LIMIT. This matchesNewsletter.deliveries_for, which loaded every processed newsletter delivery for the member (including HTMLcontent) before the controller paginated. That second page query also dropped the in-memory newsletter preload, so_newsletter.html.erbN+1’dNewsletterper row.Change
deliveries_foris now an unloaded relation. The index action loads one page + 1 (LIMIT 21) to decide “show more”, and omitscontent.idx_mail_deliveries_on_member_mailable_createdon(member_id, mailable_type, created_at). A leadingstatecolumn (as in(member_id, mailable_type, state, created_at)) made SQLite keep using the older(member_id, created_at)index forORDER BY created_at; EXPLAIN confirms the three-column index is used.state NOT IN (draft, processing)is applied after the seek.MailDeliverybelongs tosource_newslettervia the virtualmailable_idcolumn.deliveries_forusesincludes(:source_newsletter), so the collection partial does not query per row.Behavior/UI is unchanged: same 20-item list, same “show more” offset pagination, same processed-only filter.
Tests
MailDeliveryload must be an explicit column list withLIMIT/OFFSET, includesubject, and omitcontent(aSELECT *fails this check).deliveries_for.Review follow-up
Dropped unused
Newsletter.preload_sources!(admin still inlines its own preload). Removed the deadExtra 24pagination assertion. Tightened the content-omit SQL check soSELECT *fails.Notes
The
MailDelivery Exists?gate is unchanged. The large win is avoiding the unboundedSELECT *.