Performance: N+1 Queries
Plutonium renders association values for you: a display :author, a table column :organization, a grid or kanban card showing a related name. Reading an association off a record that wasn't loaded with it costs a query, so an index page runs one query for the page plus one per row per association.
Spotting it
Tail the log and reload an index. A run of near-identical SELECT * FROM users WHERE id = ? lines, one per row, is the signature.
tail -f log/development.logbullet reports them in development if you'd rather be told than look.
The cost matters most where each query is a network round trip. On Postgres or MySQL an index page can spend most of its time waiting; on SQLite the same page is cheaper, though the query count is identical. Either way the count grows with page size, so a listing that is fine at 20 rows may not be at 200.
Collections preload themselves
Index pages, kanban boards and CSV exports already eager-load the associations and attachments they render. The field set comes from the policy, so the framework knows it before the collection loads and can preload exactly those. Nothing to declare, and nothing to keep in step when a field is added or removed.
Each rendering passes its own field set, because they differ: the index renders its permitted attributes, an export renders permitted_attributes_for_export, and a kanban card renders its card_fields.
It covers every association kind — belongs_to, has_one, has_many — and attachments on both ActiveStorage and Shrine.
Turn it off globally:
# config/initializers/plutonium.rb
Plutonium.configure do |config|
config.auto_eager_load_collections = false
endOr per resource:
class PostsController < ::ResourceController
private
def auto_eager_load_collections? = false
endEager loading by hand
Anything the framework can't see still needs declaring: an association read inside a custom column block, or one rendered on a show page.
One listing
Override filtered_resource_collection. super keeps authorization scoping, search, filters, scopes and sorting:
class PostsController < ::ResourceController
private
def filtered_resource_collection = super.includes(:author, :category)
endEverywhere
When an association is read on a show page, an export or a typeahead, put it in the policy's relation_scope:
class PostPolicy < ResourcePolicy
relation_scope do |relation|
default_relation_scope(relation).includes(:author, :category)
end
endA custom relation_scope must still call default_relation_scope, or scoping is dropped. See Behavior › Policies.
Automatic eager loading
For the cases above that the framework can't resolve for you, Goldiloader removes the bookkeeping. It hooks association traversal: reading post.author on a record from a collection loads that association for the whole collection in one query. No includes anywhere.
# Gemfile
gem "goldiloader"It works without per-model configuration. Before adding it:
- It assumes uniform access. Reading an association on one record loads it for every record, which over-fetches when only one row needed it.
has_onewith an order and a limit is a sharp edge: eager loading appliesLIMIT 1to the whole query rather than per parent.- It disables itself for associations declared with
limit,offsetorfinder_sql. - Opt out per query (
Post.all.auto_include(false)), per association (has_many :comments, -> { auto_include(false) }) or globally (Goldiloader.globally_enabled = false).
ar_lazy_preload takes the same idea from the other end, preloading lazily on first access, with ArLazyPreload.config.auto_preload = true for automatic behaviour everywhere. Its docs warn that enabling it on an existing app can surface edge cases.
Related
- Search fallback. A resource with no
searchblock falls back to a leading-wildcardLIKE, which cannot use a b-tree index. Write an explicitsearchblock for large tables — see Resource › Query. - Page size. Query cost scales with rows per page.
