2009-04-23 12 views
8

Sto correndo su un blocco stradale per un problema più grande.Controllo della sovrapposizione dell'intervallo di tempo, il problema del watchman [SQL]

Come parte di una query di grandi dimensioni, ho bisogno di risolvere un problema di "guardiano notturno". Ho una tabella con cambiamenti di programma come tale:

ID | Start   | End 
1 | 2009-1-1 06:00 | 2009-1-1 14:00 
2 | 2009-1-1 10:00 | 2009-1-1 18:00 
3 | 2009-2-1 20:00 | 2009-2-2 04:00 
4 | 2009-2-2 06:00 | 2009-2-2 14:00 

Come parte di una query, ho bisogno di determinare se v'è almeno 1 sentinella in una camera in qualsiasi momento per un determinato intervallo di tempo.

Quindi, se ho specificato la gamma 2009-1-1 06:00 al 2009-1-1 12:00, il risultato è vero, perché turni 1 e 2 si fondono per coprire questo periodo di tempo - di fatto qualsiasi numero di turni potrebbe essere incatenato a tenere l'orologio in su. Tuttavia se ho controllato 2009-2-1 22:00 a 2009-1-2 10:00, il risultato è falso perché c'è una pausa tra 4 e 6 del mattino seguente.

vorrei implementare questa sia in LINQ, o come una funzione definita dall'utente in SQL Server (2005), poiché in entrambi i casi questa è solo una parte della logica di una query grande che deve essere eseguito per identificare gli elementi che richiedono attenzione. Il set di dati reale coinvolge circa un centinaio di record di turni che intersecano un determinato periodo di tempo, ma non sempre coprono l'intero intervallo.

Il più vicino che ho trovato è How to group ranged values using SQL Server per serie di numeri, ma dipende da ogni gamma terminando poco prima della gamma prossime partenze. Se potessi costruire la stessa visione unificata degli orologi, prendendo in considerazione solo orologi sovrapposti, sarebbe banale controllare se un tempo specifico fosse coperto. Una visione unificata sarebbe simile a questa:

Start   | End 
2009-1-1 06:00 | 2009-1-1 18:00 
2009-2-1 20:00 | 2009-2-2 04:00 
2009-2-2 06:00 | 2009-2-2 14:00 

Nota: Tutta questa faccenda sarebbe relativamente facile da implementare, semplicemente tirando tutti i dati e l'uso di alcuni ciclo manuale su di esso, tuttavia, che è il sistema attuale, e la sua piuttosto lento a causa del numero di turni e del numero di intervalli di tempo che devono essere controllati.

+0

L'intervallo di date non deve essere impostato su "Quindi, se si specifica l'intervallo 2009-1-1 12:00 a 2009-1-1 06:00", si inverta a "2009-1-1 06:00 a 2009-1-1 12:00 "? – Sung

+2

@David Potrebbe essere utile scaricare questo ebook: "Sviluppo di applicazioni database orientate al tempo in SQL" (http://www.cs.arizona.edu/people/rts/tdbbook.pdf). Ha un sacco di buone informazioni su query SQL complesse su tabelle con intervalli di date. –

+0

+1 - Sto lavorando su un problema simile, identificando disgiunti e sovrapposizioni – spencer7593

risposta

2

Ecco un modo per appiattire intervallo di date come questo

Start   | End 
2009-1-1 06:00 | 2009-1-1 18:00 
2009-2-1 20:00 | 2009-2-2 04:00 
2009-2-2 06:00 | 2009-2-2 14:00 

Bisogna confrontare precedentieprossimi date in ogni riga e vedere se

  • fila attuali La data di inizio cade tra l'intervallo di date della riga precedente.
  • Riga corrente La data di fine cade tra l'intervallo di date della riga successiva.

alt text

Utilizzando sopra il codice, applicazione UDF è semplice come seguito.

create function fnThereIsWatchmenBetween(@from datetime, @to datetime) 
returns bit 
as 
begin 
    declare @_Result bit 

    declare @FlattenedDateRange table (
     Start datetime, 
     [End] datetime 
    ) 

    insert @FlattenedDateRange(Start, [End]) 
    select distinct 
      Start = 
       case 
        when Pv.Start is null then Curr.Start 
        when Curr.Start between Pv.Start and Pv.[End] then Pv.Start 
        else Curr.Start 
       end, 
      [End] = 
       case 
        when Curr.[End] between Nx.Start and Nx.[End] then Nx.[End] 
        else Curr.[End] 
       end 
    from shift Curr 
      left join shift Pv on Pv.ID = Curr.ID - 1 --; prev 
      left join shift Nx on Nx.ID = Curr.ID + 1 --; next 

    if exists( select 1 
       from FlattenedDateRange R 
       where @from between R.Start and R.[End] 
         and @to between R.Start and R.[End]) begin 
     set @_Result = 1 --; There is/are watchman/men during specified date range 
    end 
    else begin 
     set @_Result = 0 --; There is NO watchman 
    end 

    return @_Result 
end 
+0

e quando, ad esempio, abbiamo turni: 12-2 e 4-6 e controlliamo 1-6. quello dovrebbe fallire, poiché 2-4 è vuoto. – nlucaroni

+0

Il codice è stato aggiornato. – Sung

+0

Molto bello.Ironia della sorte, perché questo faceva parte di un problema più grande e più complesso, ho trovato un modo più veloce di fare tutto creando un tipo di tabella di ricerca alimentata da trigger, spostando la maggior parte del costo della CPU a quando i record sono creati (che è molto meno frequente di quando devono essere letti e analizzati). Tuttavia, ho a che fare con questi tipi di intervalli sovrapposti molto qui, quindi questo sarà molto utile abbastanza presto. – David

1

Un intervallo non presidiato inizia ovviamente alla fine di un periodo di osservazione o all'inizio dell'intero intervallo di tempo che si sta verificando. Quindi hai bisogno di una query che seleziona tutti gli elementi di questo set che non hanno uno spostamento sovrapposto. La query sarà simile a:

select 1 
from shifts s1 where not exists 
    (select 1 from shifts s2 
    where s2.start<=s1.end and s2.end > s1.end 
    ) 
    and s1.end>=start_of_range and s1.end< end_of_range 
union 
select 1 
where not exists 
    (select 1 from shifts s2 
     where s2.start<=start_of_range and s2.end > start_of_range 
    ) 

Se questo non è vuoto, si ha un intervallo incustodito. Sospetto che verrà eseguito in quadratica, quindi potrebbe essere più lento di "sort, fetch and loop".

+0

Ho qualche problema a fare questo lavoro. Presumo che tu intenda per questo filtrare da qualche parte anche per l'ora di fine, ma aggiungendo anche questo, o semplicemente assicurando che siano passati solo turni validi (facendo una panoramica di turni), i risultati sembrano non correlati all'intervallo fornito - lo stesso i risultati vengono forniti indipendentemente dal fatto che l'intervallo sia all'interno o all'esterno dei turni. – David

+0

A destra, la sottoquery principale ha completamente ignorato l'intervallo - controllo aggiunto –

0

Un modo è quello di creare una tabella temporanea con una riga per ogni valore di tempo che richiede da verificare (che è funzione della risoluzione delle turni).

Se fossero minuti, avrebbe 60 * 24 = 1440 righe per un giorno; circa 10K righe per una settimana.

Poi l'SQL è relativamente semplice:

SELECT COUNT (1)
DA #minutes m
LEFT JOIN turni s ON m.checktime TRA s.start_time E s.end_time
HAVING COUNT (1) = 0

Questo ha il vantaggio di poter mostrare anche quanti turni coprono lo stesso tempo.

Il tempo di esecuzione dovrebbe essere trascurabile, date le scale che hai descritto.

0

Stavo osservando gli intervalli di date e ho pensato di rivedere questa domanda. Posso cadere a faccia qui, ma sembra queste due condizioni basterebbero

(1) Shift is not at beginning of range and has no left neighbour 

OR 

(2) Shift is not at end of range and has no right neighbour. 

apprezzare questo potrebbe non essere il più efficiente.

CREATE TABLE times 
(
TimeID int, 
StartTime Time, 
EndTime Time 
) 

INSERT INTO times 
VALUES 
(1,'10:00:00','11:00:00'), 
(2,'11:00:00','12:00:00'), 
(3,'13:00:00','14:00:00'), 
(4,'14:30:00','15:00:00'), 
(5,'15:00:00','16:00:00'), 
(6,'16:00:00','17:00:00') 

declare @start_of_range time ='09:30:00' 
declare @end_of_range time = '17:30:00' 



select timeID,StartTime,EndTime 
from times s1 where 
-- No left neighbour and not at beginning of range 
    not exists 
    (select 1 from times s2 
    where s2.startTime < s1.startTime and s2.endTime >= s1.startTime 
    ) 
    and s1.StartTime>@start_of_range 
    or 
-- No right neighbour and not at end of range 
    not exists 
    (select 1 from times s2 
    where s2.startTime <= s1.endTime and s2.endTime > s1.endTime 
    ) 
    and s1.EndTime<@end_of_range 

set di risultati

timeID StartTime EndTime 
1 10:00:00.0000000 11:00:00.0000000 
2 11:00:00.0000000 12:00:00.0000000 
3 13:00:00.0000000 14:00:00.0000000 
4 14:30:00.0000000 15:00:00.0000000 
6 16:00:00.0000000 17:00:00.0000000 

In realtà è solo necessario controllare sia i vicini sinistra oi vicini di casa a sinistra, fino a quando si fare in modo che l'inizio e la fine della serie è selezionata, quindi si potrebbe introdurre l'inizio di gamma come un intervallo di manichino e basta controllare i vicini a destra come segue: -

select * from 
(
select timeID,StartTime,EndTime 
from times union select 0,@start_of_range,@start_of_range) s1 
where 
    not exists 
    (select 1 from times s2 
    where s2.startTime<=s1.endTime and s2.endTime > s1.endTime 
    ) 
    and s1.EndTime<@end_of_range 

set di risultati

timeID StartTime EndTime 
0 09:30:00.0000000 09:30:00.0000000 
2 11:00:00.0000000 12:00:00.0000000 
3 13:00:00.0000000 14:00:00.0000000 
6 16:00:00.0000000 17:00:00.0000000 
Problemi correlati