大佬求助,生成 6000 条数据居然花费 30 分钟? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
xzour
V2EX    程序员

大佬求助,生成 6000 条数据居然花费 30 分钟?

  • &nbs;
  •   xzour 2020-12-17 16:41:26 +08:00 3579 次点击
    这是一个创建于 1767 天前的主题,其中的信息可能已经有所发展或是发生改变。

    不知不觉,做开发已有 2 年,传统行业的 IT 开发,需求的大概是全能手,SAP 上了一期,对接了 SAP,了解了一些 ABAP 语法,写了几个接口,慢慢的对语言不在执着,对技术也不再执着,因为出身工厂,对工厂业务还算熟悉,对解决方案的思考还算比较热衷。所以就沉浸在计算的一块,但是技术算法认知的先天不足,所以想请大佬把把脉,以后的学习方向。比如以下一题:

    1. 一个财务的一个清账模拟功能
    2. 已知系统目前有几千个客户
    3. 计算周期为一月一次清账
    • 大概业务逻辑如下:
    1. 有 3 个数组,第一个数组是客户组,大概有几千条。第二个数组是各个收款明细,有几千条,不一定全部客户都有,第三个数组是应收发票,大概有几千条,不一定全部客户都有
    2. 需求是每个客户的收款与发票对碰核销,记录每笔核销的记录及每个客户的期末余额。

    目前这个模块我是通过暴力遍历实现的,但是速度太慢了,逻辑如下

    1. 遍历每个客户
    2. 读取该客户的收款及发票
    3. 遍历收款,取发票一条一条核销,一条销完,换另一张发票,未销完,记录发票 INDEX 及剩余金额
    4. 最后将结果批量插入数据库。 大概 6000 多条核销明细花了我 30 分钟+ 不可忍受。

    因为没到月末,基本收款及发票是不一定真实的,有可能会冲销掉。实时清账就有可能反清账,所以目前清账功能财务需求是预清账,点击就跑,期末定了再关账。

    第二个问题是关于动态计算的表达式引擎问题,2W 条数据动态计算,用了 C#的库,居然还要几分钟。 大概需求如下
    一个单据,有很多属性,然后不同的属性,会对应不同的成本及利润计算公式。所以我把它放在前端用动态表达式的方式配置,而不是代码写 IF (因为情况大概目前有 70+种,再加以后的新的营销活动啥的,财务提一个需求就要改一次代码太麻烦,所以找了一些动态表达式的库,通过前端配置触发条件) for example.

    1. 某个人(参与 A 活动),提成是,毛利提成》销售额提成,则毛利提成,反之销售额提成
    2. 某个人是负责 A 区域的,结果此张单成交区域在区域外,提成需打折。
    3. 某个活动提成不计成本,直接按某个比例算提成。等等。

    1.目前逻辑是,遍历 2W 条,先匹配布尔条件生效判定(可能不止 1 个布尔条件),但终归 70+种情况组组合,最后得到的算法,然后计算提成。

    因为用了 C#的表达式引擎,Flee ( Fast Lightweight Expression Evaluator ),目前用时 3~5 分钟,但是这 5 分钟不太适合体感使用,因为在结算的时候,表达式会调整的,然后重算验证就等的不耐烦了。

    题外话:公司的技术大佬是中小方案很熟练,BS/CS 端直接上手,打印报表配置,运维都可以,中小企业不可少的人才,但是技术深度不行,喜欢代码一把梭,项目 MVC 还是我来了之后分的,以前直接控制器+静态类打天下,我想了一下我又不想成为绑定公司的人才,所以对多语言的学习比较抗拒,对技术逻辑比较感兴趣,对技术遇到的难题,也喜欢思考,但是非科班,所以很多时候都是暴力算法,然后因公司上市,业务发展的复杂度也越来越高,传统行业对 IT 技术人才是不太尊重,感觉目前的代码不重构,会因为复杂度,迟早黑盒子一样的存在.....

    28 条回复    2020-12-18 20:59:11 +08:00
    Easzz
        1
    Easzz  
       2020-12-17 16:51:26 +08:00
    可以每天 /月生成历史数据,出报表直接统计天 /月的数据即可,不用查询明细。
    linksNoFound
        2
    linksNoFound  
       2020-12-17 16:57:38 +08:00
    你看看 io 和 cpu 哪个跑满了,都还空着就试试多进程吧,比改代码判断逻辑简单
    MakeItGreat
        3
    MakeItGreat  
       2020-12-17 17:00:20 +08:00 via Android
    我好像觉得 Excel 更简单
    xzour
        4
    xzour  
    OP
       2020-12-17 17:13:11 +08:00
    @MakeItGreat 就是因为财务随着业务复杂度越来越高,汇总数据太麻烦,开始考虑上系统了呀。
    whosesmile
        5
    whosesmile  
       2020-12-17 17:39:00 +08:00
    我做前端的,看了第一个题目,瞎说下:
    遍历每个客户感觉没必要,因为每个你无非是要找当前客户的的收款明细,倒不如直接一次性检索出所有的收款明细按用户 ID 分组,同样检索所有发票也按用户 ID 分组,然后这两个组都按用户 ID 排序,然后剩下的是算法层次的事情,找两个集合的对对碰。

    前提是你的数据量不大,你的内存足够装下这么多数据,这里的优化是不需要每个 for 迭代都去查询 DB,全部换成内存操作了。
    Bazingal
        6
    Bazingal  
       2020-12-17 17:39:59 +08:00 via Android
    数组转字典,另外是不是可以遍历发票找对应的收款和客户
    whileFalse
        7
    whileFalse  
       2020-12-17 17:40:01 +08:00
    我估计你套了双层 /三层循环。请善用字典。
    whosesmile
        8
    whosesmile  
       2020-12-17 17:41:15 +08:00
    因为我理解你是每个月对账,所以这个用户的数据不是全库的,而是按时间维度收窄的,你的内存应该装得下的。
    Bazingal
        9
    Bazingal  
       2020-12-17 17:43:55 +08:00 via Android
    计算部分根据实际情况使用异步或者并行
    whileFalse
        10
    whileFalse  
       2020-12-17 17:44:38 +08:00
    另外,对“技术深度不行表现在没有 MVC”不能苟同。
    xzour
        11
    xzour  
    OP
       2020-12-17 18:22:34 +08:00
    @whileFalse 一个微型的 ERP 没有 MVC 分层思想不严重吗?
    xzour
        12
    xzour  
    OP
       2020-12-17 18:24:44 +08:00
    @whileFalse 具体表现为控制器方法都是为 3~1000 行的代码,db,业务逻辑耦合一起。
    xzour
        13
    xzour  
    OP
       2020-12-17 18:31:11 +08:00
    @whileFalse 大佬举个数组转字典的优化例子,不太懂怎么用。
    xzour
        14
    xzour  
    OP
       2020-12-17 18:31:28 +08:00
    @Bazingal 大佬举个数组转字典的优化例子,不太懂怎么用。
    xzour
        15
    xzour  
    OP
       2020-12-17 18:33:26 +08:00
    @whosesmile 目前是一次取 DB,客户,收款,发票,然后是内存检索客户对应的收款发票对冲的。 你的思路是个好思路客户 ID 是可以排序的。谢谢
    laminux29
        16
    laminux29  
       2020-12-18 01:16:48 +08:00
    1.C/Cpp 以及传统主流数据库,对于 1000 * ( 1000 + 1000 )规模的两层 for 循环或游标遍历,甚至 1000 * 1000 * 1000 规模的 3 层循环或游标遍历,根本毫无压力。

    如果有瓶颈,需要分析瓶颈在哪。

    比如常见的错误实践,在编程语言中这样写:

    for
    ----for
    ----.----for
    ----.----.----string SQL = "SELECT *....";
    ----.----.----db.search(SQL);

    那么主要的瓶颈就在 [db.search(SQL);] 这条语句这里,具体一点就是编程语言的数据库客户端驱动,与数据库的接口调用这里。这种问题需要把程序逻辑全部移动到数据库的存储过程里,然后编程语言这边对数据库进行一次调用,瓶颈就解决了。


    2.就算不改这些东西,总体时间也只是 30 分钟的话,可以考虑增加 N 台服务器,把数据分为 N 组,每台跑一组,这样总体时间可以缩减到 30 分钟 ÷ N + 汇总时间。
    xzour
        17
    xzour  
    OP
       2020-12-18 09:09:23 +08:00
    @laminux29 为什么要用存储过程这种方式?而不是在遍历之前把所有数据先查询好?是考虑内存限制的问题吗?在多数据源的情况下是不是不好处理了
    l00t
        18
    l00t  
       2020-12-18 09:22:11 +08:00
    @xzour #17 他是怀疑你每次都只读一条,这样大量时间耗费在数据库交互上。如果你一次全部读出来,那自然是无所谓,这个位置也不会是 db.search
    no1xsyzy
        19
    no1xsyzy  
       2020-12-18 09:52:39 +08:00
    @laminux29 6000*6000*6000 按 NOIP 估法,遍历总量除以 1e8 为秒数,大约 36 分钟
    O(n^3) 增长挺快的。
    no1xsyzy
        20
    no1xsyzy  
       2020-12-18 10:02:04 +08:00
    第二个问题可以用局部表达式结果缓存来优化
    动态修改的体感上的话,也可以通过每个被计算的对象依次计算,有值就推给前端来实现至少前几个能够立即响应(简单的 UX 设计)
    甚至前端做动态滚动显示,滚动到的范围才会被求值(需要前端能力)
    laminux29
        21
    laminux29  
       2020-12-18 10:12:08 +08:00
    @xzour 我的意思是,只要不在程序里 for 循环然后一条一条去查就行了。存储过程是我建议的方式,你用其他方式也行。总之瓶颈不要被限制在数据库客户端驱动的 rpc call 就行。
    jj783850915
        22
    jj783850915  
       2020-12-18 12:26:42 +08:00 via Android
    善用表驱动 避免非必要的查找
    tlday
        23
    tlday  
       2020-12-18 12:54:51 +08:00
    第一个问题感觉本质上是数据库结构设计有问题,依我对财务极其粗浅的理解,收款和发票理想不该是一对一,最次不该是多对多的关系么。

    第二个问题我建议你还是从根本上解决,从后端实现,不知道为什么你觉得改前端代码就不算改代码了...而且前端传表达式这种东西,漏洞,bug 会非常多。我不是很懂计算提成这种东西敢放在前端计算是什么操作。即便只是 bool 条件,也是很容易存在很大漏洞的。更别提如果后面上了权限管理,你可能需要 hack 表达式解析引擎了。我查了一下你说的这个 Flee,298 个 star,上一次提交是二月份合并了两个 pr,再上一次提交已经是去年三月份了,这种不仅用的人少而且 maintainer 不活跃的库真的不建议用在生产环境。
    xzour
        24
    xzour  
    OP
       2020-12-18 13:51:33 +08:00
    @tlday 第一个问题:公司的业务收款发票不是一对一,但是给业务员的提成是实时回款结算,回多少结算多少,并且根据产品性质,营销活动,所在区域,下单日期等等落实到具体单据综合结算的。比如一个客户分别在一个月内买了 N 个商品(不同属性),前期预付款 30%,2 个月后付款 70%,这还算比较简单的。你也可以理解为 2W 多条数据,目前有 70 种提成条件这样子。
    所以收款明细与发票,最多能保持总额账的话是相等的。但不代表每期是相等的。收款不是一笔收完的。可以理解为多对多关系。
    第二条,是因为 C#的解决方案好像是没有 JAVA 解决方案多还是,个人能力有限,不能很好的找到替代方案,之前的公司方案是用 sql 来计算 BOOL 和数值的。公式解放给财务配置,我想这是未来财务自动化一个很大的方向。我是想参考 OA 自动工作流中如何自定义审批流来实现的。(云之家,泛微、企业微信的一些审批第三方件都支持),即拖拉思维导图,写上触发条件,即走哪一条线,(当然这次一条数据),比我 2W 条数据会少很多。体感也不错。
    至于 eval 的问题,当然弊端多多,但是既然商业上有这种产品,说明他们是有克服前端传过去执行所产生的的问题。就比如这个 Flee,它分三步,会事先限定每个变量的类型,再渲染公式计算树,最后才计算 bool 或者值,也可以传入自定义的静态方法,反射调用?安全性比我用 sql,用 eval(Javascript )安全多了,当然,表达式引擎 JAVA 好像比较多的解决方案,如果有人有接触过这种动态表达式的开发,不胜感激。
    tlday
        25
    tlday  
       2020-12-18 20:13:49 +08:00
    @xzour 如果我是你,就实时核销,他们要反清帐,对应核销记录就置 revoked 。怎么实时核销,想写了就每次创建收款记录 /发票记录时自己算,不想写了让财务人员自己选,自由还给他们,做错了是他们的问题,你的责任是保证系统不出错,逻辑没问题,尽量提示他们的低级疏忽,尽量简化他们的操作,但没有义务承担他们所有操作的责任。

    如果要看客户期末余额,就收款 /发票两个表 group by user_id,sum 一下相减。
    如果要看发票剩余金额,就发票 /核销两个表 filter by revoked, group by fapiao_id,sum 一下 filter 出值不为 0 的。



    如果不想大改系统,就用 whosesmile 的办法。你要做的实际上是创建这样一个结构:
    [
    { 用户 id: xxx, 收款记录: [],发票记录: [] },
    { 用户 id: xxx, 收款记录: [],发票记录: [] }....
    ]
    然后遍历整个数组,每个项里面的两个数组采用类似“合并两个有序数组”的方法对对碰。
    xzour
        26
    xzour  
    OP
       2020-12-18 20:34:00 +08:00
    @tlday 大概理解了 3N 遍历与 N^3 遍历的代码区别了,十分感谢!我应该是为了省事,没有重新组装数据结构而导致的,手动核销他们是不愿意的,因为核销逻辑大体思路上就是先进先销,都是重复工作,需要解放劳动力的,未来财务更多是检查,现在这种核销,这只是一种规则,未来有可能只是一种配置,据我了解,这也是一种财务自动化的一个具体实施点,类似 SAP 的自动核销,只是 SAP 目前的核销没有细节到发票构成,不满足我司的使用习惯。


    楼上一些回复转字典,HashTable 的我倒现在还不太懂。很抱歉会产生 X-Y 的问题。
    tlday
        27
    tlday  
       2020-12-18 20:48:05 +08:00
    @xzour 他们的意思是类似这种结构
    收款记录:HashMap {
    xxx: [],
    yyy: []...
    }
    发票记录:HashMap {
    xxx: [].
    yyy: []...
    }
    然后遍历 HashMap 两边同 key (用户 id )的数组进行对对碰。


    也可以
    HashMap{
    xxx: Map{ 收款记录: [], 发票记录: [] ... }
    yyy: Map{ 收款记录: [], 发票记录: [] ... } ...
    }



    上面三种结构效果几乎都是等同的,时间复杂度的数量级上不会有什么差异.
    xzour
        28
    xzour  
    OP
       2020-12-18 20:59:11 +08:00
    @tlday 这个我就理解清晰多了,我以为是什么 HashMap<key,value> 能把 value 的这个<T>实体的某些字段做成类似索引的效果,达成 O(1),举的例子又是 int,这就超出我对它的认知了。原来是唯一 key 做文章。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2511 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 11:13 PVG 19:13 LAX 04:13 JFK 07:13
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86