使用mysql用户变量查询日期范围重叠数据

今天接到一个需求,一个人有多个任务,每个任务都有一个开始时间和结束时间,统计属于这个人的任务开始时间和结束时间有重合的任务。支持分页和筛选。

现在将需求抽象为如下场景:

任务表task

id operator_id start_date end_date
1 1 2021-10-01 00:00:00 2021-10-02 00:00:00
2 1 2021-10-03 00:00:00 2021-10-08 00:00:00
3 1 2021-10-04 00:00:00 2021-10-05 00:00:00
4 1 2021-10-07 00:00:00 2021-10-10 00:00:00
5 2 2021-10-01 00:00:00 2021-10-02 00:00:00
6 2 2021-10-03 00:00:00 2021-10-08 00:00:00
7 2 2021-10-04 00:00:00 2021-10-05 00:00:00
8 2 2021-10-07 00:00:00 2021-10-10 00:00:00

按照上述需求,应该返回如下数据:

id operator_id start_date end_date
2 1 2021-10-03 00:00:00 2021-10-08 00:00:00
3 1 2021-10-04 00:00:00 2021-10-05 00:00:00
4 1 2021-10-07 00:00:00 2021-10-10 00:00:00
6 2 2021-10-03 00:00:00 2021-10-08 00:00:00
7 2 2021-10-04 00:00:00 2021-10-05 00:00:00
8 2 2021-10-07 00:00:00 2021-10-10 00:00:00

思路

统计时间是否重叠,我们需要查询出所有开始时间和结束时间有值的任务,然后根据人去分组,每个人下的任务互相比较,获取到日期区间有重叠的任务,然后再根据筛选条件和分页条件对任务进行分页和筛选。

代码实现

在这样的想法下,最直接的实现是用代码在内存中去实现。

首先查询出所有的任务,然后根据operatorId分组,然后for循环获得所有时间区间重叠的task id,再把所有的task id代入到task表中去做分页和字段筛选(此处不做代码演示)。

这样的实现在功能上没问题的,但是如果任务数量比较大的时候,就需要我们复盘下结果返回的过程,看看有没有优化空间。

首先要做一次sql查询,将所有的任务放到内存中,然后在内存中循环对比,获取时间区间冲突的任务id集合,然后拿到任务id集合作为参数传入新的sql查询中,来做分页和筛选操作。在这个过程中,多次sql查询有一定开销,查询结果映射到内存中有一些开销,而这一连串操作都是数据筛选操作,能不能将这些操作交给专门做数据存储和筛选的数据库做呢?

sql实现

根据以上面的思路,很容易的到如下的查询sql

select 
t1.*
from task t1, task t2
where t1.operator_id = t2.operator_id
and t1.id <> t2.id
and t1.start_date >= t2.start_date 
and t1.start_date < t2.end_date
union
select 
t2.*
from task t1, task t2
where t1.operator_id = t2.operator_id
and t1.id <> t2.id
and t1.start_date >= t2.start_date 
and t1.start_date < t2.end_date

上述sql虽然很简单,但在实际实践当中,查询却很慢。这里假设一个人有1w个任务,查询过程中每个任务都要和其他9999个任务对比时间区间,笛卡尔积为10000 * 9999 = 99990000,显然这样的查询遍历的次数太多了,导致查询极其缓慢,这样的慢sql需要优化一下。

思路优化

查询日期区间是否重复,假设有n条任务数据,则需要n*(n-1)次比较,如果能把对比次数降低,查询速度应该能得到提升。因此我们的目标是降低日期对比的次数。

我们可以根据operatorId和开始时间对数据进行排序,下图是以一个用户为例。

1.jpg

排序后的数据,如果下一行数据的开始时间在上一行数据的时间区间内,则这两条记录时间区间有重叠。这样就只需要和上一条记录做一次时间对比,而不是n-1次,大大节省了对比次数。

按上述规则,我们可以得到task2和task3两个时间重叠的任务,由于task4和task3没有时间重叠,无法返回,但实际上task4和task2是有时间重叠的,这种情况怎么处理呢?

我们注意到task3的时间区间是完全被包含在task2的时间区间内的,如果是这种情况,就需要和时间范围较大的那条记录做对比了,因此task4的时间区间应该和task2的时间区间对比,这样就可以返回task2,task3和task4三条数据了。

有了这样的思路后,接下来就是用sql实现了。

mysql用户定义变量

使用sql实现上述查询的难点是如何记录上一条任务的开始时间和结束时间。此时就需要使用mysql的用户定义变量了。

用户变量是一个生命周期为session的变量,声明后在当次会话中的所有sql都可以获取到该变量的值。我们可以用set或者select来初始化:

set @start = 1, @finish = 10;  
select @start = 1, @finish = 10;  
select * from places where place between @start and @finish;

根据这个特性我们可以声明一个用户定义变量,来存储查询结果每条记录的前一行记录的开始时间和结束时间,用来判断时间区间是否有重叠。

select 
temp.id, 
temp.operator_id,
temp.start_date,
temp.end_date
from (
    select 
    t1.id,
    t1.operator_id,
    t1.start_date,
    t1.end_date,
    if(
        @lastOperatorId = t1.operator_id,
        t1.start_date >= @lastStartDate and t1.start_date < @lastEndDate,
        0
    ) as overlap,
    @lastOperatorId := t1.operator_id as last_operator_id,
    case
        when t1.end_date < @lastEndDate
        then @lastStartDate := @lastStartDate
        else @lastStartDate := t1.start_date
    end as last_start,
    case
        when t1.end_date < @lastEndDate
        then @lastEndDate := @lastEndDate
        else @lastEndDate := t1.end_date
    end as last_end
    from task t1,
    (
        select @lastOperatorId := 0, 
        @lastStarDate := '1000-01-01 00:00:00',
        @lastEndDate := '1000-01-01 00:00:00'
    )  sqlVars
    order by t1.operator_id asc, t1.start_date asc, t1.id asc
) temp
where temp.overlap = 1

首先我们先声明一个sqlVars的临时表以及@lastOperatorId@lastStarDate@lastEndDate三个用户定义变量,分别用于记录上一条task的操作人,开始时间以及结束时间。

@lastOperatorId相当于对操作人进行分组,只对比同一个人的开始时间和结束时间。如果当前记录的开始时间在上一条记录的时间区间内,则overlap标记为1,表示重叠。

接下来是将当前行的值重新赋值给变量,如果当前任务的时间区间在上一条记录的时间区间内,则不赋值。

经过测试后,发现每个人的第一条数据没有返回。原因是第一条数据是和后一条数据重叠的,与前一条数据不重叠,那该如何解决丢数据的问题呢?解决办法也很简单,倒序再查一遍即可。

select 
distinct
temp.id, 
temp.operator_id,
temp.start_date,
temp.end_date
from (
    select temp1.* from
    (
        select 
        t1.id,
        t1.operator_id,
        t1.start_date,
        t1.end_date,
        if(
            @lastOperatorId = t1.operator_id,
            t1.start_date >= @lastStartDate and t1.start_date < @lastEndDate,
            0
        ) as overlap,
        @lastOperatorId := t1.operator_id as last_operator_id,
        case
            when t1.end_date < @lastEndDate
            then @lastStartDate := @lastStartDate
            else @lastStartDate := t1.start_date
        end as last_start,
        case
            when t1.end_date < @lastEndDate
            then @lastEndDate := @lastEndDate
            else @lastEndDate := t1.end_date
        end as last_end
        from task t1,
        (
            select @lastOperatorId := 0, 
            @lastStarDate := '1000-01-01 00:00:00',
            @lastEndDate := '1000-01-01 00:00:00'
        )  sqlVars
        order by t1.operator_id asc, t1.start_date asc, t1.id asc
    ) temp1
    union
    select temp2.* from
    (
        select 
        t1.id,
        t1.operator_id,
        t1.start_date,
        t1.end_date,
        if(
            @descLastOperatorId = t1.operator_id,
            @descLastStartDate >= t1.start_date and @descLastStartDate < t1.end_date,
            0
        ) as overlap,
        @descLastOperatorId := t1.operator_id as last_operator_id,
        @descLastStartDate := t1.start_date as last_start,
        @descLastEndDate := t1.end_date as last_end
        from task t1,
        (
            select @descLastOperatorId := 0, 
            @descLastStarDate := '1000-01-01 00:00:00',
            @descLastEndDate := '1000-01-01 00:00:00'
        )  sqlVars
        order by t1.operator_id asc, t1.start_date desc, t1.id desc
    ) temp2
) temp
where temp.overlap = 1

这里需要注意的是,倒序查询的sql中要声明新的用户定义变量,以防止和上一个sql的变量重复,导致查询结果不正确。

到目前为止,我们就解决了使用sql查询表中重叠时间区间的问题。本文提供一种sql解决的方案,如有更好的解决方案欢迎交流沟通。

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

推荐阅读更多精彩内容