Skip to content
Advertisement

How to aggregate json fields when using GROUP BY clause in postgres?

I have the following table structure in my Postgres DB (v12.0)

id | pieces | item_id | material_detail
---|--------|---------|-----------------
1  | 10     | 2       | [{"material_id":1,"pieces":10},{"material_id":2,"pieces":20},{"material_id":3,"pieces":30}]

2  | 20     | 2       | [{"material_id":1,"pieces":40}
3  | 30     | 3       | [{"material_id":1,"pieces":20},{"material_id":3,"pieces":30}

I am using GROUP BY query for this records, like below

SELECT SUM(PIECES) FROM detail_table GROUP BY item_id HAVING item_id =2 

Using which I will get the total pieces as 30. But how could I get the count of total pieces from material_detail group by material_id.

I want result something like this

 pieces |  material_detail
 -------| ------------------
  30    |  [{"material_id":1,"pieces":50},{"material_id":2,"pieces":20},{"material_id":3,"pieces":30}]

As I am from MySQL background, I don’t know how to achieve this with JSON fields in Postgres.

Note: material_detail column is of JSONB type.

Advertisement

Answer

You are aggregating on two different levels. I can’t think of a solution that wouldn’t need two separate aggregation steps. Additionally to aggregate the material information all arrays of the item_id have to be unnested first, before the actual pieces value can be aggregated for each material_id. Then this has to be aggregated back into a JSON array.

with pieces as (

  -- the basic aggregation for the "detail pieces"
  select dt.item_id, sum(dt.pieces) as pieces
  from detail_table dt
  where dt.item_id = 2
  group by dt.item_id

), details as (

  -- normalize the material information and aggregate the pieces per material_id
  select dt.item_id, (m.detail -> 'material_id')::int as material_id, sum((m.detail -> 'pieces')::int) as pieces
  from detail_table dt
    cross join lateral jsonb_array_elements(dt.material_detail) as m(detail)
  where dt.item_id in (select item_id from pieces) --<< don't aggregate too much    
  group by dt.item_id, material_id

), material as (

  -- now de-normalize the material aggregation back into a single JSON array
  -- for each item_id
  select item_id, jsonb_agg(to_jsonb(d) - 'item_id') as material_detail
  from details d
  group by item_id

)
-- join both results together
select p.item_id, p.pieces, m.material_detail
from pieces p
  join material m on m.item_id = p.item_id
; 

Online example

Advertisement