PostgreSQL_行列轉換pivot_unpivot

背景

作過數據清洗ETL工做的都知道,行列轉換是一個常見的數據整理需求。在不一樣的編程語言中有不一樣的實現方法,好比SQL中使用case+group,或者Power BI的M語言中用拖放組件實現。今天正好須要在PostgreSQL中處理一個數據行列轉換,就把這個方法記錄下來。html

首先明確一下啥叫行列轉換,由於這個叫法也不是很統一,有的地方叫轉置,有的地方叫透視,不一而足。咱們就如下表爲例,定義以下:算法

  • 從表1這種變成表2這種,叫透視(pivot)
  • 反之叫逆透視(unpivot)

表1sql

項目 月份 金額
A 1 10
A 2 20
A 2 30
B 1 30

表2編程

項目 1月 2月
A 10 50
B 30

構造樣本數據

構造一個表以格式保存數據json

drop table if exists demo.tf_pivot;
create table demo.tf_pivot (
    city text,
    year integer,
    month integer,
    income integer
);

insert into demo.tf_pivot values('a', 2018, 1, 1);
insert into demo.tf_pivot values('a', 2018, 2, 1);
insert into demo.tf_pivot values('a', 2018, 3, 1);
insert into demo.tf_pivot values('a', 2018, 4, 1);
insert into demo.tf_pivot values('a', 2019, 1, 2);
insert into demo.tf_pivot values('a', 2019, 2, 2);
insert into demo.tf_pivot values('a', 2019, 3, 2);
insert into demo.tf_pivot values('a', 2019, 4, 2);

insert into demo.tf_pivot values('b', 2018, 1, 3);
insert into demo.tf_pivot values('b', 2018, 2, 3);
insert into demo.tf_pivot values('b', 2018, 3, 3);
insert into demo.tf_pivot values('b', 2018, 4, 3);
insert into demo.tf_pivot values('b', 2019, 1, 4);
insert into demo.tf_pivot values('b', 2019, 2, 4);
insert into demo.tf_pivot values('b', 2019, 3, 4);
insert into demo.tf_pivot values('b', 2019, 4, 4);
複製代碼

構造一個表以格式保存數據編程語言

drop table if exists demo.tf_unpivot;
create table demo.tf_unpivot(
	city text,
	year int,
	m01 int,
	m02 int,
	m03 int,
	m04 int
);

insert into demo.tf_unpivot values('a', 2018, 1,2,3,4);
insert into demo.tf_unpivot values('a', 2019, 10,20,30,40);
insert into demo.tf_unpivot values('b', 2018, 100,200,300,400);
複製代碼

透視Pivot

CASE語法

首先展現一下傳統的case when語法。在PostgreSQL中,還提供了一個filter語法(簡化case when)函數

-- case when , 在PG中可使用filter
select
  city,
  sum(income) filter (where year=2018) as "2018",
  sum(income) filter (where year=2019) as "2019"
from demo.tf_pivot 
group by city
order by city;

-- 結果
 city | 2018 | 2019
------+------+------
 a    |    4 |    8
 b    |   12 |   16
(2 rows)
複製代碼

CROSSTAB語法

在PostgreSQL中,若是安裝了tablefunc擴展,就可使用crosstab()函數來簡化pivot操做。crosstab()提供了多個版本,這裏僅演示最基礎的版本 crosstab(sql text, sql text)。post

基礎用法

crosstab()透視操做簡單直接,關鍵點說明以下ui

  • 第一個參數,帶有按X,Y彙總的SQL子句,返回X,Y,Value格式的數據集;
  • 第二個參數,SQL子句,返回用於水平表頭中透視內容的全部值;
  • 使用AS子句明確指定返回的每個字段名稱和類型;

代碼以下spa

-- 
select * from crosstab(
   -- 基礎查詢,返回X,Y,V格式的數據集
   'select city,year,sum(income) from demo.tf_pivot group by city,year order by 1, 2',
   -- 產生水平表頭的查詢
   'select distinct year from demo.tf_pivot order by 1')
  -- 由於crosstab()返回的結果是不能動態肯定的,因此須要指定字段名稱和類型 
  as ("city" text,
      "y2018" int, -- 這裏的類型是字段內容的類型,不是表頭
      "y2019" int);
複製代碼

結果以下

city | y2018 | y2019
------+-------+-------
 a    |     4 |     8
 b    |    12 |    16
(2 rows)
複製代碼

多維表格的用法

在實際應用中,可能咱們須要的最終結果包括多維數據,好比下面這種,行上面有兩個維度,分別是city和year,而後對month進行透視。(這個結果和咱們構造的樣例表demo.tf_unpivot是同樣的)

city | year | m01 | m02 | m03 | m04
------+------+-----+-----+-----+-----
 a    | 2018 |   1 |   1 |   1 |   1
 a    | 2019 |   2 |   2 |   2 |   2
 b    | 2018 |   3 |   3 |   3 |   3
 b    | 2019 |   4 |   4 |   4 |   4
(4 rows)
複製代碼

由於原生crosstab僅能支持X,Y兩個維度,因此咱們設計一個取巧算法來達到目的

  • 在X上構造一個由多個字段構成的字段;
  • crosstab透視;
  • 使用split_part()函數把組合字段拆分爲多個結果字段;
-- 
select 
	-- 把拼接字段進行拆分
	split_part(city_year, '~', 1) as city,
	split_part(city_year, '~', 2)::int as year,
	m01,m02,m03,m04
from crosstab(
   -- 把多個字段拼接爲一個,而後執行聚合操做
   'select city || ''~'' || year, month, sum(income) from demo.tf_pivot group by city || ''~'' || year, month order by 1,2,3',
   -- 
   'select distinct month from demo.tf_pivot order by 1')
  as ("city_year" text, -- 結果集中指定拼接字段類型爲text
      "m01" int,
	  "m02" int,
	  "m03" int,
      "m04" int);
複製代碼

逆透視Unpivot

PostgreSQL沒有提供函數來實現unpivot操做,不過咱們可使用PG提供的幾個高級功能來間接實現需求。須要用到的函數和語法包括:

  • row_to_json() 把行數據轉換爲json串;
  • json_each_text() 把最外層的json對象轉換成Key/Value,每一個對象一行;
  • lateral 獨立子查詢內支持JOIN子查詢外面的表(這個尚未搞太明白 :-D)

原始數據以下

select * from demo.tf_unpivot;
 city | year | m01 | m02 | m03 | m04
------+------+-----+-----+-----+-----
 a    | 2018 |   1 |   2 |   3 |   4
 a    | 2019 |  10 |  20 |  30 |  40
 b    | 2018 | 100 | 200 | 300 | 400
(3 rows)
複製代碼

代碼以下

--
select
   r.city,
   r.year,
   key as month_str, 
   substr(key, 2)::int as month_int, -- 把 m01 轉換成 1 
   value::int as income
 from (select city, year, row_to_json(t.*) as line from demo.tf_unpivot t) as r
  join lateral json_each_text(r.line) on (key ~* '^m[0-9]+');
複製代碼

結果以下

city | year | month_str | month_int | income
------+------+-----------+-----------+--------
 a    | 2018 | m01       |         1 |      1
 a    | 2018 | m02       |         2 |      2
 a    | 2018 | m03       |         3 |      3
 a    | 2018 | m04       |         4 |      4
 a    | 2019 | m01       |         1 |     10
 a    | 2019 | m02       |         2 |     20
 a    | 2019 | m03       |         3 |     30
 a    | 2019 | m04       |         4 |     40
 b    | 2018 | m01       |         1 |    100
 b    | 2018 | m02       |         2 |    200
 b    | 2018 | m03       |         3 |    300
 b    | 2018 | m04       |         4 |    400
(12 rows)
複製代碼
相關文章
相關標籤/搜索