【问题标题】:SQL-style GROUP BY aggregate functions in jq (COUNT, SUM and etc)jq 中的 SQL 风格的 GROUP BY 聚合函数(COUNT、SUM 等)
【发布时间】:2018-06-27 12:31:46
【问题描述】:

以前在这里问过类似的问题:

计算单个键的项目数:jq count the number of items in json by a specific key

计算对象值的总和: How do I sum the values in an array of maps in jq?

问题

如何模拟 COUNT 聚合函数,它的行为应该与其 SQL 原始函数相似?让我们进一步扩展这个问题以包含其他常规 SQL 函数:

  • 计数
  • SUM / MAX/ MIN / AVG
  • ARRAY_AGG

最后一个不是标准 SQL 函数 - 它来自 PostgreSQL,但非常有用。

输入端是一个有效的 JSON 对象流。为了演示,让我们选择一个关于主人和他们的宠物的简单故事。

模型和数据

基础关系:所有者

id name  age
 1 Adams  25
 2 Baker  55
 3 Clark  40
 4 Davis  31

基础关系:宠物

id name  litter owner_id
10 Bella      4        1
20 Lucy       2        1
30 Daisy      3        2
40 Molly      4        3
50 Lola       2        4
60 Sadie      4        4
70 Luna       3        4

来源

从上面我们得到一个派生关系 Owner_Pet(上述关系的 SQL JOIN 的结果)以 JSON 格式呈现给我们的 jq 查询(源数据):

{ "owner_id": 1, "owner": "Adams", "age": 25, "pet_id": 10, "pet": "Bella", "litter": 4 }
{ "owner_id": 1, "owner": "Adams", "age": 25, "pet_id": 20, "pet": "Lucy",  "litter": 2 }
{ "owner_id": 2, "owner": "Baker", "age": 55, "pet_id": 30, "pet": "Daisy", "litter": 3 }
{ "owner_id": 3, "owner": "Clark", "age": 40, "pet_id": 40, "pet": "Molly", "litter": 4 }
{ "owner_id": 4, "owner": "Davis", "age": 31, "pet_id": 50, "pet": "Lola",  "litter": 2 }
{ "owner_id": 4, "owner": "Davis", "age": 31, "pet_id": 60, "pet": "Sadie", "litter": 4 }
{ "owner_id": 4, "owner": "Davis", "age": 31, "pet_id": 70, "pet": "Luna",  "litter": 3 }

请求

以下是示例请求及其预期输出:

  • 统计每位主人的宠物数量:
{ "owner_id": 1, "owner": "Adams", "age": 25, "pets_count": 2 }
{ "owner_id": 2, "owner": "Baker", "age": 55, "pets_count": 1 }
{ "owner_id": 3, "owner": "Clark", "age": 40, "pets_count": 1 }
{ "owner_id": 4, "owner": "Davis", "age": 31, "pets_count": 3 }
  • 总结每个所有者的幼崽数量得到他们的 MAX(MIN/AVG):
{ "owner_id": 1, "owner": "Adams", "age": 25, "litter_total": 6, "litter_max": 4 }
{ "owner_id": 2, "owner": "Baker", "age": 55, "litter_total": 3, "litter_max": 3 }
{ "owner_id": 3, "owner": "Clark", "age": 40, "litter_total": 4, "litter_max": 4 }
{ "owner_id": 4, "owner": "Davis", "age": 31, "litter_total": 9, "litter_max": 4 }
  • ARRAY_AGG 宠物每位主人:
{ "owner_id": 1, "owner": "Adams", "age": 25, "pets": [ "Bella", "Lucy" ] }
{ "owner_id": 2, "owner": "Baker", "age": 55, "pets": [ "Daisy" ] }
{ "owner_id": 3, "owner": "Clark", "age": 40, "pets": [ "Molly" ] }
{ "owner_id": 4, "owner": "Davis", "age": 31, "pets": [ "Lola", "Sadie", "Luna" ] }

【问题讨论】:

    标签: sql json group-by aggregate-functions jq


    【解决方案1】:

    扩展jq解决方案:

    自定义count()函数:

    jq -sc 'def count($k): group_by(.[$k])[] | length as $l | .[0] 
                           | .pets_count = $l 
                           | del(.pet_id, .pet, .litter); 
            count("owner_id")' source.data
    

    输出:

    {"owner_id":1,"owner":"Adams","age":25,"pets_count":2}
    {"owner_id":2,"owner":"Baker","age":55,"pets_count":1}
    {"owner_id":3,"owner":"Clark","age":40,"pets_count":1}
    {"owner_id":4,"owner":"Davis","age":31,"pets_count":3}
    

    自定义sum()函数:

    jq -sc 'def sum($k): group_by(.[$k])[] | map(.litter) as $litters | .[0] 
                         | . + {litter_total: $litters | add, litter_max: $litters | max} 
                         | del(.pet_id, .pet, .litter); 
            sum("owner_id")' source.data
    

    输出:

    {"owner_id":1,"owner":"Adams","age":25,"litter_total":6,"litter_max":4}
    {"owner_id":2,"owner":"Baker","age":55,"litter_total":3,"litter_max":3}
    {"owner_id":3,"owner":"Clark","age":40,"litter_total":4,"litter_max":4}
    {"owner_id":4,"owner":"Davis","age":31,"litter_total":9,"litter_max":4}
    

    自定义array_agg()函数:

    jq -sc 'def array_agg($k): group_by(.[$k])[] | map(.pet) as $pets | .[0] 
                               | .pets = $pets | del(.pet_id, .pet, .litter); 
            array_agg("owner_id")' source.data
    

    输出:

    {"owner_id":1,"owner":"Adams","age":25,"pets":["Bella","Lucy"]}
    {"owner_id":2,"owner":"Baker","age":55,"pets":["Daisy"]}
    {"owner_id":3,"owner":"Clark","age":40,"pets":["Molly"]}
    {"owner_id":4,"owner":"Davis","age":31,"pets":["Lola","Sadie","Luna"]}
    

    【讨论】:

      【解决方案2】:

      这是一个很好的练习,但 SO 不是一种编程服务,因此我将在此重点介绍 jq 中通用解决方案的一些关键概念,即使对于非常大的集合也是如此。

      GROUPS_BY

      这里提高效率的关键是避免使用内置的group_by,因为它需要排序。因为 jq 基本上是面向流的,所以下面的 GROUPS_BY 定义同样是面向流的。它利用了基于键的查找的效率,同时避免在字符串上调用tojson

      # emit a stream of the groups defined by f
      def GROUPS_BY(stream; f): 
        reduce stream as $x ({};
           ($x|f) as $s
           | ($s|type) as $t
           | (if $t == "string" then $s else ($s|tojson) end) as $y
           | .[$t][$y] += [$x] )
         | .[][] ;
      

      distinctcount_distinct

      # Emit an array of the distinct entities in `stream`, without sorting
      def distinct(stream): 
        reduce stream as $x ({};
            ($x|type) as $t
            | (if $t == "string" then $x else ($x|tojson) end) as $y
            | if (.[$t] | has($y)) then . else .[$t][$y] += [$x] end )
         | [.[][]] | add ;
      
      
      # Emit the number of distinct items in the given stream
      def count_distinct(stream):
         def sum(s): reduce s as $x (0;.+$x);
         reduce stream as $x ({};
             ($x|type) as $t
             | (if $t == "string" then $x else ($x|tojson) end) as $y
             | .[$t][$y] = 1 )
         | sum( .[][] ) ;
      

      便利功能

      def owner: {owner_id,owner,age};
      

      示例:“COUNT the number of pets per owner”

      GROUPS_BY(inputs; .owner_id)
      | (.[0] | owner) + {pets_count: count_distinct(.[]|.pet_id)}
      

      调用:jq -nc -f program1.jq input.json

      输出:

      {"owner_id":1,"owner":"Adams","age":25,"pets_count":2}
      {"owner_id":2,"owner":"Baker","age":55,"pets_count":1}
      {"owner_id":3,"owner":"Clark","age":40,"pets_count":1}
      {"owner_id":4,"owner":"Davis","age":31,"pets_count":3}
      

      示例:“将每个所有者的幼崽数量相加并得到它们的 MAX”

      GROUPS_BY(inputs; .owner_id)
      | (.[0] | owner)
        + {litter_total: (map(.litter) | add)}
        + {litter_max:  (map(.litter) | max)}
      

      调用:jq -nc -f program2.jq input.json

      输出:如给定。

      示例:“ARRAY_AGG 宠物每位主人”

      GROUPS_BY(inputs; .owner_id)
      | (.[0] | owner) + {pets: distinct(.[]|.pet)}
      

      调用:jq -nc -f program3.jq input.json

      输出:

      {"owner_id":1,"owner":"Adams","age":25,"pets":["Bella","Lucy"]}
      {"owner_id":2,"owner":"Baker","age":55,"pets":["Daisy"]}
      {"owner_id":3,"owner":"Clark","age":40,"pets":["Molly"]}
      {"owner_id":4,"owner":"Davis","age":31,"pets":["Lola","Sadie","Luna"]}
      

      【讨论】:

      • 非常感谢Peak。我是这种语言的新手,我很难找到错误,但目前我没有得到预期的结果。例如,您的 count 为 Adam 返回 1,而不是 2。Totals 和 max 也返回不同的结果。
      • 1. GROUPS_BY 没有被分号正确终止。您必须添加它才能获得任何结果。 2. 出现差异的原因可能是您在此处省略了使用inputs 时所需的-n 选项。我会说清楚的。
      【解决方案3】:

      这是一个替代方案,不使用任何带有基本 JQ 的自定义函数。 (我冒昧地去掉了问题的多余部分)

      计数

      In> jq -s 'group_by(.owner_id) |  map({ owner_id: .[0].owner_id, count: map(.pet) | length})'
      Out>[{"owner_id": "1","pets_count": 2}, ...]
      

      总和

      In> jq -s 'group_by(.owner_id) | map({owner_id: .[0].owner_id, sum: map(.litter) | add})'
      Out> [{"owner_id": "1","sum": 6}, ...]
      

      最大

      In> jq -s 'group_by(.owner_id) | map({owner_id: .[0].owner_id, max: map(.litter) | max})'
      Out> [{"owner_id": "1","max": 4}, ...]
      

      聚合

      In> jq -s 'group_by(.owner_id) | map({owner_id: .[0].owner_id, agg: map(.pet) })'
      Out> [{"owner_id": "1","agg": ["Bella","Lucy"]}, ...]
      

      当然,这些可能不是最有效的实现,但它们很好地展示了如何自己实现自定义功能。不同函数之间的所有变化都在最后一个 map 和管道之后的函数 | 内(lengthaddmax

      第一个 map 迭代不同的组,从第一个项目中获取名称,然后再次使用 map 迭代相同组的项目。不如 SQL 漂亮,但也没有那么复杂。

      我今天学习了 JQ,并且已经成功地做到了这一点,所以这对任何入门的人来说都是令人鼓舞的。 JQ 既不像 sed,也不像 SQL,但也不是特别难。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-08-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-30
        • 1970-01-01
        • 2012-12-07
        相关资源
        最近更新 更多