# ActiveRecord vs Sequel

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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/9e23248d-aef4-4237-4cc6-3a115fdecd00/orig =1920x1005)

[ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html) 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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/474cabd8-44f8-4d50-0fb9-e346535d5000/md1x =1200x600)
[Sequel](https://sequel.jeremyevans.net/) 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:

```bash
gem install activerecord
```

For a standalone application, establish a database connection:

```ruby
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`:

```yaml
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:

```bash
gem install sequel
```

Connect to a database using a connection string or hash:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
# 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`:

```ruby
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:

```ruby
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:

```ruby
# 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:

```ruby
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:

```ruby
# 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:

```ruby
# 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:

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

## Handling associations

ActiveRecord declares associations using class methods in the model:

```ruby
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:

```ruby
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:

```ruby
# 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:

```ruby
# 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:

```ruby
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:

```ruby
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:

```ruby
# 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:

```ruby
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`:

```ruby
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:

```bash
rails db:migrate
rails db:rollback
rails db:migrate:status
```

Sequel migrations support both Ruby DSL and raw SQL:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
# 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:

```ruby
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:

```ruby
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:

```ruby
# 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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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.