Arel in Rails: How to Write Advanced SQL Queries with Ruby

Arel (A Relational Algebra) is a Ruby library used under the hood by ActiveRecord to build SQL queries. It gives developers more control over how queries are constructed, allowing them to build complex, optimized SQL that ActiveRecord alone can’t easily express.

Why Should You Use Arel?

Rails ActiveRecord is great for simple queries, but sometimes:

  • You need complex joins, subqueries, or conditions.
  • Performance optimization that plain ActiveRecord can’t express.
  • Raw SQL flexibility with Ruby-like syntax.
  • You want to avoid writing unsafe or error-prone raw SQL strings.

Arel gives you:

  • SQL power + Ruby syntax
  • Chainable query building
  • Safer and cleaner alternatives to raw SQL

Basic Arel Setup

user_table = Arel::Table.new(:users)

Now you can use user_table to build complex queries.

10 Arel Usage Examples

1. Simple WHERE condition

user_table = Arel::Table.new(:users)
query = user_table.project(Arel.star).where(user_table[:age].gt(18))
puts query.to_sql
# SELECT * FROM "users" WHERE "users"."age" > 18

2. AND condition

query = user_table.where(user_table[:active].eq(true).and(user_table[:age].gt(21)))

3. OR condition

query = user_table.where(user_table[:name].eq('John').or(user_table[:name].eq('Jane')))

4. JOIN two tables

orders = Arel::Table.new(:orders)
query = user_table.join(orders).on(user_table[:id].eq(orders[:user_id]))

5. SELECT specific columns

query = user_table.project(user_table[:id], user_table[:email])

6. ORDER BY clause

query = user_table.order(user_table[:created_at].desc)

7. LIMIT & OFFSET

query = user_table.take(10).skip(5)

8. GROUP BY

query = user_table.group(user_table[:country])

9. HAVING clause

query = user_table.group(user_table[:country])
       .having(user_table[:age].average.gt(30))

10. Subquery usage

subquery = user_table.project(user_table[:id]).where(user_table[:admin].eq(true))
main_table = Arel::Table.new(:posts)
query = main_table.where(main_table[:user_id].in(subquery))

10 Common Questions & Answers

QuestionAnswer
What is Arel?A Ruby library to build SQL queries programmatically.
Is Arel used in Rails by default?Yes, ActiveRecord uses Arel under the hood.
Why not just use ActiveRecord?Arel is better for complex, dynamic, or performant queries.
Can I mix Arel with ActiveRecord?Yes, but you’ll have to use .where(Arel condition) manually.
How do I convert Arel to SQL?Use query.to_sql.
Does Arel prevent SQL injection?Yes, it escapes inputs properly.
What’s the downside of Arel?Slightly more verbose and less intuitive than ActiveRecord.
Can Arel build INSERT or UPDATE queries?It’s not recommended—use ActiveRecord for that.
Does Arel support custom functions?Yes, via Arel::Nodes::NamedFunction.
Should I replace ActiveRecord with Arel?No. Use Arel only when necessary (for complex queries).

Alternatives to Arel

ToolDescription
Raw SQLDirect SQL, fast but error-prone and not DB-agnostic.
SequelA Ruby ORM that provides SQL DSL like Arel.
Squeel (deprecated)Extended Arel DSL, now replaced by Arel itself.
ArelHelpers gemAdds helpers to make Arel easier in Rails.

Real-World Use Case / Case Study

Scenario: You’re building an analytics dashboard that shows:

  • Users who made more than 5 orders
  • Orders must be within the last 30 days
  • Sorted by total amount spent

Using ActiveRecord alone:

User.joins(:orders)
    .where('orders.created_at > ?', 30.days.ago)
    .group('users.id')
    .having('COUNT(orders.id) > 5')
    .order('SUM(orders.amount) DESC')

With Arel (dynamic, safe, reusable):

users     = Arel::Table.new(:users)
orders    = Arel::Table.new(:orders)

join      = users.join(orders).on(users[:id].eq(orders[:user_id]))
where     = orders[:created_at].gt(30.days.ago)
group     = users[:id]
having    = orders[:id].count.gt(5)
order     = orders[:amount].sum.desc

query = join.project(users[Arel.star])
           .where(where)
           .group(group)
           .having(having)
           .order(order)

puts query.to_sql

This query can now be reused, modified, and made dynamic safely—ideal for reporting and analytics features in production apps.

Best Practices

  • Use Arel only when ActiveRecord falls short.
  • Always escape input; Arel does it for you.
  • Keep Arel logic in query objects or services (not in controllers).
  • Combine with ActiveRecord relations when needed.
  • Profile SQL performance after adding Arel-based queries.

Arel Terms with Descriptions

Core Classes & Modules

TermDescription
Arel::TableRepresents a database table in Arel. Used to define and interact with tables.
Arel::NodesContains SQL expression nodes like And, Or, Eq, NotEq, etc.
Arel::Attributes::AttributeRepresents a column/field of a table. Example: users[:name]
Arel::SelectManagerBuilds SELECT queries (most common manager).
Arel::InsertManagerBuilds INSERT queries.
Arel::UpdateManagerBuilds UPDATE queries.
Arel::DeleteManagerBuilds DELETE queries.

Basic SQL Operations

TermDescription
projectSELECT clause – defines which columns to select.
whereAdds a WHERE clause to a query.
joinAdds a JOIN clause (inner join).
outer_joinAdds a LEFT OUTER JOIN.
onSets the ON condition for a join.
groupAdds a GROUP BY clause.
havingAdds a HAVING clause for filtered aggregates.
orderAdds an ORDER BY clause.
take(n)Adds a LIMIT clause.
skip(n)Adds an OFFSET clause.
distinctAdds SELECT DISTINCT.

Comparison & Logical Operators

TermDescription
eq(value)= equals condition.
not_eq(value)!= not equals condition.
gt(value)> greater than.
gteq(value)>= greater than or equal to.
lt(value)< less than.
lteq(value)<= less than or equal to.
matches(value)LIKE query (e.g., LIKE '%keyword%').
does_not_match(value)NOT LIKE.
in(values)IN clause (WHERE x IN (1, 2, 3)).
not_in(values)NOT IN clause.
and(other_condition)Combines with AND logic.
or(other_condition)Combines with OR logic.

SQL Functions & Nodes

TermDescription
Arel::Nodes::NamedFunctionCall SQL functions (e.g., COUNT, SUM).
Arel.starRepresents * (select all).
Arel.sql('RAW_SQL')Injects raw SQL string (use cautiously).
Arel::Nodes::AsAssigns aliases (e.g., SELECT name AS user_name).
countShortcut for COUNT(column) via NamedFunction.
sumShortcut for SUM(column).
avgAverage aggregate function.
max, minMax and Min aggregates.
asc / descSort direction in ORDER BY.

Joins and Aliases

TermDescription
join(table)Inner join another table.
outer_join(table)Left outer join.
alias(table, name)Aliases a table or subquery.
on(condition)Join condition.

Arel Managers

ManagerUse
Arel::SelectManagerBuilding SELECT statements (most common).
Arel::InsertManagerBuilding INSERT statements.
Arel::UpdateManagerBuilding UPDATE queries.
Arel::DeleteManagerBuilding DELETE queries.

Arel Terms with Examples and Descriptions (Cheat Sheet)

TermDescriptionExampleSQL Output
Arel::Table.new(:users)Defines a reference to the users table.users = Arel::Table.new(:users)
projectSELECT columns.users.project(users[:id], users[:name])SELECT "users"."id", "users"."name"
whereWHERE clause (conditions).users.where(users[:age].gt(18))WHERE "users"."age" > 18
eq(value)Equals (=) condition.users[:name].eq('John')"users"."name" = 'John'
not_eq(value)Not equals (!=) condition.users[:active].not_eq(true)"users"."active" != true
gt(value)Greater than.users[:age].gt(21)"users"."age" > 21
gteq(value)Greater than or equal.users[:age].gteq(21)"users"."age" >= 21
lt(value)Less than.users[:age].lt(50)"users"."age" < 50
lteq(value)Less than or equal.users[:age].lteq(50)"users"."age" <= 50
matches('%keyword%')LIKE clause.users[:email].matches('%@gmail.com')"users"."email" LIKE '%@gmail.com'
does_not_match('%admin%')NOT LIKE.users[:role].does_not_match('%admin%')"users"."role" NOT LIKE '%admin%'
in([1, 2, 3])IN clause.users[:id].in([1, 2, 3])"users"."id" IN (1, 2, 3)
not_in([1, 2])NOT IN clause.users[:id].not_in([1, 2])"users"."id" NOT IN (1, 2)
and(...)AND condition.users[:age].gt(18).and(users[:active].eq(true))"age" > 18 AND "active" = true
or(...)OR condition.users[:role].eq('admin').or(users[:role].eq('editor'))"role" = 'admin' OR "role" = 'editor'
join(...)INNER JOIN.users.join(orders).on(users[:id].eq(orders[:user_id]))INNER JOIN orders ON users.id = orders.user_id
outer_join(...)LEFT OUTER JOIN.users.outer_join(orders).on(users[:id].eq(orders[:user_id]))LEFT OUTER JOIN orders ON users.id = orders.user_id
on(...)JOIN condition.join.on(users[:id].eq(orders[:user_id]))ON users.id = orders.user_id
order(...)ORDER BY clause.users.order(users[:created_at].desc)ORDER BY users.created_at DESC
asc / descSorting direction.users[:name].asc, users[:age].descORDER BY name ASC / age DESC
take(10)LIMIT.users.take(10)LIMIT 10
skip(5)OFFSET.users.skip(5)OFFSET 5
group(...)GROUP BY clause.users.group(users[:country])GROUP BY country
having(...)HAVING condition for aggregates.users.having(users[:age].average.gt(30))HAVING AVG(age) > 30
Arel.star* select all columns.users.project(Arel.star)SELECT *
Arel.sql('RAW_SQL')Insert raw SQL (use cautiously).Arel.sql('NOW()')NOW()
NamedFunctionSQL functions like COUNT, SUM.Arel::Nodes::NamedFunction.new('COUNT', [users[:id]])COUNT(users.id)
alias(...)Alias for column or table.users[:id].as('user_id')users.id AS user_id
SelectManagerSELECT builder.users.project(...).to_sqlBuilds SQL SELECT
InsertManagerINSERT builder.For inserting data using Arel.
UpdateManagerUPDATE builder.For update queries.
DeleteManagerDELETE builder.For delete queries.

Basic Usage of Arel::InsertManager

users = Arel::Table.new(:users)

manager = Arel::InsertManager.new
manager.into(users)

manager.insert([
  [users[:name], 'John Doe'],
  [users[:email], 'john@example.com'],
  [users[:created_at], Time.now]
])

sql = manager.to_sql
puts sql

Learn more about Rails setup

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top