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

就是非常不起眼的帐号去重计数,用 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 step1FROM defined_events e1 JOIN eventgroup ON eventgroup.id = e1.eventgroupWHERE 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_timeFROM defined_events e2 JOIN e1 ON e1.sessionid = e2.sessionid AND visittime > step1_time JOIN eventgroup ON eventgroup.id = e2.eventgroupWHERE 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_timeFROM defined_events e3 JOIN e2 ON e2.sessionid = e3.sessionid AND visittime > step2_timeJOIN eventgroup ON eventgroup.id = e3.eventgroupWHERE 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, tep3FROM 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 ) cntFROM DT AS A JOIN DT AS B ON A.loc=B.loc AND A.tn=B.tnWHERE A.id=a AND B.id<>aGROUP 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.mzph.cn/bicheng/53318.shtml

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

相关文章

缓存解决方案。Redis 和 Amazon ElastiCache 比较

欢迎来到雲闪世界。Redis 和 Amazon ElastiCache 等缓存解决方案是通过将频繁访问的数据存储在内存中来提高应用程序性能的热门选择。让我们从实施简单性、性能、成本和维护方面对它们进行比较。 实施简单 设置 Redis 需要在基础设施或云实例上安装和配置 Redis 服务器。它可…

解决 Android 上的 .NET MAUI/Xamarin.AndroidX 应用调用 ASP.NET Core API 端点时 SSL 连接被拒绝的问题

从虚拟机调用本地API报各种 SSL 连接不上的错误&#xff0c;这给本地调试造成了极大的不便&#xff0c;在被这个问题困扰了多日以后&#xff0c;终于在GitHub上找到答案 基于这个 帖子 &#xff0c;有一个回复 他写了一个帮助类&#xff0c;专门用来调试本地的API&#xff0c;…

常见的图纸加密软件方式:推荐10个最好用的图纸加密软件

在当今竞争激烈的商业环境中&#xff0c;保护设计图纸等敏感信息已成为企业不可忽视的任务。图纸加密软件作为一项重要的信息安全措施&#xff0c;不仅能够防止未经授权的访问&#xff0c;还能确保设计工作的机密性和完整性。面对市场上琳琅满目的加密软件&#xff0c;选择一款…

变压器结构

变压器结构提供磁路&#xff0c;通常称为“变压器铁芯”&#xff0c;旨在为磁场提供流动路径。该磁路对于两个输入和输出绕组之间感应电压必不可少。 然而&#xff0c;这种变压器结构&#xff08;两个绕组缠绕在不同的支路上&#xff09;效率不高&#xff0c;因为初级绕组和次…

Secret

Secret 在 Kubernetes 中&#xff0c;Secret 是一种用于存储敏感信息的资源&#xff0c;比如密码、OAuth 令牌、SSH 密钥等。Secret 允许你以安全的方式将这些信息传递给 Pod&#xff0c;而不是硬编码在镜像或配置文件中。 配置管理中心&#xff1a;configMap、Secret Secre…

数据库和MySQL

ER图 实体&#xff08;矩形&#xff09;&#xff1a;通常是现实世界的业务对象&#xff0c;当然使用一些逻辑对象也可以。 属性&#xff08;椭圆&#xff09;&#xff1a;实体拥有的属性。 联系&#xff08;菱形&#xff09;&#xff1a;实体与实体之间的关系。 函数依赖 函数依…

使用python+opencv解析图像和文本数据

1. 创建虚拟环境 新建文件夹, 并在文件夹中创建虚拟环境,可以使用Vscode打开文件夹, 然后在终端中输入以下命令: python -m venv venv2. 激活虚拟环境 在终端中输入以下命令: venv\Scripts\activate3. 安装依赖 在终端中输入以下命令: pip install opencv-pythonpip inst…

深度学习示例2-多输入多输出的神经网络模型

一、代码示例 from tensorflow import keras from tensorflow.keras import layers import numpy as np# 定义 多输入 多输出的模型 vocabulary_size = 1000 num_tags = 100 num_departments = 4title = keras.Input(shape=(vocabulary_size,), name = "title") tex…

kali——msfconsole的使用

目录 前言 msfconsole的使用基础 启动mfsconsole 各种辅助模块的使用 端口扫描模块&#xff08;portscan&#xff09; 查看端口扫描的模块 使用模块 查看所需设置 手动设置 执行 退出 各种漏洞模块的使用 nmap扫描主机漏洞 查看漏洞模块 使用漏洞模块 查看所需设…

Prometheus通过node_exporter监控Node节点,Node节点的详细指标解读

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

吴恩达深度学习笔记:卷积神经网络(Foundations of Convolutional Neural Networks)1.9-1.10

目录 第四门课 卷积神经网络&#xff08;Convolutional Neural Networks&#xff09;第一周 卷积神经网络&#xff08;Foundations of Convolutional Neural Networks&#xff09;1.9 池化层&#xff08;Pooling layers&#xff09;1.10 卷 积 神 经 网 络 示 例 &#xff08; …

Linux_kernel汇编驱动06

一、ARM汇编语言&#xff08;GUN-gcc编译器下&#xff09; 1、语句格式 {symbol} {instruction|directive|pseudo-instruction} { comment} symbol&#xff1a;为符号。 在ARM汇编语言中&#xff0c;符号必须从一行的行头开始&#xff0c;并且符号中不能包含空格。 在指令和伪指…

基于C++实现(MFC界面)家谱管理系统

一、题目&#xff1a;家谱管理系统 二、内容&#xff1a; 2.1 概述 2.1.1 选题原因 做此题的原因是因为可以比较方便的记录家族历代成员的情况与关系&#xff0c;能很好的保存家族每一代的信息&#xff0c;而不用人工纸质的方式来存取家谱&#xff0c;更便于人们保存和使用…

爬虫练习(js逆向解密)

目标 网站地址交易列表 - 福建省公共资源交易电子公共服务平台 (fj.gov.cn) 抓取内容如下&#xff1a; 分析 查找js代码 点击下一页翻页的时候&#xff0c;查看请求返回的数据&#xff0c;发现data数据是经过加密后得到的 通过全局搜索data,发现有两千多个结果&#xff0c;一个…

AI超强语音转文本SenseVoice,本地化部署教程!

文章目录 模型介绍SenseVoice在线预览链接本地化部署VsCode 远程连接 模型介绍 SenseVoice专注于高精度多语言语音识别、情感辨识和音频事件检测 多语言识别&#xff1a; 采用超过40万小时数据训练&#xff0c;支持超过50种语言&#xff0c;识别效果上优于Whisper模型。富文本…

跨境电商代购系统中前台基本功能介绍:帮助更快的了解跨境代购业务

前台多语言&#xff1a;可支持语言有中文&#xff08;繁体&#xff09;中文&#xff08;简体&#xff09;英文等。多语言使用百度翻译引擎接口实现&#xff0c;翻译效果与百度一致&#xff1b;网站语言分为两大块&#xff1a;1.系统后台有语言包可以编辑修改网站标题以及发布文…

WPS中JS宏使用说明(持续优化...)

前言 好久没发文章了&#xff0c;今天闲来无事发篇文章找找之前的码字感觉。 正文 最近在写教案&#xff0c;发现之前的技术又可以派上用场了。就是JS&#xff0c;全称JavaScript&#xff0c;这个语言太强大了&#xff0c;我发现WPS里的宏现在默认就是JS。功能选项如下图&…

MySQL数据库安装(详细)—>Mariadb的安装(day21)

该网盘链接有效期为7天&#xff0c;有需要评论区扣我&#xff1a; 通过网盘分享的文件&#xff1a;mariadb-10.3.7-winx64.msi 链接: https://pan.baidu.com/s/1-r_w3NuP8amhIEedmTkWsQ?pwd2ua7 提取码: 2ua7 1 双击打开安装软件 本次安装的是mariaDB&#xff0c;双击打开mar…

Java运行环境的下载、安装、配置与运行

一、实验目的及要求 目的&#xff1a;掌握如何下载java JDK软件包&#xff0c;如何设置Java程序的运行环境&#xff0c;如何编写与运行Java程序&#xff0c;了解Java概貌。 要求&#xff1a; 1、安装Java JDK软件包&#xff1b; 2、练习编写简单的Java Application程序并掌握…

vscode安装使用plantuml插件

使用 VSCode 插件 如果你在 Visual Studio Code 中使用 PlantUML 插件&#xff0c;你可以按照以下步骤生成图片&#xff1a; 安装 PlantUML 插件&#xff1a; 在 VSCode 的扩展市场中搜索并安装 PlantUML 插件。 配置插件&#xff1a; 打开设置&#xff0c;确保插件配置正确。…