Select Specific Columns

Reduce Memory Usage by Selecting Specific Columns

As your application grows, so do your database tables. If you keep fetching all columns, those extra fields, especially large text or JSON blobs can quietly eat up a lot of memory. This post shows how to reduce memory usage in your Rails apps by selecting only the columns you need from the database.

5 min read
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.

- Donald Knuth

In this post, we're going to explore how to reduce memory used by your Rails applications by only querying the data you actually need from the database.

Early in a project, it's common (and practical) to fetch the whole records, including all the columns using Model.all or Model.where query methods. At this stage, it doesn't even matter. Most models don't have that many columns, and the data isn't large.

However, as the application grows, tables grow, new columns are added (some holding large blobs of data). When you fetch these records from the database into your Rails application, all this data is stored in application memory and the memory usage starts to creep up.

Let's look at a simple example. We'll continue where we left off in the previous post on profiling Rails applications with Debugbar. In that post, we created a Member model backed by a members table with just three columns: name, email, and team_id.

Profiling Ruby on Rails Applications with Rails Debugbar
This post shows how you can get a better understanding of your Ruby on Rails application performance with the Rails Debugbar, a profiling tool inspired by Laravel Debugbar. It also covers how to spot N+1 queries, reduce object allocations, and optimize SQL queries to improve page load times.

Now imagine we get a new requirement: each member needs a bio. It’s a reasonable change, so let's add a new column on the members table.

bin/rails generate migration AddBioToMembers bio:text
bin/rails db:migrate

Next, we update the seeds file to generate some fake bios. Each bio will contain 5–10 paragraphs of text. With 500 rows, that’s about 2,500–5,000 paragraphs total—enough to start noticing memory impact.

# db/seeds.rb 
require "faker"

10.times do
  team = Team.create(name: Faker::Company.name)
end

Team.find_each do |team|
  50.times do
    Member.create(
      name: Faker::Name.name,
      email: Faker::Internet.email,
      team_id: team.id,
      join_date: Faker::Date.between(from: 1.year.ago, to: Date.today),
      bio: Faker::Lorem.paragraphs(number: rand(5..10)).join("\n\n")
    )
  end
end

Now let’s load the page that lists all members.

# app/controllers/members_controller.rb
class MembersController < ApplicationController
  def index
    @members = Member.includes(:team).order(:name)
  end
end

The view renders a simple table with just the name, email, and team name:

<% @members.each do |member| %>
  <tr class="hover:bg-gray-50">
    <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
      <%= member.name %>
    </td>
    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
      <%= member.email %>
    </td>
    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
      <%= member.team.name %>
    </td>
  </tr>
<% end %>

Let's load the page. It works fine and shows all the expected data. For this, Rails runs two queries:

  • one query to fetch all 500 members, instantiating 500 Member objects
  • another query for teams for those members, instantiating 10 Team objects.
Members Index Page with Queries
Members Index Page with Queries

So far, so good… but here's the problem: we’ve now got 500 full-text bios sitting in memory—even though we're not displaying them.

Let's inspect the memory using Rack MiniProfiler.

You can view memory usage by appending ?pp=profile-memory to any URL while MiniProfiler is enabled. You can also fine-tune the output with a few query parameters:

  • memory_profiler_allow_files – filter to only include certain files
  • memory_profiler_ignore_files – exclude specific files
  • memory_profiler_top – control how many lines to show (defaults to 50)

Since we're only interested in application's memory usage, let's reload the page with following URL:

http://localhost:3000/members?pp=profile memory&memory_profiler_allow_files=app
Memory Usage to Load Members
Memory Usage to Load Members

As you can see, we are allocating and retaining lots of memory, and we are not even showing the member bios on this page. The reason is in the query Rails runs behind the scenes:

SELECT * FROM members;

Rails is fetching all columns from the members table, including the bio. Even though we only need a member’s name, email, and team, we’re pulling in everything.

And since each bio is several paragraphs of text, this adds up quickly. All that data is loaded into memory for every row—even though it's never used.

So let’s fix that by selecting only the columns we actually need.

class MembersController < ApplicationController
  def index
    @members = Member
                 .includes(:team)
                 .select(:id, :name, :email, :team_id)
                 .order(:name)
  end
end

Now reload the page.

Reduced Memory Usage by Fetching Specific Columns
Reduced Memory Usage by Fetching Specific Columns

As you can see, we are down from over ~600 KB to ~5KB. That's much, much better. By selecting only the necessary columns, we’ve reduced a lot of memory usage on this page.

But, premature optimization...?

Now, you might be thinking: “Come on, it’s just 600 KB—who cares?” Didn't Donald Knuth say "premature optimization is the root of all evil" or something like that?

And honestly, that’s fair… to a point. This example is intentionally simple to make the concept clear. But in real-world applications, especially those running in memory-constrained environments, handling thousands of concurrent users, or processing large datasets—these kinds of small inefficiencies add up fast.

It’s not just about this one page or this one query. It’s about building good habits. If you're consistently over-fetching data across your app, the memory overhead can become very real, very quickly. And if you're dealing with serialized blobs, file metadata, rich text, or large JSON fields? You’ll definitely feel the impact.

That said, this doesn’t mean you need to grep your codebase for every instance of Model.all and start micro-optimizing everything. Good caching can mitigate a lot of this. But if you're working on pages that are heavily used, run complex queries, or load lots of associated records—it’s worth being intentional about what you're fetching.


That's a wrap. I hope you found this article helpful and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.