optimisation des requêtes: intervalles de temps

10

Dans l'ensemble, j'ai deux types d'intervalles de temps:

presence time et absence time

absence time peuvent être de différents types (par exemple, pauses, absences, jour spécial, etc.) et les intervalles de temps peuvent se chevaucher et / ou se recouper.

Il n'est pas certain que seules des combinaisons plausibles d'intervalles existent dans les données brutes, par exemple. les intervalles de présence qui se chevauchent n'ont pas de sens, mais peuvent exister. J'ai essayé d'identifier les intervalles de temps de présence résultants de plusieurs façons maintenant - pour moi, le plus confortable semble être celui qui suit.

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

voir SQL-Fiddle pour quelques données de démonstration.

Les données brutes existent dans différentes tables sous forme de "starttime" - "endtime"ou "starttime" - "duration".

L'idée était d'obtenir une liste ordonnée de chaque horodatage avec une somme continue "bitmaskée" d'intervalles ouverts à chaque fois pour estimer le temps de présence.

Le violon fonctionne et donne des résultats estimés, même si les heures de début de différents intervalles sont égales. Aucun indice n'est utilisé dans cet exemple.

Est-ce la bonne façon de réaliser la tâche remise en question ou existe-t-il une manière plus élégante pour cela?

Si cela est pertinent pour répondre: la quantité de données peut atteindre plusieurs dizaines de milliers de jeux de données par employé et par table. sql-2012 n'est pas disponible pour calculer une somme cumulée de prédécesseurs en ligne dans l'ensemble.


Éditer:

Vient d'exécuter la requête sur une plus grande quantité de données de test (1000, 10 000, 100 000, 1 million) et peut voir que le temps d'exécution augmente de façon exponentielle. De toute évidence, un drapeau d'avertissement, non?

J'ai changé la requête et supprimé l'agrégation de la somme mobile par une mise à jour excentrique.

J'ai ajouté une table auxiliaire:

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

et j'ai déplacé le calcul de la somme mobile à cet endroit:

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

voir SQL-Fiddle ici

Le temps d'exécution a diminué à 3 secondes pour 1 million d'entrées dans la table "worktime".

La question reste la même : quel est le moyen le plus efficace de résoudre ce problème?

Nico
la source
Je suis sûr qu'il y aura des conflits à ce sujet, mais vous pourriez essayer de ne pas le faire dans un CTE. Utilisez plutôt des tables temporaires et voyez si c'est plus rapide.
rottengeek
Juste une question de style: je n'ai jamais vu personne mettre tous les noms de colonne et de table entre guillemets. Est-ce la pratique de toute votre entreprise? Je trouve ça vraiment inconfortable. Ce n'est pas nécessaire à mon avis, et donc augmente le bruit sur le signal ...
ErikE
La méthode @ErikE Above fait partie d'un énorme addon. Certains des objets sont créés dynamiquement et dépendent du choix d'entrée de l'utilisateur final. Ainsi, par exemple, des blancs peuvent apparaître dans les noms de table ou de vue. les guillemets doubles autour de ceux-ci ne laisseront pas la requête planter ...!
Nico
@Nico dans mon monde qui se fait généralement avec des crochets, puis aime [this]. J'aime mieux que les guillemets doubles, je suppose.
ErikE
Les crochets @ErikE sont tsql. la norme est les guillemets doubles! de toute façon, je l'ai appris de cette façon et donc je m'y suis habitué!
Nico

Réponses:

3

Je ne peux pas répondre à votre question sur la meilleure façon. Mais je peux proposer une manière différente de résoudre le problème, qui peut être meilleure ou non. Son plan d'exécution est relativement plat et je pense qu'il fonctionnera bien. (J'ai hâte de savoir alors partagez les résultats!)

Je m'excuse d'avoir utilisé mon propre style de syntaxe au lieu du vôtre - cela aide la requête de sorcellerie à venir lorsque tout s'aligne à sa place habituelle.

La requête est disponible dans un SqlFiddle . J'ai ajouté un chevauchement pour EmpID 1 juste pour être sûr d'avoir couvert. Si vous constatez finalement que des chevauchements ne peuvent pas se produire dans les données de présence, vous pouvez supprimer la requête finale et les Dense_Rankcalculs.

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;

Remarque: les performances de cette requête seraient améliorées si vous combiniez les trois tables et ajoutiez une colonne pour indiquer le type d'heure: travail, pause ou absence.

Et pourquoi tous les CTE, demandez-vous? Parce que chacun est forcé par ce que je dois faire pour les données. Il y a un agrégat, ou j'ai besoin de mettre une condition WHERE sur une fonction de fenêtrage ou de l'utiliser dans une clause où les fonctions de fenêtrage ne sont pas autorisées.

Maintenant, je vais aller voir si je ne peux pas imaginer une autre stratégie pour y parvenir. :)

Pour le plaisir, j'inclus ici un "diagramme" que j'ai fait pour aider à résoudre le problème:

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

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

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

Les trois ensembles de tirets (séparés par des espaces) représentent, dans l'ordre: les données de présence, les données d'absence et le résultat souhaité.

ErikE
la source
Merci pour cette approche. Je vais le vérifier à mon retour au bureau et vous donner des résultats d'exécution avec une base de données plus grande.
Nico
Le temps d'exécution est nettement supérieur à la 1ère approche. Je n'ai pas eu le temps de vérifier si de nouveaux indices pouvaient encore le diminuer. Va vérifier dès que possible!
Nico
J'ai une autre idée que je n'ai pas eu le temps de travailler. Pour ce qu'elle vaut, votre requête renvoie des résultats incorrects avec des plages qui se chevauchent dans toutes les tables.
ErikE
J'ai vérifié cela à nouveau, voir ce violon qui a des intervalles qui se chevauchent complètement dans les trois tableaux. il renvoie des résultats corrects, comme je peux le voir. pourriez-vous fournir un cas dans lequel des résultats erronés sont retournés? n'hésitez pas à ajuster les données de démonstration en violon!
Nico
bien, j'ai compris. en cas d'intervalles se croisant dans une table, les résultats devenaient fous. vérifiera cela.
Nico