The last record - according to whom?

Engineering Insights

#
Min Read
Published On
March 13, 2025
Updated On
March 14, 2025
The last record - according to whom?

by Andrew Palmer

TL;DR

Unless you explicitly tell the database the order in which to return records, it'll just give you the
records you requested in whatever order it wants.

Background

Let's say I have an author model, and authors have many books. Each author's most-recent book is considered
their "current book", and the current book is very important for some business reason that we don't need
to get into. Since an author has_many :books, we'll define their current_book as books.last.

class Author < ApplicationRecord
  has_many :books

  ...

  def current_book
    books.last
  end
end
class Book < ApplicationRecord
  belongs_to :author

  ...

end

Great. So now, if an author has 2 books and we try to access the current one, we'll get back the one
with the largest id:

irb(main):001:0> author.current_book
  Book Load (6.8ms)  SELECT  "books".* FROM "books" WHERE "books"."author_id" = $1 ORDER BY "books"."id" DESC LIMIT $2  [["author_id", 37], ["LIMIT", 1]]
=> #<Book id: 2, author_id: 1, created_at: "2019-10-21 17:28:26", updated_at: "2019-10-21 17:28:26">

Note the ORDER BY "books"."id" in the generated SQL above.

That happens because ActiveRecord's .last method finds the last record, ordered by primary key if
no order is specified. Perfect.

Or is it?

If you dig into the source a little bit, you'll see that there's a little more going on than just a
simple query ordered by primary key:

# https://github.com/rails/rails/blob/f250da5397b967fcba58356547bc26127c1be93e/activerecord/lib/active_record/relation/finder_methods.rb#L147
def last(limit = nil)
  return find_last(limit) if loaded? || has_limit_or_offset?

  result = ordered_relation.limit(limit)
  result = result.reverse_order!

  limit ? result.reverse : result.first
end

That first line, specifically find_last(limit) if loaded?, is important. So if the association
is already loaded into memory, we'll return the last record of the association in memory instead of
running the result = ordered_relation.limit(limit) line. The ordered_relation in that line is
responsible for ordering the query by id, like we expect.

Problem

Let's write a test to demonstrate the problem that can arise based on how your code is written:

require "rails_helper"

RSpec.describe Service, type: :service do
  describe "#do_something" do
    it "passes the current book to OtherService" do
      author = create(:author)
      previous_book = create(:book, author: author, id: 1, title: "Always Start with A")
      current_book = create(:book, author: author, id: 2, title: "Basically the Best")

      allow(OtherService).to receive(:do_something_with_current_book)

      Service.new.do_something

      expect(OtherService).to have_received(:do_something_with_current_book).with(current_book.title)
    end
  end
end

If we ever load a bunch of books into memory ordered by anything other than id, subsequently calling .current_book will return a different book than we expect, assuming we expect the "last" book to be the
author's most-recent book. This example is contrived, but illustrates the point:

class Service
  def do_something
    Author.includes(:books).order("books.title desc").each do |author|
      OtherService.do_something_with_current_book(author.current_book.title)
    end
  end
end
Service
  #do_something
    passes the correct book to OtherService (FAILED - 1)

Failures:

  1) Service#do_something passes the current book to OtherService
     Failure/Error: expect(OtherService).to have_received(:do_something_with_current_book).with(current_book.title)

       #<OtherService (class)> received :do_something_with_current_book with unexpected arguments
         expected: ("Basically the Best")
              got: ("Always Start with A")

Finished in 0.72237 seconds (files took 11.15 seconds to load)
1 example, 1 failure

In this case we're explicitly sorting the books by title, which ensures the last book returned is different from the author's most-recent book. In a less-contrived and more realistic scenario with more data, where no order is explicitly specified, the responsibility of ordering results gets delegated to the database, which may or may not sort the results by primary key. As a result, without specifying an order, eager-loading an association might load records into memory sorted by something other than primary key, and the .last record of that association might not be what you expect it to be.

Solution

Fortunately, this is a complicated problem that has a quick solution, if you know to look for it: explicitly order the query instead of relying on .last to do it for you:

class Author < ApplicationRecord
  has_many :books

  ...

  def current_book
    books.order(:id).last
  end
end
Service
  #do_something
    passes the correct book to OtherService

Finished in 1 second (files took 11.59 seconds to load)
1 example, 0 failures

Outcome/Takeaways

The column by which the association is ordered doesn't have to be the id. Regardless, the important point is that just because .last often returns the record you're looking for doesn't mean that it always will, unless you tell it to.

Learn more about how The Gnar builds Ruby on Rails applications.

Author headshot
Written by
, The Gnar Company

Related Insights

See All Articles
Product Insights
How Much Does Custom Software Development Cost? (The Real Answer)

How Much Does Custom Software Development Cost? (The Real Answer)

Most software projects go over budget because they're priced before the problem is understood. Learn how structured discovery gives you a guaranteed build price before development starts.
Product Insights
How to Choose the Best Software Development Agency in Boston (2026)

How to Choose the Best Software Development Agency in Boston (2026)

A Boston agency founder's guide to evaluating software development companies. Learn the four agency types, red flags to avoid, and questions to ask in 2026.
Engineering Insights
10 Ways to Get Better Results From Claude Code

10 Ways to Get Better Results From Claude Code

A recent Hacker News thread turned into a goldmine of practical advice from developers using Claude Code daily. After reading through hundreds of comments, a clear pattern emerged: the developers getting the best results aren't writing better prompts — they're building better workflows around the tool.
Previous
Next
See All Articles