Rails 7: Handling Associations That Span Multiple Databases
GitHub runs 30 databases in its Rails monolith, split across 15 primaries and 15 replicas using functional partitioning. Each primary hosts a different schema, which means associating records that live in separate primaries has always been a problem for Active Record. Joins across these clusters fail, and engineers previously had to fall back to hand-written SQL: select IDs from the first table, then run a second query against the target table.
That workaround has now been pushed upstream into Rails itself. GitHub developed the feature internally, stabilized it in production, and contributed it to Rails 7 as the disable_joins option for has_many :through associations.
The Problem with Cross-Database Joins
Consider three models that each belong to a different database connection:
# table dogs in database animals
class Dog < AnimalsRecord
has_many: treats, through: :humans
has_many :humans
end
# table humans in database people
class Human < PeopleRecord
has_many :treats
has_many :dogs
end
# table treats in database default
class Treat < ApplicationRecord
has_many :dogs, through: :humans
has_many :humans
end
Loading the dog.treats association would normally generate a single SQL join:
SELECT treats.* FROM treats INNER JOIN humans ON treats.human_id = humans.id WHERE humans.dog_id = 2
Because Dog, Human, and Treat each inherit from different base classes tied to different primaries, executing that join produces an error:
ActiveRecord::StatementInvalid (Table 'people_db_cluster.humans' doesn't exist)
MySQL does have workarounds for federated joins across clusters, but they are rarely performant or need extra setup. The practical choice was either manual SQL or a framework-level solution.
Introducing disable_joins
GitHub first prototyped this behavior in an internal gem roughly two years ago. The approach was deliberately implemented outside Rails first so it could be battle-tested at scale, with attention to production performance and developer friction, before being considered for upstream. Once stable, the code was contributed to Rails.
The change itself is compact. A new boolean option, disable_joins, was added to has_many :through associations. When enabled, Active Record emits separate queries per database instead of one join query. The option cannot be applied at runtime because associations are lazily loaded—the SQL is already constructed by the time the association object executes. The Rails implementation includes a new scoping class that handles ordering, limits, scopes, and other association options across the split queries.
Applications opt in declaratively:
class Dog < AnimalsRecord
has_many: treats, through: :humans, disable_joins: true
has_many :humans
end
With this in place, calling dog.treats no longer produces a single join. Instead, Rails runs the queries independently:
SELECT "humans"."id" FROM "humans" WHERE "humans"."dog_id" = ? [["dog_id", 1]]
SELECT "treats".* FROM "treats" WHERE "treats"."human_id" IN (?, ?, ?) [["human_id", 1], ["human_id", 2], ["human_id", 3]]
Performance and Behavioral Caveats
Disabling joins comes with trade-offs worth understanding before adoption.
Multiple queries across multiple databases can be slower than a single join on one database. This is true regardless of whether you write the SQL yourself or rely on disable_joins. Efficient indexes and careful benchmarking remain essential before enabling this option on hot paths.
The more subtle issue involves order and limit. With a real join, MySQL can apply ordering against a column from the joined table—for instance, ordering returned treats by an ID on the human table. With separate queries, the database cannot do this. Rails preserves expected behavior by sorting results in memory, using the order they would have appeared in had the join executed. That has a practical ceiling: avoid applying in-memory ordering and limits to result sets with hundreds of thousands of records.
The feature is also not a general replacement for joins. It specifically handles associations that cross database boundaries—a situation that becomes more common as applications scale horizontally or split data by domain.
What This Means for the Framework
The contribution is small in code size but significant in scope. Rails applications using multiple primary databases can now keep using association APIs instead of assembling multi-step queries by hand. The feature also moves out of GitHub’s private gem into the public framework, where broader usage will surface edge cases and refine the implementation.
For GitHub, the change represents the payoff of years of upgrade work that brought the monolith off its Rails fork and onto current releases, making upstream contributions like this one possible.



