用戶留存(app)統計

一個產品的用戶留存關係到該產品是否健康的發展

實現效果:

表詳情:

註冊表:d_user_register201704  以月份分表  (uid 唯一)

CREATE TABLE `d_user_register201704` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` bigint(20) NOT NULL DEFAULT '0' COMMENT '用戶id',
  `inviteUid` bigint(20) NOT NULL,
  `name` varchar(32) NOT NULL DEFAULT '',
  `account` varchar(32) NOT NULL DEFAULT '' COMMENT '用戶名',
  `systemType` smallint(3) NOT NULL DEFAULT '0' COMMENT '用戶註冊系統標識(ios-0 android-1...)',
  `authorizationType` smallint(3) NOT NULL DEFAULT '0' COMMENT '用戶註冊第三方授權標識',
  `time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `time` (`time`) USING BTREE,
  KEY `uid` (`uid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

登錄表:d_user_register201704 同樣以月份分表(0 代表登錄  用戶每登錄一次就會插入一條數據  很大的數據冗餘  數據大約300W條左右)


實現思路:

首先將要查時間區間的每一天所對應的用戶留存數據計算出來(考慮到分表我們使用union 進行聯查)

實現sql

    select count(uid) as onl,date_format(time,'%H') as hour  from d_user_login201704 where id in 
(select * from ((select min(id) from d_user_login201704 where type=0 and  time>= '2017-04-09 00:00:00'
 and time<='2017-04-09 23:59:59' group by(uid))
 as tmptable)) group by hour;  

筆者考慮了好長時間也沒有想到除了循環查詢 能更好的查詢辦法

下面依次循環 要查詢的日期 考慮到分表 我們使用union  日期唯一所有使用union 想要去重就使用union all

select count(DISTINCT(uid)) as liu,date_format(time,'%Y%m%d') as day from d_user_login201704 where uid in (select  uid  from `d_user_register201704`  
where time BETWEEN '2017-04-08 00:00:00' and '2017-04-08 23:59:59')  and time BETWEEN '2017-04-09'  and  '2017-05-09' group by day  union 
 select count(DISTINCT(uid)) as liu,date_format(time,'%Y%m%d') as day from d_user_login201705 where uid in (select  uid  from `d_user_register201704`  
where time BETWEEN '2017-04-08 00:00:00' and '2017-04-08 23:59:59')  and time BETWEEN '2017-04-09'  and  '2017-05-09' group by day


下面貼出代碼:

php   控制器

 /**
     * 用戶留存
     * @param Request $request
     * @return $this
     */
    public function userKeepView(Request $request){

        $data['start_time'] = $request->input('start_time', date('Y-m-d',strtotime("-15 day") ));
        $data['end_time'] = $request->input('end_time', date('Y-m-d'));


        $start_month = date('Ym',strtotime($data['start_time']));
        $end_month = date('Ym',strtotime($data['end_time']));
        $table_prefix = 'd_user_register';

        //獲得查詢表集合
        $select_set = [];
        for($i=$start_month;$i<=$end_month;$i++){
            $select_set[] = intval($i);
        }

        //獲得已有表集合
        $table_prefix_length = strlen($table_prefix);
        $tables = DB::connection('log')->select("show tables like 'd_user_register%'");

        $register_tables_set = [];


        foreach($tables as $key=>$value){
            $value = (array)$value;
            $table = $value['Tables_in_dingdlog (d_user_register%)'];
            $res = (int)substr($table,$table_prefix_length);
            array_push($register_tables_set,$res);
        }

        $register_tables_set = array_filter($register_tables_set);

        sort($register_tables_set);


        //獲取最終查詢表交集
        $register_tables = array_values(array_intersect($register_tables_set,$select_set));

        //循環查詢
        $user_keep_data = [];

        $first_table = current($register_tables);
        $end_table = end($register_tables);
        //鎖定存在表時間區間
        $first = date('Y-m-d 00:00:00',strtotime($first_table.'01'));
        $end = date('Y-m-d 23:59:59',strtotime($end_table."01 +1month -1day"));

        if(strtotime($data['start_time'])<strtotime($first)){
            $data['start_time'] = $first;
        }
        if(strtotime($data['end_time'])>strtotime($end)){
            $data['end_time'] = $end;
        }

        //循環時間區間以天查詢
        for($i=strtotime($data['start_time']);$i<=strtotime($data['end_time']);$i+=24*3600){
           //當前時間
            $self_time = date('Y-m-d',$i);
            $self_next_time = date('Y-m-d',$i+24*3600);
            $self_month = date('Ym',$i);
            //30天查詢
            $self_next_month = date('Y-m-d',strtotime($self_time.'+ 31 day'));
            $select_next_table = date('Ym',strtotime('+1month -1day'));

            //先將本天的用戶留存率算出來

            $uid_set = DB::connection('log')->table($table_prefix.$self_month)->where('time','>=',$self_time.' 00:00:00')
                      ->where('time','<=',$self_time.' 23:59:59')->lists('uid');
            $self_total = count($uid_set);
            $date[] = date('Y.m.d',$i);
            $total[] =  $self_total;

            if(in_array($select_next_table,$register_tables_set)){
                //使用union連接上查詢
                $result = DB::connection('log')->table('d_user_login'.$select_next_table)
                ->select(DB::raw("count(DISTINCT(uid)) as liu,date_format(time,'%Y%m%d') as day"))
                ->whereBetween('time',[$self_next_time,$self_next_month])->where('type','=','0')->whereIn('uid',$uid_set)->groupby('day');
                $user_keep_data [] =  DB::connection('log')->table('d_user_login'.$self_month)
                 ->select(DB::raw("count(DISTINCT(uid)) as liu,date_format(time,'%Y%m%d') as day"))
                 ->whereBetween('time',[$self_next_time,$self_next_month])->where('type','=','0')->whereIn('uid',$uid_set)->groupby('day')
                 ->union($result)->get();
            }else{
                $user_keep_data [] =  DB::connection('log')->table('d_user_login'.$self_month)
                 ->select(DB::raw("count(DISTINCT(uid)) as liu,date_format(time,'%Y%m%d') as day"))
                ->whereBetween('time',[$self_next_time,$self_next_month])->where('type','=','0')->whereIn('uid',$uid_set)->groupby('day')->get();

            }
        }
        rsort($date);
        $total = array_reverse($total,false);
        $user_keep_data = array_reverse($user_keep_data,false);
        return view('chart/userKeepView')->with('user_keep_data', $user_keep_data)
            ->with('total', $total)
            ->with('date',$date)
            ->with('data',$data);
    }

html


<!DOCTYPE html>
<html lang="zh-cn">
@include('layouts.head')
<body>
<div class="container-fluid">
    <div class="panel panel-default">
        <div class="panel-heading">
           用戶留存
        </div>
        <div class="panel-body">
            <form action="{{action('Home\ChartController@userKeepView')}}" method="post" role="form">
                <div class="row">
                    <div class="col-md-6 form-inline">
                        <div class="form-group form-inline">
                            <label for="">開始時間</label>
                            <input type="text" name="start_time" class="form-control form_date" value="{{$data['start_time']}}" readonly placeholder="請輸入id"/>
                        </div>
                        <div class="form-group form-inline">
                            <label for="">結束時間</label>
                            <input type="text" name="end_time" class="form-control form_date" value="{{$data['end_time']}}" readonly placeholder="請輸入id"/>
                        </div>
                    </div>
                    @if(session('msg'))
                        <p style="color:red">{{session('msg')}}</p>
                    @endif
                    <div class="col-md-3">
                        <label for=""> </label>
                        <div class="form-group">
                            <input type="submit" class="btn btn-sm btn-primary" value="查詢"/>
                        </div>
                    </div>
                </div>
            </form>
        </div>
    </div>
    <table class="table table-responsive table-bordered">
        <thead>
        <th>日期</th>
        <th>當天註冊人數</th>
        <th>次日留存</th>
        <th>2日留存</th>
        <th>3日留存</th>
        <th>4日留存</th>
        <th>5日留存</th>
        <th>6日留存</th>
        <th>7日留存</th>
        <th>14日留存</th>
        <th>30日留存</th>
        </thead>
        <tbody>
        @if($user_keep_data)
            @foreach($user_keep_data as $key=>$value)
                <tr>
                    <td>{{$date[$key]}}</td>
                    <td>{{$total[$key]}}</td>
                    @foreach($value as $k=>$v)
                            @if($k<7||$k==13||$k==29)
                            <td>{{number_format($v->liu/$total[$key]*100,2,'.','')}}%</td>
                            @endif
                    @endforeach
                </tr>
            @endforeach
        @else
            <tr>
                <td colspan="4">暫無數據</td>
            </tr>
        @endif
        </tbody>
    </table>
</div>
</body>
<script type="text/javascript">
    $('.form_date').datetimepicker({
        language: 'zh-CN', /*加載日曆語言包,可自定義*/
        weekStart: 1, /*星期*/
        todayBtn: 0, /*當天*/
        autoclose: true,//選中之後自動隱藏日期選擇框
        todayHighlight: 1, /*今天高亮顯示*/
        startView: 2, /*4年3月2日1小時*/
        minView: 2, /*0分1小時2天*/
        format: 'yyyy-mm-dd',
        forceParse: 0,
        showMeridian:true
    });
</script>
</html>


explain 測試sql :

explain select count(DISTINCT(uid)) as liu,date_format(time,'%Y%m%d') as day from d_user_login201704 
where uid in (select  uid  from `d_user_register201704`  where time BETWEEN '2017-04-08 00:00:00' and '2017-04-08 23:59:59')
and time BETWEEN '2017-04-09'  and  '2017-05-09' group by day  union  select count(DISTINCT(uid)) as liu,date_format(time,'%Y%m%d') as day from
 d_user_login201705 where uid in (select  uid  from `d_user_register201704`  where time BETWEEN '2017-04-08 00:00:00' and '2017-04-08 23:59:59') 
and time BETWEEN '2017-04-09'  and  '2017-05-09' group by day



由於 login5 表沒有建立索引 所以沒有使用到      (筆者在 register 表 uid 以及 login  表的 uid  和 time 上建立了索引)

查詢197ms   循環得出結果需要2S 左右   需要優化的地方還有很多!!


發佈了150 篇原創文章 · 獲贊 112 · 訪問量 72萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章