Back to Scaling Ruby Applications guides

ActiveRecord vs Sequel

Stanley Ulili
Updated on October 10, 2025

ActiveRecord, the ORM built into Rails, makes it easy to work with relational databases by handling most tasks automatically. Sequel takes a different path, focusing on SQL transparency and giving developers more control for performance tuning.

This comparison looks at how each library handles queries, schema changes, and model design to help you choose the best fit for your application’s data layer.

What is ActiveRecord?

Screenshot of ActiveRecord documentation

ActiveRecord implements the Active Record pattern where model objects carry both data and behavior. It abstracts SQL behind Ruby methods that feel natural to Rails developers.

The library assumes convention over configuration. A User model automatically maps to a users table, and methods like User.find(1) generate appropriate SQL without explicit query writing. This approach reduces boilerplate code in typical web applications.

ActiveRecord ships as part of Rails but works independently. Sinatra and Hanami applications can use ActiveRecord for database access, though the tight Rails integration means you'll encounter Rails conventions even outside the framework.

What is Sequel?

Screenshot of Sequel Github
Sequel provides database access through explicit dataset manipulation rather than model-centric abstractions. Sequel emphasizes SQL visibility and gives developers direct control over query generation.

The library separates dataset operations from model behavior. You construct queries using Ruby methods that closely mirror SQL structure, making it clear what database operations will execute. This transparency helps when optimizing queries or debugging performance issues.

Sequel runs independently of any web framework. The library's standalone nature means it doesn't assume Rails conventions or impose framework-specific patterns. This flexibility makes Sequel suitable for background jobs, data processing scripts, and non-web applications where ActiveRecord's assumptions don't fit.

Quick Comparison

Feature ActiveRecord Sequel
Design Philosophy Convention over configuration Explicit query construction
Primary Pattern Active Record Data Mapper (optional)
Framework Coupling Rails (can be used independently) Framework-agnostic
Query Interface Model methods and scopes Dataset chaining
Raw SQL Visibility Abstracted (requires .to_sql) Transparent
Performance Overhead Higher (more abstraction layers) Lower (minimal abstraction)
Migration System TimestampedMigrations with DSL Reversible migrations with SQL fallback
Plugin System Rails concerns and gems Built-in plugin architecture
Association Loading Eager loading via includes Eager loading via eager
Database Support PostgreSQL, MySQL, SQLite, others PostgreSQL, MySQL, SQLite, others (more databases)

Installation and setup

ActiveRecord typically comes with Rails, but you can install it separately:

 
gem install activerecord

For a standalone application, establish a database connection:

 
require 'active_record'

ActiveRecord::Base.establish_connection(
  adapter: 'postgresql',
  host: 'localhost',
  database: 'myapp_development',
  username: 'postgres',
  password: 'password'
)

Rails applications configure this through config/database.yml:

 
development:
  adapter: postgresql
  host: localhost
  database: myapp_development
  username: postgres
  password: password

The configuration loads automatically when Rails starts, connecting all models to the database without additional setup.

Sequel requires explicit installation:

 
gem install sequel

Connect to a database using a connection string or hash:

 
require 'sequel'

# Connection string
DB = Sequel.connect('postgres://postgres:password@localhost/myapp_development')

# Or configuration hash
DB = Sequel.connect(
  adapter: 'postgres',
  host: 'localhost',
  database: 'myapp_development',
  user: 'postgres',
  password: 'password'
)

The connection object (DB) provides access to all database operations. Unlike ActiveRecord's global connection, Sequel encourages passing this object explicitly or storing it as a constant.

Defining models

ActiveRecord models inherit from ActiveRecord::Base and follow Rails naming conventions:

 
class User < ActiveRecord::Base
  # Table name automatically inferred as 'users'
  # Primary key automatically assumed as 'id'
end

# The model is immediately usable
user = User.find(1)
user.email = 'new@example.com'
user.save

The inheritance brings hundreds of methods including finders, validations, callbacks, and associations. These methods assume standard Rails conventions about table names, primary keys, and timestamps.

You can override conventions when needed:

 
class Person < ActiveRecord::Base
  self.table_name = 'people_table'
  self.primary_key = 'person_id'
end

Sequel separates the model layer from database access. You can query tables directly without defining models:

 
# Query without a model
users = DB[:users].where(active: true).all

# Each row returns as a hash
users.first[:email]  # => "user@example.com"

For model behavior, inherit from Sequel::Model:

 
class User < Sequel::Model
  # Table name inferred from class name
  # Can be explicitly set with set_dataset :users
end

# Usage similar to ActiveRecord
user = User[1]  # Find by primary key
user.email = 'new@example.com'
user.save

Sequel models provide less functionality by default. Features like validations and associations require explicit plugin loading:

 
class User < Sequel::Model
  plugin :validation_helpers
  plugin :timestamps

  def validate
    super
    validates_presence [:email, :username]
    validates_unique :email
  end
end

Writing queries

ActiveRecord generates queries through model methods and scopes:

 
# Simple queries
User.where(active: true)
User.where('created_at > ?', 1.week.ago)
User.order(created_at: :desc).limit(10)

# Chaining conditions
User.where(active: true)
    .where('created_at > ?', 1.week.ago)
    .order(created_at: :desc)

# Scopes for reusable query fragments
class User < ActiveRecord::Base
  scope :active, -> { where(active: true) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
end

User.active.recent.order(created_at: :desc)

The query methods return relation objects that lazily execute SQL. You can chain multiple conditions before the database query runs, typically when you iterate results or call methods like .to_a or .count.

To see the generated SQL:

 
User.where(active: true).to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"active\" = TRUE"

Sequel constructs queries through dataset chaining with syntax closer to SQL:

 
# Simple queries
DB[:users].where(active: true)
DB[:users].where(Sequel.lit('created_at > ?', Time.now - 7*24*60*60))
DB[:users].order(Sequel.desc(:created_at)).limit(10)

# Chaining conditions
DB[:users]
  .where(active: true)
  .where { created_at > Time.now - 7*24*60*60 }
  .order(Sequel.desc(:created_at))

# Model-based queries
User.where(active: true)
    .where { created_at > Time.now - 7*24*60*60 }
    .order(Sequel.desc(:created_at))

Sequel's virtual row blocks provide SQL-like syntax:

 
# Complex conditions with virtual row blocks
User.where { (status == 'active') & (created_at > Time.now - 30*24*60*60) }
User.where { name.like('%john%') }
User.where { age > 18 }

The SQL remains visible throughout query construction. Calling .sql shows the exact query:

 
User.where(active: true).sql
# => "SELECT * FROM \"users\" WHERE (\"active\" IS TRUE)"

Handling associations

ActiveRecord declares associations using class methods in the model:

 
class User < ActiveRecord::Base
  has_many :posts
  has_many :comments, through: :posts
  belongs_to :organization
end

class Post < ActiveRecord::Base
  belongs_to :user
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
  belongs_to :user
end

These declarations create methods for navigating relationships:

 
user = User.find(1)
user.posts  # Returns all posts for this user
user.posts.create(title: 'New Post')  # Creates associated post
user.comments  # Returns comments through posts

ActiveRecord lazy-loads associations by default. Accessing user.posts executes a separate query for posts. This causes N+1 query problems when iterating collections:

 
# N+1 problem: One query for users, then one query per user for posts
users = User.all
users.each do |user|
  puts user.posts.count  # Separate query for each user
end

Solve this with eager loading:

 
# Two queries total: One for users, one for all posts
users = User.includes(:posts)
users.each do |user|
  puts user.posts.count  # Uses preloaded data
end

Sequel defines associations similarly but with different syntax:

 
class User < Sequel::Model
  one_to_many :posts
  many_to_many :comments, join_table: :posts
  many_to_one :organization
end

class Post < Sequel::Model
  many_to_one :user
  one_to_many :comments
end

class Comment < Sequel::Model
  many_to_one :post
  many_to_one :user
end

Association methods work like ActiveRecord:

 
user = User[1]
user.posts  # Returns all posts
user.add_post(title: 'New Post')  # Creates associated post
user.comments  # Returns comments through posts

Sequel also lazy-loads by default but provides eager loading:

 
# Eager load with .eager
users = User.eager(:posts).all
users.each do |user|
  puts user.posts.count  # Uses preloaded data
end

# Eager load multiple associations
User.eager(:posts, :organization).all

# Nested eager loading
User.eager(posts: :comments).all

Sequel's eager loading tends to generate fewer queries than ActiveRecord's includes in complex scenarios, especially with nested associations.

Migrations and schema management

ActiveRecord migrations use a Ruby DSL that abstracts SQL:

 
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :email, null: false
      t.string :username, null: false
      t.boolean :active, default: true
      t.timestamps
    end

    add_index :users, :email, unique: true
  end
end

The change method automatically reverses for rollbacks. Rails determines how to undo operations like create_table (by dropping the table) and add_column (by removing the column).

For operations that can't automatically reverse, use up and down:

 
class UpdateUserData < ActiveRecord::Migration[7.0]
  def up
    execute "UPDATE users SET status = 'active' WHERE last_login > NOW() - INTERVAL '30 days'"
  end

  def down
    execute "UPDATE users SET status = 'inactive'"
  end
end

Run migrations with Rake tasks:

 
rails db:migrate
rails db:rollback
rails db:migrate:status

Sequel migrations support both Ruby DSL and raw SQL:

 
Sequel.migration do
  change do
    create_table(:users) do
      primary_key :id
      String :email, null: false
      String :username, null: false
      TrueClass :active, default: true
      DateTime :created_at
      DateTime :updated_at

      index :email, unique: true
    end
  end
end

Like ActiveRecord, change blocks automatically reverse. For explicit control:

 
Sequel.migration do
  up do
    run "UPDATE users SET status = 'active' WHERE last_login > NOW() - INTERVAL '30 days'"
  end

  down do
    run "UPDATE users SET status = 'inactive'"
  end
end

Run migrations programmatically:

 
require 'sequel'
DB = Sequel.connect('postgres://localhost/myapp')

Sequel.extension :migration
Sequel::Migrator.run(DB, 'db/migrations')

Sequel's migration system works identically whether you use Rails, Sinatra, or standalone scripts. The same migration files run in any Ruby application.

Validations and callbacks

ActiveRecord includes validations in the model:

 
class User < ActiveRecord::Base
  validates :email, presence: true, uniqueness: true
  validates :username, presence: true, length: { minimum: 3 }
  validates :age, numericality: { greater_than: 0 }, allow_nil: true

  validate :email_format

  private

  def email_format
    unless email =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
      errors.add(:email, "must be a valid email address")
    end
  end
end

Validations run automatically before saving:

 
user = User.new(email: 'invalid')
user.save  # => false
user.errors.full_messages  # => ["Username can't be blank", "Email must be a valid email address"]

# Bypass validations if needed
user.save(validate: false)

ActiveRecord provides extensive callback hooks:

 
class User < ActiveRecord::Base
  before_validation :normalize_email
  before_save :encrypt_password
  after_create :send_welcome_email
  after_commit :notify_admin, on: :create

  private

  def normalize_email
    self.email = email.downcase.strip if email.present?
  end

  def encrypt_password
    self.password_digest = BCrypt::Password.create(password) if password.present?
  end

  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
end

These callbacks execute automatically at specific points in the object lifecycle, keeping related logic centralized in the model.

Sequel requires loading the validation plugin:

 
class User < Sequel::Model
  plugin :validation_helpers

  def validate
    super
    validates_presence [:email, :username]
    validates_unique :email
    validates_min_length 3, :username
    validates_format /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i, :email
  end
end

Validations run before saving:

 
user = User.new(email: 'invalid')
user.valid?  # => false
user.errors  # => {:username=>["is not present"], :email=>["is invalid"]}
user.save  # Raises Sequel::ValidationFailed

# Save without validations
user.save(validate: false)

Sequel provides callback hooks through plugins:

 
class User < Sequel::Model
  plugin :validation_helpers
  plugin :timestamps
  plugin :hook_class_methods

  def before_validation
    self.email = email.downcase.strip if email
    super
  end

  def before_create
    self.password_digest = BCrypt::Password.create(password) if password
    super
  end

  def after_create
    super
    send_welcome_email
  end

  private

  def send_welcome_email
    # Email logic
  end
end

The explicit super calls give you control over callback execution order. Forgetting super can prevent parent class callbacks from running, which makes debugging easier but requires attention.

Working with JSON and PostgreSQL features

ActiveRecord added strong PostgreSQL support in recent versions:

 
class CreateEvents < ActiveRecord::Migration[7.0]
  def change
    create_table :events do |t|
      t.jsonb :data, default: {}
      t.inet :ip_address
      t.tsrange :duration
      t.timestamps
    end

    add_index :events, :data, using: :gin
  end
end

Query JSONB columns using PostgreSQL operators:

 
# Find events with specific JSON data
Event.where("data->>'type' = ?", 'click')
Event.where("data @> ?", { user_id: 123 }.to_json)

# Use ActiveRecord's newer syntax
Event.where("data @> :query", query: { user_id: 123 }.to_json)

Access JSON data through attribute methods:

 
event = Event.first
event.data  # => {"type"=>"click", "user_id"=>123}
event.data['type']  # => "click"

# Modify JSON data
event.data['timestamp'] = Time.now.to_i
event.save

Sequel handles PostgreSQL features through extensions:

 
Sequel.extension :pg_json
DB.extension :pg_json

class CreateEvents < Sequel::Migration
  def change
    create_table(:events) do
      primary_key :id
      jsonb :data, default: '{}'
      inet :ip_address
      tsrange :duration
      DateTime :created_at
      DateTime :updated_at

      index :data, type: :gin
    end
  end
end

Query JSONB with native PostgreSQL operator support:

 
# JSON queries using Sequel's PostgreSQL extension
Event.where(Sequel.pg_jsonb(:data).get_text('type') => 'click')
Event.where(Sequel.pg_jsonb(:data).contains(user_id: 123))

# Raw SQL for complex queries
Event.where(Sequel.lit("data @> ?", {user_id: 123}.to_json))

Access JSON data directly:

 
event = Event.first
event.data  # => {"type"=>"click", "user_id"=>123}

# Modify JSON data
event.data['timestamp'] = Time.now.to_i
event.save

Sequel's PostgreSQL extension provides methods for every PostgreSQL operator, making advanced features accessible without raw SQL strings.

Transaction handling

ActiveRecord wraps operations in transactions using blocks:

 
ActiveRecord::Base.transaction do
  user = User.create!(email: 'user@example.com')
  profile = Profile.create!(user: user, bio: 'Hello')
  user.update!(confirmed: true)
end

Raising an exception rolls back all operations in the transaction. ActiveRecord automatically rolls back when save methods with ! fail:

 
ActiveRecord::Base.transaction do
  user = User.create!(email: 'user@example.com')
  # This will raise and rollback the transaction
  profile = Profile.create!(user: user, bio: nil)  # Fails validation
end

You can manually trigger rollbacks:

 
ActiveRecord::Base.transaction do
  user = User.create!(email: 'user@example.com')

  if external_api_call_fails?
    raise ActiveRecord::Rollback  # Silently rolls back
  end
end

Nested transactions use savepoints:

 
User.transaction do
  user = User.create!(email: 'user@example.com')

  Profile.transaction do
    profile = Profile.create!(user: user)
    raise ActiveRecord::Rollback  # Only rolls back profile creation
  end

  # User creation remains
end

Sequel handles transactions similarly:

 
DB.transaction do
  user = User.create(email: 'user@example.com')
  profile = Profile.create(user_id: user.id, bio: 'Hello')
  user.update(confirmed: true)
end

Exceptions automatically rollback:

 
DB.transaction do
  user = User.create(email: 'user@example.com')
  # Validation failure raises exception and rolls back
  profile = Profile.create(user_id: user.id, bio: nil)
rescue Sequel::ValidationFailed => e
  # Transaction already rolled back
  puts "Failed: #{e.message}"
end

Manual rollback:

 
DB.transaction do
  user = User.create(email: 'user@example.com')

  raise Sequel::Rollback if external_api_call_fails?
end

Sequel supports savepoints for nested transactions:

 
DB.transaction do
  user = User.create(email: 'user@example.com')

  DB.transaction(savepoint: true) do
    profile = Profile.create(user_id: user.id)
    raise Sequel::Rollback  # Only rolls back profile
  end

  # User creation remains
end

Both libraries handle transactions reliably, with similar APIs and behavior.

Final thoughts

Both ActiveRecord and Sequel give Ruby applications reliable ways to work with databases. ActiveRecord is tightly integrated with Rails and comes with a large ecosystem, which makes it the practical choice in most Rails projects. Its conventions cut down on boilerplate and keep codebases consistent, which is especially valuable for teams.

Sequel, in contrast, appeals to those who want more control. It favors explicit query building and strong performance, and because it is not tied to Rails, it works well in standalone apps, background jobs, or projects where Rails conventions do not fit. Its ecosystem is smaller and may require more custom code, but the trade-off is flexibility and speed.