PostgreSQL(PG) 中实现 行转列(Pivot) 的典型场景。
🧩 一、业务背景说明
我们有一个电商业务系统,需要分析商品在不同月份的销量情况。
原始数据以"订单明细"形式存储,每条记录包含:
- 商品 ID
- 销售日期(精确到日)
- 销量(整数)
目标:按商品统计每月销量,并将月份作为列头横向展示(即"行转列"),同时额外计算:
- 当月销量(比如当前是 2025 年 6 月,则显示 6 月销量)
- 前半年总销量(1~6 月)
- 后半年总销量(7~12 月)
- 全年总销量(1~12 月)
💡 注意:PG 没有内置
PIVOT关键字(如 Oracle/SQL Server),需用 条件聚合(Conditional Aggregation) 实现行转列。
二、表结构设计与示例数据
1. 商品信息表(可选,用于展示商品名称)
sql
CREATE TABLE product (
product_id INT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL
);
2. 订单明细表(核心表)
sql
CREATE TABLE order_detail (
id SERIAL PRIMARY KEY,
product_id INT NOT NULL,
sale_date DATE NOT NULL, -- 销售日期
quantity INT NOT NULL CHECK (quantity > 0)
);
3. 插入示例数据(2025 年全年,两个商品)
sql
-- 商品
INSERT INTO product VALUES
(101, '无线蓝牙耳机'),
(102, '智能手环');
-- 订单明细(模拟每月销量)
INSERT INTO order_detail (product_id, sale_date, quantity) VALUES
-- 耳机
(101, '2025-01-15', 120),
(101, '2025-02-10', 95),
(101, '2025-03-22', 140),
(101, '2025-04-05', 110),
(101, '2025-05-18', 130),
(101, '2025-06-30', 150),
(101, '2025-07-12', 160),
(101, '2025-08-25', 170),
(101, '2025-09-08', 155),
(101, '2025-10-19', 180),
(101, '2025-11-03', 200),
(101, '2025-12-20', 220),
-- 手环
(102, '2025-01-20', 80),
(102, '2025-02-14', 70),
(102, '2025-03-05', 90),
(102, '2025-04-12', 85),
(102, '2025-05-25', 100),
(102, '2025-06-10', 110),
(102, '2025-07-18', 120),
(102, '2025-08-30', 130),
(102, '2025-09-15', 125),
(102, '2025-10-22', 140),
(102, '2025-11-11', 160),
(102, '2025-12-05', 180);
✅ 所有
quantity均为整数,符合要求。
三、需求拆解
我们要输出如下格式的结果(假设当前月份是 2025年6月):
| product_id | product_name | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | first_half | second_half | total_year |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 101 | 无线蓝牙耳机 | 120 | 95 | 140 | 110 | 130 | 150 | 160 | 170 | 155 | 180 | 200 | 220 | 645 | 1185 | 1830 |
| 102 | 智能手环 | 80 | 70 | 90 | 85 | 100 | 110 | 120 | 130 | 125 | 140 | 160 | 180 | 535 | 1035 | 1570 |
其中:
Jan~Dec:对应各月销量(若某月无销售则为 0)first_half= Jan + Feb + ... + Junsecond_half= Jul + ... + Dectotal_year= first_half + second_half
四、SQL 实现(PostgreSQL)
使用 条件聚合 + CASE WHEN 实现行转列:
sql
SELECT
p.product_id,
p.product_name,
-- 行转列:每个月作为一列
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 1 THEN od.quantity END), 0) AS "Jan",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 2 THEN od.quantity END), 0) AS "Feb",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 3 THEN od.quantity END), 0) AS "Mar",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 4 THEN od.quantity END), 0) AS "Apr",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 5 THEN od.quantity END), 0) AS "May",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 6 THEN od.quantity END), 0) AS "Jun",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 7 THEN od.quantity END), 0) AS "Jul",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 8 THEN od.quantity END), 0) AS "Aug",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 9 THEN od.quantity END), 0) AS "Sep",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 10 THEN od.quantity END), 0) AS "Oct",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 11 THEN od.quantity END), 0) AS "Nov",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) = 12 THEN od.quantity END), 0) AS "Dec",
-- 前半年(1~6月)
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) BETWEEN 1 AND 6 THEN od.quantity END), 0) AS first_half,
-- 后半年(7~12月)
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM od.sale_date) BETWEEN 7 AND 12 THEN od.quantity END), 0) AS second_half,
-- 全年
COALESCE(SUM(od.quantity), 0) AS total_year
FROM product p
LEFT JOIN order_detail od
ON p.product_id = od.product_id
AND EXTRACT(YEAR FROM od.sale_date) = 2025 -- 限定年份
GROUP BY p.product_id, p.product_name
ORDER BY p.product_id;
五、关键点解析
| 技术点 | 说明 |
|---|---|
EXTRACT(MONTH FROM date) |
提取日期中的月份(1~12) |
CASE WHEN ... THEN quantity END |
条件聚合:只对特定月份求和 |
COALESCE(..., 0) |
处理 NULL(某月无销售时显示 0) |
LEFT JOIN |
确保即使某商品全年无销量也出现在结果中 |
GROUP BY product |
按商品分组,聚合各月销量 |
| 无 PIVOT 语法 | PG 需手动用 CASE 模拟行转列 |
📊 六、执行结果(基于上述数据)
运行后将得到:
| product_id | product_name | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | first_half | second_half | total_year |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 101 | 无线蓝牙耳机 | 120 | 95 | 140 | 110 | 130 | 150 | 160 | 170 | 155 | 180 | 200 | 220 | 645 | 1185 | 1830 |
| 102 | 智能手环 | 80 | 70 | 90 | 85 | 100 | 110 | 120 | 130 | 125 | 140 | 160 | 180 | 535 | 1035 | 1570 |