skinny_includes lets you specify exactly which columns to load when eager loading ActiveRecord associations. Instead of pulling every column from associated records, you pick what you need. Rails still prevents N+1 queries; you just stop wasting memory on columns you’re not using.
Why it exists
ActiveRecord’s includes loads all columns from associated records. That’s usually fine. But when your tables have large JSON blobs, text fields, or binary data, “all columns” becomes expensive fast.
Load 5,000 comments with Post.includes(:comments) and you get every column on every comment—including that metadata_json field nobody needs at render time. Memory balloons. Queries slow down. The N+1 prevention you wanted comes with overhead you didn’t.
The fix is obvious: only load the columns you need. Rails doesn’t offer this—though you can write queries for it, obviously! Skinny Includes does.
How it works
Two methods: with_columns whitelists columns, without_columns blacklists them.
Post.includes(:comments).with_columns(comments: [:body, :author])
This loads only id, post_id, body, and author from comments. Primary keys and foreign keys are always included automatically—the gem handles that so associations can still be connected in memory.
The blacklist version works the same way:
Post.includes(:comments).without_columns(comments: [:metadata_json, :body])
Everything except those two columns gets loaded.
Multiple associations
Pass them all at once:
Post.with_columns(
comments: [:author],
tags: [:name]
)
Nested includes
For associations on associations, use the hash syntax:
Post.with_columns(
comments: {
columns: [:body],
include: { author: [:name] }
}
)
This loads comments with only body, then loads each comment’s author with only name. Nests as deep as you need.
Scoped associations
If you have scoped associations, the gem respects them:
class Post < ApplicationRecord
has_many :published_comments, -> { where(published: true) }, class_name: 'Comment'
end
Post.includes(:published_comments).with_columns(published_comments: [:body])
Only published comments load. Only the body column loads. Both constraints apply.
What it supports
Works with has_many, has_one, and belongs_to. Compatible with includes, preload, and eager_load—though the gem converts eager_load to its own loading strategy internally. Requires ActiveRecord 7.0+ and Ruby 3.0+ because I’m too lazy to find the internals for others.
How it actually works
The gem intercepts ActiveRecord’s relation loading. When you call with_columns, it removes the association from Rails’ standard eager loading, then manually loads each association with a selective SELECT. Records get grouped and assigned to their parents. Associations get marked as loaded so Rails doesn’t try to query again. The query count stays the same; you’re just fetching less data per query.