ottimizzazione delle query: intervalli di tempo


10

Principalmente, ho due tipi di intervalli di tempo:

presence time e absence time

absence time può essere di diversi tipi (es. pause, assenze, giorno speciale e così via) e gli intervalli di tempo possono sovrapporsi e / o intersecarsi.

È non sicuro, che esistono solo combinazioni plausibili di intervalli di dati grezzi, ad es. gli intervalli di presenza sovrapposti non hanno senso, ma possono esistere. Ho cercato di identificare gli intervalli di tempo-presenza risultanti in molti modi ora - per me, il più comodo sembra essere quello che segue.

;with "timestamps"
as
(
    select
        "id" = row_number() over ( order by "empId", "timestamp", "opening", "type" )
        , "empId"
        , "timestamp"
        , "type"
        , "opening"
    from
    (
        select "empId", "timestamp", "type", case when "types" = 'starttime' then 1 else -1 end as "opening" from
        ( select "empId", "starttime", "endtime", 1 as "type" from "worktime" ) as data
        unpivot ( "timestamp" for "types" in ( "starttime", "endtime" ) ) as pvt
        union all
        select "empId", "timestamp", "type", case when "types" = 'starttime' then 1 else -1 end as "opening" from
        ( select "empId", "starttime", "endtime", 2 as "type" from "break" ) as data
        unpivot ( "timestamp" for "types" in ( "starttime", "endtime" ) ) as pvt
        union all
        select "empId", "timestamp", "type", case when "types" = 'starttime' then 1 else -1 end as "opening" from
        ( select "empId", "starttime", "endtime", 3 as "type" from "absence" ) as data
        unpivot ( "timestamp" for "types" in ( "starttime", "endtime" ) ) as pvt
    ) as data
)
select 
      T1."empId"
    , "starttime"   = T1."timestamp"
    , "endtime"     = T2."timestamp"
from 
    "timestamps" as T1
    left join "timestamps" as T2
        on T2."empId" = T1."empId"
        and T2."id" = T1."id" + 1
    left join "timestamps" as RS
        on RS."empId" = T2."empId"
        and RS."id" <= T1."id"      
group by
    T1."empId", T1."timestamp", T2."timestamp"
having
    (sum( power( 2, RS."type" ) * RS."opening" ) = 2)
order by 
    T1."empId", T1."timestamp";

vedi SQL-Fiddle per alcuni dati dimostrativi.

I dati grezzi esistono in diverse tabelle sotto forma di "starttime" - "endtime"o "starttime" - "duration".

L'idea era di ottenere un elenco ordinato di tutti i timestamp con una somma mobile "bitmasked" di intervalli aperti ogni volta per stimare il tempo di presenza.

Il violino funziona e fornisce risultati stimati, anche se gli inizi di intervalli diversi sono uguali. In questo esempio non vengono utilizzati indici.

È questo il modo giusto per raggiungere un compito controverso o esiste un modo più elegante per farlo?

Se pertinente per la risposta: la quantità di dati sarà fino a diverse diecimila serie di dati per dipendente per tabella. sql-2012 non è disponibile per calcolare una somma variabile di predecessori in linea in forma aggregata.


modificare:

Ho appena eseguito la query con una quantità maggiore di dati di test (1000, 10.000, 100.000, 1 milione) e vedo che il tempo di esecuzione aumenta in modo esponenziale. Ovviamente una bandiera di avvertimento, giusto?

Ho modificato la query e rimosso l'aggregazione della somma variabile tramite un aggiornamento stravagante.

Ho aggiunto una tabella ausiliaria:

create table timestamps
(
  "id" int
  , "empId" int
  , "timestamp" datetime
  , "type" int
  , "opening" int
  , "rolSum" int
)

create nonclustered index "idx" on "timestamps" ( "rolSum" ) include ( "id", "empId", "timestamp" )

e mi sono trasferito calcolando la somma variabile in questo luogo:

declare @rolSum int = 0
update "timestamps" set @rolSum = "rolSum" = @rolSum + power( 2, "type" ) * "opening" from "timestamps"

vedi SQL-Fiddle qui

Il tempo di esecuzione è diminuito a 3 secondi rispetto a 1 milione di voci nella tabella "orario di lavoro".

La domanda rimane la stessa : qual è il modo più efficace per risolverlo?


Sono sicuro che ci sarà contesa su questo, ma potresti provare a non farlo in un CTE. Utilizzare invece le tabelle temporanee e vedere se è più veloce.
rottengeek

Solo una domanda di stile: non ho mai visto nessuno mettere tutti i nomi delle colonne e delle tabelle tra virgolette. È questa la pratica di tutta la tua azienda? Lo trovo decisamente scomodo. Non è necessario dal mio punto di vista, e quindi aumenta il rumore sul segnale ...
ErikE

@ErikE Il metodo sopra è parte di un enorme addon. Alcuni oggetti vengono creati in modo dinamico e dipendono dalla scelta dell'input dell'utente finale. Quindi, ad esempio, gli spazi possono apparire in nomi di tabelle o viste. le doppie virgolette intorno a quelle non lasceranno andare in crash la query ...!
Nico,

@Nico nel mio mondo che di solito è fatto con parentesi quadre quindi mi piace [this]. Mi piace solo meglio delle doppie virgolette, immagino.
ErikE,

Le parentesi quadre di @ErikE sono tsql. lo standard è virgolette doppie! comunque, l'ho imparato in quel modo e quindi in qualche modo ci sono abituato!
Nico,

Risposte:


3

Non posso rispondere alla tua domanda sul modo assolutamente migliore. Ma posso offrire un modo diverso di risolvere il problema, che può essere o non essere migliore. Ha un piano di esecuzione ragionevolmente piatto e penso che funzionerà bene. (Sono ansioso di sapere quindi condividi i risultati!)

Chiedo scusa per aver usato il mio stile di sintassi al posto del tuo - aiuta a fare da mago alla query quando tutto si allinea al suo solito posto.

La query è disponibile in un SqlFiddle . Ho gettato una sovrapposizione per EmpID 1 solo per essere sicuro di averlo coperto. Se alla fine scopri che non possono verificarsi sovrapposizioni nei dati di presenza, puoi rimuovere la query finale e i Dense_Rankcalcoli.

WITH Points AS (
  SELECT DISTINCT
    T.EmpID,
    P.TimePoint
  FROM
    (
      SELECT * FROM dbo.WorkTime
      UNION SELECT * FROM dbo.BreakTime
      UNION SELECT * FROM dbo.Absence
    ) T
    CROSS APPLY (VALUES (StartTime), (EndTime)) P (TimePoint)
), Groups AS (
  SELECT
    P.EmpID,
    P.TimePoint,
    Grp =
      Row_Number()
      OVER (PARTITION BY P.EmpID ORDER BY P.TimePoint, X.Which) / 2
  FROM
    Points P
    CROSS JOIN (VALUES (1), (2)) X (Which)
), Ranges AS (
  SELECT
    G.EmpID,
    StartTime = Min(G.TimePoint),
    EndTime = Max(G.TimePoint)
  FROM Groups G
  GROUP BY
    G.EmpID,
    G.Grp
  HAVING Count(*) = 2
), Presences AS (
  SELECT
    R.*,
    P.Present,
    Grp =
       Dense_Rank() OVER (PARTITION BY R.EmpID ORDER BY R.StartTime)
       - Dense_Rank() OVER (PARTITION BY R.EmpID, P.Present ORDER BY R.StartTime)
  FROM
    Ranges R
    CROSS APPLY (
      SELECT
        CASE WHEN EXISTS (
          SELECT *
          FROM dbo.WorkTime W
          WHERE
            R.EmpID = W.EmpID
            AND R.StartTime < W.EndTime
            AND W.StartTime < R.EndTime
        ) AND NOT EXISTS (
          SELECT *
          FROM dbo.BreakTime B
          WHERE
            R.EmpID = B.EmpID
            AND R.StartTime < B.EndTime
            AND B.StartTime < R.EndTime
        ) AND NOT EXISTS (
          SELECT *
          FROM dbo.Absence A
          WHERE
            R.EmpID = A.EmpID
            AND R.StartTime < A.EndTime
            AND A.StartTime < R.EndTime
        ) THEN 1 ELSE 0 END
    ) P (Present)
)
SELECT
  EmpID,
  StartTime = Min(StartTime),
  EndTime = Max(EndTime)
FROM Presences
WHERE Present = 1
GROUP BY
  EmpID,
  Grp
ORDER BY
  EmpID,
  StartTime;

Nota: le prestazioni di questa query sarebbero migliorate combinando le tre tabelle e aggiungendo una colonna per indicare che tipo di tempo era: lavoro, interruzione o assenza.

E perché tutti i CTE, chiedi? Perché ognuno è costretto da ciò che devo fare ai dati. C'è un aggregato o devo inserire una condizione WHERE in una funzione di windowing o usarla in una clausola in cui le funzioni di windowing non sono consentite.

Ora andrò a vedere se non riesco a escogitare un'altra strategia per raggiungere questo obiettivo. :)

Per divertimento includo qui un "diagramma" che ho fatto per aiutare a risolvere il problema:

------------
   -----------------
                ---------------
                           -----------

    ---    ------   ------       ------------

----   ----      ---      -------

I tre gruppi di trattini (separati da spazi) rappresentano, nell'ordine: dati di presenza, dati di assenza e il risultato desiderato.


Grazie per questo approccio. Lo controllerò quando torno in ufficio e ti darò risultati di runtime con una base di dati più ampia.
Nico,

Il tempo di esecuzione è decisamente molto più elevato del primo approccio. Non ho avuto tempo di verificare se ulteriori indici potrebbero ancora ridurlo. Verificherà il prima possibile!
Nico,

Ho un'altra idea che non ho avuto il tempo di iniziare a lavorare. Per quello che vale, la tua query restituisce risultati errati con intervalli sovrapposti in tutte le tabelle.
ErikE,

L'ho verificato di nuovo, vedo questo violino che ha intervalli completamente sovrapposti in tutte e tre le tabelle. restituisce risultati corretti, come posso vedere. potresti fornire un caso in cui vengono restituiti risultati errati? sentiti libero di regolare i dati della demo in fiddle!
Nico,

va bene, ho capito il tuo punto. in caso di intervalli intersecanti in una tabella, i risultati si arrabbiarono. controllerò questo.
Nico,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.