A Common Table Expression (CTE) is a temporary named result set, derived from a simple query. CTEs help us write and maintain complex queries. Below are some of the use cases of CTEs.
- When you need to reference a derived table multiple times in a single query.
- When you need to organize long and complex queries.
How to create a CTE in SQL?
Initiate a CTE using WITH
, then the name of the CTE followed by AS
. Define the query within the bracket. Later reference the CTE in the subsequent query.
Let's take an example of a posts
table that has many comments
associated with it.
We will first create a CTE query that gives a list of posts with at least one comment. Then use this CTE to fetch the ID
and NAME
of the post. Since CTE is a temporary result set, we should always run the CTE query & the subsequent query together.
WITH posts_with_comments AS (
SELECT * FROM posts WHERE comments_count > 0
)
SELECT id, name FROM posts_with_comments;
# Results
# | ID | NAME |
# | 4 | apple |
# | 3 | android |
How to create a CTE in Rails < 7.1?
In Rails < 7.1, we can use Arel to generate CTE. We will use the same example we used above. Let's first build the Arel table.
posts_table = Arel::Table.new(:posts)
Next, define the query which will act as a CTE. Note the usage of Arel.star
, a convenient method for the *
character. This CTE fetches all the posts having at least one comment.
posts_with_comments_expression = posts_table.where(posts_table[:comments_count].gt(0)).project(Arel.star)
Now build the subsequent query and attach the CTE to this subsequent query. Use Arel::Nodes::As
to mimic the AS
command and map CTE with Arel table posts
.
query = Post.select(:id, :name).arel.with(Arel::Nodes::As.new(posts_table, posts_with_comments_expression)).to_sql
=> "WITH \"posts\" AS (SELECT * FROM \"posts\" WHERE \"posts\".\"comments_count\" > 0) SELECT \"posts\".\"id\", \"posts\".\"name\" FROM \"posts\""
Let's run this query from the rails console.
Loading development environment (Rails 7.0.3.1)
2.7.6 :001 > ActiveRecord::Base.connection.exec_query(query)
SQL (0.4ms) WITH "posts" AS (SELECT * FROM "posts" WHERE "posts"."comments_count" > 0) SELECT "posts"."id", "posts"."name" FROM "posts"
=> #<ActiveRecord::Result:0x00007fa9840278c8 @columns=["id", "name"], @rows=[[4, "apple"], [3, "android"]], @hash_rows=nil, @column_types={}>
You can see how tedious it is to build a CTE using Arel.
How to create a CTE in Rails >= 7.1?
.with
query method makes it super easy to build and chain complex CTE queries. Let's take the same example we used in the SQL segment. First, create a CTE query having a post with at least one comment.
Post.with(posts_with_comments: Post.where("comments_count > ?", 0))
.with
is only one part of the equation. Once we have CTE results we also need to create a subsequent query that uses this CTE.
We can write a from
query method that will alias the CTE table to the ActiveRecord table. Use the select
method to create a subsequent query that fetches ID
& NAME
of the posts.
Loading development environment (Rails 7.1.0.alpha)
2.7.6 :001 > Post.with(posts_with_comments: Post.where("comments_count > ?", 0)).from("posts_with_comments AS posts").select("posts.id, posts.name")
# Results
=> #<ActiveRecord::Relation [#<Post id: 4, name: "apple">, #<Post id: 3, name: "android">]>
Rails 7.1 has made it easy for us to write complex queries. Currently, CTEs are only supported with the following database adapters:
- MariaDB (version 10.2.1 and above)
- MySQL (version 8.0.1 and above)
- PostgreSQL (all supported versions)
- SQLite (version 3.8.3 and above)
So please check your database adapter version before using CTE in Rails.