UNPIVOT 语句允许将多个列堆叠为较少的列。在基本情况下,多个列被堆叠为两列:一个 NAME 列(包含源列的名称)和一个 VALUE 列(包含来自源列的值)。
DuckDB 实现了 SQL 标准的 UNPIVOT 语法和简化的 UNPIVOT 语法。两者都可以利用 COLUMNS 表达式来自动检测需要取消透视(unpivot)的列。PIVOT_LONGER 也可以代替 UNPIVOT 关键字使用。
关于 UNPIVOT 语句如何实现的详细信息,请参阅 Pivot Internals 页面。
PIVOT语句是UNPIVOT语句的逆运算。
简化版 UNPIVOT 语法
完整的语法图见下文,但简化版 UNPIVOT 语法可以使用电子表格数据透视表的命名惯例概括为
UNPIVOT dataset
ON column(s)
INTO
NAME name_column_name
VALUE value_column_name(s)
ORDER BY column(s)_with_order_direction(s)
LIMIT number_of_rows;
示例数据
所有示例均使用下述查询产生的数据集
CREATE OR REPLACE TABLE monthly_sales
(empid INTEGER, dept TEXT, Jan INTEGER, Feb INTEGER, Mar INTEGER, Apr INTEGER, May INTEGER, Jun INTEGER);
INSERT INTO monthly_sales VALUES
(1, 'electronics', 1, 2, 3, 4, 5, 6),
(2, 'clothes', 10, 20, 30, 40, 50, 60),
(3, 'cars', 100, 200, 300, 400, 500, 600);
FROM monthly_sales;
| empid | dept | Jan | Feb | Mar | Apr | May | Jun |
|---|---|---|---|---|---|---|---|
| 1 | electronics | 1 | 2 | 3 | 4 | 5 | 6 |
| 2 | clothes | 10 | 20 | 30 | 40 | 50 | 60 |
| 3 | cars | 100 | 200 | 300 | 400 | 500 | 600 |
手动 UNPIVOT
最典型的 UNPIVOT 转换是将已经透视的数据重新堆叠到名称列和值列中。在这种情况下,所有月份将被堆叠到一个 month 列和一个 sales 列中。
UNPIVOT monthly_sales
ON jan, feb, mar, apr, may, jun
INTO
NAME month
VALUE sales;
| empid | dept | month | sales |
|---|---|---|---|
| 1 | electronics | Jan | 1 |
| 1 | electronics | Feb | 2 |
| 1 | electronics | Mar | 3 |
| 1 | electronics | Apr | 4 |
| 1 | electronics | May | 5 |
| 1 | electronics | Jun | 6 |
| 2 | clothes | Jan | 10 |
| 2 | clothes | Feb | 20 |
| 2 | clothes | Mar | 30 |
| 2 | clothes | Apr | 40 |
| 2 | clothes | May | 50 |
| 2 | clothes | Jun | 60 |
| 3 | cars | Jan | 100 |
| 3 | cars | Feb | 200 |
| 3 | cars | Mar | 300 |
| 3 | cars | Apr | 400 |
| 3 | cars | May | 500 |
| 3 | cars | Jun | 600 |
使用 COLUMNS 表达式动态 UNPIVOT
在许多情况下,需要取消透视的列数不容易提前确定。对于此数据集,上述查询每次添加新月份时都需要修改。COLUMNS 表达式可用于选择所有非 empid 或 dept 的列。这实现了动态取消透视,无论添加多少个月份,它都能正常工作。下面的查询返回的结果与上面相同。
UNPIVOT monthly_sales
ON COLUMNS(* EXCLUDE (empid, dept))
INTO
NAME month
VALUE sales;
| empid | dept | month | sales |
|---|---|---|---|
| 1 | electronics | Jan | 1 |
| 1 | electronics | Feb | 2 |
| 1 | electronics | Mar | 3 |
| 1 | electronics | Apr | 4 |
| 1 | electronics | May | 5 |
| 1 | electronics | Jun | 6 |
| 2 | clothes | Jan | 10 |
| 2 | clothes | Feb | 20 |
| 2 | clothes | Mar | 30 |
| 2 | clothes | Apr | 40 |
| 2 | clothes | May | 50 |
| 2 | clothes | Jun | 60 |
| 3 | cars | Jan | 100 |
| 3 | cars | Feb | 200 |
| 3 | cars | Mar | 300 |
| 3 | cars | Apr | 400 |
| 3 | cars | May | 500 |
| 3 | cars | Jun | 600 |
取消透视(UNPIVOT)为多个值列
UNPIVOT 语句具有额外的灵活性:支持超过 2 个目标列。当目标是减少数据集的透视程度(而不是完全堆叠所有已透视的列)时,这非常有用。为了说明这一点,下面的查询将生成一个数据集,其中每个季度内的月份数(第 1、2 或 3 个月)都有一个单独的列,并且每个季度有一行。由于季度的数量少于月份的数量,这使数据集变长了,但没有上面那么长。
为实现这一点,在 ON 子句中包含了多组列。q1 和 q2 别名是可选的。ON 子句中每组列的数量必须与 VALUE 子句中的列数相匹配。
UNPIVOT monthly_sales
ON (jan, feb, mar) AS q1, (apr, may, jun) AS q2
INTO
NAME quarter
VALUE month_1_sales, month_2_sales, month_3_sales;
| empid | dept | quarter | month_1_sales | month_2_sales | month_3_sales |
|---|---|---|---|---|---|
| 1 | electronics | q1 | 1 | 2 | 3 |
| 1 | electronics | q2 | 4 | 5 | 6 |
| 2 | clothes | q1 | 10 | 20 | 30 |
| 2 | clothes | q2 | 40 | 50 | 60 |
| 3 | cars | q1 | 100 | 200 | 300 |
| 3 | cars | q2 | 400 | 500 | 600 |
在 SELECT 语句中使用 UNPIVOT
UNPIVOT 语句可以作为 CTE(公用表表达式或 WITH 子句)或子查询包含在 SELECT 语句中。这允许 UNPIVOT 与其他 SQL 逻辑一起使用,并允许在单个查询中使用多个 UNPIVOT。
CTE 内部不需要 SELECT,UNPIVOT 关键字可以被视为取代了它的位置。
WITH unpivot_alias AS (
UNPIVOT monthly_sales
ON COLUMNS(* EXCLUDE (empid, dept))
INTO
NAME month
VALUE sales
)
SELECT * FROM unpivot_alias;
UNPIVOT 可以用于子查询中,且必须用括号括起来。请注意,这种行为与 SQL 标准的 Unpivot 不同,后续示例中会进行说明。
SELECT *
FROM (
UNPIVOT monthly_sales
ON COLUMNS(* EXCLUDE (empid, dept))
INTO
NAME month
VALUE sales
) unpivot_alias;
UNPIVOT 语句中的表达式
DuckDB 允许在 UNPIVOT 语句中使用表达式,前提是它们只涉及单个列。这些可用于执行计算以及 显式转换。例如
UNPIVOT
(SELECT 42 AS col1, 'woot' AS col2)
ON
(col1 * 2)::VARCHAR,
col2;
| name | value |
|---|---|
| col1 | 84 |
| col2 | woot |
简化版 UNPIVOT 完整语法图
以下是 UNPIVOT 语句的完整语法图。
SQL 标准 UNPIVOT 语法
完整的语法图见下文,但 SQL 标准 UNPIVOT 语法可以概括为
FROM [dataset]
UNPIVOT [INCLUDE NULLS] (
[value-column-name(s)]
FOR [name-column-name] IN [column(s)]
);
请注意,name-column-name 表达式中只能包含一列。
SQL 标准手动 UNPIVOT
若要使用 SQL 标准语法完成基本 UNPIVOT 操作,只需进行少量添加。
FROM monthly_sales UNPIVOT (
sales
FOR month IN (jan, feb, mar, apr, may, jun)
);
| empid | dept | month | sales |
|---|---|---|---|
| 1 | electronics | Jan | 1 |
| 1 | electronics | Feb | 2 |
| 1 | electronics | Mar | 3 |
| 1 | electronics | Apr | 4 |
| 1 | electronics | May | 5 |
| 1 | electronics | Jun | 6 |
| 2 | clothes | Jan | 10 |
| 2 | clothes | Feb | 20 |
| 2 | clothes | Mar | 30 |
| 2 | clothes | Apr | 40 |
| 2 | clothes | May | 50 |
| 2 | clothes | Jun | 60 |
| 3 | cars | Jan | 100 |
| 3 | cars | Feb | 200 |
| 3 | cars | Mar | 300 |
| 3 | cars | Apr | 400 |
| 3 | cars | May | 500 |
| 3 | cars | Jun | 600 |
使用 COLUMNS 表达式进行 SQL 标准动态 UNPIVOT
COLUMNS 表达式可用于动态确定 IN 列列表。即使向数据集添加了额外的 month 列,这也将继续有效。它产生的结果与上述查询相同。
FROM monthly_sales UNPIVOT (
sales
FOR month IN (columns(* EXCLUDE (empid, dept)))
);
取消透视(UNPIVOT)为多个值列的 SQL 标准实现
UNPIVOT 语句具有额外的灵活性:支持超过 2 个目标列。当目标是减少数据集的透视程度(而不是完全堆叠所有已透视的列)时,这非常有用。为了说明这一点,下面的查询将生成一个数据集,其中每个季度内的月份数(第 1、2 或 3 个月)都有一个单独的列,并且每个季度有一行。由于季度的数量少于月份的数量,这使数据集变长了,但没有上面那么长。
为实现这一点,多列被包含在 UNPIVOT 语句的 value-column-name 部分中。IN 子句中包含了多组列。q1 和 q2 别名是可选的。IN 子句中每组列的数量必须与 value-column-name 部分中的列数相匹配。
FROM monthly_sales
UNPIVOT (
(month_1_sales, month_2_sales, month_3_sales)
FOR quarter IN (
(jan, feb, mar) AS q1,
(apr, may, jun) AS q2
)
);
| empid | dept | quarter | month_1_sales | month_2_sales | month_3_sales |
|---|---|---|---|---|---|
| 1 | electronics | q1 | 1 | 2 | 3 |
| 1 | electronics | q2 | 4 | 5 | 6 |
| 2 | clothes | q1 | 10 | 20 | 30 |
| 2 | clothes | q2 | 40 | 50 | 60 |
| 3 | cars | q1 | 100 | 200 | 300 |
| 3 | cars | q2 | 400 | 500 | 600 |
SQL 标准 UNPIVOT 完整语法图
以下是 SQL 标准版本 UNPIVOT 语句的完整语法图。