Understanding Elixir’s Ecto Querying DSL: The Basics

Share this article

elixir logo

This article will take a look at the basics of querying with Ecto, a domain-specific language (DSL) for writing queries and interacting with databases in Elixir. This will involve going through joins, associations, aggregation functions, and so on.

A basic knowledge of Elixir is assumed, and knowing the fundamentals of Ecto beforehand will help too.

Ectoing Application

All of the examples in this article series can be run against my demonstrative Ectoing application. I highly encourage you to set this up (as described below) and run all of the queries as you read along. This will help to solidify your understanding by playing around with the examples to see what works and what doesn’t work.

Let’s quickly set up the application:

git clone https://github.com/tpunt/ectoing
cd ectoing
mix deps.get

# don't forget to update the credentials in config/config.exs
mix ecto.create
mix ecto.migrate

# populate the database with some dummy data
mix run priv/repo/seeds.exs

(I’ve chosen to use MySQL for this. The examples throughout this article should work uniformly across all supported databases, so whilst the Mariaex dependency could be switched out to use another database, I would advise against this. This is because some examples in the next article will contain MySQL-dependent code.)

The database structure is as follows:

Ectoing application ERD

Basic Querying

Let’s start with some basic queries to get a feel for Ecto’s querying DSL.

Note that whilst all examples can be executed in the Elixir shell (via iex -S mix in the ectoing base directory), the Ecto.Query module will have to be imported first. This will make all of the querying DSL macros (such as from) available to us whilst working in the shell.

Let’s start with the most trivial of queries — selecting all users with their complete records:

SELECT * FROM users;
query = Ectoing.User

Ectoing.Repo.all query

(All examples will firstly show the SQL syntax, and then how this translates into the Ecto query syntax.)

To get the complete records for all users, we simply perform a query on the desired model (in this case, it’s Ectoing.User). This works because Ecto will, by default, return all of the fields defined in the corresponding model’s schema definition if its select clause is omitted. Ecto’s Repo.all/2 function is then used to execute the query, where it fetches a list of results (since we expect more than one result from the query). Let’s take a quick look at one of these returned records:

[%Ectoing.User{__meta__: #Ecto.Schema.Metadata<:loaded>,
  firstname: "Thomas",
  friends_of: #Ecto.Association.NotLoaded<association :friends_of is not loaded>,
  friends_with: #Ecto.Association.NotLoaded<association :friends_with is not loaded>,
  id: 1,
  inserted_at: #Ecto.DateTime<2016-05-15T20:23:58Z>,
  messages: #Ecto.Association.NotLoaded<association :messages is not loaded>,
  surname: "Punt",
  updated_at: #Ecto.DateTime<2016-05-15T20:23:58Z>,
  username: "tpunt"},
  ...
]

A struct of type Ectoing.User has been returned, where members of the struct match the field names for the Ectoing.User model. Notably, we also have some associations (friend, friends, and messages) not loaded that have been embedded in there. We’ll be covering Ecto’s associations in more detail later, but for now just note that they are Ecto’s solution for managing the loading of foreign key relationships across models.

Quite often though, we won’t want to fetch the whole record. So let’s just select a user’s first name and last name:

SELECT firstname, surname FROM users;
query = from u in Ectoing.User,
  select: [u.firstname, u.surname]

Ectoing.Repo.all query

In the from macro, we specify the model (Ectoing.User) to select data from and use a convenience variable (u) to reference it. The select clause is then used to select the columns u.firstname and u.surname as a list, making the resulting value a list of two element lists:

[["Thomas", "Punt"], ["Liam", "Mann"], ["John", "Doe"], ["Jane", "Doe"],
 ["Bruno", "Škvorc"]]

The select clause enables us to pattern match inside of it, so whilst square brackets were used above to return the columns as a list, we could quite easily have returned a list of tuples or a list of maps instead:

query = from u in Ectoing.User,
  select: {u.firstname, u.surname}

Ectoing.Repo.all query

# result
[{"Thomas", "Punt"}, {"Liam", "Mann"}, {"John", "Doe"}, {"Jane", "Doe"},
 {"Bruno", "Škvorc"}]

query = from u in Ectoing.User,
  select: %{firstname: u.firstname, surname: u.surname}

Ectoing.Repo.all query

# result
[%{firstname: "Thomas", surname: "Punt"}, %{firstname: "Liam", surname: "Mann"},
 %{firstname: "John", surname: "Doe"}, %{firstname: "Jane", surname: "Doe"},
 %{firstname: "Bruno", surname: "Škvorc"}]

Querying API Styles

So far, we’ve been using the keywords query syntax for our queries. This is generally the most prevalent syntax, but occasionally you’ll see Ecto’s alternative querying API syntax: the macro syntax. Let’s take a look at this alternative syntax by translating the above query to select the first names and surnames of all users:

query = (Ectoing.User                   
|> select([u], [u.firstname, u.surname]))

Ectoing.Repo.all query

(The parentheses encapsulating the whole query aren’t needed, but have been included because they enable for the code to be easily copied and pasted directly into IEx.)

This time, we pass the model as the first argument to select/3, where the second argument specifies the binding variable for the model (in this case, it’s u). The third argument selects the columns, which can (again) be pattern matched to return tuples or maps instead of lists.

Just so that we can get used to both syntax styles, I’ll demonstrate both from now on alongside the SQL code for each query.

Restrictive Querying and Customizing Result Sets

Almost always, we will want to select only a subset of records from the database. This can be done by a range of functions that closely resemble their clause counterparts in SQL. These include where (in combination with the comparison operators in and like (there is an ilike, though it has been deprecated in Ecto 2.0)), limit, offset, and distinct.

For example, we can select all users with surnames equal to “doe” with the following:

SELECT * FROM users WHERE surname = "doe";
surname = "doe"

# Keywords query syntax
query = from u in Ectoing.User,
  where: u.surname == ^surname

# Macro syntax
query = (Ectoing.User
|> where([u], u.surname == ^surname))

Ectoing.Repo.all query

The above comparison is case-insensitive and returns back both of the John Doe and Jane Doe users. Notice the usage of the pin operator on the surname variable: this is to (explicitly) interpolate variables into the query. Such interpolated variables have their values automatically casted to the underlying column type defined in the model’s schema definition.

Let’s try something a little more complex by selecting all distinct surnames, ordering them, and then limiting the result set:

SELECT DISTINCT surname FROM users LIMIT 3 ORDER BY surname;
# Keywords query syntax
query = from u in Ectoing.User,
  select: u.surname,
  distinct: true,
  limit: 3,
  order_by: u.surname

# Macro syntax
query = (Ectoing.User
|> select([u], u.surname)
|> distinct(true)
|> limit(3)
|> order_by([u], u.surname))

Ectoing.Repo.all query
# ["Doe", "Mann", "Punt"]

The distinct/3 function above will select distinct values according to the column(s) specified in the select/3 function. distinct/3 also allows for columns to be directly passed to it (i.e. distinct: u.surname), enabling for certain columns to be selected by distinctly, and then for alternative columns to be returned (via select/3). Since MySQL does not support the DISTINCT ON syntax, however, we are unable to do this when using the MySQL adapter (the Postgres adapter allows this, for example). The final order_by/3 then orders the result set (in ascending order, by default) according to the passed columns.

For a complete list of operators and functions supported in Ecto’s querying API, check out its documentation. Also, for a complete list of literals that can be used in Ecto queries, see its query expressions documentation.

Aggregation Queries

Ecto provides us with a number of aggregation functions that we’d typically find in SQL, including: group_by, having, count, avg, sum, min, and max.

Let’s try a couple of these out by selecting users who have an average rating of 4 or greater by their friends:

SELECT friend_id, avg(friend_rating) AS avg_rating
FROM friends
GROUP BY friend_id
    HAVING avg_rating >= 4
ORDER BY avg_rating DESC;
# Keywords query syntax
query = from f in Ectoing.Friend,
  select: %{friend_id: f.friend_id, avg_rating: avg(f.friend_rating)},
  group_by: f.friend_id,
  having: avg(f.friend_rating) >= 4,
  order_by: [desc: avg(f.friend_rating)]

# Macro syntax
query = (Ectoing.Friend
|> select([f], %{friend_id: f.friend_id, avg_rating: avg(f.friend_rating)})
|> group_by([f], f.friend_id)
|> having([f], avg(f.friend_rating) >= 4)
|> order_by([f], [desc: avg(f.friend_rating)]))

Ectoing.Repo.all query
# [%{avg_rating: #Decimal<4.0000>, friend_id: 3},
#  %{avg_rating: #Decimal<4.0000>, friend_id: 5}]

It’s a little less elegant from Ecto’s perspective, since we can’t alias the average rating column — requiring us to use the same column calculation three times in the query. But as with the previous examples, Ecto’s querying DSL maps very closely to raw SQL, which makes it quite easy to use.

Conclusion

We’ve covered the absolute fundamentals of Ecto’s querying DSL in this article, showing how closely it maps to raw SQL. In the next article, Elixir’s Ecto Querying DSL: Beyond the Basics, we cover some more complex topics, including joins, composition queries, SQL fragment injection, loading associations, and query prefixing.

Frequently Asked Questions (FAQs) about Elixir’s Ecto Querying DSL

What is the basic structure of an Ecto query in Elixir?

An Ecto query in Elixir is structured in a way that is similar to SQL. It starts with the from keyword, followed by the variable and the schema it represents. The select keyword is used to specify the fields to be returned. The where keyword is used to filter the results based on certain conditions. Here’s a basic example:

query = from p in Post,
select: p.title,
where: p.published == true

How can I use the distinct function in Ecto queries?

The distinct function in Ecto queries is used to return unique results. It is used in conjunction with the from keyword. Here’s an example:

query = from p in Post,
distinct: p.author_id
This query will return posts with unique author IDs.

How can I use joins in Ecto queries?

Joins in Ecto queries are used to combine rows from two or more tables based on a related column. The join keyword is used followed by the variable, the schema it represents, and the condition for the join. Here’s an example:

query = from p in Post,
join: c in Comment, on: c.post_id == p.id
This query will return all posts and their associated comments.

How can I use the order_by function in Ecto queries?

The order_by function in Ecto queries is used to sort the results based on a certain field. It is used in conjunction with the from keyword. Here’s an example:

query = from p in Post,
order_by: [desc: p.published_at]
This query will return posts ordered by their published date in descending order.

How can I use the limit and offset functions in Ecto queries?

The limit and offset functions in Ecto queries are used to limit the number of results returned and to specify the starting point for the results. Here’s an example:

query = from p in Post,
limit: 5,
offset: 10
This query will return 5 posts starting from the 11th post.

How can I use the group_by function in Ecto queries?

The group_by function in Ecto queries is used to group the results based on a certain field. It is used in conjunction with the from keyword. Here’s an example:

query = from p in Post,
group_by: p.author_id
This query will return posts grouped by their author IDs.

How can I use the having function in Ecto queries?

The having function in Ecto queries is used to filter the results after they have been grouped. It is used in conjunction with the group_by keyword. Here’s an example:

query = from p in Post,
group_by: p.author_id,
having: count(p.id) > 5
This query will return authors who have written more than 5 posts.

How can I use the preload function in Ecto queries?

The preload function in Ecto queries is used to load associated data. It is used in conjunction with the from keyword. Here’s an example:

query = from p in Post,
preload: :comments
This query will return posts and their associated comments.

How can I use the update function in Ecto queries?

The update function in Ecto queries is used to update existing records. It is used in conjunction with the from keyword. Here’s an example:

query = from p in Post,
where: p.id == 1,
update: set: [title: "New Title"]
This query will update the title of the post with ID 1.

How can I use the delete function in Ecto queries?

The delete function in Ecto queries is used to delete existing records. It is used in conjunction with the from keyword. Here’s an example:

query = from p in Post,
where: p.id == 1,
delete
This query will delete the post with ID 1.

Thomas PuntThomas Punt
View Author

Thomas is a recently graduated Web Technologies student from the UK. He has a vehement interest in programming, with particular focus on server-side web development technologies (specifically PHP and Elixir). He contributes to PHP and other open source projects in his free time, as well as writing about topics he finds interesting.

databasesEctoelixirRalphM
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week