【AI大数据工程师特训笔记】第08讲:集合运算与超级函数

目录

[第 1 章 集合运算基础](#第 1 章 集合运算基础)

[1.1 什么是集合运算](#1.1 什么是集合运算)

[1.2 电力行业数据准备](#1.2 电力行业数据准备)

[1.3 UNION 与 UNION ALL(并集)](#1.3 UNION 与 UNION ALL(并集))

[1.4 INTERSECT 与 INTERSECT ALL(交集)](#1.4 INTERSECT 与 INTERSECT ALL(交集))

[1.5 EXCEPT 与 EXCEPT ALL(差集)](#1.5 EXCEPT 与 EXCEPT ALL(差集))

[1.6 集合运算的注意事项](#1.6 集合运算的注意事项)

[1.7 性能优化与常见错误](#1.7 性能优化与常见错误)

[第 2 章 超级函数(多维聚合)](#第 2 章 超级函数(多维聚合))

[2.1 为什么需要多维聚合](#2.1 为什么需要多维聚合)

[2.2 电力行业多维数据准备](#2.2 电力行业多维数据准备)

[2.3 GROUPING SETS:自定义维度组合](#2.3 GROUPING SETS:自定义维度组合)

[2.4 ROLLUP:层级递进汇总](#2.4 ROLLUP:层级递进汇总)

[2.5 CUBE:全交叉汇总](#2.5 CUBE:全交叉汇总)

[2.6 识别小计行:GROUPING() 与 GROUPING_ID()](#2.6 识别小计行:GROUPING() 与 GROUPING_ID())

[2.7 性能对比](#2.7 性能对比)

[第 3 章 行列转换](#第 3 章 行列转换)

[3.1 什么是行转列与列转行](#3.1 什么是行转列与列转行)

[3.2 行转列:静态 CASE WHEN + GROUP BY](#3.2 行转列:静态 CASE WHEN + GROUP BY)

[3.3 半动态行转列:crosstab 扩展](#3.3 半动态行转列:crosstab 扩展)

[3.4 列转行:UNION ALL、LATERAL、hstore、jsonb](#3.4 列转行:UNION ALL、LATERAL、hstore、jsonb)

[3.5 超宽表处理策略](#3.5 超宽表处理策略)

[第 4 章 补充知识点与综合练习](#第 4 章 补充知识点与综合练习)

[4.1 GROUPING() 深度解析(区分汇总 NULL 与原始 NULL)](#4.1 GROUPING() 深度解析(区分汇总 NULL 与原始 NULL))

[4.2 电力行业综合练习题](#4.2 电力行业综合练习题)

[第 5 章 总结](#第 5 章 总结)


第 1 章 集合运算基础

1.1 什么是集合运算

理论讲解

集合运算是指将两个SELECT查询的结果集看作数学中的集合,然后进行并集(UNION)交集(INTERSECT)差集(EXCEPT)操作。不同于 JOIN 是在横向 (增加列)上连接表,集合运算是在纵向(增加行)上拼接结果。

  • 并集:把两个结果集上下堆叠在一起(列数必须相同)。

  • 交集:取出同时出现在两个结果集中的行。

  • 差集:取出出现在第一个结果集但不出现在第二个结果集中的行。

与 JOIN 的本质区别

  • 方向:集合运算是纵向 (行数增加),JOIN横向(列数增加)。

  • 列要求:集合运算要求两个SELECT输出的列数相同 且对应列类型兼容JOIN无此限制。

  • 重复行:集合运算默认会去重 (除非使用ALL);JOIN默认保留所有行。

口诀:纵向拼、列对齐、去重看 ALL。

1.2 电力行业数据准备

为了演示,我们创建一个电力行业的数据库模式,包含发电厂信息表、发电记录表、售电记录表等。

sql 复制代码
-- 创建电力行业模式
CREATE SCHEMA IF NOT EXISTS power;

-- 1. 发电厂表
CREATE TABLE power.plants (
    plant_id   SERIAL PRIMARY KEY,
    plant_name VARCHAR(50) NOT NULL,
    region     VARCHAR(20),   -- 区域:华东、华北、华南等
    capacity   NUMERIC(10,2)  -- 装机容量(MW)
);

-- 2. 发电记录表(每日每厂发电量)
CREATE TABLE power.generation (
    gen_id    SERIAL PRIMARY KEY,
    plant_id  INTEGER REFERENCES power.plants(plant_id),
    gen_date  DATE NOT NULL,
    gen_mwh   NUMERIC(12,2)   -- 发电量(兆瓦时)
);

-- 3. 售电记录表(每日每厂售电量)
CREATE TABLE power.sales (
    sale_id   SERIAL PRIMARY KEY,
    plant_id  INTEGER REFERENCES power.plants(plant_id),
    sale_date DATE NOT NULL,
    sale_mwh  NUMERIC(12,2)   -- 售电量(兆瓦时)
);

-- 插入发电厂数据
INSERT INTO power.plants (plant_name, region, capacity) VALUES
('华能电厂', '华东', 1200),
('大唐电厂', '华北', 800),
('华电电厂', '华东', 600),
('国电电厂', '华南', 1000);

-- 插入发电记录(部分日期)
INSERT INTO power.generation (plant_id, gen_date, gen_mwh) VALUES
(1, '2025-01-01', 950),
(1, '2025-01-02', 1020),
(2, '2025-01-01', 700),
(2, '2025-01-02', 680),
(3, '2025-01-01', 500),
(4, '2025-01-01', 880),
(4, '2025-01-02', 910);

-- 插入售电记录
INSERT INTO power.sales (plant_id, sale_date, sale_mwh) VALUES
(1, '2025-01-01', 900),
(1, '2025-01-02', 980),
(2, '2025-01-01', 650),
(3, '2025-01-01', 480),
(4, '2025-01-01', 800),
(4, '2025-01-02', 850);

说明 :上面数据中,generation表有 7 行,sales表有 6 行。注意电厂 2 在 1 月 2 日有发电但无售电记录,电厂 3 在 1 月 2 日无任何记录等,便于演示集合运算。

1.3 UNION 与 UNION ALL(并集)

理论讲解

UNION 将两个查询的结果上下拼接 ,并自动去除重复行 (按照所有列比较)。UNION ALL 则保留所有行(包括重复行),性能更好。

使用场景:合并多个来源的同类数据,比如合并不同月份或不同电厂的报表。

案例 :查询所有在2025-01-01发电售电记录的日期(不去重 vs 去重)。

sql 复制代码
-- UNION:去重
SELECT gen_date AS activity_date 
FROM power.generation 
WHERE gen_date = '2025-01-01'

union

SELECT sale_date
FROM power.sales 
WHERE sale_date = '2025-01-01';
-- 结果:只有一行 '2025-01-01'(重复被合并)

---------------------------------------------------------------
-- UNION ALL:保留重复
SELECT gen_date 
FROM power.generation 
WHERE gen_date = '2025-01-01'

UNION all

SELECT sale_date 
FROM power.sales 
WHERE sale_date = '2025-01-01';
-- 结果:4行(发电4条 + 售电4条,共8条,但示例数据只有4+3? 实际发电4条(电厂1,2,3,4),售电4条(电厂1,2,3,4),共8行)

注意UNION ALL 不排序、不去重,速度最快。

1.4 INTERSECT 与 INTERSECT ALL(交集)

理论讲解

INTERSECT 返回同时出现在两个结果集中的行(去重)。INTERSECT ALL 保留重复次数(取两个结果集中出现次数的最小值)。

使用场景:找出既发电又售电的日期或电厂。

案例 :找出在2025-01-01既发电又售电的电厂 ID。

sql 复制代码
-- INTERSECT:去重
SELECT plant_id 
FROM power.generation 
WHERE gen_date = '2025-01-01'

intersect

SELECT plant_id 
FROM power.sales 
WHERE sale_date = '2025-01-01';

-- 结果:电厂1,2,4(电厂3只有发电,没有售电;电厂2有发电也有售电,被包含)

1.5 EXCEPT 与 EXCEPT ALL(差集)

理论讲解

EXCEPT 返回出现在第一个结果集但不出现在第二个结果集 的行(去重)。EXCEPT ALL 会考虑重复次数(减去第二个结果集出现的次数)。

注意A EXCEPT BB EXCEPT A 结果完全不同。

案例 :找出在2025-01-01发电但没有售电的电厂 ID。

sql 复制代码
SELECT plant_id 
FROM power.generation 
WHERE gen_date = '2025-01-01'

except

SELECT plant_id 
FROM power.sales 
WHERE sale_date = '2025-01-01';

-- 结果:电厂3(它只有发电,没有售电记录)

1.6 集合运算的注意事项

理论讲解

(1)列对齐 :两个SELECT列数必须相同 ,对应列的数据类型必须兼容(可隐式转换)。结果集的列名以第一个SELECT为准。

(2) NULL 处理 :在集合运算中,NULL = NULL 被视为真(与普通比较不同)。因此包含 NULL 的行会正常参与并、交、差。

(3)排序与分页ORDER BYLIMIT只能出现在整个集合运算的最后(不能单独对子查询排序,除非子查询加括号)。PostgreSQL 14+允许子查询内部排序,但对外层整体排序仍只能写在末尾。

(4)性能 :默认的去重操作会引入排序或哈希,如果业务允许重复行,尽量使用ALL变体提速。

案例:演示列不对齐的错误及修复。

sql 复制代码
-- 错误:列数不同
SELECT plant_id 
FROM power.generation

union

SELECT plant_id 
FROM power.sales;  -- 报错

-- 修复:补 NULL 使列数对齐
SELECT plant_id
     ,NULL::numeric AS dummy 
FROM power.generation

union

SELECT plant_id
      ,sale_mwh 
FROM power.sales;

1.7 性能优化与常见错误

技巧 说明
优先用 ALL 不需要去重时,UNION ALLUNION 快很多(避免排序/哈希)
提前过滤 在子查询中用WHERE减少参与集合运算的行数
并行 PostgreSQL 11+ 会自动并行集合运算,可调整max_parallel_workers_per_gather
统计信息 定期ANALYZE,避免优化器选择低效的哈希策略

常见错误速查

  • each UNION query must have the same number of columns → 列数不一致,补NULL

  • UNION types text and integer cannot be matched → 类型不兼容,显式转换(如::text)。

  • ORDER BY position 3 does not exist → 外层ORDER BY使用了第二个SELECT的列索引,应使用第一个SELECT的列名或序号。

第 2 章 超级函数(多维聚合)

2.1 为什么需要多维聚合

理论讲解

在实际报表中,我们常常需要同时查看不同维度组合的汇总结果,例如:

  • 全年总销售额

  • 各区域销售额

  • 各季度销售额

  • 各区域各季度销售额

传统做法是写多条SELECT再用UNION ALL拼起来,这样数据库会扫描多次表,性能差且代码冗长。

超级函数GROUPING SETSROLLUPCUBE)允许一次扫描表,同时计算出多种粒度的汇总,代码量减少 90%,性能提升数倍。

2.2 电力行业多维数据准备

我们创建一张发电销售事实表,包含时间、区域、电厂类型、发电量、售电量等维度。

sql 复制代码
-- 电力销售事实表
CREATE TABLE power.fact_electricity (
    year      INTEGER,
    quarter   INTEGER,
    month     INTEGER,
    region    TEXT,
    plant_type TEXT,        -- 火电、水电、风电等
    gen_mwh   NUMERIC,
    sale_mwh  NUMERIC
);

INSERT INTO power.fact_electricity VALUES
(2025,1,1,'华东','火电',1000,950),
(2025,1,1,'华东','风电',400,380),
(2025,1,2,'华北','火电',800,750),
(2025,1,2,'华北','水电',300,290),
(2025,2,3,'华东','火电',1100,1050),
(2025,2,3,'华南','风电',500,480);

2.3 GROUPING SETS:自定义维度组合

理论讲解

GROUPING SETS 允许你显式列出想要的汇总维度组合 ,每个组合用括号括起来。空括号()表示总计。数据库会对每个组合分别聚合,然后合并结果(一次扫描)。

案例:按年合计、按区域合计、按年+区域合计,以及总计。

sql 复制代码
SELECT year
      ,region
      ,SUM(gen_mwh) AS total_gen
FROM power.fact_electricity
GROUP BY GROUPING SETS (
    (year),           -- 年合计
    (region),         -- 区域合计
    (year, region),   -- 年+区域
    ()                -- 总计
)
ORDER BY year NULLS LAST, region NULLS LAST;

结果解释NULL出现在某个维度时表示该行是该维度的汇总(例如yearNULLregionNULL的行是总计)。

2.4 ROLLUP:层级递进汇总

理论讲解

ROLLUP 是对GROUP BY列的一个层级降维 过程。假设ROLLUP(a,b,c)会生成:(a,b,c)(a,b)(a)(),即从最细粒度逐步向上汇总,顺序敏感。

案例:按年、季度、月递进汇总发电量。

sql 复制代码
SELECT year
      ,quarter
      ,month
      ,SUM(gen_mwh) AS total_gen
FROM power.fact_electricity
GROUP BY ROLLUP(year, quarter, month)
ORDER BY year, quarter, month;

输出会包含:

  • 每月明细(year, quarter, month 都有值)

  • 每季度小计(month 为 NULL)

  • 每年小计(quarter 和 month 为 NULL)

  • 总计(全 NULL)

2.5 CUBE:全交叉汇总

理论讲解

CUBE(a,b) 会生成所有可能的维度组合:(a,b)(a)(b)(),共2^n种。与ROLLUP不同,CUBE不要求层级顺序,是对称的。

案例:按区域和电厂类型全交叉汇总售电量。

sql 复制代码
SELECT region, plant_type, SUM(sale_mwh) AS total_sale
FROM power.fact_electricity
GROUP BY CUBE(region, plant_type);

结果包含:区域+类型明细、区域合计、类型合计、总计。非常适合交叉分析。

2.6 识别小计行:GROUPING() 与 GROUPING_ID()

理论讲解

当汇总结果中出现NULL时,我们无法区分这个NULL原始数据 还是汇总占位符GROUPING()函数返回1表示该列是汇总生成的NULL,返回0表示是原始值(包括真正的NULL)。

在 PostgreSQL 中,GROUPING_ID()不存在,但可以通过多个GROUPING()组合成二进制数来模拟。

案例 :给上述ROLLUP结果添加标签,区分年小计、月小计等。

sql 复制代码
SELECT year
      ,quarter
      ,month
      ,SUM(gen_mwh) AS total_gen
      ,CASE WHEN GROUPING(year) = 1    THEN '总计' ELSE year::text    END AS year_label
      ,CASE WHEN GROUPING(quarter) = 1 THEN '全年' ELSE quarter::text END AS quarter_label
      ,CASE WHEN GROUPING(month) = 1   THEN '全季' ELSE month::text   END AS month_label
FROM power.fact_electricity
GROUP BY ROLLUP(year, quarter, month);

模拟 GROUPING_ID :手工计算 GROUPING(year)*2^2 + GROUPING(quarter)*2^1 + GROUPING(month)*2^0 得到一个整数,7表示全汇总行。

2.7 性能对比

方式 扫描次数 执行时间(参考)
多条UNION ALL 多次
GROUPING SETS / ROLLUP / CUBE 1 次 快(6~10 倍提升)

第 3 章 行列转换

3.1 什么是行转列与列转行

  • 行转列:将一列中的不同值(如月份)变成多个列头,使数据更便于阅读(宽表)。

  • 列转行:将多个列合并为一列(长表),便于后续聚合或导入其他系统。

3.2 行转列:静态 CASE WHEN + GROUP BY

场景:按月展示每个电厂的发电量(列头为 1 月、2 月...)。

sql 复制代码
SELECT plant_id
      ,SUM(CASE WHEN EXTRACT(MONTH FROM gen_date)=1 THEN gen_mwh ELSE 0 END) AS jan
      ,SUM(CASE WHEN EXTRACT(MONTH FROM gen_date)=2 THEN gen_mwh ELSE 0 END) AS feb
FROM power.generation
WHERE EXTRACT(YEAR FROM gen_date)=2025
GROUP BY plant_id;

优化 :可用FILTER子句替代CASE,更清晰。

sql 复制代码
SUM(gen_mwh) FILTER (WHERE EXTRACT(MONTH FROM gen_date)=1) AS jan

3.3 半动态行转列:crosstab 扩展

当列值不固定(比如产品种类未知)时,可以使用tablefunc扩展中的crosstab函数。需要预先知道列的类型。

sql 复制代码
CREATE EXTENSION IF NOT EXISTS tablefunc;

SELECT * FROM crosstab(
    'SELECT plant_id
           ,EXTRACT(MONTH FROM gen_date)::text
           ,gen_mwh
     FROM power.generation 
     WHERE EXTRACT(YEAR FROM gen_date)=2025
     ORDER BY 1,2',
    'SELECT generate_series(1,12)::text'
) AS ct(plant_id int, "1" numeric, "2" numeric, ...);

3.4 列转行:UNION ALL、LATERAL、hstore、jsonb

方法 1:UNION ALL(最通用但冗长)

sql 复制代码
SELECT plant_id
      ,1 AS month
      ,jan AS gen_mwh 
FROM monthly_gen 
WHERE jan IS NOT NULL

UNION ALL

SELECT plant_id
      ,2
      ,feb 
FROM monthly_gen 
WHERE feb IS NOT NULL;

方法 2:LATERAL + VALUES(一次扫描,性能更好)

sql 复制代码
SELECT plant_id
      ,month
      ,gen_mwh
FROM monthly_gen,
LATERAL (VALUES (1, jan), (2, feb), (3, mar)) AS t(month, gen_mwh)
WHERE gen_mwh IS NOT NULL;

方法 3:hstore(一键展开所有列)

sql 复制代码
SELECT plant_id
      ,(each(hstore(monthly_gen))).key AS month
      ,(each(...)).value AS gen_mwh
FROM monthly_gen;

方法 4:jsonb_each_text(最灵活,支持动态列名)

sql 复制代码
SELECT plant_id
      ,key AS month
      ,value::numeric AS gen_mwh
FROM monthly_gen,jsonb_each_text(to_jsonb(monthly_gen) - 'plant_id');

3.5 超宽表处理策略

当列数超过 1600(PostgreSQL 单表列上限),或者列值不确定时,不应强行转列,而应使用JSONBhstore存储键值对,并通过 GIN 索引加速查询。

sql 复制代码
CREATE MATERIALIZED VIEW power.gen_json AS
SELECT plant_id
      ,jsonb_object_agg(to_char(gen_date,'YYYY-MM-DD'), gen_mwh) AS daily_gen
FROM power.generation
GROUP BY plant_id;

-- 查询某电厂特定日期的发电量
SELECT daily_gen->>'2025-01-01' 
FROM power.gen_json WHERE plant_id=1;

第 4 章 补充知识点与综合练习

4.1 GROUPING() 深度解析(区分汇总 NULL 与原始 NULL)

生活例子 :超市小票汇总表里出现的 NULL 可能是"某天所有水果"的汇总占位符,也可能是水果名本身缺失。GROUPING()函数返回1表示汇总占位符,0表示原始数据(包含真实 NULL)。在电力行业,用GROUPING()可以将汇总行中的 NULL 替换为有意义的文字(如"总计")。

4.2 电力行业综合练习题

基于power模式,请完成以下题目:

(1)集合运算 :查询2025-01-01既发电又售电的电厂名称(使用INTERSECT)。

(2)ROLLUP:按区域、电厂类型、年份汇总发电量,要求输出每个层级的小计。

(3)行转列 :将2025年各电厂的月发电量转换为宽表(列名为 1~12 月),未发电月份填 0。

(4)列转行:将上一步生成的宽表再转回长表(plant_id, month, gen_mwh)。

(5)GROUPING():在 2 的结果中增加一列,标明当前行是"明细"、"区域小计"、"类型小计"还是"总计"。

第 5 章 总结

知识点 核心要点 电力行业应用示例
集合运算 纵向合并,列对齐,默认去重 合并各电厂发电与售电记录
GROUPING SETS 一次扫描,多维度组合 年/季/区域任意组合汇总
ROLLUP 层级递进,顺序敏感 年→季→月逐级汇总
CUBE 全交叉,对称 区域×类型所有组合分析
GROUPING() 识别汇总 NULL 将 NULL 替换为"总计"
行转列 CASEcrosstab 月发电量变成 12 列
列转行 UNION ALLLATERAL 宽表变成长表便于存储

最后口诀

  • 集合运算上下拼,列对齐,ALL 去重要分清。

  • 多维聚合一次扫,SETS/ROLLUP/CUBE,性能好。

  • NULL 真假难分辨,GROUPING 函数来帮忙。

  • 行列转换宽窄变,CASE 转列,LATERAL 转行。

相关推荐
蓝速科技1 小时前
蓝速科技 3D 全息数字人舱实景效能与选型指南
大数据·人工智能·科技·3d·交互
蘑菇丁1 小时前
招聘大数据运维工程师(郑州)
大数据·运维
searchforAI1 小时前
我的Obsidian知识库,现在可以自动剪藏笔记到本地了
人工智能·笔记·学习·音视频·ai工具·obsidian·视频总结
cy_cy0022 小时前
折幕影院怎样实现虚实一体?
大数据·科技·人机交互·交互·软件构建
andafaAPS2 小时前
安达发|aps高级排产:电动工具行业智能制造的核心引擎
大数据·人工智能·制造·安达发aps·aps高级排产·aps自动排产
lcj25112 小时前
vector的基本使用 + 手搓成员变量 size capacity begin end operator[] reserve扩容 拷贝构造 赋值析构
开发语言·c++·笔记·面试
penngo2 小时前
FlowLoom:基于 Apache Spark 的可视化数据处理平台
大数据·spark·apache
码农天天2 小时前
轻人力运营实践:中小企业如何通过AI智能体矩阵实现组织重构?
大数据·人工智能·时序数据库
cd_949217212 小时前
水处理市场升级,台州海德能环保科技凭技术创新与服务并重脱颖而出
大数据·运维·科技