Ecto.Query.with_cte
with_cte
, go back to Ecto.Query module for more information.
A common table expression (CTE) also known as WITH expression.
name
must be a compile-time literal string that is being used
as the table name to join the CTE in the main query or in the
recursive CTE.
IMPORTANT! Beware of using CTEs. In raw SQL, CTEs can be used as a mechanism to organize queries, but said mechanism has no purpose in Ecto since Ecto queries are composable by definition. In other words, if you need to break a large query into parts, use all of the functionality in Elixir and in this module to structure your code. Furthermore, breaking a query into CTEs can negatively impact performance, as the database may not optimize efficiently across CTEs. The main use case for CTEs in Ecto is to provide recursive definitions, which we outline in the following section. Non-recursive CTEs can often be written as joins or subqueries, which provide better performance.
Options
:as
- the CTE query itself or a fragment
Recursive CTEs
Use recursive_ctes/2
to enable recursive mode for CTEs.
In the CTE query itself use the same table name to leverage
recursion that has been passed to the name
argument. Make sure
to write a stop condition to avoid an infinite recursion loop.
Generally speaking, you should only use CTEs in Ecto for
writing recursive queries.
Expression examples
Products and their category names for breadcrumbs:
category_tree_initial_query =
Category
|> where([c], is_nil(c.parent_id))
category_tree_recursion_query =
Category
|> join(:inner, [c], ct in "category_tree", on: c.parent_id == ct.id)
category_tree_query =
category_tree_initial_query
|> union_all(^category_tree_recursion_query)
Product
|> recursive_ctes(true)
|> with_cte("category_tree", as: ^category_tree_query)
|> join(:left, [p], c in "category_tree", on: c.id == p.category_id)
|> group_by([p], p.id)
|> select([p, c], %{p | category_names: fragment("ARRAY_AGG(?)", c.name)})
It's also possible to pass a raw SQL fragment:
@raw_sql_category_tree """
SELECT * FROM categories WHERE c.parent_id IS NULL
UNION ALL
SELECT * FROM categories AS c, category_tree AS ct WHERE ct.id = c.parent_id
"""
Product
|> recursive_ctes(true)
|> with_cte("category_tree", as: fragment(@raw_sql_category_tree))
|> join(:inner, [p], c in "category_tree", on: c.id == p.category_id)
If you don't have any Ecto schema pointing to the CTE table, you can pass a tuple with the CTE table name as the first element and an Ecto schema as the second element. This will cast the result rows to Ecto structs as long as the Ecto schema maps to the same fields in the CTE table:
{"category_tree", Category}
|> recursive_ctes(true)
|> with_cte("category_tree", as: ^category_tree_query)
|> join(:left, [c], p in assoc(c, :products))
|> group_by([c], c.id)
|> select([c, p], %{c | products_count: count(p.id)})
Keyword syntax is not supported for this feature.
Limitation: CTEs on schemas with source fields
Ecto allows developers to say that a table in their Ecto schema maps to a different column in their database:
field :group_id, :integer, source: :iGroupId
At the moment, using a schema with source fields in CTE may emit invalid queries. If you are running into such scenarios, your best option is to use a fragment as your CTE.