前段时间,在项目上有这样一个场景,计算某段时间的日均余额,这个时间段有本月、本季、本年。。。
逻辑实现就是累计这个时间段内每一天的余额,除以这个时间段内的天数。
其中源表结构是这样的(已去掉其他业务逻辑,只抽取算累计值相关代码):
create table ll_account_bal(--余额拉链表,记录这个时间段的余额,区间段是左闭右开
account_no varchar(18),--账号
begin_date date,--余额开始日期
end_date date,--余额结束日期
bal decimal(18,2));--余额
累计的时间段假如是本年,目标表如下:
create table cumu_bal(
account_no varchar(18),--账号
y_cumu_bal decimal(18,2));--本年余额累计值
y_cumu_bal需要按天累计从本年1月1日的余额,到当前日期的余额。
那么问题来了,刚上线的时候,这部分数据怎么初始化呢,数据可是很庞大,时间跨度也很大啊。
咨询了业界好几位有资历的DBA,给的答案都是分多个批量,一天一天往上累计,这也是目前项目里面常规的处理方式。
求助无果,不想就这样被打败,其实主要是害怕后续业务规则再有改动,或者某个细节没注意算错,再去重新累计,太重量级,怕给以后的自己添麻烦,费点时间自己琢磨琢磨吧。
本场景中,检索一次余额拉链表时间复杂度为O(1),决定执行效率的有这几个因素:
表中数据量 r:
决定O(1)的成本,我们可以通过增加where条件尽量缩小捞库的范围,优化sql提高执行效率等方式,提高效率,但是无法降低一个度
指标数量N (实际开发中有 本月/本季/本年 的 种类1余额/种类2余额/...... 本例中已简化只用了本年累计余额, 这些个指标根据乘法一交叉,需要计算的指标数量是N):
如果对每个指标检索一次余额拉链表,那时间复杂度就是O(N),我们尽量把所有指标放到一个逻辑中去时间,将时间复杂度降为O(1)
累计天数 d
如果分批量每天往上累一次,那时间负责度就是O(d),我们尝试是否能把这个因素抹去,将时间复杂度降为O(1)
如下几种方案:
1. 按照每个指标每天往上累计,时间复杂度O(ND)
2. 在一个逻辑中计算指标,并按天累计,也就是目前传统的处理方式,时间复杂度O(d)
3. 将天数也封装到逻辑中,搭建一个算法的框架,推导出一套公式,套用公式计算,时间复杂度O(1),这也是本帖下面要说的内容。
若要实现方案3,必须在检索的时候计算每行的累计值,最后通过sum group by来实现对每个阶段结果的累计。
步骤一:计算每行累计的阶段结果
对于表中任意一行数据,我们引入4个变量:
期初日期i_date,如本年是2021年1月1日
本行开始日期b_date
本行结束日期e_date
当天日期t_date
对于每行数据,有效累计余额为,本行开始日期到本行结束日期,与当期日期即本年求交集,乘本行余额,因此对于每行数据,我们有:
r_cumu_bal=[b_date,e_date)∩[i_date,t_date]*bal (r_cumu_bal表示行累计值的阶段结果值)
对于以上四个变量,我们有如下永恒规则:
e_date>b_date
t_date>=i_date
t_date>=b_date
以上命题等价于:
r_cumu_bal={min(e_date-1,t_date)-[max(b_date,i_date)-1]}*bal
带入边界条件验证公式,如图所示
1. 对于整个逻辑i_date和t_date是定值,i_date永远不会变,t_date由初始化日期确定,因此边界条件为:
t_date=i_date
2. 对于每行数据,边界条件有:
i_date=e_date
i_date=b_date
e_date=t_date
b_date=t_date
带入公式验证后,公式均试用。
但有一种特殊情况,实际开发的时候,我们有本年累计,本月累计等N个指标,因为要一次检索计算这N个指标,所以捞库的where条件应以最大时间段的过滤,但是在计算本月累计时,可能存在如图灰色的线段条情况,与当期并无交集,比如初始化时间为2021年2月27日,表中存在2021年1月1日-2021年1月10日的数据,计算本月累计时显然应该算成0。但是带入公式后,计算的累计值为负数,因此需要对以上公式进行调整:
r_cumu_bal={min(e_date-1,t_date)-[max(b_date,i_date)-1]}*bal>0?{min(e_date-1,t_date)-[max(b_date,i_date)-1]}*bal:0
将公式转化为sql为:
select
account_no,
decode(sign(min(days(end_date)-1,days(t_date))-(days(max(begin_date,i_date))-1)),-1,0,min(days(end_date)-1,days(t_date))-(days(max(begin_date,i_date))-1) )*bal r_cumu_bal
from ll_account_bal
假如表中数据为:
假如初始化日期为2021-02-27,那执行的sql语句为:
select
account_no,
decode(sign(min(days(end_date)-1,days('2021-02-27'))-(days(max(begin_date,'2021-01-01'))-1)),-1,0,min(days(end_date)-1,days('2021-02-27'))-(days(max(begin_date,'2021-01-01'))-1) )*bal r_cumu_bal
from ll_account_bal
结果为:
验证正确,实际开发中要造几条数据验证边界条件,做好充分验证确保正确性。
步骤二:使用sum group by 计算累计值
修改sql如下:
select
account_no,
sum(decode(sign(min(days(end_date)-1,days('2021-02-27'))-(days(max(begin_date,'2021-01-01'))-1)),-1,0,min(days(end_date)-1,days('2021-02-27'))-(days(max(begin_date,'2021-01-01'))-1) )*bal ) r_cumu_bal
from ll_account_bal
group by account_no
结果为:
验证成功,ok,这样上线后发现自己某天累计错了,业务想改规则,也不怕了,有框架套公式重新一执行就ok了。
此时还要注意优化sql,降低sql执行时间,就比如我的sql平时在仿真环境跑得很快,初始化当天执行的有些慢了,后来领导获取db2建议建上两个索引,就很快了,即使时间复杂度是O(1)了,也要继续优化,争取做到最优。