百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

比 SQL 快出数量级的大数据计算技术

wptr33 2025-02-18 13:42 29 浏览

SQL 经常跑得很慢

SQL 是最常用的大数据计算语言,但是,SQL 经常跑得很慢,严重浪费硬件资源。

某银行的反洗钱准备计算,36 亿行,11 节点的 Vertica 集群跑了 1.5 小时。

某电商漏斗运算,3 亿行,Snow Flake 的 Medium 型 4 节点集群超过 3 分钟没算出来。

某时空碰撞任务,250 亿行,Click House 5 节点集群跑了 1800 秒。

这些案例的数据量不算很大,小的只有几 GB,大的也就几百 GB,表现出来的性能却不如人意。

是不是这些任务过于复杂,就只能这么慢呢?并不是,这些任务用 SPL 改写后,性能都有大幅改观。

反洗钱准备计算,SPL 单机只要 26 秒,快了 208 倍,竟然把跑批任务跑成了实时查询!

漏斗运算,SPL 单机 10 秒完成。

时空碰撞任务,SPL 单机只要 350 秒,快了 5 倍。

换用 SPL 后,不仅计算性能快出了数量级,而且硬件还更少了,集群变单机。事实上,连代码也更短了。

为什么 SQL 就跑不出这个性能呢?

由 TOP N 问题探究 SQL 慢的原因

SQL 会跑这么慢,是因为写不出低复杂度的算法。

举个例子,从一亿条数据中取前十名。SQL 写出来有个 order by 字样:

SELECT TOP 10 * FROM Orders ORDER BY Amount DESC

这意味着要排序。如果真做一亿条记录的大排序,那计算量就大了去了。

其实,只要保持一个十条记录的小集合,遍历一亿条数据的时候不断比较、替换小集合中的记录,就可以得到前十名了,计算量比大排序小了 8 倍!遗憾的是,SQL 没有显式的集合数据类型,无法描述这样的计算过程,只能写成 order by,然后指望数据库去优化。还好,几乎所有数据库都会优化,采用更高效的算法,不会傻到真去做大排序。

但是,情况稍复杂些呢?比如我们把需求改成计算分组内的前 10 名,SQL 写出来是这样的。

SELECT * FROM (
    SELECT *, 
       ROW_NUMBER() OVER (PARTITION BY Area ORDER BY Amount DESC) rn  
    FROM Orders )
 WHERE rn<=10

这和前面全量数据取前 10 名的写法相差很大,要借助窗口函数造个组内序号,再用子查询过滤出符合条件的记录,有点“绕”了。

无论如何,SQL 中还是有 ORDER BY,运算逻辑还是排序。实测发现,同样的数据量,Oracle 计算组内前 10 名比全集前 10 名慢出几十倍,按说多个分组应该只慢一点点才对。Oracle 很可能做了排序,甚至是外存排序。

数据库优化引擎面对稍复杂的情况就晕了,只能老老实实按 SQL 的逻辑去执行,常常是个高复杂度的运算逻辑,所以性能就会非常差。

当然,也许哪天数据库又进化了,碰到这种情况也会优化了。但总有更复杂的情况,而数据库优化引擎的进化速度是很慢的。

更复杂的对象事件问题

用 TOP N 来举例,是因为它比较简单易于理解,事实上在现实中并不经常要做这种运算。而且,对于 TOP N 这样的简单问题,即使复杂化到分组 TOP N 的情况,SQL 优化器经过一些改良,也还是能对付的。

但实际情况中的计算常常要远比 TOP N 复杂,前面提到的案例,业务需求都有一定的复杂性,数据量不算大但计算量大。SQL 写不出好算法,只能依靠数据库的优化引擎,而优化引擎却常常靠不住。

比如典型的对象事件问题。这里的对象可以是电商系统用户、银行账号、游戏玩家、手机、车辆等等,通常会有个唯一的 ID,对象涉及的事件都记录在这个 ID 下,例如电商系统操作事件、银行账号的交易记录、手机的通话记录、用户的操作日志等。

对象事件问题的实际计算任务也很多,比如电商系统指定操作步骤的用户流失率,银行账号的交易额超过 1 万元的交易次数、节假日发生的交易额,计算每张信用卡是否连续三天都有超过 1000 元的交易,游戏用户购买某类装备的金额,新注册的游戏用户下次登录时刻的间隔天数,手机通话时间不超过 3 秒的次数,…。

这类问题非常普遍,可以说占了数据分析业务中一半以上的场景。这都是有业务意义的数据分析了,其中有相当一部分会涉及有序运算,用 SQL 都会非常难写,不仅代码繁琐,而且运算效率低下。

下面通过电商漏斗分析这个典型的事件对象问题,继续探究 SQL 慢的原因。

对象事件问题的任务

对象事件问题的任务是针对每个 ID 中符合一定条件的事件,计算出某种聚合值,然后再做 ID 的整体统计。

具体到漏斗分析,就是找出每个用户在一个较短的时间范围,按照次序完成一个给定步骤序列的最多步数。

比如给定三个步骤:浏览商品、查看详情、确认下单,要计算的聚合值是:每个用户在 3 天内按次序最多完成这三个步骤中的几步。有了每一个用户的最多步数,只要简单计数,就可以整体统计出,走到每个步骤的用户数量,进而分析哪一步的用户流失率最严重了。

对象事件计算的特征

要弄清楚 SQL 解决这个问题的难点,先看对象事件计算的几个特征:

一,ID 数量非常多,几千万到几十亿;

二,同一 ID 的事件数并不多,几条到几千条;

三,针对事件的计算复杂,步骤多;

四,计算时 ID 之间无关。

SQL 实现对象事件问题导致大表 JOIN

对象事件之间的复杂关联计算会涉及多条互相依赖的事件记录。

SQL 对跨行运算的支持很弱,要用 JOIN 把多行记录拼到一行中才能进一步运算。而且,参与运算的事件都有条件,一般要先用子查询选出来再 JOIN。计算中涉及的事件越多,参与 JOIN 的子查询越多。

比如下图中的三步漏斗,就涉及 ID1 的多条记录,先用多个子查询筛选合适的事件记录,把多行记录 JOIN 到一行中,才能做进一步运算。

这些子查询还会有依赖性,比如第 2 步事件要在第一步事件的基础上寻找,子查询本身也要用 JOIN 计算。

这些子查询都会基于巨大的事件表,大表 JOIN 本来就很难做,这里还要反复多次 JOIN,跑得慢就不奇怪了,跑崩也很正常。

SQL 实现对象事件问题涉及大结果集 GROUP BY

用 SQL 计算最后聚合值时,又会涉及大结果集的 GROUP BY。

因为期望的结果中,每个 ID 对应一套聚合值,JOIN 的结果并没有这个特征,同一个 ID 可能会 JOIN 出很多条记录。这时候,还要针对 ID 再做一次 GROUP BY。ID 数量非常多,这种大结果集的分组性能非常差,并行计算效果也不好。

有时,计算目标是对 ID 计数,GROUP BY 会退化成 COUNT DISTINCT,但复杂度数量级并没有变。

SQL 实现对象事件问题的脚本

这是一个 SQL 写的三步漏斗分析,其中有多个和 ID 相关的大表 JOIN、GROUP BY 以及 COUNT DISTINCT。

WITH e1 AS (
    SELECT userid, visittime AS step1_time, MIN(sessionid) AS sessionid, 1 AS step1
    FROM events e1 JOIN eventgroup ON eventgroup.id = e1.eventgroup
    WHERE visittime >= DATE_ADD(arg_date,INTERVAL -14 day) AND visittime < arg_date AND eventgroup.name = 'SiteVisit'
    GROUP BY userid,visittime
), e2 AS (
    SELECT e2.userid, MIN(e2.sessionid) AS sessionid, 1 AS step2, MIN(visittime) AS step2_time, MIN(e1.step1_time) AS step1_time
    FROM events e2 JOIN e1 ON e1.sessionid = e2.sessionid AND visittime > step1_time JOIN eventgroup ON eventgroup.id = e2.eventgroup
    WHERE visittime < DATE_ADD(step1_time ,INTERVAL +1 day) AND eventgroup.name = 'ProductDetailPage'
    GROUP BY e2.userid
), e3 AS (
    SELECT e3.userid, MIN(e3.sessionid) AS sessionid, 1 AS step3, MIN(visittime) AS step3_time, MIN(e2.step1_time) AS step1_time
    FROM events e3 JOIN e2 ON e2.sessionid = e3.sessionid AND visittime > step2_time JOIN eventgroup ON eventgroup.id = e3.eventgroup
    WHERE visittime < DATE_ADD(step1_time ,INTERVAL +1 day) AND (eventgroup.name = 'OrderConfirmationType1')
    GROUP BY e3.userid
)
SELECT s.devicetype AS devicetype,
    COUNT(DISTINCT CASE WHEN fc.step1 IS NOT NULL THEN fc.step1_userid  ELSE NULL END) AS step1_count,
    COUNT(DISTINCT CASE WHEN fc.step2 IS NOT NULL THEN fc.step2_userid  ELSE NULL END) AS step2_count,
    COUNT(DISTINCT CASE WHEN fc.step3 IS NOT NULL THEN fc.step3_userid  ELSE NULL END) AS step3_count,
FROM (
    SELECT e1.step1_time AS step1_time, e1.userid AS userid, e1.userid AS step1_userid, e2.userid AS step2_userid,e3.userid AS step3_userid,
           e1.sessionid AS step1_sessionid, step1, step2, step3
    FROM e1 LEFT JOIN e2 ON e1.userid=e2.userid LEFT JOIN e3 ON e2.userid=e3.userid ) fc
LEFT JOIN sessions s ON fc.step1_sessionid = s.id 
GROUP BY s.devicetype


解决对象事件问题的正确思路

其实,只要利用前面说的特征,解决对象事件问题并不难。

我们可以把事件按 ID 排序,每个 ID 的事件不多,每次把一个 ID 的所有事件读入内存。在内存中,使用过程化的语言实现这种复杂计算就很容易了。

因为 ID 之间无关,所以内存关联运算仅在一个 ID 的事件范围内进行,不会有大表 JOIN。每次都会完成一个 ID 聚合值的计算,后续不需要 GROUP BY ID,COUNT DISTINCT 也会变成简单的 COUNT。这个算法不仅计算量少,而且占用内存小,很容易并行。

可惜,用 SQL 无法实现这样的高效算法。这主要是因为 SQL 缺乏离散性,无法实现过程化的复杂跨行运算,只能借助 JOIN。而且,SQL 的集合无序,即使刻意有序存储也无法利用。

SPL 如何计算 Top N

SQL 没有显式的集合数据类型,无法描述前面说的那种低复杂度的算法,只能指望数据库去优化。而 SPL 则可以从算法层面解决这些问题。

SPL 把 Top N 理解为,与 SUM、MAX 一样的聚合运算,只不过返回值是一个集合而已。这样就相当于实现了前面提到的高效算法,代码写出来是这样的:

Orders.groups(;top(10;-Amount))

Orders.groups(Area;top(10;-Amount))

这个语句中没有 order by 字样,也就不会做大排序,而采用小集合的算法了。而且,这里分组前 10 名和全集前 10 名的写法基本一样,只是多了分组键。

SPL 解决对象事件问题

再看对象事件问题,用 SPL 就很容易实现前面说的高效算法。

SPL 的集合有序,能够刻意保证存储的次序,且提供有序游标,可以一次读入一个 ID 的事件数据。SPL 语法中强化了离散性,可以很方便按自然思维写出多步骤跨行运算的代码。

这是 SPL 实现同样漏斗分析的代码:


A

1

=eventgroup=file("eventgroup.btx").import@b()

2

=st=long(elapse(arg_date,-14)),et=long(arg_date),eet=long(arg_date+1)

3

=A1.(case(NAME,"SiteVisit":1,"ProductDetailPage":2,"OrderConfirmationType1":3;null))

4

=file("events.ctx").open()

5

=A4.cursor@m(USERID,SESSIONID,VISITTIME,EVENTGROUP;VISITTIME>=st && VISITTIME

6

=file("sessions.ctx").open().cursor@m(USERID,ID,DEVICETYPE;;A5)

7

=A5.joinx@m(USERID:SESSIONID,A6:USERID:ID,DEVICETYPE)

8

=A7.group(USERID)

9

=A8.new(~.align@a(3,EVENTGROUP):e,e(1).select(VISITTIME

10

=A9.run(e=join@m(e1:e1,SESSIONID;e2:e2,SESSIONID).select(e2=e2.select(VISITTIME>e1.VISITTIME && VISITTIME

11

=A10.run(e0=e1.id(DEVICETYPE),e1=e.min(e1.VISITTIME),e2=e.min(e2),e=e.min(e1.SESSIONID),e3=e3.select(SESSIONID==e && VISITTIME>e2 && VISITTIME

12

=A11.news(e;~:DEVICETYPE,e2,e3).groups(DEVICETYPE;count(1):STEP1_COUNT,count(e2):STEP2_COUNT,count(e3):STEP3_COUNT)

A8 每次读入一个 ID 的所有事件,然后用 A9 到 A12 四句代码实现复杂判断逻辑,最后 A12 分组统计时也只要简单计数就可以了,不用再考虑去重了。

SPL 解决老大难问题 JOIN

SPL 还能解决 SQL 的老大难问题 JOIN。

SQL 对于 JOIN 的定义并不涉及主键 ,但几乎所有有业务意义的 JOIN 都和主键相关 SQL 无法利用这个特征。采用双边 HASH JOIN 算法不仅计算量大,在外存和集群时的性能还不可控。

SPL 重新定义 JOIN,把 JOIN 分类,可以充分利用主键特征来降低计算量,不仅计算快,也没有不可控的问题。

结果,SPL 的关联表运算性能常常超过 SQL 的宽表。

进一步讨论

SQL 为什么写不出我们期望的高性能算法呢,表面上看是因为对某些运算的支持不足,比如有序计算,分组子集等。更深层原因在于其理论基础 - 关系代数过于简单,缺少必要的数据类型和相应的基础运算。五十年前的理论体系不能表达现代应用的计算需求,也是很正常的。

那么工程上的手段是否可以解决 SQL 慢的问题呢?

比如分布式并行计算。这种手段有时候可以提高性能,但成本太高。

SQL 调优能起到的作用也有限,对于复杂情况不管用。调优确实能通过改变执行路径来采用某些高效算法,但相当多高性能算法在 SQL 中根本没有。

用 UDF 编写高性能代码,原则上可行,但成本太高,可操作性差。集群环境中还会带来复杂的调度和运维问题。而且,如果不能改变数据库存储方案,仅用 UDF 也很难提速。

总之,工程上的优化方法成本高、效果差,没有什么有效手段。SQL 慢是理论层面的问题,工程优化只能有限改善,不能根除。

SPL 则没有继续采用关系代数,而是发明了离散数据集理论,这就会有更丰富的数据类型和基础运算:

图中带星号的都是 SPL 独创的算法。

有了这些基础,就容易写出低复杂度的代码,充分利用硬件资源,实现比 SQL 快出数量级的高性能计算。

SPL 其实提供了集群计算能力,不过在 SPL 的性能优化案例中,大部分场景单机已经足够,集群功能很少有机会得到历练,以至于 SPL 几乎不宣传分布式能力。SPL已开源,欢迎前往乾学院交流沟通。

相关推荐

oracle数据导入导出_oracle数据导入导出工具

关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...

继续学习Python中的while true/break语句

上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个...

python continue和break的区别_python中break语句和continue语句的区别

python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...

简单学Python——关键字6——break和continue

Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...

2-1,0基础学Python之 break退出循环、 continue继续循环 多重循

用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...

Python 中 break 和 continue 傻傻分不清

大家好啊,我是大田。...

python中的流程控制语句:continue、break 和 return使用方法

Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...

L017:continue和break - 教程文案

continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...

作为前端开发者,你都经历过怎样的面试?

已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...

面试被问 const 是否不可变?这样回答才显功底

作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...

2023金九银十必看前端面试题!2w字精品!

导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。...

前端面试总结_前端面试题整理

记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...

由浅入深,66条JavaScript面试知识点(七)

作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录...

2024前端面试真题之—VUE篇_前端面试题vue2020及答案

添加图片注释,不超过140字(可选)...

今年最常见的前端面试题,你会做几道?

在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...