Investigate preload batching inefficiency: 44 separate zuora_rate_plans queries instead of 1
Problem
Billing::Usage::SubscriptionsFinder (SaaS path) loads subscriptions with eager loading:
::Zuora::Local::Subscription
.latest_versions_covering_date(name: subscription_names, date: date)
.includes(:all_rate_plans, :rate_plan_charges)
.index_by(&:name)
The intent is to batch-load zuora_rate_plans and zuora_rate_plan_charges in a single query each. However, a production Sentry span shows 44 separate zuora_rate_plans IN queries (400 ms total) instead of a single batch query.
Similarly, the self-managed SelfManaged::SubscriptionsFinder uses the same pattern (line 50-53).
Evidence from production span
-
44
zuora_rate_plansIN queries — 400 ms total -
1
zuora_rate_plan_chargesIN query — 358 ms (this one batched correctly) -
1
zuora_subscriptionsDISTINCT ON query — 46 ms (correct)
The rate plans are split into 44 individual queries rather than 1, suggesting ActiveRecord's eager loading is not batching them correctly.
Suspected causes
-
distinct_on_namescope interference: Thelatest_versions_covering_datescope usesDISTINCT ON (name)viaselect(Arel::Nodes::DistinctOn...). This custom SELECT may cause ActiveRecord to lose track of the primary keys needed for eager loading, forcing it to fall back to per-record loading. -
index_by(&:name)materializing the relation: Ifindex_bytriggers enumeration beforeincludesresolves, the eager loading may execute per-record rather than in batch. -
ActiveRecord
includesvspreloadbehavior: With complex scopes (DISTINCT ON, subqueries),includesmay silently degrade toeager_load(LEFT JOIN) or per-record loading. Explicitpreloadmay work better here.
Proposed investigation
- Check if replacing
.includeswith.preloadresolves the batching - Test whether removing
distinct_on_namefrom the scope allows proper batching - Consider loading subscriptions first, then batch-loading rate plans in a separate explicit query:
subscriptions = ...latest_versions_covering_date(...).to_a Zuora::Local::RatePlan.where(subscription_id: subscriptions.map(&:id)).each do |rp| # associate manually end
Relevant files
-
app/finders/billing/usage/subscriptions_finder.rb:15-18— SaaS finder with.includes -
app/finders/billing/usage/self_managed/subscriptions_finder.rb:50-53— SM finder with.includes -
app/models/zuora/local/subscription.rb:81-95—latest_versions_covering_datescope withdistinct_on_name -
app/models/zuora/local/subscription.rb:97-99—distinct_on_namescope usingArel::Nodes::DistinctOn