用于 GraphQL 的 API 中的聚合

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 的计数,这实际上是项目的计数。

注释

当前聚合操作(例如countsumavgminmax)仅适用于数值或定量字段。 例如,整数、浮点数、日期。 不能在文本字段上使用它们。 例如,您无法对字符串计算“平均值”。 计划支持对其他类型(例如文本)的聚合操作(如将来可能的函数,串联或字典序最小/最大值),但目前尚不可用。

限制和最佳做法

在 GraphQL 中使用聚合时,需要考虑一些重要的规则和限制。 这些可确保查询有效且结果可预测,尤其是在分页结果时。

  1. 聚合和原始项互斥: 目前,不能 同时检索同一查询中已分组的摘要数据和原始项列表。 集合的 groupBy 聚合查询返回分组数据,而不是普通项列表。 例如,在我们的 API 中,products(...) 查询要么在不使用 groupBy 的情况下返回产品列表,要么在使用 groupBy 时返回分组结果列表,但不会同时返回这两个列表。 你可能会注意到,在上面的聚合示例中,将显示字段 group 和聚合字段,而通常 items 的产品列表不存在。 如果尝试在一个查询中请求常规项目和组,GraphQL 引擎会返回错误或不允许进行这种选择。 如果需要原始数据和聚合数据,则必须运行两个单独的查询,或等待将来可能会解除此限制的更新。 此设计旨在保持响应结构明确,因此,查询处于“聚合模式”或“列表项模式”。

  2. 对分组结果进行排序(orderBy 与主键): 获取聚合组时,除非指定显式排序顺序,否则不保证返回组的顺序。 强烈建议对聚合查询使用或sort参数来定义应在结果中对组进行排序的方式,尤其是在分组键本身不唯一orderBy或没有明显的默认顺序时。 例如,如果按 category 哪个名称分组,结果应按类别名称按字母顺序返回,还是按最高计数的顺序返回,还是按插入顺序返回? 如果没有orderBy,分组结果可能会被按数据库决定的任意顺序返回。 此外,如果计划使用 limit/offset 或游标分页对分组结果进行分页,则需要一个稳定的排序顺序,以确保分页能够正确工作。 在许多系统中,如果主键是分组的一部分,使每个组自然可由该键识别,则结果可能默认为按该排序。 但是,如果 groupBy 字段中不存在主键,则必须指定一个 orderBy 子句以获取一致的排序。

  3. 非重复聚合用法: 需要忽略聚合中的重复值时,应使用 非重复 修饰符。 例如, count(field: category_id, distinct: true) 对唯一类别进行计数。 如果想要了解 此组中有多少个不同的 X,这非常有用。 Distinct 还可以用于计算总和或平均值 - 例如,sum (field: price, distinct: true) 会在每个组中仅对每个唯一的价格值加一次。 这种情况不太常见,但可用于补充完整性。 在重复项倾斜数据的情况下使用不同的聚合。 例如,如果一种产品可以通过数据库联接多次出现,那么唯一计数可以确保它只被计数一次。

通过牢记这些限制和准则,可以生成有效的 GraphQL 聚合查询,从而产生强大的见解。 聚合功能对于报告和分析用例非常有用,但它确实需要仔细构建查询。 始终仔细核对groupBy字段是否与所选的输出字段一致,为了实现可预测的顺序特别是在分页时,请添加排序,并根据数据类型适当地使用去重和聚合函数。