# Build an Ecto select a field at a time

An Ecto query has one `select`, which can make optional query helpers awkward. `select_merge/3` lets each helper add fields to an existing map select.

```elixir
defmodule MyApp.Blog do
  import Ecto.Query

  alias MyApp.Blog.Post

  def posts_query do
    from p in Post,
      as: :post,
      where: p.published,
      order_by: [desc: p.inserted_at],
      select: %{post: p}
  end

  def with_comment_count(query) do
    from [post: p] in query,
      left_join: c in assoc(p, :comments),
      group_by: p.id,
      select_merge: %{comment_count: count(c.id)}
  end

  def with_author_name(query) do
    from [post: p] in query,
      join: a in assoc(p, :author),
      select_merge: %{author_name: a.name}
  end
end
```

Each helper adds the field it owns. They can then be combined at the call site:

```elixir
posts_query()
|> with_comment_count()
|> Repo.all()

posts_query()
|> with_comment_count()
|> with_author_name()
|> Repo.all()
```

The values passed to `select_merge` must be maps. Starting with `%{post: p}` above leaves room for computed fields while keeping the complete `%Post{}` under the `:post` key.

If a query has no explicit select, Ecto selects the source struct. A merge into that struct may only set fields declared on the schema, including virtual fields. Use a map when you need arbitrary keys:

```elixir
# Raises if Post does not declare :comment_count.
from p in Post, select: p, select_merge: %{comment_count: 7}

# A map select accepts the additional key.
from p in Post, select: %{id: p.id, title: p.title}, select_merge: %{comment_count: 7}
```

Do not use `select_merge` to set association fields; preloading the association will overwrite them. Ecto also handles a struct from an outer join specially: when the joined row is missing, the selected value remains `nil` rather than becoming a struct full of `nil` fields.

[Ecto.Query.select_merge/3 docs](https://hexdocs.pm/ecto/Ecto.Query.html#select_merge/3)


---

Created by: almirsarajcic
Date: August 16, 2026
URL: https://elixirdrops.net/d/bQ8c2V6U
