一个update函数执行时间从5分钟提升到0.5s的进化之旅

接触php一年左右,系统初写的时候我没有过多的考虑性能问题,能简则简,能用一句sql解决的问题绝不多写一个标点符号的代码,于是为更新汇总子订单成本写了这个函数

    /**
     * 更新子订单成本
     * @param array $tidList 可选 要更新的tid 不传参数全量更新
     * @return int
     */
    public function updateOrderCost(array $tidList = [])
    {
        $sql = "UPDATE taobao_trade_order a SET a.cost = IFNULL((SELECT SUM(cost) FROM taobao_sku_modify WHERE tid = a.tid AND oid = a.oid AND status = 1),0)";
        if (count($tidList)) {
            $tid = implode(',', $tidList);
            $sql .= " WHERE tid in ({$tid})";
        }
        return DB::SlimPDO()->exec($sql);
    }

使用中因为$tidList一直都是有传参进行部分更新,每次更新条目最多不超过10条,并没察觉到有什么效率问题。
然后今天偶然来个空值全量更新,好啦,瞬间mysql锁死,整个系统处于瘫痪状态,等了足足差不多 5分钟 才把这段语句执行完。
taobao_trade_order只有不到3000条数据,taobao_sku_modify也就6000多而已,这么点数据量让系统锁死5分钟,这段代码亟待优化了。
出于我能简就简的习惯,我一上来依然是

从sql语句上着手,看看是否有提升空间。

tid,oid,status几个关键字段已经设置索引。
然后观察到这段代码执行的时候每次SET cost的时候都要执行后面的SELECT 语句,等于要执行2000多次SELECT ,我要是把这段SELECT的数据建一个临时表,然后update从这个临时表里面读数据再更新如何呢?
可是很快发现这个创建临时表的语句我 写不出来.....

同系统下另一个函数有这样的一个sql语句

UPDATE taobao_trade a SET a.refund_fee = (SELECT SUM(refund_fee) FROM taobao_trade_order WHERE tid = a.tid) //45s

这条语句的表现比上面那个略好,嗯....主表 taobao_trade 1800条 子表 taobao_trade_order 不到3000条的数据,执行一次 45s

这个条件简单 我先拿这条语句创建临时表并更新

CREATE TEMPORARY TABLE tmp_table (     
    SELECT tid, SUM(refund_fee) AS refund_fee FROM taobao_trade_order GROUP BY tid 
)  //创建临时表 0.02s

UPDATE taobao_trade a SET a.refund_fee = (SELECT refund_fee FROM tmp_table WHERE tid = a.tid) //执行更新 14.22s

好嘛,确实有用,从45s升级到14.22s 效率提升了3倍还多,但这效率。。。依然并卵。
于是我也不再烦恼上面那个语句怎么写CREATE TEMPORARY TABLE了,这条路不通!
不过如果有人知道这种多个字段去重,并按某个字段的字段量来汇总另一个字段怎么建表希望不吝赐教。

既然mysql上面没有好的解决办法,只能考虑从

php代码上优化

通俗的讲,这段要实现的是通过库存变更表taobao_sku_modify计算每个子订单成本,然后赋值到子订单表相关联的子订单上,通过tid和oid2个字段判断关联性。
我首先把代码改成这样

public function updateOrderCost(array $tidList = [])
    {
        $search = DB::SlimPDO()
            ->select(['tid', 'oid', 'cost'])
            ->from('taobao_sku_modify')
            ->where('status', '=', 1)
            ->where('oid', '<>', '');

        if (count($tidList)) {
            $search->whereIn('tid', $tidList);
        }

        $list = $search->orderBy('tid')->execute()->fetchAll();

        function update($result)
        {
            DB::SlimPDO()
                ->update(['cost' => $result['cost']])
                ->table('taobao_trade_order')
                ->where('tid', '=', $result['tid'])
                ->where('oid', '=', $result['oid'])
                ->execute();
        }

        $result = [
            'tid' => '',
            'oid' => '',
            'cost' => 0
        ];

        foreach ($list as $item) {
            if ($result['tid'] !== $item['tid']) {
                if ($result['tid']) {
                    update($result);
                }
                $result = [
                    'tid' => $item['tid'],
                    'oid' => $item['oid'],
                    'cost' => 0
                ];
            } elseif ($result['oid'] !== $item['oid']) {
                update($result);
                $result['oid'] = $item['oid'];
                $result['cost'] = 0;
            }
            $result['cost'] += $item['cost'];
        }
        update($result);
    }

这就是我更愿意一句sql解决的原因,与原函数做一样的事情,一句sql变成了这么长一段函数,我已尽量简洁,但是看起来还是没有sql语句直观,算了,执行看看效率。
执行结果是卓有成效的,因为这次系统只卡了 86.977087020874s
从5分钟提升到差不多一分半钟,效率提升了3倍,我想如果把数据提取成临时表进行更新也差不多是这个效率了,因为它们逻辑一样。
这种效率显然不是我想要的,而且我也很快找出了问题,update函数执行了3000多次,但是执行结果都是0,也就是之前执行更新cost数据与重新计算的cost一样,并没有数据被更新,我是否可以在php层判断结果是否一致,而避免不断的做update呢,当然可以,我把函数上半部分的代码改成这样

 public function updateOrderCost(array $tidList = [])
    {
        $search = DB::SlimPDO()
            ->select(['tid', 'oid', 'cost'])
            ->from('taobao_sku_modify')
            ->where('status', '=', 1)
            ->where('oid', '<>', '');

        $sql = "SELECT cost, tid, oid FROM taobao_trade_order";         //新增

        if (count($tidList)) {
            $search->whereIn('tid', $tidList);
            $tid = implode(',', $tidList);
            $sql .= " WHERE tid in ({$tid})";                           //新增
        }

        $list = $search->orderBy('tid')->execute()->fetchAll();

        $orderList = DB::SlimPDO()->query($sql)->fetchAll();            //新增

        $_update = function ($result) use ($orderList)                  //优化
        {
            if (!Func::array_find($orderList, $result)) {               //通过判断orderList中是否有与result键名键值一致的数组
                DB::SlimPDO()
                    ->update(['cost' => $result['cost']])
                    ->table('taobao_trade_order')
                    ->where('tid', '=', $result['tid'])
                    ->where('oid', '=', $result['oid'])
                    ->execute();
            }
        };

执行看看,好的,有效,这次的执行时间是 29.660382032394s
还能更快吗,这效率依然不够啊,这段代码的主要时间浪费在Func::array_find的查询上,这是一个自定义的函数,用在判断参数一的二维数组是否有包含参数二的所有同键名键值的数组,参数二可以是闭包函数也可以是字符串。
我认为Func::array_find已经没有提升空间,只能找别的方法来替代,第一个想到的是in_array,可是in_array不能筛二维数组,那我把源数据直接改成一维的吧,反正只有三个字段
为了得到一维数组我把

$orderList = DB::SlimPDO()->query($sql)->fetchAll();

改成

$orderList = array_map(function ($item) {
            return $item['tid'] . $item['oid'] . $item['cost'];
        }, DB::SlimPDO()->query($sql)->fetchAll());

然后更新了update函数的判断方式

$_update = function ($result) use ($orderList)    
        {
            $test = $result['tid'] . $result['oid'] . $result['cost'];
            if (!in_array($test, $orderList)) {
                DB::SlimPDO()
                    ->update(['cost' => $result['cost']])
                    ->table('taobao_trade_order')
                    ->where('tid', '=', $result['tid'])
                    ->where('oid', '=', $result['oid'])
                    ->execute();
            }
        };

这次我信心满满,因为已经用了系统内置的函数,应该没什么大问题了。
按下执行键的时候我快斯巴达了,还是卡住了,这次的执行时间是 30.925987958908s
跟自定义的Func::array_find判断函数比 in_array并没有带来性能的提升。
怎么办,我已经得到了差不多3000条数据的一维数组$orderList ,要让它进行差不多3000次的重复值判断,要怎么样才能把性能提升到可以接受的地步?
我搜罗了一番,找到了 iseet() 和 array_key_exists()这两个函数,并最终选则了isset
isset只能判断键名 这个好办,用array_flip把我的$orderList键值互转一下

$orderList = array_flip(array_map(function ($item) {
            return $item['tid'] . $item['oid'] . $item['cost'];
        }, DB::SlimPDO()->query($sql)->fetchAll()));

然后把判断语句

if (!in_array($test, $orderList)) 

改成

if (!isset($orderList[$test])) 

执行,一颗心放下了,最终执行时间 0.45861196517944s
终于到了可以接受的程度,这是最终的函数代码,以我目前技术能想到的最优方案

    /**
     * 更新子订单成本
     * @param array $tidList 可选 要更新的tid 不传参数全量更新
     */
    public function updateOrderCost(array $tidList = [])
    {
        $search = DB::SlimPDO()
            ->select(['tid', 'oid', 'cost'])
            ->from('taobao_sku_modify')
            ->where('status', '=', 1)
            ->where('oid', '<>', '');

        $sql = "SELECT cost, tid, oid FROM taobao_trade_order";

        if (count($tidList)) {
            $search->whereIn('tid', $tidList);
            $tid = implode(',', $tidList);
            $sql .= " WHERE tid in ({$tid})";
        }

        $list = $search->orderBy('tid')->execute()->fetchAll();

        $orderList = array_flip(array_map(function ($item) {
            return $item['tid'] . $item['oid'] . $item['cost'];
        }, DB::SlimPDO()->query($sql)->fetchAll()));

        $Update = function ($result) use ($orderList) {
            $test = $result['tid'] . $result['oid'] . $result['cost'];
            if (!isset($orderList[$test])) {
                DB::SlimPDO()
                    ->update(['cost' => $result['cost']])
                    ->table('taobao_trade_order')
                    ->where('tid', '=', $result['tid'])
                    ->where('oid', '=', $result['oid'])
                    ->execute();
            }
        };

        $result = [
            'tid' => '',
            'oid' => '',
            'cost' => 0
        ];

        foreach ($list as $item) {
            if ($result['tid'] !== $item['tid']) {
                if ($result['tid']) {
                    $Update($result);
                }
                $result = [
                    'tid' => $item['tid'],
                    'oid' => $item['oid'],
                    'cost' => 0
                ];
            } elseif ($result['oid'] !== $item['oid']) {
                $Update($result);
                $result['oid'] = $item['oid'];
                $result['cost'] = 0;
            }
            $result['cost'] += $item['cost'];
        }
        $Update($result);
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容