使用mysql用戶變量查詢?nèi)掌诜秶丿B數(shù)據(jù)

今天接到一個需求,一個人有多個任務(wù),每個任務(wù)都有一個開始時間和結(jié)束時間,統(tǒng)計屬于這個人的任務(wù)開始時間和結(jié)束時間有重合的任務(wù)。支持分頁和篩選。

現(xiàn)在將需求抽象為如下場景:

任務(wù)表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

按照上述需求,應(yīng)該返回如下數(shù)據(jù):

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

思路

統(tǒng)計時間是否重疊,我們需要查詢出所有開始時間和結(jié)束時間有值的任務(wù),然后根據(jù)人去分組,每個人下的任務(wù)互相比較,獲取到日期區(qū)間有重疊的任務(wù),然后再根據(jù)篩選條件和分頁條件對任務(wù)進行分頁和篩選。

代碼實現(xiàn)

在這樣的想法下,最直接的實現(xiàn)是用代碼在內(nèi)存中去實現(xiàn)。

首先查詢出所有的任務(wù),然后根據(jù)operatorId分組,然后for循環(huán)獲得所有時間區(qū)間重疊的task id,再把所有的task id代入到task表中去做分頁和字段篩選(此處不做代碼演示)。

這樣的實現(xiàn)在功能上沒問題的,但是如果任務(wù)數(shù)量比較大的時候,就需要我們復(fù)盤下結(jié)果返回的過程,看看有沒有優(yōu)化空間。

首先要做一次sql查詢,將所有的任務(wù)放到內(nèi)存中,然后在內(nèi)存中循環(huán)對比,獲取時間區(qū)間沖突的任務(wù)id集合,然后拿到任務(wù)id集合作為參數(shù)傳入新的sql查詢中,來做分頁和篩選操作。在這個過程中,多次sql查詢有一定開銷,查詢結(jié)果映射到內(nèi)存中有一些開銷,而這一連串操作都是數(shù)據(jù)篩選操作,能不能將這些操作交給專門做數(shù)據(jù)存儲和篩選的數(shù)據(jù)庫做呢?

sql實現(xiàn)

根據(jù)以上面的思路,很容易的到如下的查詢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雖然很簡單,但在實際實踐當(dāng)中,查詢卻很慢。這里假設(shè)一個人有1w個任務(wù),查詢過程中每個任務(wù)都要和其他9999個任務(wù)對比時間區(qū)間,笛卡爾積為10000 * 9999 = 99990000,顯然這樣的查詢遍歷的次數(shù)太多了,導(dǎo)致查詢極其緩慢,這樣的慢sql需要優(yōu)化一下。

思路優(yōu)化

查詢?nèi)掌趨^(qū)間是否重復(fù),假設(shè)有n條任務(wù)數(shù)據(jù),則需要n*(n-1)次比較,如果能把對比次數(shù)降低,查詢速度應(yīng)該能得到提升。因此我們的目標(biāo)是降低日期對比的次數(shù)。

我們可以根據(jù)operatorId和開始時間對數(shù)據(jù)進行排序,下圖是以一個用戶為例。

1.jpg

排序后的數(shù)據(jù),如果下一行數(shù)據(jù)的開始時間在上一行數(shù)據(jù)的時間區(qū)間內(nèi),則這兩條記錄時間區(qū)間有重疊。這樣就只需要和上一條記錄做一次時間對比,而不是n-1次,大大節(jié)省了對比次數(shù)。

按上述規(guī)則,我們可以得到task2和task3兩個時間重疊的任務(wù),由于task4和task3沒有時間重疊,無法返回,但實際上task4和task2是有時間重疊的,這種情況怎么處理呢?

我們注意到task3的時間區(qū)間是完全被包含在task2的時間區(qū)間內(nèi)的,如果是這種情況,就需要和時間范圍較大的那條記錄做對比了,因此task4的時間區(qū)間應(yīng)該和task2的時間區(qū)間對比,這樣就可以返回task2,task3和task4三條數(shù)據(jù)了。

有了這樣的思路后,接下來就是用sql實現(xiàn)了。

mysql用戶定義變量

使用sql實現(xiàn)上述查詢的難點是如何記錄上一條任務(wù)的開始時間和結(jié)束時間。此時就需要使用mysql的用戶定義變量了。

用戶變量是一個生命周期為session的變量,聲明后在當(dāng)次會話中的所有sql都可以獲取到該變量的值。我們可以用set或者select來初始化:

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

根據(jù)這個特性我們可以聲明一個用戶定義變量,來存儲查詢結(jié)果每條記錄的前一行記錄的開始時間和結(jié)束時間,用來判斷時間區(qū)間是否有重疊。

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的操作人,開始時間以及結(jié)束時間。

@lastOperatorId相當(dāng)于對操作人進行分組,只對比同一個人的開始時間和結(jié)束時間。如果當(dāng)前記錄的開始時間在上一條記錄的時間區(qū)間內(nèi),則overlap標(biāo)記為1,表示重疊。

接下來是將當(dāng)前行的值重新賦值給變量,如果當(dāng)前任務(wù)的時間區(qū)間在上一條記錄的時間區(qū)間內(nèi),則不賦值。

經(jīng)過測試后,發(fā)現(xiàn)每個人的第一條數(shù)據(jù)沒有返回。原因是第一條數(shù)據(jù)是和后一條數(shù)據(jù)重疊的,與前一條數(shù)據(jù)不重疊,那該如何解決丟數(shù)據(jù)的問題呢?解決辦法也很簡單,倒序再查一遍即可。

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的變量重復(fù),導(dǎo)致查詢結(jié)果不正確。

到目前為止,我們就解決了使用sql查詢表中重疊時間區(qū)間的問題。本文提供一種sql解決的方案,如有更好的解決方案歡迎交流溝通。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容