ecto-query-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEcto 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 for posts) allow for more complex conditions and
selections. The function selects only specific fields from the schema.
pstruct/2elixir
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)显式绑定(如用代表posts)支持更复杂的条件和字段选择。函数用于从Schema中仅选择特定字段。
pstruct/2Dynamic 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.nameelixir
query = from u in MyApp.User,
where: u.age > 0,
select: u.nameMultiple 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
undefinedelixir
undefinedCreate 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]
endelixir
def most_recent_from(query, minimum_date) do
from p in query,
where: p.published_at > ^minimum_date,
order_by: [desc: p.published_at]
endUsage
使用方式
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: pUse the keyword for alternative conditions. For more complex OR logic,
consider using .
orEcto.Query.dynamic/2elixir
from p in MyApp.Post,
where: p.category == "elixir" or p.category == "phoenix",
select: p使用关键字设置备选条件。对于更复杂的OR逻辑,考虑使用。
orEcto.Query.dynamic/2IN 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 operator checks if a field value exists in a list of values. Use the
pin operator to interpolate the list variable.
inelixir
categories = ["elixir", "phoenix", "ecto"]
query = from p in MyApp.Post,
where: p.category in ^categories,
select: p
MyApp.Repo.all(query)inLike and ILike for Pattern Matching
用于模式匹配的Like和ILike
elixir
search_term = "%elixir%"
query = from p in MyApp.Post,
where: like(p.title, ^search_term),
select: pelixir
search_term = "%elixir%"
query = from p in MyApp.Post,
where: like(p.title, ^search_term),
select: pCase-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
undefinedelixir
undefinedSelect 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
undefinedelixir
undefinedCount 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
undefinedelixir
undefinedSingle 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
undefinedelixir
undefinedSimple 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
clause specifies the join condition.
onelixir
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)内连接仅返回在两个表中都有匹配记录的结果。子句指定连接条件。
onJoin 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 helper uses the association definition from your schema, making
joins more maintainable and less error-prone than manually specifying foreign keys.
assoc/2elixir
query = from p in MyApp.Post,
join: c in assoc(p, :comments),
select: {p, c}
MyApp.Repo.all(query)assoc/2Left 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
undefinedelixir
undefinedPreload 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
undefinedelixir
undefinedDefine 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
undefinedelixir
undefinedUse 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
endelixir
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
endUsage
使用方式
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
endThe macro builds query fragments that can be composed at runtime.
This is more flexible than string-based query building and prevents SQL injection.
dynamic/2elixir
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
enddynamic/2Distinct Queries
去重查询
elixir
undefinedelixir
undefinedDistinct 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
undefinedelixir
undefinedPessimistic 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)
endLateral 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注入
^ - 将查询组合成小型、可复用的函数
- 使用避免关联数据的N+1查询问题
preload - 仅选择你需要的字段,减少数据传输
- 使用和
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 to avoid N+1 query problems with associations
preload - Select only the fields you need to reduce data transfer
- Add when using
order_byandlimitfor consistent paginationoffset - Use helper instead of manual foreign key joins
assoc/2 - Leverage for complex conditional queries
Ecto.Query.dynamic/2 - 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 in loops instead of batch operations
Repo.all/1 - Building queries with string concatenation (SQL injection risk)
- Not adding when using pagination
order_by - 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 in loops instead of batch preloading
Repo.preload/2 - 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
—