数据库太慢跑崩的一大罪魁

news2024/11/15 19:36:19

就是非常不起眼的帐号去重计数,用 SQL 写就是 COUNT(DISTINCT …)。

帐号去重计数在商业分析中很常见也有重要的业务意义。这里的帐号可能是用户 ID、银行帐户、手机号、车牌号、…。计算逻辑基本一样,就是从某个时段的历史数据中统计出有多少个帐号满足某个条件。
比如,上个月有多少辆汽车去过北京?上周有多少手机在凌晨 2 点 -4 点之间有过通话?今年收到过海外汇款的银行帐户有多少?…
历史数据通常只会记录发生过的事件,比如某辆车在某时刻在某地出现,某个手机在某时刻和谁通话,某个银行帐户在某日期发生过汇入汇出,…。用指定条件直接筛选历史记录会得到很多帐号相同的记录(某辆车可能多次去过北京,…),但只能按 1 次计数,所以要做去重处理。

去重计数中的筛选条件经常并不会像上面那例子中那么简单。比如,今年发生过周消费总额超过 1000 元的信用卡帐户有多少?需要先统计帐户每周的消费额再来筛选;上月中发生过连续三天消费额超过 100 元的帐户有多少?需要对帐户的交易进行更复杂的存在性判断。不过,无论怎样的条件,最后都归结于计算 COUNT(DISTINCT)。
著名的电商漏斗统计就是这类复杂化的 COUNT(DISTINCT),只不过它会更复杂一些。漏斗统计涉及多步有次序的事件,每一步都对应有一个 COUNT(DISTINCT),以便和上一步的 COUNT(DISTINCT) 一起计算这一步的客户流失率;下一步的 COUNT(DISTINCT) 要在上一步的基础上筛选。并且要考虑事件发生的次序。整个过程比较复杂,但本质上仍然是在做 COUNT(DISTINCT)。

COUNT(DISTINCT) 却一直是数据库计算的难题,通常都会非常慢,如果数据量大(帐号数多,这也是常态),还有可能导致数据库崩掉。
这是为什么呢?

因为 COUNT(DISINCT) 计算量很大。COUNT(DISTINCT id) 需要把遍历过的不同的 id 都保存成一个列表,下一个 id 要和这个列表比对才知道是不是新的,以决定是否要增加计数值并将其添加进列表中。而普通的 COUNT(id) 并不需要保存遍历过的 id,当然也不需要比对,这显然要简单很多。COUNT(DISTINCT) 在 SQL 中的地位看起来和 COUNT,SUM 差不多,但计算速度却要慢得多。
而且,很多数据库在计算 COUNT(DISTINCT id) 时,会把上述的 id 列表放在内存中,这样才能高速的访问和比对,但如果帐号数很多时,内存很可能就装不下,于是跑崩不可避免。如果把这个 id 列表缓存到外存,虽然可以避免崩掉,但访问和比对都不方便,性能会进一步急剧下降。

好在,COUNT(DISTINCT id) 计算中的 id 通常只是一列数据,100G 内存在极限时可能装下几十亿个 id,超出大多数应用的帐号数量了,所以计算常规的 COUNT(DISTINCT) 虽然慢一点,还不致于会频繁跑崩。
但情况再复杂一些就不一定了。比如,漏斗统计会有多个搅和在一起的 COUNT(DISTINCT),写出 SQL 会有嵌套的 JOIN,这时候想跑得快,占用的内存就会大得多(JOIN 也会导致内存和性能之间的严重矛盾),跑崩的概率也就会陡增。

下面是用 SQL 实现的某实际场景的三步漏斗分析:

WITH e1 AS (
    SELECT userid, visittime AS step1_time, MIN(sessionid) AS sessionid, 1 AS step1
    FROM defined_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 defined_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 defined_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 funnel_conversions.step1 IS NOT NULL THEN funnel_conversions.step1_userid ELSE NULL END) AS step1_count,
    COUNT(DISTINCT CASE WHEN funnel_conversions.step2 IS NOT NULL THEN funnel_conversions.step2_userid ELSE NULL END) AS step2_count,
    COUNT(DISTINCT CASE WHEN funnel_conversions.step3 IS NOT NULL THEN funnel_conversions.step3_userid ELSE NULL END) AS step3_count,
    COUNT(DISTINCT CASE WHEN funnel_conversions.step3 IS NOT NULL THEN funnel_conversions.step3_userid ELSE NULL END)
        / COUNT(DISTINCT CASE WHEN funnel_conversions.step1 IS NOT NULL THEN funnel_conversions.step1_userid ELSE NULL END) AS step3_rate
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, tep3
    FROM e1 LEFT JOIN e2 ON e1.userid=e2.userid LEFT JOIN e3 ON e2.userid=e3.userid ) funnel_conversions
LEFT JOIN sessions s ON funnel_conversions.step1_sessionid = s.id
GROUP BY s.devicetype

可以看出,这里不仅有多个 COUNT(DISTINCT),还有多个自关联子查询以实现复杂的漏斗步骤判断。这个 SQL 语句在 Snowflake 的 Medium 级集群(4 节点)三分钟没跑出结果。

那么,该怎么解决这个讨厌的 COUNT(DISTINCT) 呢?
其实并不难,只要把数据按帐号排序后,COUNT(DISINCT) 就很容易算了。
数据对 id 有序时,再计算 COUNT(DISTINCT id) 时,只要保存上一个 id 这一个值就可以了。下一个 id 如果和上一个 id 相等,则增加当前保存 id 的计数,如果不同则替换这个保存的 id 并将计数清 0。不需要在一个大列表中查找比对,只保存一个 id 时占用的内存几乎可以忽略不计。跑得快也不可能崩掉。
对于漏斗这种复杂任务也没问题,数据对 id 有序时,可以每次读入一个 id 的数据进内存,然后可以方便地进行复杂的计算,过程中不会涉及其它 id 的数据,原则上内存只要能装下一个 id 的数据就可以了,同时保存多套 COUNT(DISTINCT) 的计数值也没问题。

可惜,关系数据库和 SQL 做不到这一点。
作为关系数据库理论基础的关系代数是基于无序集合的,SQL 中集合成员(表的记录)没有次序,数据库在存储数据在理论上也不支持有序。上述的优化逻辑在 SQL 中无法实现。

esProc SPL 可以!
esProc SPL 严格地说并不是一个数据库,而是个专业的计算引擎。它不再采用关系代数了,而是自创了以有序集合为基础的离散数据集理论并发明了新程序语言 SPL。esProc 在存储数据时会刻意支持有序,SPL 也提供了丰富的有序计算方法,这样就可以高性能低资源地实现帐号去重统计了。

SPL 的 icount 函数用去重计数,缺省情况会用前述方法实现,即保持一个遍历过的不同 id 的列表,下一个 id 要和列表对比。数据对 id 有序则使用 icount@o,这时候 SPL 就会采用有序去重计数的算法,即只保持上一个 id 值用于比对。
特别地,SPL 还支持有序游标,数据对 id 有序时,可以每次取出 id 相同的一批数据,然后进行复杂的计算以判断当前 id 是否满足筛选条件,之后的计数可以直接用 COUNT,连保持上一个 id 再比对的动作都不需要了(在游标取数时已经比对过了)。

漏斗分析就可以用这个机制实现:

A
1=now()
2=eventgroup=file("eventgroup.btx").import@b()
3=devicetype=file("devicetype.btx").import@b()
4=long(elapse(arg_date,-14))
5=long(arg_date)
6=long(arg_date+1)
7=A2.(case(NAME,"SiteVisit":1,"ProductDetailPage":2,"OrderConfirmationType1":3;null))
8=file("defined_events.ctx").open()
9=A8.cursor@m(USERID,SESSIONID,VISITTIME,EVENTGROUPNO;VISITTIME>=A4 && VISITTIME<A6,EVENTGROUPNO:A7:#)
10=sessions=file("sessions.ctx").open().cursor@m(USERID,ID,DEVICETYPENO;;A9)
11=A9.joinx@m(USERID:SESSIONID,A10:USERID:ID,DEVICETYPENO)
12=A11.group(USERID)
13=A12.new(~.align@a(3,EVENTGROUPNO):e,e(1).select(VISITTIME<A5).group@u1(VISITTIME):e1,e(2).group@o(SESSIONID):e2,e(3):e3)
14=A13.run(e=join@m(e1:e1,SESSIONID;e2:e2,SESSIONID).select(e2=e2.select(VISITTIME>e1.VISITTIME && VISITTIME<e1.VISITTIME+86400000).min(VISITTIME) ) )
15=A14.run(e0=e1.id(DEVICETYPENO),e1=e.min(e1.VISITTIME),e2=e.min(e2),e=e.min(e1.SESSIONID),e3=e3.select(SESSIONID==e && VISITTIME>e2 && VISITTIME<e1+86400000).min(VISITTIME),e=e0)
16=A15.news(e;~:DEVICETYPE,e2,e3)
17=A16.groups(DEVICETYPE;count(1):STEP1_COUNT,count(e2):STEP2_COUNT,count(e3):STEP3_COUNT,null:STEP3_RATE)
18=A17.run(DEVICETYPE=devicetype.m(DEVICETYPE).DEVICETYPE,STEP3_RATE=STEP3_COUNT/STEP1_COUNT)
19=interval@s(A1,now())

(SPL 代码写在格子里,这和普通程序语言很不像,参考这里写在格子里的程序语言)

A12 每次读出一个 USERID 的数据进行后续判断,到 A17 再计算时就直接用 count 函数,不需要 icount 了。这个代码不仅更简洁通用(做更多步漏斗只要改 A7,而 SQL 代码要加很多子查询),跑得也更快,使用 Snowflake 同规格的 EC2,只用单台 10 秒即可完成。

其实,去重计数只是个表现,这个问题的实质在于以帐号为单位来切分数据再做处理。按帐号做分组汇总也是类似的运算,而有些汇总并不是简单的 SUM/MAX 这些,有时要很复杂的过程才能计算出来。比如计算每部手机通话时长小于 3 秒的次数;计算上月信用卡帐户的新增积分,规则是有连续三天消费超过 100 元时这几天的积分将加倍;…
信息系统中几乎所有事件性质的数据都会挂在某个帐号下,所以这类运算非常普遍,在各种查询跑批任务中都会碰到,可以说是最常见的业务逻辑模型之一了。有了 esProc SPL 这种基于有序存储上的有序运算,这一大类问题就都可以简洁且高性能的实现了,而对于 SQL 体系的关系数据库即非常困难。

这里举一个变种例子,一个时空碰撞问题,找出和某指定手机在同一时间段和同一地点出现过次数最多的前 20 个手机,数据规模约 250 亿行。SQL 写出来大概是这样:

WITH DT AS ( SELECT DISTINCT id, ROUND(tm/900)+1 as tn, loc FROM T WHERE tm<3*86400)
SELECT * FROM (
    SELECT B.id id, COUNT( DISINCT B.tn ) cnt
    FROM DT AS A JOIN DT AS B ON A.loc=B.loc AND A.tn=B.tn
    WHERE A.id=a AND B.id<>a
    GROUP BY id )
ORDER BY cnt DESC
LIMIT 20

这里又有嵌套的 DISTINCT 运算以及自关联 JOIN,单节点的 ClickHouse 直接崩掉,动用了 5 节点的集群用了 30 多分钟才跑出来。
SPL 代码利用了有序存储和前面提到的有序游标,可以有效避免这些困难,只用一个节点不到 6 分钟就跑完。

A
1=now()
2>NL=100000,NT=3*96
3=file("T.ctx").open()
4=A3.cursor(tm,loc;id==a).fetch().align(NL*NT,(loc-1)*NT+tm\900+1)
5=A3.cursor@mv(;id!=a && A4((loc-1)*NT+tm\900+1))
6=A5.group@s(id;icount@o(tm\900):cnt).total(top(-20;cnt))
7=interval@ms(A1,now())

细心的读者可能会发现,esProc SPL 的算法有效性依赖于数据对 id 有序,而数据产生次序通常不会是 id,而是时间。那么,这个算法是不是只能应用于事先排序过的历史数据上,对来不及一起排序的新数据就无效了呢?
esProc 已经考虑到这一点,SPL 的复组表可以在数据进入时实现增量排序,实时保证数据在读出时对 id 有序,可以让这套有序计算方案应用到最新的数据上。而且,这类运算通常都会涉及时间区间,SPL 的虚表支持双维有序机制,可以迅速将时间区间外的数据过滤掉,进一步提升运算性能。

esProc 是个纯 Java 软件,能在任何有 JVM 的环境下运算,可以无缝地嵌入到 Java 程序中,非常轻量地将数据仓库的运算能力赋予给各种场景下的应用中。
esProc 提供了可视的开发环境,支持单步执行、设置断点、所见即所得的结果预览,开发调试要比 SQL 和存储过程方便得多。
SPL 还有完善的流程控制语句,像 for 循环,if 分支都不在话下,还支持子程序调用,拥有存储过程才有的过程化能力,可以全面取代 SQL 和存储过程。

最后,esProc SPL 是开源免费的。SPL开源地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2106947.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

k8s的Ingress控制器安装

Ingress文档地址&#xff1a;Ingress文档 1.安装helm 官网地址&#xff1a;helm官网安装 wget https://get.helm.sh/helm-v3.2.3-linux.amd64.tar.gz tar -zxvf helm-v3.2.3-linux-amd64.tar.gz cp linux-amd64/helm /usr/local/bin/ rootmaster01:~# helm version version.B…

​覆盖90%开发场景的《Flutter开发手册》​

哈喽&#xff0c;我是老刘 我本人是带客户端团队的&#xff0c;带着团队切换到Flutter上也已经差不多6年了。 但也不是只写客户端App。基于Flutter来说&#xff0c;也写过桌面端、Web端程序。 但是坦白说&#xff0c;真没有一个Flutter项目是同时覆盖6个平台的。 为啥呢&…

分布式版本管理工具——Git拉取Github项目到本地仓库

Git拉取&#xff08;下载&#xff09;github项目到本地仓库 一、前言二、拉取github项目至本地三、结束语 一、前言 不得不说&#xff0c;如果懂得利用好Git以及github&#xff0c;能够在很大程度上帮助到我们的日常学习与工作&#xff0c;这里我简单的给大家介绍如何使用Git下…

内部类(定义在类的内部)

a.成员内部类&#xff08;Inner Class&#xff09; 1.不能单独存在&#xff0c;必须依附于外部类。 2.Outer.Inner inner outer.new Inner(); 3.内部类可以访问当前外部类的private属性和方法&#xff0c;可以用”外部类名.this“访问外部实例的属性和方法。 …

Windows键盘快捷方式

键盘快捷方式是两个或多个键的组合&#xff0c;可用于执行通常需要鼠标或其他指针设备才能执行的任务。 使用键盘快捷方式你可以更轻松地与电脑进行交互&#xff0c;从而在使用 Windows 和其他应用时节省时间和精力。 大多数应用还提供加速键&#xff0c;以让你能够更轻松地使…

机器人笛卡尔空间轨迹规划原理与MATLAB实现

机器人笛卡尔空间轨迹规划是指在给定的笛卡尔坐标系&#xff08;通常是三维空间坐标系&#xff09;中规划机器人的末端执行器&#xff08;如抓手、焊枪等&#xff09;的移动路径。这种规划方式直观且易于理解&#xff0c;因为它直接关联到任务空间中机器人的位置和姿态。下面将…

分享购买率拉满的8个商品详情页设计技巧!

电子商务网站的商品详情页是商家和设计师关注的重点之一。这一页面在用户决策是否购买商品方面发挥着至关重要的作用。如果一个优质商品没有配套有效的详情页&#xff0c;其转化率将会显著下降&#xff0c;从而影响流量和销量&#xff0c;最终可能导致店铺的淘汰。无论销售何种…

[ComfyUI]Flux​:不花钱免费白嫖最强反推JoyCaption​,仅需几步无门槛轻松搞定

大家好我是极客菌&#xff01;&#xff01;&#xff01; 今天文章主题将为大家介绍一款优秀的图像反推模型&#xff1a;Joy Caption。这是由作者Fancy Feast开发的Joy Caption模型&#xff0c;是在谷歌的SigLIP模型和Meta的最新Llama3.1 模型的基础之上&#xff0c;使用Adapte…

数学建模竞赛论文写作方法

数模竞赛论文结构剖析 摘要问题重述模型假设符号说明问题分析模型建立模型求解结果及分析检验与推广模型评价参考文献附录 摘要 摘要一般应包括&#xff1a; 用一两句话说明原题中要求解决的问题明确说明建立了什么模型&#xff0c;在数学上属于什么类型&#xff0c;建模的…

Spring Boot技术构建的创新在线拍卖系统

系统测试 1.1系统测试的目的 程序设计不能保证没有错误&#xff0c;这是一个开发过程&#xff0c;在错误或错误的过程中都是难以避免的。虽然这是不可避免的&#xff0c;但我们不能使这些错误始终存在于系统中&#xff0c;错误可能会造成无法估量的后果&#xff0c;如系统崩溃&…

PyTorch:优化读取LMDB数据的五大策略

LMDB:http://www.lmdb.tech/doc/index.html lmdb存放千万级别数据IO问题 https://github.com/OFA-Sys/Chinese-CLIP/issues/46 PyTorch LMDB读取慢的原因 LMDB是一种嵌入式键值存储引擎&#xff0c;它具有快速、可靠和高效的特点。然而&#xff0c;与常规文件格式相比&#…

提前购|基于SSM+vue的创新型产品提前购平台(源码+数据库+文档)

创新型产品提前购平台 基于SSMvue的创新型产品提前购平台 一、前言 二、系统设计 三、系统功能设计 系统功能实现 后台模块实现 管理员模块实现 发布企业管理实现 个体管理实现 投资企业管理实现 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选…

110001安庆巡检_工艺巡检

安庆巡检_工艺巡检 一. 工艺配置二. 点检计划三. 点检任务四. 复检任务1. 复检列表1.1 页面展示 2. 复检任务下发2.1 操作说明2.2 业务说明2.3 表关联说明ps_recheck_task工艺工序参数_复检详情表 3. 复检详情2.1 获取参数点检详情2.2 获取复检详情列表 4. app端复检任务提交4.…

1989-2022年各省基尼系数、城镇基尼系数、农村基尼系数数据

1989-2022年各省基尼系数、城镇基尼系数、农村基尼系数数据 1、时间&#xff1a;1989-2022年 2、来源&#xff1a;各省年鉴、统计年鉴 3、范围&#xff1a;31省 4、指标&#xff1a;基尼系数、城镇基尼系数、农村基尼系数 5、计算说明&#xff1a;参照田为民的文章《中国基…

河南省第三届职业技能大赛 网站技术(世赛选拔)项目任务书

河南省第三届职业技能大赛 网络安全&#xff08;世赛选拔&#xff09;项目样题 Module_A 网页设计制作&#xff1a;一、项目简介&#xff1a;二、具体要求1. 网站基本元素2. 内容区域3.网站页面开发 三、该模块要收集的文件 Module_B 后台模块开发:Module_C 前端游戏设计:一、游…

Matter协议解析

Matter协议文档&#xff1a; https://csa-iot.org/wp-content/uploads/2024/05/matter-1-3-core-specification.pdf https://csa-iot.org/wp-content/uploads/2024/05/matter-application-cluster-specification.pdf https://csa-iot.org/wp-content/uploads/2024/05/matter-1…

2024年【陕西省安全员C证】免费试题及陕西省安全员C证作业模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年陕西省安全员C证免费试题为正在备考陕西省安全员C证操作证的学员准备的理论考试专题&#xff0c;每个月更新的陕西省安全员C证作业模拟考试祝您顺利通过陕西省安全员C证考试。 1、【多选题】下列关于坡度大于25…

坐牢第三十五天(c++)

一.作业 1.使用模版类自定义栈 代码&#xff1a; #include <iostream> using namespace std; template<typename T> // 封装一个栈 class stcak { private:T *data; //int max_size; // 最大容量int top; // 下标 public:// 无参构造函数stcak();// 有参…

Linux操作系统 :文件管理(实验报告)

一、实验目的 熟练掌握Shell特性和文件管理命令的使用方法 二、实验环境 硬件&#xff1a;PC电脑一台&#xff0c;网络正常&#xff1b; 配置&#xff1a;win10系统&#xff0c;内存大于8G 硬盘500G及以上 软件&#xff1a;VMware、Ubuntu16.04、 三、实验内容 Linux的文件…

【python】socket 入门以及多线程tcp链接

Socket 入门 及 多线程tcp链接 网络基础知识三要素 Socket是套接字的意思,是网络编程的核心对象,通信两端都独有自己的Socket对象, 数据在两个Socket之间通过 字节流(TCP协议) 或者 数据报包(UDP协议)的形式进行传输. 本文主要针对tcp流程进行讲解 socket-tcp流程图 1.创建服…