那么 SPL 的上手难度究竟如何呢?这里我们以 SQL 为起点讨论一下这个问题。
1
SQL 一直以来都是使用最广泛的结构化数据查询语言,在实现一般的查询计算时非常简单。像分组汇总一句简单的 group by 就实现了,相对 Java 这种要写几十行的高级语言简直不能更简单。而且,SQL 的语法设计也符合英语习惯,查询数据时就像说一句英语,这样也大大降低了使用难度。
select max (consecutive_day)
from (select count(*) (consecutive_day
from (select sum(rise_mark) over(order by trade_date) days_no_gain
from (select trade_date,
case when closing_price>lag(closing_price) over(order by trade_date)
then 0 else 1 END rise_mark
from stock_price ) )
group by days_no_gain)
使用另一个思路,把交易记录分组,连续在上涨的记录都分到一组,这样只要计算出最大的那一组的成员数就可以了。分组和统计都是 SQL 支持的运算,但是 SQL 只有等值分组,没有按照数据的次序来做的有序分组,结果只能用子查询和窗口函数硬造分组标记,将连续上涨的记录的分组标记设置成相同值,这样才能再进行等值分组求出期望的最大值,这种很绕的写法要理解一下才能看懂。而且这还是利用了 SQL 在 2003 标准中提供的窗口函数,可以直接计算比昨天的涨幅,从而比较方便地计算出这个标记,但仍然需要几层嵌套。如果是更早期的 SQL92 标准,连涨计算都很难,整个句子还会复杂很多倍。
SELECT TOP 10 x FROM T ORDER BY x DESC
这个查询用了 ORDER BY,严格按此逻辑执行,意味要将全量数据做排序,而大数据排序是一个很慢的动作。如果内存不够还要向外存写缓存,多次磁盘读写更会使性能急剧下降。
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY Area ORDER BY Amount DESC) rn
FROM Orders )
WHERE rn<=10
这里要先借助窗口函数造一个组内序号出来(组内排序),再用子查询过滤出符合条件的记录。由于集合化不够彻底,需要用分区、排序、子查询才能变相实现,导致这个 SQL 变得有些绕。而且这时候,大部分数据库的优化器就会犯晕了,猜不出这句 SQL 的目的,只能老老实实地执行按语句书写的逻辑去执行排序(这个语句中还是有 ORDER BY 的字样),结果性能陡降。
而这些正是 SPL 要解决的问题。
2
SPL 没有再基于 SQL 的关系代数体系,而是发明了新的离散数据集理论以及在此基础上实现的 SPL 语言(相当于把 SQL 的高楼推倒重盖)。SPL 支持过程计算,并提供了有序计算等多种计算机制,在算法实现上与 SQL 有很大不同。
A | |
1 | =stock_price.sort(trade_date) |
2 | =0 |
3 | =A1.max(A2=if(closing_price> closing_price[-1],A2+1,0)) |
即使使用 SQL 的实现逻辑,SPL 也写起来也很简单:
stock_price.sort(trade_date).group@i(closing_price<closing_price[-1]).max(~.len())
语法简洁会大幅提升开发效率,开发成本随之降低。同时,也会带来计算性能上的好处。
A | ||
1 | =file(“data.ctx”).create().cursor() | |
2 | =A1.groups(;top(10,amount)) | 金额在前 10 名的订单 |
3 | =A1.groups(area;top(10,amount)) | 每个地区金额在前 10 名的订单 |
像前面的 TopN 运算在 SPL 中被认为是和 SUM 和 COUNT 一样的聚合运算,只不过返回值是个集合而已。这样可以将高复杂度的排序转换成低复杂度的聚合运算,而且很还能扩展应用范围。
另一方面,有些解法由于我们没有高斯聪明确实想不到,但高斯已经想到了,我们只要学会就可以了。1+2+…+100 会,2+4+…+500 也能会,常用的招术并不多, 做一些练习就都能掌握。但确实也不是天生就能会的,需要一些训练,训练多了,这些手段就变成“自然”思维了,难度也并不大。
3
其实在实际业务中,SQL 很难应付的场景还有很多。这里我们试举几个玩爆 SQL 的例子。
-
复杂有序计算:用户行为转换漏斗分析
-
多步骤大数据量跑批
-
大数据上多指标计算,反复用关联多
with e1 as (
select uid,1 as step1,min(etime) as t1
from event
where etime>= to_date('2021-01-10') and etime<to_date('2021-01-25')
and eventtype='eventtype1' and …
group by 1),
e2 as (
select uid,1 as step2,min(e1.t1) as t1,min(e2.etime) as t2
from event as e2
inner join e1 on e2.uid = e1.uid
where e2.etime>= to_date('2021-01-10') and e2.etime<to_date('2021-01-25')
and e2.etime > t1 and e2.etime < t1 + 7
and eventtype='eventtype2' and …
group by 1),
e3 as (
select uid,1 as step3,min(e2.t1) as t1,min(e3.etime) as t3
from event as e3
inner join e2 on e3.uid = e2.uid
where e3.etime>= to_date('2021-01-10') and e3.etime<to_date('2021-01-25')
and e3.etime > t2 and e3.etime < t1 + 7
and eventtype='eventtype3' and …
group by 1)
select
sum(step1) as step1,
sum(step2) as step2,
sum(step3) as step3
from
e1
left join e2 on e1.uid = e2.uid
left join e3 on e2.uid = e3.uid
SQL 由于缺乏有序计算且集合化不够彻底,需要迂回成多个子查询反复 JOIN 的写法,编写理解都很困难而且运算性能非常低下。这段代码和漏斗的步骤数量相关,每增加一步数就要再增加一段子查询,实现很繁琐,即使这样,这个计算也并不是所有数据库都能算出来。
A | |
1 | =["etype1","etype2","etype3"] |
2 | =file("event.ctx").open() |
3 | =A2.cursor(id,etime,etype;etime>=date("2021-01-10") && etime<date("2021-01-25") && A1.contain(etype) && …) |
4 | =A3.group(uid).(~.sort(etime)) |
5 | =A4.new(~.select@1(etype==A1(1)):first,~:all).select(first) |
6 | =A5.(A1.(t=if(#==1,t1=first.etime,if(t,all.select@1(etype==A1.~ && etime>t && etime<t1+7).etime, null)))) |
7 | =A6.groups(;count(~(1)):STEP1,count(~(2)):STEP2,count(~(3)):STEP3) |
这个计算按照自然想法,其实只要按 uid 分组后,循环每个分组按照事件类型列表分别查看是否有对应记录(时间),只是第一个事件比较特殊(需要单独处理),查找到后将其作为第二个事件的输入参数即可,此后第 2 到第 N 个事件的处理方式相同(可以用通用代码表达),最后按照用户分组计数即可。
上述 SPL 的解法与自然思维基本一致,利用有序、集合化分组等特性简单 7 步就可以完成,很简洁。同时,这段代码能够处理任意步骤数的漏斗。由于只遍历一次数据就可以完成计算,不涉及外存交互,性能也更高。
4
不过,SPL 作为一门程序语言,想要使用 SPL 达到理想效果,还是要求使用者对 SPL 提供的函数和算法有一定了解,才能从诸多函数中选择适合的,这也是 SPL 初学者感到困惑的地方。SPL 提供的是一套工具箱,使用者根据实际问题开箱选择工具,是先拧螺丝,还是先裁木板完全由需要决定,但一旦掌握了工具箱内各个工具的使用方法,以后无论遇到什么工程问题都能很好解决,即使要对某些现有的东西进行改造(性能优化)也会游刃有余。而 SQL 提供的工具很少,这就会导致有时即使想到好方法也无从下手,经常需要通过很绕的方式才能实现,不仅难,还很慢。
发表评论