MYSQL经典SQL之时长统计

概述

假设有一张表,记录了用户的登入、登出信息,我们需要根据这些信息统计用户的在线时长,相应的设置用户等级,就像挂QQ升级

数据如下:

mysql> select * from user_sessions;
+----+----------+-------+----------+----------+
| id | platform | usr   | start    | end      |
+----+----------+-------+----------+----------+
|  1 | web      | user1 | 08:30:00 | 10:30:00 |
|  2 | web      | user2 | 08:30:00 | 08:45:00 |
|  3 | web      | user1 | 09:00:00 | 09:30:00 |
|  4 | web      | user2 | 09:15:00 | 10:30:00 |
|  5 | web      | user1 | 09:15:00 | 09:30:00 |
|  6 | web      | user2 | 10:30:00 | 14:30:00 |
|  7 | web      | user1 | 10:45:00 | 11:30:00 |
|  8 | web      | user2 | 11:00:00 | 12:30:00 |
|  9 | app      | user1 | 08:30:00 | 08:45:00 |
| 10 | app      | user2 | 09:00:00 | 09:30:00 |
| 11 | app      | user1 | 11:45:00 | 12:00:00 |
| 12 | app      | user2 | 12:30:00 | 14:00:00 |
| 13 | app      | user1 | 12:45:00 | 13:30:00 |
| 14 | app      | user2 | 13:00:00 | 14:00:00 |
| 15 | app      | user1 | 14:00:00 | 16:30:00 |
| 16 | app      | user2 | 15:30:00 | 17:00:00 |
+----+----------+-------+----------+----------+

我们的站点有两个入口:web端和app端

很明显,这些数据时间上是有重叠的,例如user1在08:30~10:30这个时间段内用三个web端和一个app端登录,但他的时长应该只能算作2个小时

要正确统计,我们需要三步:

  1. 获取用户的重叠时间段中最早的起始时间
  2. 获取用户的重叠时间段中最晚的结束时间
  3. 前两步的起始时间与结束时间合并,得到综合的时间段,再进行统计

第一步:

查询用户名、起始时间
其中起始时间需要满足的条件是:不包含于其它时间段,即不存在一条记录来满足条件“s>start且s<=end”
得到的结果可能存在重复,所以用distinct去重
这样找到的s就是用户重叠时间段的最早时间。如下:

mysql> select distinct usr,start from user_sessions as a where not exists(select * from user_sessions as b where a.usr=b.usr and a.start>b.start and a.start<=b.end);
+-------+----------+
| usr   | start    |
+-------+----------+
| user1 | 08:30:00 |
| user2 | 08:30:00 |
| user1 | 10:45:00 |
| user2 | 09:00:00 |
| user1 | 11:45:00 |
| user1 | 12:45:00 |
| user1 | 14:00:00 |
| user2 | 15:30:00 |
+-------+----------+

第二步:

查询用户名、结束时间
这步于第一步一样道理,结束时间满足的条件是:不包含于其它时间段,即不存在一条记录满足“e>=start且e<end”
结果如下:

mysql> select distinct usr,end from user_sessions as a where not exists(select * from user_sessions as b where a.usr=b.usr and a.end>=b.start and a.end<b.end);
+-------+----------+
| usr   | end      |
+-------+----------+
| user1 | 10:30:00 |
| user2 | 08:45:00 |
| user2 | 14:30:00 |
| user1 | 11:30:00 |
| user1 | 12:00:00 |
| user1 | 13:30:00 |
| user1 | 16:30:00 |
| user2 | 17:00:00 |
+-------+----------+

第三步:

合并前两步的结果
为了方便查询和叙述,我们将前两步建立对应的视图:

mysql> create view v_s as select distinct usr,start from user_sessions as a where not exists(select * from user_sessions as b where a.usr=b.usr and a.start>b.start and a.start<=b.end);

mysql> create view v_e as select distinct usr,end from user_sessions as a where not exists(select * from user_sessions as b where a.usr=b.usr and a.end>=b.start and a.end<b.end);

查看v_s和v_e两个视图:

mysql> select * from v_s;
+-------+----------+
| usr   | start    |
+-------+----------+
| user1 | 08:30:00 |
| user2 | 08:30:00 |
| user1 | 10:45:00 |
| user2 | 09:00:00 |
| user1 | 11:45:00 |
| user1 | 12:45:00 |
| user1 | 14:00:00 |
| user2 | 15:30:00 |
+-------+----------+

mysql> select * from v_e;
+-------+----------+
| usr   | end      |
+-------+----------+
| user1 | 10:30:00 |
| user2 | 08:45:00 |
| user2 | 14:30:00 |
| user1 | 11:30:00 |
| user1 | 12:00:00 |
| user1 | 13:30:00 |
| user1 | 16:30:00 |
| user2 | 17:00:00 |
+-------+----------+

可以看到,两个视图总行数是相等的,但并不是按行对应的,所以并不是单纯的将两个结果横向合并!

方案一:

对于v_s中的起始时间,我们应该在v_e中找“usr相同且end>start”的最小值
例如,对于v_s中user1起始时间为08:30:00,我们应该找到v_e中end>08:30:00且usr=user1的记录,然后取min得到结果为10:30:00作为与之对应的结束时间
SQL及结果如下:

mysql> select distinct v_s.usr,v_s.start,(select min(end) from v_e where v_e.end>v_s.start and v_e.usr=v_s.usr) as end from v_s,v_e where v_s.usr=v_e.usr;
+-------+----------+----------+
| usr   | start    | end      |
+-------+----------+----------+
| user1 | 08:30:00 | 10:30:00 |
| user2 | 08:30:00 | 08:45:00 |
| user1 | 10:45:00 | 11:30:00 |
| user2 | 09:00:00 | 14:30:00 |
| user1 | 11:45:00 | 12:00:00 |
| user1 | 12:45:00 | 13:30:00 |
| user1 | 14:00:00 | 16:30:00 |
| user2 | 15:30:00 | 17:00:00 |
+-------+----------+----------+

在此基础上,按usr进行分组,统计用户的总时长,得到最终结果:

mysql> select usr,sec_to_time(sum(timestampdiff(second,start,end))) as time from (select distinct v_s.usr,v_s.start,(select min(end) from v_e where v_e.end>v_s.start and v_e.usr=v_s.usr) as end from v_s,v_e where v_s.usr=v_e.usr) as c group by usr;
+-------+----------+
| usr   | time     |
+-------+----------+
| user1 | 06:15:00 |
| user2 | 07:15:00 |
+-------+----------+

其中,timestampdiff函数是计算两个时间的差值,第一个参数用来指定其单位,这里指定为秒;
sec_to_time函数是将求和后的秒数再转为时间格式来显示

方案二

既然v_s与v_e的数据不是按行对应的,那么我们可以对其排序,使其按行对应后,就可以直接按行合并
为了按行合并,我们同时加上行号,让两个子查询行号匹配即可:

mysql> set @sno=0;
mysql> set @eno=0;
mysql> select s.usr,s.start,e.end from (select @sno:=@sno+1 as sno,usr,start from v_s order by usr,start) as s, (select @eno:=@eno+1 as eno,usr,end from v_e order by usr,end) as e where s.sno=e.eno;
+-------+----------+----------+
| usr   | start    | end      |
+-------+----------+----------+
| user1 | 08:30:00 | 10:30:00 |
| user1 | 10:45:00 | 11:30:00 |
| user1 | 11:45:00 | 12:00:00 |
| user1 | 12:45:00 | 13:30:00 |
| user1 | 14:00:00 | 16:30:00 |
| user2 | 08:30:00 | 08:45:00 |
| user2 | 09:00:00 | 14:30:00 |
| user2 | 15:30:00 | 17:00:00 |
+-------+----------+----------+

之后的分组统计与方案一相同

扩展

实际场景中,我们经常将不同的方式的在线时长加上奖励倍数,来鼓励用户更多的使用某种入口。
例如,我们为了让用户更多的使用app登录,规定app的在线时长奖励2倍,即app在线一小时相当于web在线两小时。
这时,我们就需要将不同的登录方式区分开来统计。要区分开也很简单,只需要添加SQL条件让platform相同即可,例如第一步修改为:

mysql> select distinct usr,platform,start from user_sessions as a where not exists(select * from user_sessions as b where a.usr=b.usr and a.platform=b.platform and a.start>b.start and a.start<=b.end);
+-------+----------+----------+
| usr   | platform | start    |
+-------+----------+----------+
| user1 | web      | 08:30:00 |
| user2 | web      | 08:30:00 |
| user2 | web      | 09:15:00 |
| user1 | web      | 10:45:00 |
| user1 | app      | 08:30:00 |
| user2 | app      | 09:00:00 |
| user1 | app      | 11:45:00 |
| user2 | app      | 12:30:00 |
| user1 | app      | 12:45:00 |
| user1 | app      | 14:00:00 |
| user2 | app      | 15:30:00 |
+-------+----------+----------+

之后的步骤类似,在最终统计时分别统计web和app的时长,并将app的时长乘以2再与web时长相加即可,不再赘述。

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

推荐阅读更多精彩内容