ecto-query-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Ecto Query Patterns

Ecto Query 模式

Master Ecto's powerful Query DSL to build efficient, composable database queries. This skill covers the query syntax, filtering, joining, aggregation, preloading associations, and advanced query composition patterns.
掌握Ecto强大的Query DSL,构建高效、可组合的数据库查询。本技能涵盖查询语法、过滤、连接、聚合、预加载关联以及高级查询组合模式。

Basic Query with from Macro

基于from宏的基础查询

elixir
import Ecto.Query, only: [from: 2]
elixir
import Ecto.Query, only: [from: 2]

Basic query using keyword syntax

使用关键字语法的基础查询

query = from u in "users", where: u.age > 18, select: u.name
query = from u in "users", where: u.age > 18, select: u.name

Execute the query

执行查询

MyApp.Repo.all(query)

Queries are built using the `from/2` macro and only sent to the database when
passed to a `Repo` function like `all/1`, `one/1`, or `get/2`. The keyword syntax
provides a readable way to construct queries.
MyApp.Repo.all(query)

查询使用`from/2`宏构建,只有当传递给`Repo`函数(如`all/1`、`one/1`或`get/2`)时才会发送到数据库。关键字语法提供了一种可读性强的查询构建方式。

Query with Schema Module

结合Schema模块的查询

elixir
query = from u in MyApp.User,
          where: u.age > 18,
          select: u.name

MyApp.Repo.all(query)
Using a schema module instead of a table name string provides better type safety and allows Ecto to use the schema's field definitions for validation and casting.
elixir
query = from u in MyApp.User,
          where: u.age > 18,
          select: u.name

MyApp.Repo.all(query)
使用Schema模块而非表名字符串能提供更好的类型安全性,并且允许Ecto使用Schema的字段定义进行验证和类型转换。

Bindingless Query Construction

无绑定查询构建

elixir
from MyApp.Post,
  where: [category: "fresh and new"],
  order_by: [desc: :published_at],
  select: [:id, :title, :body]
Bindingless syntax allows building queries without explicit variable bindings. This works well for simple queries and when using keyword list syntax for conditions.
elixir
from MyApp.Post,
  where: [category: "fresh and new"],
  order_by: [desc: :published_at],
  select: [:id, :title, :body]
无绑定语法允许在没有显式变量绑定的情况下构建查询。这在简单查询和使用关键字列表语法设置条件时非常适用。

Query with Explicit Bindings

带显式绑定的查询

elixir
query = from p in MyApp.Post,
          where: p.category == "fresh and new",
          order_by: [desc: p.published_at],
          select: struct(p, [:id, :title, :body])

MyApp.Repo.all(query)
Explicit bindings (like
p
for posts) allow for more complex conditions and selections. The
struct/2
function selects only specific fields from the schema.
elixir
query = from p in MyApp.Post,
          where: p.category == "fresh and new",
          order_by: [desc: p.published_at],
          select: struct(p, [:id, :title, :body])

MyApp.Repo.all(query)
显式绑定(如用
p
代表posts)支持更复杂的条件和字段选择。
struct/2
函数用于从Schema中仅选择特定字段。

Dynamic Query Variables

动态查询变量

elixir
category = "fresh and new"
order_by = [desc: :published_at]
select_fields = [:id, :title, :body]

query = from MyApp.Post,
          where: [category: ^category],
          order_by: ^order_by,
          select: ^select_fields

MyApp.Repo.all(query)
The pin operator
^
allows interpolating Elixir values into queries. This is essential for parameterized queries and prevents SQL injection.
elixir
category = "fresh and new"
order_by = [desc: :published_at]
select_fields = [:id, :title, :body]

query = from MyApp.Post,
          where: [category: ^category],
          order_by: ^order_by,
          select: ^select_fields

MyApp.Repo.all(query)
针操作符
^
允许将Elixir值插入到查询中。这对于参数化查询至关重要,能防止SQL注入。

Where Clause with Expressions

带表达式的Where子句

elixir
query = from u in MyApp.User,
          where: u.age > 0,
          select: u.name
elixir
query = from u in MyApp.User,
          where: u.age > 0,
          select: u.name

Multiple where clauses are combined with AND

多个where子句会通过AND组合

query = from u in MyApp.User, where: u.age > 18, where: u.confirmed == true, select: u
MyApp.Repo.all(query)

Query expressions support field access, comparison operators, and literals.
Multiple `where` clauses are automatically combined with AND logic.
query = from u in MyApp.User, where: u.age > 18, where: u.confirmed == true, select: u
MyApp.Repo.all(query)

查询表达式支持字段访问、比较运算符和字面量。多个`where`子句会自动通过AND逻辑组合。

Composable Queries

可组合查询

elixir
undefined
elixir
undefined

Create a base query

创建基础查询

query = from u in MyApp.User, where: u.age > 18
query = from u in MyApp.User, where: u.age > 18

Extend the query

扩展查询

query = from u in query, select: u.name
MyApp.Repo.all(query)

Queries are composable - you can build on existing queries by using them in the
`in` clause. This enables powerful query abstraction and reusability.
query = from u in query, select: u.name
MyApp.Repo.all(query)

查询是可组合的——你可以通过在`in`子句中使用现有查询来构建新查询。这实现了强大的查询抽象和复用性。

Query Composition Function Pattern

查询组合函数模式

elixir
def most_recent_from(query, minimum_date) do
  from p in query,
    where: p.published_at > ^minimum_date,
    order_by: [desc: p.published_at]
end
elixir
def most_recent_from(query, minimum_date) do
  from p in query,
    where: p.published_at > ^minimum_date,
    order_by: [desc: p.published_at]
end

Usage

使用方式

MyApp.Post |> most_recent_from(~N[2024-01-01 00:00:00]) |> MyApp.Repo.all()

Extracting query logic into functions creates reusable, testable query components.
This pattern is fundamental to building maintainable query code.
MyApp.Post |> most_recent_from(~N[2024-01-01 00:00:00]) |> MyApp.Repo.all()

将查询逻辑提取到函数中,可创建可复用、可测试的查询组件。这种模式是构建可维护查询代码的基础。

Or Where Conditions

或条件Where子句

elixir
from p in MyApp.Post,
  where: p.category == "elixir" or p.category == "phoenix",
  select: p
Use the
or
keyword for alternative conditions. For more complex OR logic, consider using
Ecto.Query.dynamic/2
.
elixir
from p in MyApp.Post,
  where: p.category == "elixir" or p.category == "phoenix",
  select: p
使用
or
关键字设置备选条件。对于更复杂的OR逻辑,考虑使用
Ecto.Query.dynamic/2

IN Query with List

带列表的IN查询

elixir
categories = ["elixir", "phoenix", "ecto"]

query = from p in MyApp.Post,
          where: p.category in ^categories,
          select: p

MyApp.Repo.all(query)
The
in
operator checks if a field value exists in a list of values. Use the pin operator to interpolate the list variable.
elixir
categories = ["elixir", "phoenix", "ecto"]

query = from p in MyApp.Post,
          where: p.category in ^categories,
          select: p

MyApp.Repo.all(query)
in
运算符用于检查字段值是否存在于值列表中。使用针操作符插入列表变量。

Like and ILike for Pattern Matching

用于模式匹配的Like和ILike

elixir
search_term = "%elixir%"

query = from p in MyApp.Post,
          where: like(p.title, ^search_term),
          select: p
elixir
search_term = "%elixir%"

query = from p in MyApp.Post,
          where: like(p.title, ^search_term),
          select: p

Case-insensitive version

不区分大小写的版本

query = from p in MyApp.Post, where: ilike(p.title, ^search_term), select: p

Use `like/2` for case-sensitive pattern matching and `ilike/2` for case-insensitive
matching. Wildcards `%` match any characters.
query = from p in MyApp.Post, where: ilike(p.title, ^search_term), select: p

使用`like/2`进行区分大小写的模式匹配,使用`ilike/2`进行不区分大小写的匹配。通配符`%`匹配任意字符。

Selecting Specific Fields

选择特定字段

elixir
undefined
elixir
undefined

Select multiple fields

选择多个字段

query = from p in MyApp.Post, select: {p.id, p.title}
MyApp.Repo.all(query) # Returns [{1, "Title 1"}, {2, "Title 2"}]
query = from p in MyApp.Post, select: {p.id, p.title}
MyApp.Repo.all(query) # 返回 [{1, "Title 1"}, {2, "Title 2"}]

Select as map

选择为map

query = from p in MyApp.Post, select: %{id: p.id, title: p.title}
MyApp.Repo.all(query) # Returns [%{id: 1, title: "Title 1"}, ...]
query = from p in MyApp.Post, select: %{id: p.id, title: p.title}
MyApp.Repo.all(query) # 返回 [%{id: 1, title: "Title 1"}, ...]

Select struct with specific fields

选择带特定字段的struct

query = from p in MyApp.Post, select: struct(p, [:id, :title, :body])
MyApp.Repo.all(query) # Returns Post structs with only selected fields loaded

Selecting specific fields instead of entire records improves query performance
by reducing data transfer and memory usage.
query = from p in MyApp.Post, select: struct(p, [:id, :title, :body])
MyApp.Repo.all(query) # 返回仅加载了选定字段的Post struct

选择特定字段而非整个记录,可通过减少数据传输和内存占用提升查询性能。

Aggregation Functions

聚合函数

elixir
undefined
elixir
undefined

Count records

统计记录数

query = from p in MyApp.Post, select: count(p.id)
MyApp.Repo.one(query) # Returns integer count
query = from p in MyApp.Post, select: count(p.id)
MyApp.Repo.one(query) # 返回整数计数

Average

平均值

query = from p in MyApp.Post, select: avg(p.rating)
query = from p in MyApp.Post, select: avg(p.rating)

Sum

求和

query = from o in MyApp.Order, select: sum(o.total)
query = from o in MyApp.Order, select: sum(o.total)

Min and Max

最小值和最大值

query = from p in MyApp.Product, select: {min(p.price), max(p.price)}

Ecto supports standard SQL aggregation functions including `count/1`, `avg/1`,
`sum/1`, `min/1`, and `max/1`.
query = from p in MyApp.Product, select: {min(p.price), max(p.price)}

Ecto支持标准SQL聚合函数,包括`count/1`、`avg/1`、`sum/1`、`min/1`和`max/1`。

Group By and Having

Group By和Having

elixir
query = from p in MyApp.Post,
          group_by: p.category,
          select: {p.category, count(p.id)}

MyApp.Repo.all(query)  # Returns [{"elixir", 10}, {"phoenix", 5}]
elixir
query = from p in MyApp.Post,
          group_by: p.category,
          select: {p.category, count(p.id)}

MyApp.Repo.all(query)  # 返回 [{"elixir", 10}, {"phoenix", 5}]

With having clause

带Having子句

query = from p in MyApp.Post, group_by: p.category, having: count(p.id) > 5, select: {p.category, count(p.id)}

Use `group_by` to group results by field values and `having` to filter groups
based on aggregate values.
query = from p in MyApp.Post, group_by: p.category, having: count(p.id) > 5, select: {p.category, count(p.id)}

使用`group_by`按字段值对结果分组,使用`having`基于聚合值过滤分组。

Order By

Order By

elixir
undefined
elixir
undefined

Single field ascending

单字段升序

query = from p in MyApp.Post, order_by: p.published_at
query = from p in MyApp.Post, order_by: p.published_at

Single field descending

单字段降序

query = from p in MyApp.Post, order_by: [desc: p.published_at]
query = from p in MyApp.Post, order_by: [desc: p.published_at]

Multiple fields

多字段

query = from p in MyApp.Post, order_by: [desc: p.published_at, asc: p.title]
query = from p in MyApp.Post, order_by: [desc: p.published_at, asc: p.title]

With nulls positioning

空值定位

query = from p in MyApp.Post, order_by: [desc_nulls_last: p.published_at]

The `order_by` option controls result ordering. You can specify ascending or
descending order, multiple fields, and null positioning.
query = from p in MyApp.Post, order_by: [desc_nulls_last: p.published_at]

`order_by`选项控制结果排序。你可以指定升序或降序、多个字段以及空值的位置。

Limit and Offset for Pagination

用于分页的Limit和Offset

elixir
undefined
elixir
undefined

Simple limit

简单限制

query = from p in MyApp.Post, limit: 10
query = from p in MyApp.Post, limit: 10

With offset for pagination

带Offset的分页

page = 2 per_page = 10
query = from p in MyApp.Post, order_by: [desc: p.published_at], limit: ^per_page, offset: ^((page - 1) * per_page)
MyApp.Repo.all(query)

Use `limit` and `offset` for pagination. Always include an `order_by` clause
to ensure consistent pagination results.
page = 2 per_page = 10
query = from p in MyApp.Post, order_by: [desc: p.published_at], limit: ^per_page, offset: ^((page - 1) * per_page)
MyApp.Repo.all(query)

使用`limit`和`offset`实现分页。为了确保分页结果的一致性,务必包含`order_by`子句。

Inner Join

内连接

elixir
query = from p in MyApp.Post,
          join: c in MyApp.Comment,
          on: c.post_id == p.id,
          select: {p.title, c.body}

MyApp.Repo.all(query)
Inner joins return only records that have matching records in both tables. The
on
clause specifies the join condition.
elixir
query = from p in MyApp.Post,
          join: c in MyApp.Comment,
          on: c.post_id == p.id,
          select: {p.title, c.body}

MyApp.Repo.all(query)
内连接仅返回在两个表中都有匹配记录的结果。
on
子句指定连接条件。

Join with assoc Helper

使用assoc助手进行连接

elixir
query = from p in MyApp.Post,
          join: c in assoc(p, :comments),
          select: {p, c}

MyApp.Repo.all(query)
The
assoc/2
helper uses the association definition from your schema, making joins more maintainable and less error-prone than manually specifying foreign keys.
elixir
query = from p in MyApp.Post,
          join: c in assoc(p, :comments),
          select: {p, c}

MyApp.Repo.all(query)
assoc/2
助手使用Schema中的关联定义,比手动指定外键连接更易于维护且不易出错。

Left Join

左连接

elixir
query = from p in MyApp.Post,
          left_join: c in assoc(p, :comments),
          select: {p, c}

MyApp.Repo.all(query)
Left joins return all records from the left table (posts) even if there are no matching records in the right table (comments). Unmatched fields are nil.
elixir
query = from p in MyApp.Post,
          left_join: c in assoc(p, :comments),
          select: {p, c}

MyApp.Repo.all(query)
左连接返回左表(posts)的所有记录,即使右表(comments)中没有匹配记录。不匹配的字段值为nil。

Preload Associations

预加载关联

elixir
undefined
elixir
undefined

Preload in separate query

在单独查询中预加载

MyApp.Repo.all(from p in MyApp.Post, preload: [:comments])
MyApp.Repo.all(from p in MyApp.Post, preload: [:comments])

Preload multiple associations

预加载多个关联

MyApp.Repo.all(from p in MyApp.Post, preload: [:comments, :author])
MyApp.Repo.all(from p in MyApp.Post, preload: [:comments, :author])

Nested preload

嵌套预加载

MyApp.Repo.all(from p in MyApp.Post, preload: [:author, comments: :likes])

Preloading fetches associated data efficiently, preventing N+1 query problems.
Separate query preloading is simpler but may require more database round trips.
MyApp.Repo.all(from p in MyApp.Post, preload: [:author, comments: :likes])

预加载能高效获取关联数据,避免N+1查询问题。单独查询预加载更简单,但可能需要更多的数据库往返次数。

Preload with Join

结合连接的预加载

elixir
query = from p in MyApp.Post,
          join: c in assoc(p, :comments),
          where: c.published_at > p.updated_at,
          preload: [comments: c]

MyApp.Repo.all(query)
When you join an association and want to filter it, you can preload the joined data using the binding variable. This creates a single, more efficient query.
elixir
query = from p in MyApp.Post,
          join: c in assoc(p, :comments),
          where: c.published_at > p.updated_at,
          preload: [comments: c]

MyApp.Repo.all(query)
当你连接一个关联并想要过滤它时,可以使用绑定变量预加载连接的数据。这会创建一个更高效的单查询。

Complex Nested Preload with Joins

带连接的复杂嵌套预加载

elixir
query = from p in MyApp.Post,
          join: c in assoc(p, :comments),
          join: l in assoc(c, :likes),
          where: l.inserted_at > c.updated_at,
          preload: [:author, comments: {c, likes: l}]

MyApp.Repo.all(query)
You can preload multiple levels of nested associations while maintaining join filters. The nested tuple syntax preserves the join bindings.
elixir
query = from p in MyApp.Post,
          join: c in assoc(p, :comments),
          join: l in assoc(c, :likes),
          where: l.inserted_at > c.updated_at,
          preload: [:author, comments: {c, likes: l}]

MyApp.Repo.all(query)
你可以预加载多层嵌套关联,同时保留连接过滤。嵌套元组语法用于保留连接绑定。

Preload After Query

查询后预加载

elixir
posts = MyApp.Repo.all(MyApp.Post)
posts_with_comments = MyApp.Repo.preload(posts, :comments)
elixir
posts = MyApp.Repo.all(MyApp.Post)
posts_with_comments = MyApp.Repo.preload(posts, :comments)

Preload with custom query

使用自定义查询预加载

comments_query = from c in MyApp.Comment, order_by: [desc: c.inserted_at] posts_with_recent_comments = MyApp.Repo.preload(posts, comments: comments_query)

The `preload/2` function can preload associations after fetching records. You
can also customize the preload query for fine-grained control.
comments_query = from c in MyApp.Comment, order_by: [desc: c.inserted_at] posts_with_recent_comments = MyApp.Repo.preload(posts, comments: comments_query)

`preload/2`函数可以在获取记录后预加载关联。你还可以自定义预加载查询以实现细粒度控制。

Subquery

子查询

elixir
undefined
elixir
undefined

Define subquery

定义子查询

subquery = from p in MyApp.Post, where: p.published == true, select: %{category: p.category, count: count(p.id)}, group_by: p.category
subquery = from p in MyApp.Post, where: p.published == true, select: %{category: p.category, count: count(p.id)}, group_by: p.category

Use subquery

使用子查询

query = from s in subquery(subquery), where: s.count > 10, select: s.category
MyApp.Repo.all(query)

Subqueries allow using the result of one query as input to another, enabling
complex analytical queries.
query = from s in subquery(subquery), where: s.count > 10, select: s.category
MyApp.Repo.all(query)

子查询允许将一个查询的结果作为另一个查询的输入,实现复杂的分析查询。

Fragment for Raw SQL

用于原生SQL的Fragment

elixir
undefined
elixir
undefined

Use SQL fragment

使用SQL片段

query = from p in MyApp.Post, where: fragment("lower(?)", p.title) == "elixir", select: p
query = from p in MyApp.Post, where: fragment("lower(?)", p.title) == "elixir", select: p

Fragment with parameters

带参数的Fragment

search = "elixir" query = from p in MyApp.Post, where: fragment("lower(?) LIKE ?", p.title, ^"%#{search}%"), select: p

The `fragment/1` function allows embedding raw SQL in queries when Ecto's DSL
doesn't support a specific database feature. Use sparingly as it reduces portability.
search = "elixir" query = from p in MyApp.Post, where: fragment("lower(?) LIKE ?", p.title, ^"%#{search}%"), select: p

`fragment/1`函数允许在查询中嵌入原生SQL,当Ecto的DSL不支持特定数据库特性时使用。请谨慎使用,因为这会降低查询的可移植性。

Query Hints

查询提示

elixir
query = from p in MyApp.Post,
          hints: ["USE INDEX FOO"],
          where: p.title == "title"
elixir
query = from p in MyApp.Post,
          hints: ["USE INDEX FOO"],
          where: p.title == "title"

Multiple hints

多个提示

query = from p in MyApp.Post, hints: "TABLESAMPLE SYSTEM(1)"
query = from p in MyApp.Post, hints: "TABLESAMPLE SYSTEM(1)"

Dynamic hints

动态提示

sample = "SYSTEM_ROWS(1)" query = from p in MyApp.Post, hints: ["TABLESAMPLE", unsafe_fragment(^sample)]

Query hints provide database-specific optimization instructions like index usage
or table sampling. Hints are database-specific and may not be portable.
sample = "SYSTEM_ROWS(1)" query = from p in MyApp.Post, hints: ["TABLESAMPLE", unsafe_fragment(^sample)]

查询提示提供数据库特定的优化指令,如索引使用或表采样。提示是数据库特定的,可能不具备可移植性。

Dynamic Query Building

动态查询构建

elixir
defmodule MyApp.PostQueries do
  import Ecto.Query

  def filter(query \\ MyApp.Post, filters) do
    query
    |> filter_by_category(filters[:category])
    |> filter_by_published(filters[:published])
    |> filter_by_search(filters[:search])
  end

  defp filter_by_category(query, nil), do: query
  defp filter_by_category(query, category) do
    from p in query, where: p.category == ^category
  end

  defp filter_by_published(query, nil), do: query
  defp filter_by_published(query, published) do
    from p in query, where: p.published == ^published
  end

  defp filter_by_search(query, nil), do: query
  defp filter_by_search(query, search) do
    from p in query, where: ilike(p.title, ^"%#{search}%")
  end
end
elixir
defmodule MyApp.PostQueries do
  import Ecto.Query

  def filter(query \\ MyApp.Post, filters) do
    query
    |> filter_by_category(filters[:category])
    |> filter_by_published(filters[:published])
    |> filter_by_search(filters[:search])
  end

  defp filter_by_category(query, nil), do: query
  defp filter_by_category(query, category) do
    from p in query, where: p.category == ^category
  end

  defp filter_by_published(query, nil), do: query
  defp filter_by_published(query, published) do
    from p in query, where: p.published == ^published
  end

  defp filter_by_search(query, nil), do: query
  defp filter_by_search(query, search) do
    from p in query, where: ilike(p.title, ^"%#{search}%")
  end
end

Usage

使用方式

filters = %{category: "elixir", published: true, search: "ecto"} MyApp.PostQueries.filter(filters) |> MyApp.Repo.all()

Building queries dynamically allows handling optional filters and complex search
criteria. Pattern matching on nil values keeps the code clean and readable.
filters = %{category: "elixir", published: true, search: "ecto"} MyApp.PostQueries.filter(filters) |> MyApp.Repo.all()

动态构建查询允许处理可选过滤条件和复杂搜索标准。对nil值进行模式匹配使代码保持简洁可读。

Ecto.Query.dynamic for Complex Conditions

用于复杂条件的Ecto.Query.dynamic

elixir
defmodule MyApp.PostQueries do
  import Ecto.Query

  def search(filters) do
    MyApp.Post
    |> where(^build_where_clause(filters))
    |> MyApp.Repo.all()
  end

  defp build_where_clause(filters) do
    Enum.reduce(filters, dynamic(true), fn
      {:category, value}, dynamic ->
        dynamic([p], ^dynamic and p.category == ^value)

      {:published, value}, dynamic ->
        dynamic([p], ^dynamic and p.published == ^value)

      {:min_rating, value}, dynamic ->
        dynamic([p], ^dynamic and p.rating >= ^value)

      _, dynamic ->
        dynamic
    end)
  end
end
The
dynamic/2
macro builds query fragments that can be composed at runtime. This is more flexible than string-based query building and prevents SQL injection.
elixir
defmodule MyApp.PostQueries do
  import Ecto.Query

  def search(filters) do
    MyApp.Post
    |> where(^build_where_clause(filters))
    |> MyApp.Repo.all()
  end

  defp build_where_clause(filters) do
    Enum.reduce(filters, dynamic(true), fn
      {:category, value}, dynamic ->
        dynamic([p], ^dynamic and p.category == ^value)

      {:published, value}, dynamic ->
        dynamic([p], ^dynamic and p.published == ^value)

      {:min_rating, value}, dynamic ->
        dynamic([p], ^dynamic and p.rating >= ^value)

      _, dynamic ->
        dynamic
    end)
  end
end
dynamic/2
宏用于构建可在运行时组合的查询片段。这比基于字符串的查询构建更灵活,且能防止SQL注入。

Distinct Queries

去重查询

elixir
undefined
elixir
undefined

Distinct on all selected fields

对所有选定字段去重

query = from p in MyApp.Post, distinct: true, select: p.category
query = from p in MyApp.Post, distinct: true, select: p.category

Distinct on specific fields

对特定字段去重

query = from p in MyApp.Post, distinct: [desc: p.published_at], select: p

The `distinct` option removes duplicate rows from results. You can specify which
fields to use for determining uniqueness.
query = from p in MyApp.Post, distinct: [desc: p.published_at], select: p

`distinct`选项用于从结果中移除重复行。你可以指定用于判断唯一性的字段。

Union Queries

联合查询

elixir
posts_query = from p in MyApp.Post,
                where: p.published == true,
                select: %{type: "post", title: p.title}

pages_query = from p in MyApp.Page,
                where: p.active == true,
                select: %{type: "page", title: p.title}
elixir
posts_query = from p in MyApp.Post,
                where: p.published == true,
                select: %{type: "post", title: p.title}

pages_query = from p in MyApp.Page,
                where: p.active == true,
                select: %{type: "page", title: p.title}

Union

联合(去重)

query = posts_query |> union(^pages_query) MyApp.Repo.all(query)
query = posts_query |> union(^pages_query) MyApp.Repo.all(query)

Union all (includes duplicates)

联合(保留重复)

query = posts_query |> union_all(^pages_query)

Union combines results from multiple queries. Use `union/2` to remove duplicates
or `union_all/2` to keep them.
query = posts_query |> union_all(^pages_query)

联合查询合并多个查询的结果。使用`union/2`移除重复项,使用`union_all/2`保留重复项。

Locking for Concurrency Control

用于并发控制的锁定

elixir
undefined
elixir
undefined

Pessimistic locking

悲观锁

query = from p in MyApp.Post, where: p.id == ^post_id, lock: "FOR UPDATE"
post = MyApp.Repo.one(query)
query = from p in MyApp.Post, where: p.id == ^post_id, lock: "FOR UPDATE"
post = MyApp.Repo.one(query)

Optimistic locking (using version field in schema)

乐观锁(使用Schema中的版本字段)

changeset = MyApp.Post.changeset(post, params) case MyApp.Repo.update(changeset) do {:ok, updated_post} -> # Success {:error, changeset} -> # Failed, possibly due to concurrent update end

Locking prevents race conditions in concurrent operations. Pessimistic locking
uses database locks, while optimistic locking uses version fields.
changeset = MyApp.Post.changeset(post, params) case MyApp.Repo.update(changeset) do {:ok, updated_post} -> # 成功 {:error, changeset} -> # 失败,可能是由于并发更新 end

锁定用于防止并发操作中的竞态条件。悲观锁使用数据库锁,而乐观锁使用版本字段。

Lateral Joins for Correlated Subqueries

用于关联子查询的横向连接

elixir
defp newest_records(parent_ids, assoc, n) do
  %{related_key: related_key, queryable: queryable} = assoc

  squery = from q in queryable,
             where: field(q, ^related_key) == parent_as(:parent_ids).id,
             order_by: {:desc, :created_at},
             limit: ^n

  query = from f in fragment("SELECT id from UNNEST(?::int[]) AS id", ^parent_ids),
             as: :parent_ids,
             inner_lateral_join: s in subquery(squery),
             on: true,
             select: s

  MyApp.Repo.all(query)
end
Lateral joins allow subqueries that reference columns from the outer query, enabling complex correlated queries like "top N per group."
elixir
defp newest_records(parent_ids, assoc, n) do
  %{related_key: related_key, queryable: queryable} = assoc

  squery = from q in queryable,
             where: field(q, ^related_key) == parent_as(:parent_ids).id,
             order_by: {:desc, :created_at},
             limit: ^n

  query = from f in fragment("SELECT id from UNNEST(?::int[]) AS id", ^parent_ids),
             as: :parent_ids,
             inner_lateral_join: s in subquery(squery),
             on: true,
             select: s

  MyApp.Repo.all(query)
end
横向连接允许子查询引用外部查询的列,实现复杂的关联查询,如“每组前N条记录”。

Named Bindings

何时使用本技能

elixir
query = from p in MyApp.Post, as: :posts
query = from [posts: p] in query,
          join: c in assoc(p, :comments), as: :comments
query = from [posts: p, comments: c] in query,
          where: c.score > 10,
          select: {p.title, c.body}
Named bindings make complex queries more readable by giving explicit names to each table or subquery in the query.
当你需要以下操作时,使用ecto-query-patterns:
  • 通过过滤、排序和分页查询数据库记录
  • 连接多个表以获取关联数据
  • 预加载关联以避免N+1查询问题
  • 使用计数、求和、平均值等函数聚合数据
  • 根据用户输入或应用逻辑构建动态查询
  • 使用子查询和分组执行复杂分析查询
  • 使用提示和索引优化查询性能
  • 使用锁定机制处理并发更新
  • 通过组合创建可复用的查询组件
  • 使用模式匹配实现搜索功能

When to Use This Skill

最佳实践

Use ecto-query-patterns when you need to:
  • Query database records with filtering, sorting, and pagination
  • Join multiple tables to fetch related data
  • Preload associations to avoid N+1 query problems
  • Aggregate data using count, sum, average, or other functions
  • Build dynamic queries based on user input or application logic
  • Perform complex analytical queries with subqueries and grouping
  • Optimize query performance with hints and indexes
  • Handle concurrent updates with locking mechanisms
  • Create reusable query components through composition
  • Implement search functionality with pattern matching
  • 始终对外部值使用针操作符
    ^
    ,防止SQL注入
  • 将查询组合成小型、可复用的函数
  • 使用
    preload
    避免关联数据的N+1查询问题
  • 仅选择你需要的字段,减少数据传输
  • 使用
    limit
    offset
    进行分页时,务必添加
    order_by
    以确保一致性
  • 使用
    assoc/2
    助手而非手动外键连接
  • 利用
    Ecto.Query.dynamic/2
    处理复杂条件查询
  • 将查询逻辑放在专用的查询模块中,而非控制器
  • 使用子查询处理复杂聚合和分析查询
  • 在开发环境中分析查询,识别性能问题
  • 为频繁查询的字段添加数据库索引
  • 过滤关联数据时,优先使用带连接的预加载
  • 对复杂多连接查询使用命名绑定
  • 独立于业务逻辑测试查询函数
  • 为复杂查询添加注释,解释其逻辑

Best Practices

常见陷阱

  • Always use the pin operator
    ^
    for external values to prevent SQL injection
  • Compose queries into small, reusable functions
  • Use
    preload
    to avoid N+1 query problems with associations
  • Select only the fields you need to reduce data transfer
  • Add
    order_by
    when using
    limit
    and
    offset
    for consistent pagination
  • Use
    assoc/2
    helper instead of manual foreign key joins
  • Leverage
    Ecto.Query.dynamic/2
    for complex conditional queries
  • Keep query logic in dedicated query modules, not controllers
  • Use subqueries for complex aggregations and analytical queries
  • Profile queries in development to identify performance issues
  • Use database indexes for frequently queried fields
  • Prefer preloading with joins when filtering associated data
  • Use named bindings for complex multi-join queries
  • Test query functions independently from your business logic
  • Document complex queries with comments explaining the logic
  • 忘记使用针操作符
    ^
    ,导致编译错误
  • 未预加载关联数据,导致N+1查询问题
  • 仅需要少数字段时却选择整个struct
  • 在循环中使用
    Repo.all/1
    而非批量操作
  • 使用字符串拼接构建查询(存在SQL注入风险)
  • 分页时未添加
    order_by
  • 未过滤就连接表,导致笛卡尔积
  • 过度使用fragment,降低查询可移植性
  • 动态查询构建中未处理nil值
  • 在应用代码中执行聚合而非数据库端
  • 必要时未将更新包装在事务中
  • 在循环中使用
    Repo.preload/2
    而非批量预加载
  • 未利用查询组合实现可复用逻辑
  • 将业务逻辑与查询构建混合
  • 未进行分析就过早优化查询
  • 使用distinct却不了解其性能影响
  • 未适时利用数据库特定功能
  • 创建过于复杂、难以维护的查询
  • 开发期间忽略数据库查询日志
  • 未测试空结果或nil值等边缘情况

Common Pitfalls

资源

官方Ecto文档

  • Forgetting the pin operator
    ^
    , causing compilation errors
  • Not preloading associations, leading to N+1 query problems
  • Selecting entire structs when only a few fields are needed
  • Using
    Repo.all/1
    in loops instead of batch operations
  • Building queries with string concatenation (SQL injection risk)
  • Not adding
    order_by
    when using pagination
  • Joining tables without filtering, causing cartesian products
  • Using fragments excessively, reducing query portability
  • Not handling nil values in dynamic query building
  • Performing aggregations in application code instead of database
  • Forgetting to wrap updates in transactions when necessary
  • Using
    Repo.preload/2
    in loops instead of batch preloading
  • Not utilizing query composition for reusable logic
  • Mixing business logic with query construction
  • Over-optimizing queries prematurely without profiling
  • Using distinct without understanding its performance impact
  • Not leveraging database-specific features when appropriate
  • Creating overly complex queries that are hard to maintain
  • Ignoring database query logs during development
  • Not testing edge cases like empty results or nil values

Resources

查询操作符和函数

Official Ecto Documentation

Query Operators and Functions

性能与优化

Performance and Optimization

社区资源

Community Resources