ActiveRecord::QuickRead
Makes Rails go faster. Speedups scale with the size and complexity of your models, up to ~13x.
How does it work?
A normal ActiveRecord query does more than run SQL. Once the rows come back, ActiveRecord
instantiates a model object for every row: allocating the object, running initialize,
typecasting each column, setting up dirty tracking, and evaluating any after_initialize callbacks.
For large result sets that per-row work adds up fast, and you end up paying for features you may not
even use (like attribute change tracking) when all you wanted was to read the data.
quick_read sidesteps all of that. It runs the exact same scoped SQL, but reads the raw rows with
connection.select_rows, which returns each row as a plain array of values instead of the
column-keyed hashes that select_all builds. Each array is then mapped positionally to a lightweight
Struct whose members mirror the model's columns:
# the Lite class generated for your model, e.g.:
Report::Lite = Struct.new(:id, :message, :status, :created_at, :updated_at)
Most of the speed comes from what's not allocated: no model objects, no per-row hash, no
typecasting, no dirty tracking, and no after_initialize callbacks. The struct is built directly
from the raw row values. The result is the same data you asked for, returned faster when you read
and serialize it. The speedup grows with the size and complexity of your model.
The structs are upgradeable, not throwaway. Each Lite instance knows its source model and can
lazily materialize a full ActiveRecord object on demand (via method_missing). So when you call a
method the struct doesn't have (like save, update, or an association such as report.author) it
transparently builds the real model behind the scenes and delegates to it. You only pay the cost of
instantiation when you actually need to write, not when you're just reading.
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add activerecord-quick_read
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install activerecord-quick_read
Usage
Add quickness to your models, then call #quick_read on an ActiveRecord relation to get a single
record, or #quick_reads to get all scoped records. Use #quick_build on your models to build a
lite instance from a hash.
Enable on a single model
class Report < ApplicationRecord
extend ActiveRecord::QuickRead
end
Enable for every model via ApplicationRecord
Instead of extending each model individually, extend ApplicationRecord once and every subclass
inherits the quick read behavior:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
extend ActiveRecord::QuickRead
end
Because models aren't fully defined until boot, the Lite structs are built lazily after Rails
initializes (via the included railtie). Any class that inherits from ApplicationRecord, even ones
defined in engines or gems, automatically gets a Lite struct.
As a side benefit, defining a struct requires reading the model's column_names, which warms
ActiveRecord's schema cache and touches each model at boot. That in turn pre-warms the models, so
your first real web requests don't pay the one-time schema-loading cost.
Reading
# A single record
report = Report.where(id: params[:id]).quick_read
report.id # => 1
report. # => "All done"
report.class # => Report::Lite
# All scoped records
reports = Report.where(status: "done").quick_reads
reports.first.title
The returned Report::Lite is a struct with the same attributes (and #to_h) as your model:
Report.order(:created_at).quick_reads.map(&:to_h)
Batching
Load your batches faster, with bigger wins on larger, more complex models:
Report.in_batches do |batch|
batch.quick_reads.each do |report|
puts report.
end
end
Building
Build a lite instance from attributes without hitting the database:
draft = Report.quick_build(id: 1, message: "hello")
Upgrading to a full Active Record object
Lite instances lazily upgrade to full-fledged ActiveRecord objects on demand. Calling any method the
struct doesn't have (like save, update, or association accessors) transparently materializes the
underlying model:
Report.where(status: "queued").quick_reads.each do |report|
report.update(status: "done")
end
report = Report.first.quick_read
report. # => loads the belongs_to association on the full model
report.save!
Reloading
Refresh a lite instance with fresh data from the database:
report = Report.first.quick_read
# ... the underlying row changes elsewhere ...
report.reload
report. # => updated value
It just works.
Benchmarks
Measured against ActiveRecord 8.1 and SQLite with 10,000 records (see benchmarks/read_benchmark.rb).
The model includes string, integer, date, decimal, text, and JSON-serialized columns to exercise
typecasting:
| Scenario | ActiveRecord | quick_read | Speedup |
|---|---|---|---|
| Read fields | 7.6 i/s | 63.7 i/s | ~8.3x |
| Convert to hashes | 4.7 i/s | 56.4 i/s | ~13.7x |
| Access associations | 1.1 i/s | 1.0 i/s | ~same |
The win comes from skipping typecasting and instantiation when reading. The speedup depends on the
size and complexity of your models: more columns, and heavier typecasting (dates, decimals, JSON,
etc.), mean a bigger gap. When you access associations, each lite struct upgrades to a full
ActiveRecord object (via method_missing), so performance is comparable to a plain ActiveRecord read.
Run it yourself:
$ bundle exec ruby benchmarks/read_benchmark.rb
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/ridiculous/activerecord-quick_reads. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the ActiveRecord::QuickRead project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.