PostgreSQL Oracle 兼容性 - 行列变换(pivot unpivot) (tablefunc, crosstab)

9 minute read

背景

行列转换是OLAP应用场景中,对数据透视常用的SQL之一。

Oracle pivot 行转列

语法如下:

SELECT ...    
FROM   ...    
PIVOT [XML]    
   (pivot_clause    
    pivot_for_clause    
    pivot_in_clause )    
WHERE ...    

In addition tothe new PIVOT keyword, we can see three new pivot clauses, described below.

(1)pivot_clause: definesthe columns to be aggregated (pivot is an aggregate operation);

(2)pivot_for_clause: definesthe columns to be grouped and pivoted;

(3)pivot_in_clause: definesthe filter for the column(s) in the pivot_for_clause (i.e. the range of valuesto limit the results to). The aggregations for each value in thepivot_in_clause will be transposed into a separate column (where appropriate).

对pivot_for_clause 指定的列进行过滤,只将指定的行转成列。

如:

SQL> WITH pivot_data AS (  -- 使用CTE语法,行转列的记录集    
 2          SELECT deptno, job, sal    -- 原始表,注意出现过的列,看PIVOT里面是如何处理的    
 3          FROM   emp    
 4          )    
 5  SELECT *    
 6  FROM   pivot_data    
 7  PIVOT (    
 8         SUM(sal)            --<-- pivot_clause ,   作为聚合项    
 9         FOR deptno          --<-- pivot_for_clause  行转列字段(聚合)    
 10        IN  (10,20,30,40)   --<-- pivot_in_clause   行转列的行取值,最后会生成这些行(聚合)    
 11       );    

未写入pivot内的列(JOB),被作为group by的字段。sal作为聚合列,deptno的内容10,20,30,40作为行列转换(GROUP BY)字段。

JOB               10         20         30         40    
--------- ---------- ---------- --------- -----------    
CLERK           1430       2090       1045    
SALESMAN                              6160    
PRESIDENT       5500    
MANAGER         2695       3272.5     3135    
ANALYST                    6600    
5 rows selected.    

Oracle unpivot 列转行

语法:

SELECT ...    
FROM  ...    
UNPIVOT [INCLUDE|EXCLUDE NULLS]    
   (unpivot_clause    
    unpivot_for_clause    
    unpivot_in_clause )    
WHERE ...    

The unpivot_clause rotatescolumns into rows.

(1)The INCLUDE EXCLUDE NULLS clausegives you the option of including or excluding null-valued rows. INCLUDE NULLS causesthe unpivot operation to include null-valued rows; EXCLUDE NULLS eliminatesnull-values rows from the return set. If you omit this clause, then the unpivotoperation excludes nulls.

这个选项用来控制unpivot 是否包含null 的记录,默认是不包含nulls的。

(2)unpivot_clause: this clause specifies a name for a column to represent the unpivotedmeasure values.

对应的具体值

(3)Inthe pivot_for_clause, specify a name for eachoutput column that will hold descriptor values, such as quarter or product.

对应转换后列的名称

(4)Inthe unpivot_in_clause, specify the input datacolumns whose names will become values in the output columns of the pivot_for_clause.These input data columns have names specifying a category value, such as Q1,Q2, Q3, Q4. The optional AS clause lets you map the input data columnnames to the specified literal values in the output columns.

具体列到行的列名

如:

SQL> SELECT *    
  2  FROM  pivoted_data    
  3  UNPIVOT (    
  4       deptsal                    --<-- unpivot_clause    
  5       FOR saldesc                --<-- unpivot_for_clause    
  6       IN (d10_sal, d20_sal, d30_sal, d40_sal)  --<-- unpivot_in_clause    
  7          );    
    
     
    
JOB       SALDESC       DEPTSAL    
---------- ---------- ----------    
CLERK     D10_SAL          1430    
CLERK     D20_SAL          2090    
CLERK     D30_SAL          1045    
SALESMAN  D30_SAL          6160    
PRESIDENT D10_SAL          5500    
MANAGER   D10_SAL          2695    
MANAGER   D20_SAL        3272.5    
MANAGER   D30_SAL          3135    
ANALYST   D20_SAL          6600    

PostgreSQL 行转列

https://www.postgresql.org/docs/devel/static/tablefunc.html

create extension tablefunc;    

使用crosstab函数接口进行行列转换。

create table tbl_sellers_info (seller text,se_year int,se_month int,se_amount int);      
insert into tbl_sellers_info values ('德哥',2011,01,123456);      
insert into tbl_sellers_info values ('德哥',2011,02,234567);      
insert into tbl_sellers_info values ('德哥',2011,03,345678);      
insert into tbl_sellers_info values ('德哥',2011,04,345678);      
insert into tbl_sellers_info values ('德哥',2011,05,567890);      
insert into tbl_sellers_info values ('贝克汉姆',2011,01,12);      
insert into tbl_sellers_info values ('贝克汉姆',2011,02,23);      
insert into tbl_sellers_info values ('贝克汉姆',2011,03,34);      
insert into tbl_sellers_info values ('贝克汉姆',2011,04,45);      
insert into tbl_sellers_info values ('贝克汉姆',2011,05,56);      
insert into tbl_sellers_info values ('卡洛斯',2011,03,12);      
insert into tbl_sellers_info values ('卡洛斯',2011,04,45);      
insert into tbl_sellers_info values ('卡洛斯',2011,05,56);      
insert into tbl_sellers_info values ('罗纳尔多',2011,02,20);      
insert into tbl_sellers_info values ('罗纳尔多',2011,03,30);      
insert into tbl_sellers_info values ('罗纳尔多',2011,04,40);      
insert into tbl_sellers_info values ('罗纳尔多',2011,05,50);      
insert into tbl_sellers_info values ('德哥',2010,01,123456);      
insert into tbl_sellers_info values ('德哥',2010,02,234567);      
insert into tbl_sellers_info values ('德哥',2010,03,345678);      
insert into tbl_sellers_info values ('德哥',2010,04,345678);      
insert into tbl_sellers_info values ('德哥',2010,05,567890);      
insert into tbl_sellers_info values ('德哥',2010,06,123456);      
insert into tbl_sellers_info values ('德哥',2010,07,234567);      
insert into tbl_sellers_info values ('德哥',2010,08,345678);      
insert into tbl_sellers_info values ('德哥',2010,09,345678);      
insert into tbl_sellers_info values ('德哥',2010,10,567890);      
insert into tbl_sellers_info values ('德哥',2010,11,123456);      
insert into tbl_sellers_info values ('德哥',2010,12,234567);      
insert into tbl_sellers_info values ('贝克汉姆',2010,11,12);      
insert into tbl_sellers_info values ('贝克汉姆',2010,12,23);      
insert into tbl_sellers_info select * from tbl_sellers_info;    
postgres=# select * from tbl_sellers_info ;    
  seller  | se_year | se_month | se_amount     
----------+---------+----------+-----------    
 德哥     |    2011 |        1 |    123456    
 德哥     |    2011 |        2 |    234567    
 德哥     |    2011 |        3 |    345678    
 德哥     |    2011 |        4 |    345678    
 德哥     |    2011 |        5 |    567890    
 贝克汉姆 |    2011 |        1 |        12    
 贝克汉姆 |    2011 |        2 |        23    
 贝克汉姆 |    2011 |        3 |        34    
 贝克汉姆 |    2011 |        4 |        45    
 贝克汉姆 |    2011 |        5 |        56    
 卡洛斯   |    2011 |        3 |        12    
 卡洛斯   |    2011 |        4 |        45    
 卡洛斯   |    2011 |        5 |        56    
 罗纳尔多 |    2011 |        2 |        20    
 罗纳尔多 |    2011 |        3 |        30    
 罗纳尔多 |    2011 |        4 |        40    
 罗纳尔多 |    2011 |        5 |        50    
 德哥     |    2010 |        1 |    123456    
 德哥     |    2010 |        2 |    234567    
 德哥     |    2010 |        3 |    345678    
 德哥     |    2010 |        4 |    345678    
 德哥     |    2010 |        5 |    567890    
 德哥     |    2010 |        6 |    123456    
 德哥     |    2010 |        7 |    234567    
 德哥     |    2010 |        8 |    345678    
 德哥     |    2010 |        9 |    345678    
 德哥     |    2010 |       10 |    567890    
 德哥     |    2010 |       11 |    123456    
 德哥     |    2010 |       12 |    234567    
 贝克汉姆 |    2010 |       11 |        12    
 贝克汉姆 |    2010 |       12 |        23    
 德哥     |    2011 |        1 |    123456    
 德哥     |    2011 |        2 |    234567    
 德哥     |    2011 |        3 |    345678    
 德哥     |    2011 |        4 |    345678    
 德哥     |    2011 |        5 |    567890    
 贝克汉姆 |    2011 |        1 |        12    
 贝克汉姆 |    2011 |        2 |        23    
 贝克汉姆 |    2011 |        3 |        34    
 贝克汉姆 |    2011 |        4 |        45    
 贝克汉姆 |    2011 |        5 |        56    
 卡洛斯   |    2011 |        3 |        12    
 卡洛斯   |    2011 |        4 |        45    
 卡洛斯   |    2011 |        5 |        56    
 罗纳尔多 |    2011 |        2 |        20    
 罗纳尔多 |    2011 |        3 |        30    
 罗纳尔多 |    2011 |        4 |        40    
 罗纳尔多 |    2011 |        5 |        50    
 德哥     |    2010 |        1 |    123456    
 德哥     |    2010 |        2 |    234567    
 德哥     |    2010 |        3 |    345678    
 德哥     |    2010 |        4 |    345678    
 德哥     |    2010 |        5 |    567890    
 德哥     |    2010 |        6 |    123456    
 德哥     |    2010 |        7 |    234567    
 德哥     |    2010 |        8 |    345678    
 德哥     |    2010 |        9 |    345678    
 德哥     |    2010 |       10 |    567890    
 德哥     |    2010 |       11 |    123456    
 德哥     |    2010 |       12 |    234567    
 贝克汉姆 |    2010 |       11 |        12    
 贝克汉姆 |    2010 |       12 |        23    
(62 rows)    

行列变换,用JSON,将需要GROUP BY的多个字段合并成1个。

select     
  js->>'seller' as seller,       
  js->>'se_year' as se_year,      
  jan  ,    
  feb  ,    
  mar  ,    
  apr  ,    
  may  ,    
  jun  ,    
  jul  ,    
  aug  ,    
  sep  ,    
  oct  ,    
  nov  ,    
  dec     
from crosstab(    
  -- 这个是需要进行行列变换的源SQL , 数据源。    
  -- 排序字段为group by字段  ,最后一个字段为转换后的内容字段,导数第二个字段为行列变换的字段(内容为枚举,比如月份)    
  -- (必须在下一个参数中提取出对应的所有枚举值)    
  $$select jsonb_build_object('seller', seller, 'se_year', se_year) as js, se_month, sum(se_amount) from tbl_sellers_info group by 1,2 order by 1$$,        
  -- 行列转换的行,有哪些值被提取出来作为列。 这个在这里代表的是月份,也就是se_month的值     
  -- 或(select * from (values('jan'),...('dec')) t(se_month))    
  'select distinct se_month from tbl_sellers_info order by 1'          
)     
as   -- crosstab 输出格式    
(  js jsonb,  -- 第一个参数SQL内对应的order by对应的字段(1个或多个)    
   Jan numeric,  -- 第一个参数SQL内对应导数第二个字段的枚举值,(行转列)    
   feb numeric,  -- ...同上    
   mar numeric,    
   apr numeric,    
   may numeric,    
   jun numeric,    
   jul numeric,    
   aug numeric,    
   sep numeric,    
   oct numeric,    
   nov numeric,    
   dec numeric    
)     
order by 1,2;    

结果

  seller  | se_year |  jan   |  feb   |  mar   |  apr   |   may   |  jun   |  jul   |  aug   |  sep   |   oct   |  nov   |  dec       
----------+---------+--------+--------+--------+--------+---------+--------+--------+--------+--------+---------+--------+--------    
 卡洛斯   | 2011    |        |        |     24 |     90 |     112 |        |        |        |        |         |        |           
 德哥     | 2010    | 246912 | 469134 | 691356 | 691356 | 1135780 | 246912 | 469134 | 691356 | 691356 | 1135780 | 246912 | 469134    
 德哥     | 2011    | 246912 | 469134 | 691356 | 691356 | 1135780 |        |        |        |        |         |        |           
 罗纳尔多 | 2011    |        |     40 |     60 |     80 |     100 |        |        |        |        |         |        |           
 贝克汉姆 | 2010    |        |        |        |        |         |        |        |        |        |         |     24 |     46    
 贝克汉姆 | 2011    |     24 |     46 |     68 |     90 |     112 |        |        |        |        |         |        |           
(6 rows)    

PostgreSQL 列转行

例子,将以上行列转换的结果,再转换回去。

with a as (  -- A对应原始数据(即需要列转行的数据)  
select     
  js->>'seller' as seller,       
  js->>'se_year' as se_year,      
  jan  ,    
  feb  ,    
  mar  ,    
  apr  ,    
  may  ,    
  jun  ,    
  jul  ,    
  aug  ,    
  sep  ,    
  oct  ,    
  nov  ,    
  dec     
from crosstab(    
  -- 这个是需要进行行列变换的源SQL , 数据源。    
  -- 排序字段为group by字段  ,最后一个字段为转换后的内容字段,导数第二个字段为行列变换的字段(内容为枚举,比如月份)    
  -- (必须在下一个参数中提取出对应的所有枚举值)    
  $$select jsonb_build_object('seller', seller, 'se_year', se_year) as js, se_month, sum(se_amount) from tbl_sellers_info group by 1,2 order by 1$$,        
  -- 行列转换的行,有哪些值被提取出来作为列。 这个在这里代表的是月份,也就是se_month的值     
  -- 或(select * from (values('jan'),...('dec')) t(se_month))    
  'select distinct se_month from tbl_sellers_info order by 1'          
)     
as   -- crosstab 输出格式    
(  js jsonb,  -- 第一个参数SQL内对应的order by对应的字段(1个或多个)    
   Jan numeric,  -- 第一个参数SQL内对应导数第二个字段的枚举值,(行转列)    
   feb numeric,  -- ...同上    
   mar numeric,    
   apr numeric,    
   may numeric,    
   jun numeric,    
   jul numeric,    
   aug numeric,    
   sep numeric,    
   oct numeric,    
   nov numeric,    
   dec numeric    
)     
order by 1,2    
)    
,     
-- b , 用jsonb把多列合并为一列,并使用jsonb_each展开。  
b as (select seller, se_year, jsonb_each(row_to_json(a)::jsonb-'seller'::text-'se_year'::text) as rec  from a)     
select seller, se_year, (b.rec).key as month, (b.rec).value as sum from b;    

结果

  seller  | se_year | month |   sum       
----------+---------+-------+---------    
 卡洛斯   | 2011    | apr   | 90    
 卡洛斯   | 2011    | aug   | null    
 卡洛斯   | 2011    | dec   | null    
 卡洛斯   | 2011    | feb   | null    
 卡洛斯   | 2011    | jan   | null    
 卡洛斯   | 2011    | jul   | null    
 卡洛斯   | 2011    | jun   | null    
 卡洛斯   | 2011    | mar   | 24    
 卡洛斯   | 2011    | may   | 112    
 卡洛斯   | 2011    | nov   | null    
 卡洛斯   | 2011    | oct   | null    
 卡洛斯   | 2011    | sep   | null    
 德哥     | 2010    | apr   | 691356    
 德哥     | 2010    | aug   | 691356    
 德哥     | 2010    | dec   | 469134    
 德哥     | 2010    | feb   | 469134    
 德哥     | 2010    | jan   | 246912    
 德哥     | 2010    | jul   | 469134    
 德哥     | 2010    | jun   | 246912    
 德哥     | 2010    | mar   | 691356    
 德哥     | 2010    | may   | 1135780    
 德哥     | 2010    | nov   | 246912    
 德哥     | 2010    | oct   | 1135780    
 德哥     | 2010    | sep   | 691356    
 德哥     | 2011    | apr   | 691356    
 德哥     | 2011    | aug   | null    
 德哥     | 2011    | dec   | null    
 德哥     | 2011    | feb   | 469134    
 德哥     | 2011    | jan   | 246912    
 德哥     | 2011    | jul   | null    
 德哥     | 2011    | jun   | null    
 德哥     | 2011    | mar   | 691356    
 德哥     | 2011    | may   | 1135780    
 德哥     | 2011    | nov   | null    
 德哥     | 2011    | oct   | null    
 德哥     | 2011    | sep   | null    
 罗纳尔多 | 2011    | apr   | 80    
 罗纳尔多 | 2011    | aug   | null    
 罗纳尔多 | 2011    | dec   | null    
 罗纳尔多 | 2011    | feb   | 40    
 罗纳尔多 | 2011    | jan   | null    
 罗纳尔多 | 2011    | jul   | null    
 罗纳尔多 | 2011    | jun   | null    
 罗纳尔多 | 2011    | mar   | 60    
 罗纳尔多 | 2011    | may   | 100    
 罗纳尔多 | 2011    | nov   | null    
 罗纳尔多 | 2011    | oct   | null    
 罗纳尔多 | 2011    | sep   | null    
 贝克汉姆 | 2010    | apr   | null    
 贝克汉姆 | 2010    | aug   | null    
 贝克汉姆 | 2010    | dec   | 46    
 贝克汉姆 | 2010    | feb   | null    
 贝克汉姆 | 2010    | jan   | null    
 贝克汉姆 | 2010    | jul   | null    
 贝克汉姆 | 2010    | jun   | null    
 贝克汉姆 | 2010    | mar   | null    
 贝克汉姆 | 2010    | may   | null    
 贝克汉姆 | 2010    | nov   | 24    
 贝克汉姆 | 2010    | oct   | null    
 贝克汉姆 | 2010    | sep   | null    
 贝克汉姆 | 2011    | apr   | 90    
 贝克汉姆 | 2011    | aug   | null    
 贝克汉姆 | 2011    | dec   | null    
 贝克汉姆 | 2011    | feb   | 46    
 贝克汉姆 | 2011    | jan   | 24    
 贝克汉姆 | 2011    | jul   | null    
 贝克汉姆 | 2011    | jun   | null    
 贝克汉姆 | 2011    | mar   | 68    
 贝克汉姆 | 2011    | may   | 112    
 贝克汉姆 | 2011    | nov   | null    
 贝克汉姆 | 2011    | oct   | null    
 贝克汉姆 | 2011    | sep   | null    
(72 rows)    

参考

http://www.oracle.com/technetwork/cn/articles/11g-pivot-101924-zhs.html

http://www.cnblogs.com/zlja/archive/2011/12/10/2449515.html

http://www.dba-oracle.com/t_pivot_examples.htm

http://www.postgresonline.com/journal/archives/283-Unpivoting-data-in-PostgreSQL.html

https://modern-sql.com/use-case/pivot

https://www.postgresql.org/docs/devel/static/tablefunc.html

《行列变换 - Use tablefunc complete row & column cross display in PostgreSQL》

Flag Counter

digoal’s 大量PostgreSQL文章入口