Ruby · Rails · databases

Arel versus Sequel: an AST inside Rails and a toolkit outside it

The useful comparison is not query syntax. Arel supports Active Record's relational tree; Sequel makes the Dataset the public database abstraction.

Simple entity relationship diagram showing one corporation owning many service businesses
Entity relationship diagram by Fishpi, public domain, converted to AVIF.

I keep seeing Arel and Sequel placed in the same box because both let Ruby describe SQL without assembling strings by hand. That is accurate at the syntax level and misleading at the architectural level.

Arel is the relational abstract syntax tree underneath Active Record. Sequel is a complete database toolkit whose central public object is a Dataset, with an ORM available when model objects are useful. One normally lives inside the Rails persistence stack; the other can be the persistence stack.

Arelrelational AST and SQL compilation inside Active Record
Sequeldatabase toolkit built around first-class datasets
Relationthe normal public query unit in a Rails application
Datasetthe normal public query unit throughout Sequel

The useful comparison is therefore not “which DSL has nicer joins?” It is “which layer should this application own?”

The abstraction boundary comes first

In an ordinary Rails application, the stable, idiomatic surface is ActiveRecord::Relation:

scope = Article.where.not(published_at: nil)
scope = scope.where(author_id: author_id) if author_id
scope.order(published_at: :desc)

Arel becomes useful when the relation API needs a composable expression tree:

articles = Article.arel_table

predicate = articles[:published_at].not_eq(nil)
predicate = predicate.and(articles[:author_id].eq(author_id)) if author_id

Article.where(predicate).order(published_at: :desc)

The result is still an Active Record relation that returns Article objects, participates in scopes and associations, and uses the Rails adapter to compile SQL.

The Sequel version starts from a different centre:

dataset = DB[:articles].exclude(published_at: nil)
dataset = dataset.where(author_id: author_id) if author_id

dataset.order(Sequel.desc(:published_at))

The result is a Sequel::Dataset. It represents the query itself, remains frozen, returns modified copies when chained, and does not execute until a result-producing method such as all, first or each is called. Attach it to Sequel::Model and it can return model instances; leave it as a plain dataset and it can return rows without asking an ORM to define the architecture.

Concern Active Record + Arel Sequel
Primary public query object ActiveRecord::Relation Sequel::Dataset
Role of the lower-level AST Arel supports and compiles Active Record queries SQL expression objects are part of the Dataset DSL
Default result shape Active Record model instances Row hashes for plain datasets; model instances with Sequel::Model
Scope Rails ORM and adapter stack Connection, pooling, query DSL, schema DSL, migrations and optional ORM
Typical reason to reach lower Compose a predicate or operation Active Record cannot express cleanly The Dataset is already the main interface
Migration cost in a Rails app Usually local and incremental Usually architectural, not a query-by-query substitution

Arel is most valuable as a narrow escape hatch

Arel earns its place when a query contains structure that string fragments hide: grouped AND/OR predicates, reusable nodes, custom joins, aliases, SQL functions or fragments that must be combined before the adapter renders them.

For example, optional filters remain explicit AST nodes:

articles = Article.arel_table

visible = articles[:published_at].not_eq(nil)
owned   = articles[:author_id].eq(author_id)
recent  = articles[:published_at].gt(30.days.ago)

predicate = visible.and(author_id ? owned : recent)
Article.where(predicate)

That can be easier to review than a growing string with manually balanced parentheses. It also keeps quoting and adapter compilation inside the Rails stack.

But Arel is not a reason to rebuild Active Record's public API from scratch. Modern Active Record already handles joins, subqueries, aggregates, Common Table Expressions and recursive CTEs through relation methods. When where, joins, merge, with, with_recursive or a small scope expresses the query clearly, dropping lower merely increases the maintenance surface.

This is the dividing line I use:

Use Arel when the relational structure is clearer as nodes. Do not use it merely because node construction feels more sophisticated than a relation.

Rails documents Arel.sql as a way to mark known-safe SQL and supports positional or named binds. The warning matters: a raw literal is not sanitised simply because it carries an Arel class name. Request parameters and model attributes still need proper binding or a higher-level query method.

Sequel makes the query itself a first-class object

Sequel's Dataset model is not a hidden compiler detail. It is the public design.

published = DB[:articles].exclude(published_at: nil)
recent    = published.where { published_at > Sequel::CURRENT_DATE - 30 }
by_author = recent.where(author_id: author_id)

by_author.order(Sequel.desc(:published_at)).all

Each operation returns another dataset. The original remains unchanged, so datasets can be shared, extended and passed across boundaries without mutable query state. Sequel's own documentation calls out this frozen, functional style and its lazy execution explicitly.

That design stays consistent beyond SELECT. Datasets can express inserts, updates and deletes; the same toolkit includes connection pooling, prepared statements, transaction controls, schema construction, migrations and adapters for multiple databases. Sequel::Model adds associations, validation and hooks, but the database layer does not disappear behind the model layer.

This makes Sequel attractive when SQL is not an implementation detail of domain models. Reporting services, ingestion pipelines, database-heavy APIs, maintenance tools and applications with several explicit database boundaries often benefit from letting datasets remain visible.

The syntax comparison is the least important part

Both libraries can produce readable SQL expressions.

Conditional predicates

# Arel inside Active Record
users = User.arel_table
predicate = users[:active].eq(true)
predicate = predicate.and(users[:team_id].eq(team_id)) if team_id
User.where(predicate)
# Sequel Dataset
users = DB[:users].where(active: true)
users = users.where(team_id: team_id) if team_id

SQL functions

# Arel
lower_name = Arel::Nodes::NamedFunction.new('LOWER', [users[:name]])
User.where(lower_name.eq('alice'))
# Sequel
DB[:users].where(Sequel.function(:lower, :name) => 'alice')

Raw fragments with binds

# Active Record / Arel
Article.order(Arel.sql('CASE status WHEN ? THEN 0 ELSE 1 END', 'featured'))
# Sequel
DB[:articles].order(Sequel.lit('CASE status WHEN ? THEN 0 ELSE 1 END', 'featured'))

The exact spellings differ, but the safety rule is the same: use structured expressions or bound values; do not interpolate untrusted input into SQL literals.

Rails integration changes the economics

Choosing Arel in a Rails application normally means choosing a smaller implementation technique inside the system already present. The models, dirty tracking, callbacks, validations, associations, fixtures, form helpers and conventions remain Active Record's.

Choosing Sequel means deciding how much of that stack should change. Sequel has its own capable ORM, association system, hooks, plugins and migrations, but they are not drop-in Active Record objects. A mature Rails codebase can have assumptions about transactions, callbacks, serializers, generators, test helpers and gems that make “replace the query builder” an inaccurate description of the work.

There are legitimate hybrid designs. A reporting or analytics boundary can use Sequel beside an Active Record application, especially against a separate database or read-only connection. The cost is conceptual duplication: two connection pools, two transaction APIs, two type systems and two sets of conventions. That can be worthwhile when the boundary is explicit; it becomes confusing when both libraries touch the same domain tables opportunistically.

Database features are not a simple scorecard

It is tempting to choose Sequel because a database feature looks awkward in Active Record, or to dismiss Sequel because Rails now has a relation method for that feature. Both shortcuts age badly.

Active Record's current query API includes non-recursive and recursive CTE construction. Arel can represent additional relational structure, and raw SQL remains available when a database-specific feature is the clearest option.

Sequel deliberately exposes advanced database work through datasets, extensions and adapter-specific methods. That often feels more direct, particularly when database capabilities are central to the application rather than exceptional. But portability is still conditional: a method can have different adapter translations, and the database ultimately decides which SQL forms exist.

The right question is not “which library supports CTEs?” Both can reach them. The right question is whether advanced SQL should remain an occasional implementation detail under an ORM or become a visible, testable part of the application's public data-access layer.

Reviewability is the practical test

A query abstraction succeeds when another developer can answer four questions without running it:

  1. Which tables and rows can this touch?
  2. Which conditions are grouped together?
  3. When does it execute?
  4. What kind of object comes back?

Active Record relations answer those questions well for most Rails CRUD and domain work. Arel helps when predicate structure would otherwise become opaque. Sequel datasets answer them well when the query itself deserves a name and a life independent of a model instance.

The failure modes are symmetrical. Too much Arel can turn ordinary Rails code into an internal-node dialect that few teammates review comfortably. Too much clever Sequel DSL can hide database-specific behaviour behind Ruby operators. In both cases, inspecting generated SQL and its execution plan remains part of the job.

A decision guide

Situation Default choice Reason
Existing Rails model query Active Record relation Lowest conceptual and integration cost
One complex predicate inside Rails Relation plus a small Arel fragment Keeps composition explicit without replacing the ORM surface
Database-specific SQL that is clearest as SQL Bound Arel.sql or Sequel.lit, narrowly Honest escape hatch; keep trust boundary visible
New Ruby service where database access is the main architecture Evaluate Sequel seriously Dataset, pooling, transactions and ORM can form one coherent toolkit
Reporting/ETL boundary with row-oriented results Sequel Dataset Query objects need not masquerade as domain models
Rails app considering a full ORM switch Treat as an architectural migration Associations, callbacks, extensions and conventions all move
Team unfamiliar with relational ASTs Prefer the clearest high-level API Reviewability beats expressive novelty

What I would write in code review

Arel and Sequel overlap in their ability to represent SQL, but they do not occupy the same layer.

Arel is usually the tool I reach for after Active Record has expressed most of the query and one relational fragment still needs a proper tree. Sequel is the tool I consider when I want the Dataset—not an Active Record model—to be the durable unit of database work.

That is why a syntax shoot-out produces the wrong winner. The choice is about ownership. Keep Active Record and use Arel narrowly when Rails owns persistence. Choose Sequel when the application should own an explicit database toolkit from connection to query to result.

Sources

  1. Original Arel and Sequel comparison gist, 21 December 2013
  2. Rails Arel API
  3. Active Record query methods, including with and with_recursive
  4. Sequel Dataset Basics
  5. Sequel querying guide
  6. Sequel: The Database Toolkit for Ruby