另类却不罕见的聚合运算

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 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
|
4月前
|
NoSQL 调度 Redis
分布式锁—5.Redisson的读写锁
Redisson读写锁(RedissonReadWriteLock)是Redisson提供的一种分布式锁机制,支持读锁和写锁的互斥与并发控制。读锁允许多个线程同时获取,适用于读多写少的场景,而写锁则是独占锁,确保写操作的互斥性。Redisson通过Lua脚本实现锁的获取、释放和重入逻辑,并利用WatchDog机制自动续期锁的过期时间,防止锁因超时被误释放。 读锁的获取逻辑通过Lua脚本实现,支持读读不互斥,即多个线程可以同时获取读锁。写锁的获取逻辑则确保写写互斥和读写互斥,即同一时间只能有一个线程获取写锁,
242 17
|
5月前
|
机器学习/深度学习 数据可视化 算法
Ray Flow Insight:让分布式系统调试不再"黑盒"
作为Ray社区的积极贡献者,我们希望将这些实践中沉淀的技术能力回馈给社区,推动Ray生态在实际场景中的应用深度和广度。因此,2024年底我们做了激活AntRay开源社区的决策,AntRay会始终保持与官方Ray版本强同步(即AntRay会紧随Ray官方社区版本而发布),内部Feature亦会加速推向AntRay以开源研发模式及时反哺内部业务,同时会将社区关注的Feature提交至Ray官方社区,实现内外部引擎双向价值流动。后续我们会以系列文章形式同步蚂蚁推向开源的新特性,本文将重点介绍:Ray Flow Insight —— 让分布式系统调试不再"黑盒"。
|
5月前
|
存储 安全 NoSQL
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
154 0
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
|
6月前
|
缓存 NoSQL Java
Redis应用—8.相关的缓存框架
本文介绍了Ehcache和Guava Cache两个缓存框架及其使用方法,以及如何自定义缓存。主要内容包括:Ehcache缓存框架、Guava Cache缓存框架、自定义缓存。总结:Ehcache适合用作本地缓存或与Redis结合使用,Guava Cache则提供了更灵活的缓存管理和更高的并发性能。自定义缓存可以根据具体需求选择不同的数据结构和引用类型来实现特定的缓存策略。
336 16
Redis应用—8.相关的缓存框架
|
6月前
|
缓存 NoSQL Java
Redis应用—9.简单应用汇总
本文主要介绍了Redis的一些简单应用。
275 26
|
6月前
|
存储 Cloud Native 数据管理
《云原生数据仓库:原理与实践》,深入浅出助力企业数智化升级
开启AI背景下的数据管理新时代,探索云原生数据仓库的核心技术
|
6月前
|
存储 人工智能 关系型数据库
云数据库是什么数据库?
云数据库是部署在云计算环境中的数据库服务,用户无需自行搭建硬件和软件环境,通过互联网即可便捷使用。相比传统数据库,云数据库降低了成本和使用门槛,具备强大的扩展性和灵活性,支持多种数据存储模型,并借鉴了关系型数据库的特性如ACID事务处理。它能够应对海量数据和高并发访问需求,适应数字化时代的挑战,未来还将融合更多新技术,进一步提升其功能和应用范围。
430 2
|
7月前
|
SQL 存储 缓存
MySQL的架构与SQL语句执行过程
MySQL架构分为Server层和存储引擎层,具有高度灵活性和可扩展性。Server层包括连接器、查询缓存(MySQL 8.0已移除)、分析器、优化器和执行器,负责处理SQL语句;存储引擎层负责数据的存储和读取,常见引擎有InnoDB、MyISAM和Memory。SQL执行过程涉及连接、解析、优化、执行和结果返回等步骤,本文详细讲解了一条SQL语句的完整执行过程。
216 3
|
6月前
|
人工智能 自然语言处理 数据可视化
autoMate:无需视觉模型!用DeepSeek-V3/R1就能实现自动化操作电脑,支持任何可视化界面
autoMate是一款基于AI和RPA的本地自动化工具,通过自然语言实现复杂任务的自动化操作,支持本地部署,确保数据安全和隐私,适合需要高效处理重复性工作的用户。
459 1
autoMate:无需视觉模型!用DeepSeek-V3/R1就能实现自动化操作电脑,支持任何可视化界面