Postgres: “累积” 表的视图

huangapple go评论71阅读模式
英文:

Postgres: "cumulative" view of table

问题

以下是您要翻译的内容:

I have a table of changes like this

CREATE TABLE IF NOT EXISTS changes (
    entity_id TEXT NOT NULL,
    column_id TEXT NOT NULL,
    value JSONB NOT NULL,
    updated_at TIMESTAMP NOT NULL
);
INSERT INTO changes VALUES ('1', 'height', to_jsonb(140), '01-01-2021 00:00:00'::TIMESTAMP);
INSERT INTO changes VALUES ('1', 'weight', to_jsonb(30), '01-01-2021 00:00:00'::TIMESTAMP);
INSERT INTO changes VALUES ('1', 'height', to_jsonb(145), '01-02-2021 00:00:00'::TIMESTAMP);
INSERT INTO changes VALUES ('1', 'weight', to_jsonb(34),'01-03-2021 00:00:00'::TIMESTAMP);


 entity_id | column_id | value |     updated_at      
-----------+-----------+-------+---------------------
 1         | height    | 140   | 2021-01-01 00:00:00
 1         | weight    | 30    | 2021-01-01 00:00:00
 1         | height    | 145   | 2021-01-02 00:00:00
 1         | weight    | 34    | 2021-01-03 00:00:00

And I want to get kinda cumulative view of this table

entity_id  | height | weight |     updated_at      
-----------+--------+--------+---------------------
 1         | 140    | 30     | 2021-01-01 00:00:00
 1         | 145    | 30     | 2021-01-02 00:00:00
 1         | 145    | 34     | 2021-01-03 00:00:00

My current query looks working

SELECT
  entity_id,
  coalesce(change->'height', lag(change->'height', 1, null) over (partition by entity_id order by updated_at)) as height,
  coalesce(change->'weight', lag(change->'weight', 1, null) over (partition by entity_id order by updated_at)) as weight,
  updated_at
FROM (
    SELECT entity_id, json_object_agg(column_id, value) as change, updated_at FROM changes
    GROUP BY entity_id, updated_at
) as changes;

But I don't like json_object_agg here and I'm sure there is a way do it without redundant aggregations? some kind of using window aggregate function that I've missed.

UPD. @SelVazi helped to make query better, but I feel it's not the final solution.

with cte as (
  SELECT
    entity_id,
    max(case when column_id = 'height' then value::int end) as height,
    max(case when column_id = 'weight' then value::int end) as weight,
    updated_at
  from changes
  GROUP by entity_id, updated_at
)
select
  entity_id,
  coalesce(height, lag(height) over (partition by entity_id order by updated_at)) as height,
  coalesce(weight, lag(weight) over (partition by entity_id order by updated_at)) as weight,
  updated_at
from cte;
英文:

I have a table of changes like this

CREATE TABLE IF NOT EXISTS changes (
    entity_id TEXT NOT NULL,
    column_id TEXT NOT NULL,
    value JSONB NOT NULL,
    updated_at TIMESTAMP NOT NULL
);
INSERT INTO changes VALUES ('1', 'height', to_jsonb(140), '01-01-2021 00:00:00'::TIMESTAMP);
INSERT INTO changes VALUES ('1', 'weight', to_jsonb(30), '01-01-2021 00:00:00'::TIMESTAMP);
INSERT INTO changes VALUES ('1', 'height', to_jsonb(145), '01-02-2021 00:00:00'::TIMESTAMP);
INSERT INTO changes VALUES ('1', 'weight', to_jsonb(34),'01-03-2021 00:00:00'::TIMESTAMP);


 entity_id | column_id | value |     updated_at      
-----------+-----------+-------+---------------------
 1         | height    | 140   | 2021-01-01 00:00:00
 1         | weight    | 30    | 2021-01-01 00:00:00
 1         | height    | 145   | 2021-01-02 00:00:00
 1         | weight    | 34    | 2021-01-03 00:00:00

And I want to get kinda cumulative view of this table

entity_id  | height | weight |     updated_at      
-----------+--------+--------+---------------------
 1         | 140    | 30     | 2021-01-01 00:00:00
 1         | 145    | 30     | 2021-01-02 00:00:00
 1         | 145    | 34     | 2021-01-03 00:00:00

My current query looks working

SELECT
  entity_id,
  coalesce(change->'height', lag(change->'height', 1, null) over (partition by entity_id order by updated_at)) as height,
  coalesce(change->'weight', lag(change->'weight', 1, null) over (partition by entity_id order by updated_at)) as weight,
  updated_at
FROM (
    SELECT entity_id, json_object_agg(column_id, value) as change, updated_at FROM changes
    GROUP BY entity_id, updated_at
) as changes;

But I don't like json_object_agg here and I'm sure there is a way do it without redundant aggregations? some kind of using window aggregate function that I've missed.

UPD. @SelVazi helped to make query better, but I feel it's not the final solution.

with cte as (
  SELECT
    entity_id,
    max(case when column_id = 'height' then value::int end) as height,
    max(case when column_id = 'weight' then value::int end) as weight,
    updated_at
  from changes
  GROUP by entity_id, updated_at
)
select
  entity_id,
  coalesce(height, lag(height) over (partition by entity_id order by updated_at)) as height,
  coalesce(weight, lag(weight) over (partition by entity_id order by updated_at)) as weight,
  updated_at
from cte;

答案1

得分: 2

这是更加复杂的问题,似乎不太容易。将身高和体重转换为列可以通过条件聚合来实现,但然后我们必须填充“缺失”的值。

我假设任何两种度量中都可能存在超过一个日期的间隙,这使得在Postgres中使用lag()不合适,因为它只能在预定义数量的行上向后查看(并且无法忽略null值)。

我们可以通过在示例数据的末尾添加一行来演示使用lag()的问题:

entity_id column_id value updated_at
1 height 140 2021-01-01 00:00:00
1 weight 30 2021-01-01 00:00:00
1 height 145 2021-02-01 00:00:00
1 weight 34 2021-03-01 00:00:00
1 weight 140 2021-04-01 00:00:00

一个解决方法是使用间隙和岛屿技术将“缺失”的值放在以非null值开头的组中,然后将其视为新值。

select entity_id, updated_at,
    max(height) over(partition by entity_id, grp_height) height,
    max(weight) over(partition by entity_id, grp_weight) weight
from (
    select c.*,
        count(height) over (partition by entity_id order by updated_at) grp_height,
        count(weight) over (partition by entity_id order by updated_at) grp_weight
    from (
        select entity_id, updated_at,
            max(value::text) filter(where column_id = 'height') height,
            max(value::text) filter(where column_id = 'weight') weight
        from changes
        group by entity_id, updated_at
    ) c
) c
order by entity_id, updated_at

fiddle

entity_id updated_at height weight
1 2021-01-01 00:00:00 140 30
1 2021-02-01 00:00:00 145 30
1 2021-03-01 00:00:00 145 34
1 2021-04-01 00:00:00 145 140
英文:

This is more complicated that it may seem. Pivoting the height and weight to columns can be done with conditional aggregation, but then we have to fill the "missing" values.

I would assume that there could be gaps of more than one date for any of the two measures, which makes lag() the wrong fit in Postgres, since it can only look back over a predefined number of rows (and cannot ignore null values).

We can demonstrate the problem with lag() by adding just one row at the end of your sample data:

entity_id column_id value updated_at
1 height 140 2021-01-01 00:00:00
1 weight 30 2021-01-01 00:00:00
1 height 145 2021-02-01 00:00:00
1 weight 34 2021-03-01 00:00:00
1 weight 140 2021-04-01 00:00:00

One workaround uses a gaps-and-islands technique to put "missing" values in groups that start with a non-null value, which then becomes the new value.

select entity_id, updated_at,
    max(height) over(partition by entity_id, grp_height) height,
    max(weight) over(partition by entity_id, grp_weight) weight
from (
    select c.*,
        count(height) over (partition by entity_id order by updated_at) grp_height,
        count(weight) over (partition by entity_id order by updated_at) grp_weight
    from (
        select entity_id, updated_at,
            max(value::text) filter(where column_id = 'height') height,
            max(value::text) filter(where column_id = 'weight') weight
        from changes
        group by entity_id, updated_at
    ) c
) c
order by entity_id, updated_at

fiddle

entity_id updated_at height weight
1 2021-01-01 00:00:00 140 30
1 2021-02-01 00:00:00 145 30
1 2021-03-01 00:00:00 145 34
1 2021-04-01 00:00:00 145 140

答案2

得分: 1

以下是您要翻译的内容:

"Instead of using json_object_agg you can use the conditional aggregation to get hight and width as columns :

SELECT entity_id, MAX(CASE WHEN column_id = 'height' THEN value::text END) AS height
, MAX(CASE WHEN column_id = 'weight' THEN value::text END) AS weight
, updated_at
FROM changes
GROUP BY entity_id, updated_at

The final query :

SELECT
entity_id,
coalesce(height, lag(height, 1, null) over (partition by entity_id order by updated_at)) as height,
coalesce(weight, lag(weight, 1, null) over (partition by entity_id order by updated_at)) as weight,
updated_at
FROM (
SELECT entity_id, MAX(CASE WHEN column_id = 'height' THEN value::text END) AS height
, MAX(CASE WHEN column_id = 'weight' THEN value::text END) AS weight
, updated_at
FROM changes
GROUP BY entity_id, updated_at
) as changes;

Result :

entity_id height weight updated_at
1 140 30 2021-01-01 00:00:00
1 145 30 2021-02-01 00:00:00
1 145 34 2021-03-01 00:00:00"

英文:

Instead of using json_object_agg you can use the conditional aggregation to get hight and width as columns :

SELECT entity_id, MAX(CASE WHEN column_id = 'height' THEN value::text END) AS height
                , MAX(CASE WHEN column_id = 'weight' THEN value::text END) AS weight
                , updated_at
FROM changes
GROUP BY entity_id, updated_at

The final query :

SELECT
  entity_id,
  coalesce(height, lag(height, 1, null) over (partition by entity_id order by updated_at)) as height,
  coalesce(weight, lag(weight, 1, null) over (partition by entity_id order by updated_at)) as weight,
  updated_at
FROM (
    SELECT entity_id, MAX(CASE WHEN column_id = 'height' THEN value::text END) AS height
                , MAX(CASE WHEN column_id = 'weight' THEN value::text END) AS weight
                , updated_at
    FROM changes
    GROUP BY entity_id, updated_at
) as changes;

Result :

entity_id	height	weight	updated_at
1	        140	    30	    2021-01-01 00:00:00
1	        145	    30	    2021-02-01 00:00:00
1	        145	    34	    2021-03-01 00:00:00

huangapple
  • 本文由 发表于 2023年5月13日 10:14:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76240813.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定