另类却不罕见的聚合运算

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS AI 助手,专业版
简介: SQL 只提供了最基础的聚合运算,稍复杂一些的任务都会非常困难;Python 要好很多,有更丰富的概念和运算,能够完成更复杂的计算任务,但是概念体系比较杂乱,语法风格在不同情况下不一致,难学难记,代码也较为繁琐。SPL 概念清晰,语法风格一致,可以写出最简洁易懂的代码。

标准 SQL 中提供了五种最常用的聚合运算:SUM/COUNT/AVG/MIN/MAX,都是对集合计算出单值。

比如日志表中找出用户 1001 第一次登录的时间,SQL 很简单:

SELECT MIN(time) FROM login WHERE user=1001

聚合运算还常常用在分组中,比如找每个用户的第一次登录时间:

SELECT user,MIN(time) FROM login GROUP BY user

语法上只是多了分组字段。

Python 也是类似的:

login[login['user']==1001]['time'].min()
login.groupby('user')['time'].min()

看起来这两句代码风格是一致的,但分组聚合的计算结果却不是二维的 DataFrame。

如果想返回 DataFrame,要写成:

login.groupby('user').agg({
   'time': 'min'})

但这和全集聚合的语法风格又有点不一致了。

esProc SPL 当然也支持这种基础形式:

login.select(user==1001).min(time)
login.groups(user;min(time))

和 SQL 一样,对象和语法风格都是一致的。

不过,聚合运算并不总以这么基础的形式出现,情况更复杂时,SPL 就会显出优势了。

  1. 非常规聚合函数
    有时候我们关心的不是聚合结果数值本身,而是与结果数值相关的信息。比如我们想找用户第一次登录的 ip 地址、浏览器类型等,而不仅仅是登录的时刻,聚合的结果就应该是记录而不是某个数值了。

SQL 的写法:

SELECT * FROM login
WHERE user =1001 and
time = (SELECT MIN(time) FROM login WHERE user=1001);

SQL 没有返回记录的聚合函数,先用子查询计算出最小登录时间,外层查询再找出该时间的其他信息,要把数据集遍历两次。

聚合运算和分组结合时,SQL 就更麻烦了,比如找每个用户第一次登录的记录:

SELECT * FROM login
JOIN (
    SELECT user,MIN(time) AS first_login_time
    FROM login
    GROUP BY user
    ) sub ON user = sub.user AND time = sub.first_login_time;

子查询先分组聚合出每个用户第一次登录的时间,再和原表连接取其他字段。

SPL 提供了“另类”聚合函数 minp、maxp,可以返回最小值、最大值对应的记录。我们想把求“最小值”改成“最小值对应记录”,只要将聚合函数 min 换成 minp 就可以了:

login.select(user==1001).minp(time)

分组聚合的情况也一样,仅仅换个聚合函数就行:

login.groups(user;minp(time))

Python 也提供了类似的函数:

login[login['user']==1001].nsmallest(1, 'time')

nsmallest(1, 'time') 找出 time 值最小的那一行,类似的还有 nlargest 函数。

不过,分组聚合时 nsmallest 就不能像 min 那样写到分组 groupby 后的 agg 中了,要借助 apply,概念不太一致,而且书写略显复杂:

login.groupby('user').apply(lambda group: group.nsmallest(1, 'time'))

因为,Python 把 nsmallest、nlargest 看成是对数据的筛选方法,计算过程和 min、max 这些聚合函数不一样。聚合函数常常可以在遍历过程中完成计算,不需要先获取完整的分组子集,这样内存占用会小很多。

SPL 的 minp、maxp 也是这样计算的,但 Python 的 nsmallest、nlargest 却不行,只能针对算出来的分组子集做聚合。

当然 SPL 也支持这种对分组子集再聚合的写法:

login.group(user;~.minp(time))

~ 就是分组子集,代码要比 Python 更简洁。

聚合运算还可能返回集合,比如常见的 topN 问题:找出员工薪酬最高的前三名。

SQL 并没有把 topN 理解为聚合函数,只能写成排序的样子:

SELECT * FROM employee ORDER BY salary DESC LIMIT 3;

这是 mysql 的写法,不同数据库的写法不同,但都包含排序。

把这个计算和分组结合,想找出每个部门薪酬最高的三个员工,用 SQL 就更麻烦了:

SELECT *
FROM(
    SELECT *,
 ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as row_num
    FROM employee
    ) 
WHERE row_num<= 3;

写法和计算全集 topN 时完全不一样,需要用窗口函数生成个序号再过滤出来,思路很绕。

SPL 则把 topN 也视为聚合运算,提供了聚合函数 top:

employee.top(-3;salary)

top 函数在遍历原表的过程中,将数据向一个大小为 N 的小集合聚合。

用到分组中也容易:

employee.groups(department;top(-3;salary))

也可以用分组子集的写法:

employee.group(department;~.top(-3;salary))

Python 的 nlargest 也能返回集合:

employee.nlargest(3, 'salary')

但用在分组中还是要借助 apply。

employee.groupby('department').apply(lambda group: group.nlargest(3, 'salary'))
  1. 复杂聚合运算
    更复杂的聚合运算就不能用单个函数实现了,需要写多个步骤。

如果是针对全集的运算,那还没什么,分步写就是了,但如果伴随着分组,那么这些步骤就要针对分组子集进行。比如:

某连锁店各门店有不同品类的商品销售量数据,计算各门店平均销量,并对每个门店求超出平均销量的商品的总销售量、以及这些商品中电子品类商品的总销售量。

没有分组子集的 SQL 对付这种问题非常麻烦:

WITH StoreAvgVolume AS (
    SELECT store, AVG(volume) AS avg_volume  
    FROM  sales
    GROUP BY store
),
AboveAvgVolume AS (
    SELECT s.store, s.product, s.category, s.volume, sav.avg_volume
    FROM sales s
    JOIN StoreAvgVolume sav ON s.store = sav.store
    WHERE s.volume > sav.avg_volume
)
SELECT store,avg_volume,
SUM(volume) AS total_volume,
SUM(CASE WHEN category = 'Electronics' THEN volume ELSE 0 END) AS electronic_volume
FROM AboveAvgVolume
GROUP BY store,avg_volume;

第一步分组计算每个网点的平均销售量。第二步用连接筛选出销量超过网点平均值的商品。第三步再次分组,计算这些商品总销售量、其中电子产品的总销售量。

SPL 分组后保持分组子集,可以对分组子集进行连续多步的计算:

sales.group(store;a=~.avg(volume):avg,(above=~.select(volume>a)).sum(volume):total,above.select(category=="Electronics").sum(volume):e_total)

聚合运算有些中间结果需要重复使用,比如代码中的 above 定义成中间变量就可以了。

Python 也有分组子集的概念,但相关的数据对象和函数却很杂乱,需要多出 Series 对象,而且字段名都要写成 tablename['fieldname'] 的啰嗦形式:

import pandas as pd
sales = pd.read_csv('sales.csv')
result = sales.groupby('store').apply(lambda group: pd.Series({
   
   'avg': (avg := group['volume'].mean()),
    'above': (above := group[group['volume'] > avg]),
    'total': (above['volume'].sum()),
    'e_total': (above[above['category'] == 'Electronics']['volume'].sum())
})).drop(columns=['above'])

聚合运算的中间结果要定义成计算列,比如 above,最后还要删掉这一列。虽然代码逻辑上没问题,但书写出来还是比 SPL 要繁琐不少。

小结一下:SQL 只提供了最基础的聚合运算,稍复杂一些的任务都会非常困难;Python 要好很多,有更丰富的概念和运算,能够完成更复杂的计算任务,但是概念体系比较杂乱,语法风格在不同情况下不一致,难学难记,代码也较为繁琐。SPL 概念清晰,语法风格一致,可以写出最简洁易懂的代码。

相关文章
|
应用服务中间件 PHP nginx
|
SQL 数据挖掘 关系型数据库
SQL中的聚合函数:数据分析的强大工具
【8月更文挑战第31天】
895 0
|
11月前
|
机器学习/深度学习 人工智能 自然语言处理
QwQ-32B为襄阳职业技术学院拥抱强化学习的AI力量
信息技术学院大数据专业学生团队与UNHub平台合作,利用QwQ-32B模型开启AI教育新范式。通过强化学习驱动,构建职业教育智能化实践平台,支持从算法开发到应用的全链路教学。QwQ-32B具备320亿参数,优化数学、编程及复杂逻辑任务处理能力,提供智能教学助手、科研加速器和产教融合桥梁等应用场景,推动职业教育模式创新。项目已进入关键训练阶段,计划于2025年夏季上线公测。
327 10
QwQ-32B为襄阳职业技术学院拥抱强化学习的AI力量
|
11月前
|
容器
鸿蒙开发:相对布局RelativeContainer
当然了,RelativeContainer组件还有着其它的属性,但是最重要的也就是位置的摆放,其实也就是相对于锚点组件的摆放;通过上述的案例,我们不难发现,所谓的左上右下,反着来就是对的,比如在锚点上边,我用bottom,在锚点下面,我用top,在实际的开发中,可极大节约我们的开发时间。
236 5
鸿蒙开发:相对布局RelativeContainer
|
10月前
|
存储 安全 NoSQL
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
364 0
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
|
11月前
|
缓存 NoSQL Java
Redis应用—9.简单应用汇总
本文主要介绍了Redis的一些简单应用。
425 26
|
11月前
|
安全 测试技术 Linux
Acunetix v25.3 发布,新增功能概览
Acunetix v25.3 (Linux, Windows) - Web 应用程序安全测试
116 1
Acunetix v25.3 发布,新增功能概览
|
11月前
|
人工智能 自然语言处理 数据可视化
autoMate:无需视觉模型!用DeepSeek-V3/R1就能实现自动化操作电脑,支持任何可视化界面
autoMate是一款基于AI和RPA的本地自动化工具,通过自然语言实现复杂任务的自动化操作,支持本地部署,确保数据安全和隐私,适合需要高效处理重复性工作的用户。
750 1
autoMate:无需视觉模型!用DeepSeek-V3/R1就能实现自动化操作电脑,支持任何可视化界面
|
11月前
|
缓存 NoSQL 测试技术
Redis压测脚本及持久化机制
Redis压测脚本及持久化机制简介: Redis性能压测通过`redis-benchmark`工具进行,可评估读写性能。持久化机制包括无持久化、RDB(定期快照)和AOF(操作日志),以及两者的结合。RDB适合快速备份与恢复,但可能丢失数据;AOF更安全,记录每次写操作,适合高数据安全性需求。两者结合能兼顾性能与安全性,建议同时开启并定期备份RDB文件以确保数据安全。
220 9
|
11月前
|
存储 算法 文件存储
探秘文件共享服务之哈希表助力 Python 算法实现
在数字化时代,文件共享服务不可或缺。哈希表(散列表)通过键值对存储数据,利用哈希函数将键映射到特定位置,极大提升文件上传、下载和搜索效率。例如,在大型文件共享平台中,文件名等信息作为键,物理地址作为值存入哈希表,用户检索时快速定位文件,减少遍历时间。此外,哈希表还用于文件一致性校验,确保传输文件未被篡改。以Python代码示例展示基于哈希表的文件索引实现,模拟文件共享服务的文件索引构建与检索功能。哈希表及其分布式变体如一致性哈希算法,保障文件均匀分布和负载均衡,持续优化文件共享服务性能。