2013-02-19 12 views
5

supponga di avere un tale tabella:Combinare intervalli datetime continui per tipo

declare @periods table (
    s date, 
    e date, 
    t tinyint 
); 

con intervalli di data senza lacune in ordine di data di inizio (s)

insert into @periods values 
('2013-01-01' , '2013-01-02', 3), 
('2013-01-02' , '2013-01-04', 1), 
('2013-01-04' , '2013-01-05', 1), 
('2013-01-05' , '2013-01-06', 2), 
('2013-01-06' , '2013-01-07', 2), 
('2013-01-07' , '2013-01-08', 2), 
('2013-01-08' , '2013-01-09', 1); 

Tutti gli intervalli di data hanno tipologie diverse (t).

È necessario combinare intervalli di date dello stesso tipo in cui non siano interrotti da intervalli degli altri tipi (con tutti gli intervalli ordinati per data di inizio).

Quindi la tabella dei risultati dovrebbe essere simile:

 s  |  e  | t 
------------|------------|----- 
2013-01-01 | 2013-01-02 | 3 
2013-01-02 | 2013-01-05 | 1 
2013-01-05 | 2013-01-08 | 2 
2013-01-08 | 2013-01-09 | 1 

Delle idee come fare questo senza cursore?


Ho una soluzione di lavoro:

declare @periods table (
    s datetime primary key clustered, 
    e datetime, 
    t tinyint, 
    period_number int 
); 

insert into @periods (s, e, t) values 
('2013-01-01' , '2013-01-02', 3), 
('2013-01-02' , '2013-01-04', 1), 
('2013-01-04' , '2013-01-05', 1), 
('2013-01-05' , '2013-01-06', 2), 
('2013-01-06' , '2013-01-07', 2), 
('2013-01-07' , '2013-01-08', 2), 
('2013-01-08' , '2013-01-09', 1); 

declare @t tinyint = null; 
declare @PeriodNumber int = 0; 
declare @anchor date; 

update @periods 
    set period_number = @PeriodNumber, 
    @PeriodNumber = case 
         when @t <> t 
          then @PeriodNumber + 1 
         else 
          @PeriodNumber 
        end, 
    @t = t, 
    @anchor = s 
option (maxdop 1); 

select 
    s = min(s), 
    e = max(e), 
    t = min(t) 
from 
    @periods  
group by 
    period_number 
order by 
    s; 

ma dubito se posso contare su un tale comportamento di UPDATE?

Utilizzo SQL Server 2008 R2.


Modifica:

Grazie a Daniele e questo articolo: http://www.sqlservercentral.com/articles/T-SQL/68467/

ho trovato tre cose importanti che sono stati mancati nella soluzione di cui sopra:

  1. Ci deve essere raggruppati indice sul tavolo
  2. Ci deve essere una variabile di ancoraggio e la chiamata della colonna cluster
  3. aggiornamento dichiarazione deve essere eseguito da un processore, vale a dire senza il parallelismo

ho cambiato la soluzione di cui sopra in accordo con queste regole.

+0

Quale versione di SQL Server? –

+0

questa soluzione funziona davvero ...? se tagli il registro ('2013-01-06', '2013-01-07', 2) restituirai lo stesso risultato .. e non avresti visto una riga per ('2013-01-05', '2013- 01-06 ', 2) e altro per (' 2013-01-07 ',' 2013-01-08 ', 2) perché sono interrotti a intervalli? – Frederic

+0

Per Martin Smith: utilizzo SQL Server 2008 R2. A Frederic: Scusa, non ti ho preso abbastanza. Sì, la soluzione di cui sopra funziona, l'ho controllata prima di postare. Una delle condizioni per il compito è che tutti gli intervalli sono continui o successivi, uno dopo l'altro senza spazi. –

risposta

1

Dal momento che le gamme sono continui, il problema essenzialmente diventa uno uno. Se solo tu avessi un criterio per aiutarti a distinguere tra diverse sequenze con lo stesso valore t, potresti raggruppare tutte le righe usando quel criterio, quindi prendi semplicemente MIN(s), MAX(e) per ogni gruppo.

Un metodo per ottenere tale criterio è utilizzare due chiamate ROW_NUMBER.Si consideri la seguente query:

SELECT 
    *, 
    rnk1 = ROW_NUMBER() OVER (    ORDER BY s), 
    rnk2 = ROW_NUMBER() OVER (PARTITION BY t ORDER BY s) 
FROM @periods 
; 

Per esempio, sarebbe tornato la seguente serie:

s   e   t rnk1 rnk2 
---------- ---------- -- ---- ---- 
2013-01-01 2013-01-02 3 1  1 
2013-01-02 2013-01-04 1 2  1 
2013-01-04 2013-01-05 1 3  2 
2013-01-05 2013-01-06 2 4  1 
2013-01-06 2013-01-07 2 5  2 
2013-01-07 2013-01-08 2 6  3 
2013-01-08 2013-01-09 1 7  3 

La cosa interessante di classifica rnk1 e rnk2 è che se si sottrae l'uno dall'altro, si vuole ottenere i valori che, insieme con t, univocamente identificare ogni distinta sequenza di righe con la stessa t:

s   e   t rnk1 rnk2 rnk1 - rnk2 
---------- ---------- -- ---- ---- ----------- 
2013-01-01 2013-01-02 3 1  1  0 
2013-01-02 2013-01-04 1 2  1  1 
2013-01-04 2013-01-05 1 3  2  1 
2013-01-05 2013-01-06 2 4  1  3 
2013-01-06 2013-01-07 2 5  2  3 
2013-01-07 2013-01-08 2 6  3  3 
2013-01-08 2013-01-09 1 7  3  4 

Sapendo ciò, è possibile applicare facilmente raggruppamento e aggregazione. Questo è ciò che la query finale potrebbe essere simile:

WITH partitioned AS (
    SELECT 
    *, 
    g = ROW_NUMBER() OVER (    ORDER BY s) 
     - ROW_NUMBER() OVER (PARTITION BY t ORDER BY s) 
    FROM @periods 
) 
SELECT 
    s = MIN(s), 
    e = MAX(e), 
    t 
FROM partitioned 
GROUP BY 
    t, 
    g 
; 

Se ti piace, si può giocare con questa soluzione at SQL Fiddle.

+0

Sì! Questa è la soluzione che stavo cercando! Grazie Andriy! –

2

Che ne dici di questo?

declare @periods table (
    s datetime primary key, 
    e datetime, 
    t tinyint, 
    s2 datetime 
); 

insert into @periods (s, e, t) values 
('2013-01-01' , '2013-01-02', 3), 
('2013-01-02' , '2013-01-04', 1), 
('2013-01-04' , '2013-01-05', 1), 
('2013-01-05' , '2013-01-06', 2), 
('2013-01-06' , '2013-01-07', 2), 
('2013-01-07' , '2013-01-08', 2), 
('2013-01-08' , '2013-01-09', 1); 

update @periods set s2 = s; 

while @@ROWCOUNT > 0 
begin 
    update p2 SET s2=p1.s 
    from @periods p1 
    join @PERIODS P2 ON p2.t = p1.t AND p2.s2 = p1.e; 
end 

select s2 as s, max(e) as e, min(t) as t 
from @periods 
group by s2 
order by s2; 
+0

GilM, grazie. Soluzione interessante e non banale. Non penserei mai in questo modo. Ma ... l'istruzione di aggiornamento in questo esempio viene eseguita 4 volte. Anche se questo non è il cursore, penso che possa essere fatto senza iterazioni. –

+0

Sospetto che sia necessaria una sorta di iterazione, anche se è nascosta all'interno di un CTE ricorsivo o qualcosa del genere. La soluzione non dipende dagli intervalli negli intervalli e dall'UPDATE che elabora le righe in ordine (che non credo sia garantita, anche con la chiave primaria). Era solo un suggerimento. Almeno penso che garantisca il risultato corretto. – GilM

+0

Sì, il risultato è garantito. Mi piace davvero la tua soluzione. Ho dovuto meditare duramente su di esso per capire la sua eleganza e il modo in cui funziona. Tuttavia non è esattamente come vorrei che fosse. Perché immagino che i cursori e l'iterazione siano malvagi, almeno fino a quel momento in cui non puoi evitarlo! :) –

0

questa è la vostra soluzione con un diverso dati sulla tabella ..

declare @periods table (
     s datetime primary key, 
     e datetime, 
     t tinyint, 
     period_number int 
    ); 

    insert into @periods (s, e, t) values 
    ('2013-01-01' , '2013-01-02', 3), 
    ('2013-01-02' , '2013-01-04', 1), 
    ('2013-01-04' , '2013-01-05', 1), 
    ('2013-01-05' , '2013-01-06', 2), 
    ('2013-01-09' , '2013-01-10', 2), 
    ('2013-01-10' , '2013-01-11', 1); 

    declare @t tinyint = null; 
    declare @PeriodNumber int = 0; 

    update @periods 
     set period_number = @PeriodNumber, 
     @PeriodNumber = case 
          when @t <> t 
           then @PeriodNumber + 1 
          else 
           @PeriodNumber 
         end, 
     @t = t; 

    select 
     s = min(s), 
     e = max(e), 
     t = min(t) 
    from 
     @periods  
    group by 
     period_number 
    order by 
     s; 

dove hanno un divario tra

('2013-01-05' , '2013-01-06', 2), 
--and 
('2013-01-09' , '2013-01-10', 2), 

la soluzione di risultati è ..

s   e   t 
    2013-01-01 2013-01-02 3 
    2013-01-02 2013-01-05 1 
    2013-01-05 2013-01-10 2 
    2013-01-10 2013-01-11 1 

isnt è stato spiato il risultato impostato così .. ??

s   e   t 
    2013-01-01 2013-01-02 3 
    2013-01-02 2013-01-05 1 
    2013-01-05 2013-01-06 2 
    2013-01-09 2013-01-10 2 
    2013-01-10 2013-01-11 1 

forse ho frainteso la regola del tuo problema ...

+0

Nel mio vero compito, costruisco quegli intervalli personalmente e mi assicuro che non ci siano lacune come nel tuo esempio. Quindi la mia domanda riguarda gli intepals che seguono uno dopo l'altro senza lacune. –

+0

@lobodava oh .. ok .. scusa, non lo sapevo.in queste condizioni la tua soluzione funzionerà bene .. per la spiegazione .. Ho postato altre risposte con l'uso di cte in alternativa .. – Frederic

2

un possibile soluzione per evitare aggiornamento e cursore deve essere utilizzando espressioni di tabella comuni ...

simili ...

declare @periods table (
     s date, 
     e date, 
     t tinyint 
    ); 


    insert into @periods values 
    ('2013-01-01' , '2013-01-02', 3), 
    ('2013-01-02' , '2013-01-04', 1), 
    ('2013-01-04' , '2013-01-05', 1), 
    ('2013-01-05' , '2013-01-06', 2), 
    ('2013-01-06' , '2013-01-07', 2), 
    ('2013-01-07' , '2013-01-08', 2), 
    ('2013-01-08' , '2013-01-09', 1); 

    with cte as (select 0 as n 
         ,p.s as s 
         ,p.e as e 
         ,p.t 
         ,case when p2.s is null then 1 else 0 end fl_s 
         ,case when p3.e is null then 1 else 0 end fl_e 
        from @periods p 
        left outer join @periods p2 
        on p2.e = p.s 
        and p2.t = p.t 
        left outer join @periods p3 
        on p3.s = p.e 
        and p3.t = p.t 

        union all 

        select n+1 as n 
         , p2.s as s 
         , p.e as e 
         ,p.t 
         ,case when not exists(select * from @periods p3 where p3.e =p2.s and p3.t=p2.t) then 1 else 0 end as fl_s 
         ,p.fl_e as fl_e 
        from cte p 
        inner join @periods p2 
        on p2.e = p.s 
        and p2.t = p.t 
        where p.fl_s = 0 

        union all 

        select n+1 as n 
         , p.s as s 
         , p2.e as e 
         ,p.t 
         ,p.fl_s as fl_s 
         ,case when not exists(select * from @periods p3 where p3.s =p2.e and p3.t=p2.t) then 1 else 0 end as fl_e 
        from cte p 
        inner join @periods p2 
        on p2.s = p.e 
        and p2.t = p.t 
        where p.fl_s = 1 
        and p.fl_e = 0 
    ) 
    ,result as (select s,e,t,COUNT(*) as count_lines 
       from cte 
       where fl_e = 1 
       and fl_s = 1 
       group by s,e,t 
       ) 
    select * from result 
    option(maxrecursion 0) 

di risultati raggiunti ...

s   e   t count_lines 
    2013-01-01 2013-01-02 3 1 
    2013-01-02 2013-01-05 1 2 
    2013-01-05 2013-01-08 2 3 
    2013-01-08 2013-01-09 1 1 
+0

Non direi che questa è la soluzione ottimale, ma mi piace l'idea di ricorsive cte . Grazie, Federic –

1

Hooray ! Ho trovato la soluzione che si adatta a me ed è fatto senza iterazione

with cte1 as ( 
    select s, t from @periods 
    union all 
    select max(e), null from @periods 
), 
cte2 as (
    select rn = row_number() over(order by s), s, t from cte1 
), 
cte3 as (
    select 
     rn = row_number() over(order by a.rn), 
     a.s, 
     a.t 
    from 
     cte2 a 
     left join cte2 b on a.rn = b.rn + 1 and a.t = b.t 
    where 
     b.rn is null 
) 
select 
    s = a.s, 
    e = b.s, 
    a.t 
from 
    cte3 a 
    inner join cte3 b on b.rn = a.rn + 1; 

Grazie a tutti per condividere i tuoi pensieri e le soluzioni!


Dettagli:

cte1 restituisce la catena delle date con i tipi dopo loro:

s   t 
---------- ---- 
2013-01-01 3 
2013-01-02 1 
2013-01-04 1 
2013-01-05 2 
2013-01-06 2 
2013-01-07 2 
2013-01-08 1 
2013-01-09 NULL -- there is no type *after* the last date 

CT2 basta aggiungere numero di riga al risultato di cui sopra:

rn  s  t 
---- ---------- ---- 
1 2013-01-01 3 
2 2013-01-02 1 
3 2013-01-04 1 
4 2013-01-05 2 
5 2013-01-06 2 
6 2013-01-07 2 
7 2013-01-08 1 
8 2013-01-09 NULL 

se produciamo tutti i campi dalla query in cte3 senza dove condizioni, si ottengono i seguenti risultati:

select * from cte2 a left join cte2 b on a.rn = b.rn + 1 and a.t = b.t; 

rn  s  t  rn  s   t 
---- ---------- ---- ------ ---------- ---- 
1 2013-01-01 3  NULL NULL  NULL 
2 2013-01-02 1  NULL NULL  NULL 
3 2013-01-04 1  2  2013-01-02 1 
4 2013-01-05 2  NULL NULL  NULL 
5 2013-01-06 2  4  2013-01-05 2 
6 2013-01-07 2  5  2013-01-06 2 
7 2013-01-08 1  NULL NULL  NULL 
8 2013-01-09 NULL NULL NULL  NULL 

per le date in cui si repeted tipo ci sono valori sul lato destro dei risultati. Quindi possiamo semplicemente rimuovere tutte le linee in cui i valori esistono sul lato destro.

rendimenti Così cte3:

rn s   t 
----- ---------- ---- 
1  2013-01-01 3 
2  2013-01-02 1 
3  2013-01-05 2 
4  2013-01-08 1 
5  2013-01-09 NULL 

Nota che a causa della rimozione di alcune righe ci sono alcune lacune nella sequenza rn, quindi dobbiamo renumber di nuovo.

Da qui solo una cosa - per trasformare le date per periodi:

select 
    s = a.s, 
    e = b.s, 
    a.t 
from 
    cte3 a 
    inner join cte3 b on b.rn = a.rn + 1; 

e abbiamo il risultato richiesto:

s   e   t 
---------- ---------- ---- 
2013-01-01 2013-01-02 3 
2013-01-02 2013-01-05 1 
2013-01-05 2013-01-08 2 
2013-01-08 2013-01-09 1 
+0

Avevi ragione, non c'è iterazione qui. Era davvero il 'cte1' che pensavo incurantemente come ricorsivo. Buon lavoro a trovare una soluzione funzionante, ancora meglio una che spiega come funziona! –

Problemi correlati