otimização de consultas: intervalos de tempo

10

Em geral, tenho dois tipos de intervalos de tempo:

presence time e absence time

absence time podem ser de tipos diferentes (por exemplo, intervalos, ausências, dias especiais etc.) e os intervalos de tempo podem se sobrepor e / ou se cruzar.

É não , com certeza, que apenas as combinações plausíveis de intervalos de existir em dados brutos, por exemplo. intervalos de presença sobrepostos não fazem sentido, mas podem existir. Tentei identificar os intervalos de tempo de presença resultantes de várias maneiras agora - para mim, o mais confortável parece ser o seguinte.

;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";

veja SQL-Fiddle para alguns dados de demonstração.

Os dados brutos existem em tabelas diferentes na forma de "starttime" - "endtime"ou "starttime" - "duration".

A idéia era obter uma lista ordenada de todos os registros de data e hora com uma soma rolante "mascarada" de intervalos abertos a cada momento para estimar o tempo de presença.

O violino funciona e fornece resultados estimados, mesmo que os tempos de início de intervalos diferentes sejam iguais. Nenhum índice é usado neste exemplo.

É este o caminho certo para realizar tarefas questionadas ou existe uma maneira mais elegante para isso?

Se relevante para responder: a quantidade de dados será de até dez mil conjuntos de dados por funcionário por tabela. O sql-2012 não está disponível para calcular uma soma contínua de predecessores in-line de forma agregada.


editar:

Apenas executou a consulta contra uma quantidade maior de dados de teste (1000, 10.000, 100.000, 1 milhão) e pode ver que o tempo de execução aumenta exponencialmente. Obviamente, uma bandeira de aviso, certo?

Alterei a consulta e removi a agregação da soma rolante por uma atualização peculiar.

Eu adicionei uma tabela auxiliar:

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 mudei o cálculo da soma rolante para este local:

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

veja SQL-Fiddle aqui

O tempo de execução diminuiu para 3 segundos em relação a 1 milhão de entradas na tabela "horário de trabalho".

A pergunta permanece a mesma : qual é a maneira mais eficaz de resolver isso?

Nico
fonte
Tenho certeza de que haverá uma disputa sobre isso, mas você pode tentar não fazer isso em um CTE. Use tabelas temporárias e veja se é mais rápido.
Rottengeek
Apenas uma pergunta de estilo: nunca vi alguém colocar todos os nomes de colunas e tabelas entre aspas duplas. Essa é a prática de toda a sua empresa? Eu definitivamente acho desconfortável. Não é necessário, na minha opinião, e, portanto, aumenta o ruído sobre o sinal ...
ErikE
O método @ErikE Above faz parte de um enorme complemento. Alguns dos objetos são criados dinamicamente e dependem da escolha do usuário final. Por exemplo, espaços em branco podem aparecer em nomes de tabela ou de exibição. aspas duplas em torno deles não deixarão a consulta falhar ...!
Nico
@ Nico no meu mundo, que geralmente é feito com colchetes, em seguida, como [this]. Eu gosto mais do que aspas duplas, eu acho.
ErikE 12/02
Os colchetes @ErikE são tsql. padrão é aspas duplas! de qualquer maneira, eu aprendi dessa maneira e, de alguma forma, estou acostumado a isso!
Nico

Respostas:

3

Não consigo responder à sua pergunta da melhor maneira possível. Mas posso oferecer uma maneira diferente de resolver o problema, que pode ou não ser melhor. Ele tem um plano de execução razoavelmente plano e acho que terá um bom desempenho. (Estou ansioso para saber, compartilhe os resultados!)

Peço desculpas por usar meu próprio estilo de sintaxe em vez do seu - isso ajuda a consultar a assistente quando tudo se alinha no seu lugar habitual.

A consulta está disponível em um SqlFiddle . Fiz uma sobreposição para o EmpID 1 apenas para ter certeza de que isso estava coberto. Se, eventualmente, você descobrir que as sobreposições não podem ocorrer nos dados de presença, poderá remover a consulta final e os Dense_Rankcálculos.

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: o desempenho desta consulta seria aprimorado. Você combinou as três tabelas e adicionou uma coluna para indicar que tipo de tempo era: trabalho, interrupção ou ausência.

E por que todos os CTEs, você pergunta? Porque cada um é forçado pelo que eu preciso fazer com os dados. Há um agregado, ou eu preciso colocar uma condição WHERE em uma função de janela ou usá-la em uma cláusula em que funções de janela não são permitidas.

Agora vou sair e ver se não consigo pensar em outra estratégia para conseguir isso. :)

Para diversão, incluo aqui um "diagrama" que fiz para ajudar a resolver o problema:

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

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

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

Os três conjuntos de traços (separados por espaços) representam, em ordem: dados de presença, dados de ausência e o resultado desejado.

ErikE
fonte
Obrigado por esta abordagem. Vou dar uma olhada quando voltar ao escritório e fornecer resultados em tempo de execução com uma base de dados maior.
Nico
O tempo de execução é definitivamente muito maior que a 1ª abordagem. Não tive tempo de verificar se outros índices ainda podem diminuir. Irá verificar o mais rapidamente possível!
Nico
Tenho outra ideia de que não tive tempo de trabalhar. Quanto vale a pena, sua consulta retorna resultados incorretos com intervalos sobrepostos em todas as tabelas.
ErikE
Eu verifiquei isso de novo, veja esse violino que tem intervalos completamente sobrepostos nas três tabelas. retorna resultados corretos, como posso ver. você poderia fornecer um caso em que resultados incorretos fossem retornados? fique à vontade para ajustar os dados de demonstração no violino!
Nico
tudo bem, eu entendi o seu ponto. no caso de intervalos cruzados em uma tabela, os resultados ficaram loucos. irá verificar isso.
Nico