Skinny Includes - Josh Brody Skinny Includes | Josh Brody
All projects
Ruby Gem

Skinny Includes

Column-selective eager loading for ActiveRecord.

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.

Stack

Ruby Rails ActiveRecord

Stay in the loop

Occasional essays on design, tools, and the craft of building things. No spam, unsubscribe anytime.

Ambient weather

The background of this site reflects the current weather and time of day in Saint Paul. The orbs shift in color and behavior based on what's happening outside my window.

Learn more about how this works