GraphQL 聚合允许通过 API 直接检索汇总的数据,例如计数、总计、平均值等,类似于 SQL GROUP BY 和聚合函数。 与其提取所有记录并在客户端计算汇总,不如要求服务器按某些字段对数据进行分组,并计算相应的聚合值。 这对于生成报表或分析非常有用,例如,在单个查询中获取每个类别的产品数或每个作者帖子的平均评分。
聚合查询返回 分组结果:每个结果表示一组共享特定字段值的记录,以及该组的计算聚合指标。 本文档介绍如何将 GraphQL 聚合功能与虚构的电子商务架构、可以提取的数据类型、示例查询以及要注意的重要限制和行为结合使用。
示例架构:电子商务商店
在此架构中,产品属于类别。 每个 Product
具有一些字段,例如价格和评级(这些数值可以进行聚合),并通过类别字段与 Category
有关联。
Category
有一个名称。 我们将使用此架构来演示聚合查询。
例如,简化的 GraphQL 类型可能如下所示:
type Category {
id: ID!
name: String!
products: [Product!]! # one-to-many relationship
}
type Product {
id: Int!
name: String!
price: Float!
rating: Int!
category_id: Int!
category: Category! # many-to-one relationship
}
type ProductResult { # automatically generated, adding groupBy capabilities
items: [Product!]!
endCursor: String
hasNextPage: Boolean!
groupBy(fields: [ProductScalarFields!]): [ProductGroupBy!]!
}
type Query {
products(
first: Int
after: String
filter: ProductFilterInput
orderBy: ProductOrderByInput
): ProductResult!
在此示例中, products
查询可以返回常规项列表,或者如果使用 groupBy
聚合结果。 让我们专注于使用此查询的groupBy
和聚合功能。
为何使用聚合查询?
通过在 GraphQL 中使用聚合查询,无需手动处理即可快速回答有关数据的问题。 例如,你可能想要提取见解,例如:
- 总计数:例如 “每个类别中的产品数?”
- 总和和平均值:例如 ,“每个类别的总收入是多少?” 或 “按类别划分的产品的平均评级?”
- 最小值/最大值:例如 ,“每个类别中最高和最低价格的项目是多少?
- 非重复值:例如 ,“我们的客户来自多少个独特的城市?” 或 “列出所有博客文章中使用的不同标记”。
聚合查询让服务器来处理数据分析,而不是在应用程序中检索所有记录并计算这些数据洞察。 这样可以减少数据传输,并使用数据库优化进行分组和计算。
聚合查询基础知识
若要执行聚合,请在 GraphQL 查询中指定参数 groupBy
,以定义如何对数据进行分组,并在结果中请求聚合字段(如计数或总和)。 响应包含分组记录的列表,每个记录都有组的键值和聚合指标。
示例 1:对每个类别的产品进行计数
让我们按类别对产品进行分组,并计算每个组中的产品数量。 查询可能如下所示:
query {
products {
groupBy(fields: [category_id])
{
fields {
category_id # grouped key values
}
aggregations {
count(field: id) # count of products in each group (count of "id")
}
}
}
}
在本查询中:
-
groupBy(fields: [category_id])
告知 Fabric GraphQL 引擎按category_id
字段对产品进行分组。 - 在结果选择中,你请求
group
,并对id
字段执行count
聚合。 使用id
能有效统计该组中的产品。
结果如下所示: 响应中的每个项都表示一个类别组。 对象 groupBy
包含分组键。 在这里,它包括 category_id
该值,并提供 count { id }
该类别中的产品数:
{
"data": {
"products": {
"groupBy": [
{
"fields": {
"category_id": 1
},
"aggregations": {
"count": 3
}
},
{
"fields": {
"category_id": 2
},
"aggregations": {
"count": 2
}
},
...
]
}
}
}
此输出告诉我们类别 1 有三个产品,第 2 类有 2 个,等等。
示例 2:求和和平均值
我们可以在一个查询中请求多个聚合指标。 假设我们想要,对于每个类别,所有产品的总价格和平均评级:
query {
products {
groupBy(fields: [category_id])
{
fields {
category_id
}
aggregations {
count(field: id) # number of products in the category
sum(field: price) # sum of all product prices in the category
avg(field: rating) # average rating of products in the category
}
}
}
}
此查询将返回以下结果:
{
"data": {
"products": {
"groupBy": [
{
"fields": {
"category_id": 1
},
"aggregations": {
"count": 3,
"sum": 2058.98,
"avg": 4
}
},
{
"fields": {
"category_id": 2
},
"aggregations": {
"count": 2,
"sum": 109.94,
"avg": 4
}
},
...
]
}
}
}
每个组对象包括类别和计算聚合,例如产品数、价格之和以及该类别中的平均分级。
示例 3:按多个字段分组
可以按多个字段进行分组以获取多级分组。 例如,如果你的产品有一个 rating
字段,则可以按两者 category_id
进行分组, rating
然后计算该组的平均值 price
:
query {
products {
groupBy(fields: [category_id, rating])
{
fields {
category_id
rating
}
aggregations {
avg(field: price)
}
}
}
}
这将按类别 和 分级的唯一组合对产品进行分组,如下所示:
{
"fields": {
"category_id": 10,
"rating": 4
},
"aggregations": {
"avg": 6.99
}
}
依此类推处理数据中的每个类别评分对。
示例 4:使用独特的
聚合功能支持 不同的 修饰符来计算或考虑唯一值。 例如,若要了解产品集合中存在多少个不同类别,可以使用非重复计数:
query {
products {
groupBy(fields: [category_id])
{
fields {
category_id
}
aggregations {
count(field: id, distinct: true)
}
}
}
}
此查询返回一个结果,其中包含每个类别的唯一产品数。 结果如下所示:
{
"data": {
"products": {
"groupBy": [
{
"fields": {
"category_id": 1
},
"aggregations": {
"count": 3
}
},
{
"fields": {
"category_id": 2
},
"aggregations": {
"count": 2
}
},
...
]
}
}
}
示例 5:使用别名
可以为聚合创建别名,以便为聚合结果提供有意义的易于理解的名称。 例如,可以将上一示例中的聚合命名为distinctProductCategoryCount
,因为它通过计数独特的产品类别来更好地理解结果。
query {
products {
groupBy(fields: [category_id])
{
fields {
category_id
}
aggregations {
distinctProductCategoryCount: count(field: id, distinct: true)
}
}
}
}
结果与自定义别名类似,但更有意义:
{
"data": {
"products": {
"groupBy": [
{
"fields": {
"category_id": 1
},
"aggregations": {
"distinctProductCategoryCount": 3
}
},
{
"fields": {
"category_id": 2
},
"aggregations": {
"distinctProductCategoryCount": 2
}
},
...
]
}
}
}
示例 6:使用 having
子句
可以使用 having
子句筛选聚合结果。 例如,可以修改前面的示例,以仅返回大于两个的结果:
query {
products {
groupBy(fields: [category_id])
{
fields {
category_id
}
aggregations {
distinctProductCategoryCount: count(field: id, distinct: true, having: {
gt: 2
})
}
}
}
}
结果返回一个值,其唯一类别包含两个以上的产品:
{
"data": {
"products": {
"groupBy": [
{
"fields": {
"category_id": 1
},
"aggregations": {
"distinctProductCategoryCount": 3
}
}
]
}
}
}
可用的聚合函数
可用的确切功能取决于实现,但常见的聚合操作包括:
- count – 组中的记录计数(或字段的非空值的数量)。
- sum – 数值字段中所有值的和。
- avg – 数值字段中值的平均值(平均值)。
- min – 字段中的最小值。
- max – 字段中的最大值。
在我们的 GraphQL API 中,通常通过指定函数名称和目标字段来请求这些字段,如示例count(field: id)
sum(field: price)
等所示。每个函数返回一个对象,允许你选择应用于的一个或多个字段。 例如,sum(field: price)
显示该组的价格字段总和,而 count(field: id)
显示 ID 的计数,这实际上是项目的计数。
注释
当前聚合操作(例如count
、sum
、avg
、min
和max
)仅适用于数值或定量字段。 例如,整数、浮点数、日期。 不能在文本字段上使用它们。 例如,您无法对字符串计算“平均值”。 计划支持对其他类型(例如文本)的聚合操作(如将来可能的函数,串联或字典序最小/最大值),但目前尚不可用。
限制和最佳做法
在 GraphQL 中使用聚合时,需要考虑一些重要的规则和限制。 这些可确保查询有效且结果可预测,尤其是在分页结果时。
聚合和原始项互斥: 目前,不能 同时检索同一查询中已分组的摘要数据和原始项列表。 集合的
groupBy
聚合查询返回分组数据,而不是普通项列表。 例如,在我们的 API 中,products(...)
查询要么在不使用groupBy
的情况下返回产品列表,要么在使用groupBy
时返回分组结果列表,但不会同时返回这两个列表。 你可能会注意到,在上面的聚合示例中,将显示字段group
和聚合字段,而通常items
的产品列表不存在。 如果尝试在一个查询中请求常规项目和组,GraphQL 引擎会返回错误或不允许进行这种选择。 如果需要原始数据和聚合数据,则必须运行两个单独的查询,或等待将来可能会解除此限制的更新。 此设计旨在保持响应结构明确,因此,查询处于“聚合模式”或“列表项模式”。对分组结果进行排序(
orderBy
与主键): 获取聚合组时,除非指定显式排序顺序,否则不保证返回组的顺序。 强烈建议对聚合查询使用或sort
参数来定义应在结果中对组进行排序的方式,尤其是在分组键本身不唯一orderBy
或没有明显的默认顺序时。 例如,如果按category
哪个名称分组,结果应按类别名称按字母顺序返回,还是按最高计数的顺序返回,还是按插入顺序返回? 如果没有orderBy
,分组结果可能会被按数据库决定的任意顺序返回。 此外,如果计划使用 limit/offset 或游标分页对分组结果进行分页,则需要一个稳定的排序顺序,以确保分页能够正确工作。 在许多系统中,如果主键是分组的一部分,使每个组自然可由该键识别,则结果可能默认为按该排序。 但是,如果 groupBy 字段中不存在主键,则必须指定一个orderBy
子句以获取一致的排序。非重复聚合用法: 需要忽略聚合中的重复值时,应使用 非重复 修饰符。 例如,
count(field: category_id, distinct: true)
对唯一类别进行计数。 如果想要了解 此组中有多少个不同的 X,这非常有用。 Distinct 还可以用于计算总和或平均值 - 例如,sum (field: price, distinct: true)
会在每个组中仅对每个唯一的价格值加一次。 这种情况不太常见,但可用于补充完整性。 在重复项倾斜数据的情况下使用不同的聚合。 例如,如果一种产品可以通过数据库联接多次出现,那么唯一计数可以确保它只被计数一次。
通过牢记这些限制和准则,可以生成有效的 GraphQL 聚合查询,从而产生强大的见解。 聚合功能对于报告和分析用例非常有用,但它确实需要仔细构建查询。 始终仔细核对groupBy
字段是否与所选的输出字段一致,为了实现可预测的顺序特别是在分页时,请添加排序,并根据数据类型适当地使用去重和聚合函数。