如何将一列中的值转置到具有不同列中的值的列中?

Bry*_*ley 5 postgresql aggregate pivot group-by

我有一个具有以下结构的数据库:

日期 角色 类型 期间
2022-04-16 护士 准备食材 45
2022-04-17 护士 打扫 30
2022-04-17 志愿者 打扫 20
2022-04-17 护士 准备食材 60

注意:我事先不知道“类型”列中的值,因为它们是由用户定义的。此外,可以有多行具有重叠的日期、角色和类型。

我正在使用一个图表库,希望将数据分组如下:

角色 准备食材 打扫
护士 105 30
志愿者 无效的 20

到目前为止,我可以使用以下查询对数据进行分组

select 
    role,
    type, 
    sum(duration) as total_minutes
from work
group by role, type;
Run Code Online (Sandbox Code Playgroud)
角色 类型 总分钟数
护士 打扫 45
护士 准备食材 20
志愿者 打扫 15
志愿者 准备食材 43

如何“旋转”/“转置”数据,以便每一行代表一个角色,其中一列包含每种类型工作的分钟总和?

实际上,我想转置类似于 Pandas DataFrame.pivot_table函数的数据,但仅使用 SQL。

Che*_*ain 4

首先,您需要使用create extension tablefunc;命令安装 tablefunc 扩展,否则数据透视功能crosstab将无法工作。

即使读完这个答案后,仍然建议您阅读 PostgreSQL on crosstab 的官方文档

至于如何做到这一点:

select *
from crosstab(
    'select
    role,
    type,
    sum(duration) as total_minutes
from work
group by role, type
order by type',
    'select distinct type from work order by type'
) as ct(
    role text,
    "Cleaning" text,
    "Food preparation" text
);
Run Code Online (Sandbox Code Playgroud)

注意order by两个查询中的显式子句,这是必须的,否则可能会错误地映射值,因为没有它的 SQL 不保证数据的顺序。

您必须type在别名中指定该列的每个可能的输出。


上面的一个更动态的版本(尽管无论如何都不完美):

create or replace function get_dynamic_transpose()
  returns text
  language plpgsql
as
$$
declare
    v_output_columns text;
begin
    select array_to_string(array_agg(distinct quote_ident(type) || ' ' || pg_typeof(type) || E' \n'),',','null')
    into v_output_columns
    from testing;

    return format(
'select *
from crosstab(
    ''select
    role,
    type,
    sum(duration) as total_minutes
from testing
group by role, type
order by type'',
    ''select distinct type from testing order by type''
) as ct(
    role text,
    %s
);', v_output_columns
    );
end;
$$;
Run Code Online (Sandbox Code Playgroud)

该函数将返回您需要执行的查询以获得所需的结果。它将动态构建输出所需的可能列的列表。这个函数肯定可以变得更通用,就像这里所做的那样,但是要做到这一点并不是一个小工作量,因为 PostgreSQL 无法返回一个它事先不知道其定义的集合。

该函数还有另一个选项,而不是返回查询字符串,它可以返回一个 json 对象数组,每个对象代表一行,并且您可以在应用程序端将此 json 拆分为普通的行和列。如果这样的解决方案是可以接受的,那么这工作得很好:

create or replace function get_dynamic_transpose_jsonb()
  returns jsonb
  language plpgsql
as
$$
declare
    v_output_columns text;
    v_query text;
    v_result jsonb;
begin
    select array_to_string(array_agg(distinct quote_ident(type) || ' ' || pg_typeof(type) || E' \n'),',','null')
    into v_output_columns
    from testing;

    v_query = format(
'select jsonb_agg(ct)
from crosstab(
    ''select
    role,
    type,
    sum(duration) as total_minutes
from testing
group by role, type
order by type'',
    ''select distinct type from testing order by type''
) as ct(
    role text,
    %s
);', v_output_columns
    );

    execute v_query into v_result;

    return v_result;
end;
$$;
Run Code Online (Sandbox Code Playgroud)

该函数的结果将类似于以下内容

[{"role": "Nurse", "Cleaning": "30", "Food preparation": null}, {"role": "Volunteer", "Cleaning": null, "Food preparation": "55"}]
Run Code Online (Sandbox Code Playgroud)