Abfrageoptimierung: Zeitintervalle

10

Hauptsächlich habe ich zwei Arten von Zeitintervallen:

presence time und absence time

absence time Es kann sich um verschiedene Arten handeln (z. B. Pausen, Abwesenheiten, besondere Tage usw.), und Zeitintervalle können sich überschneiden und / oder überschneiden.

Es ist nicht sicher, dass in Rohdaten nur plausible Kombinationen von Intervallen existieren, z. Überlappende Anwesenheitsintervalle sind nicht sinnvoll, können aber existieren. Ich habe jetzt auf viele Arten versucht, die resultierenden Anwesenheitszeitintervalle zu identifizieren - für mich scheint das bequemste das folgende zu sein.

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

Einige Demo-Daten finden Sie unter SQL-Fiddle .

Die Rohdaten liegen in verschiedenen Tabellen in Form von "starttime" - "endtime"oder vor "starttime" - "duration".

Die Idee war, eine geordnete Liste jedes Zeitstempels mit einer "bitmaskierten" fortlaufenden Summe offener Intervalle zu jedem Zeitpunkt zu erhalten, um die Anwesenheitszeit abzuschätzen.

Die Geige funktioniert und liefert geschätzte Ergebnisse, auch wenn die Startzeiten in unterschiedlichen Intervallen gleich sind. In diesem Beispiel werden keine Indizes verwendet.

Ist dies der richtige Weg, um eine fragliche Aufgabe zu erfüllen, oder gibt es dafür einen eleganteren Weg?

Falls für die Beantwortung relevant: Die Datenmenge beträgt bis zu mehreren zehntausend Datensätze pro Mitarbeiter und Tabelle. sql-2012 ist nicht verfügbar, um eine fortlaufende Summe der Vorgänger inline insgesamt zu berechnen.


bearbeiten:

Führen Sie die Abfrage gerade für eine größere Anzahl von Testdaten (1000, 10.000, 100.000, 1 Million) aus und sehen Sie, dass die Laufzeit exponentiell zunimmt. Offensichtlich eine Warnflagge, oder?

Ich habe die Abfrage geändert und die Aggregation der fortlaufenden Summe durch ein eigenartiges Update entfernt.

Ich habe eine Hilfstabelle hinzugefügt:

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

und ich habe die Berechnung der rollierenden Summe an diesen Ort verschoben:

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

siehe SQL-Fiddle hier

Die Laufzeit verringerte sich auf 3 Sekunden in Bezug auf 1 Million Einträge in der "Arbeitszeit" -Tabelle.

Die Frage bleibt gleich : Was ist der effektivste Weg, um dies zu lösen?

Nico
quelle
Ich bin mir sicher, dass es Streit geben wird, aber Sie könnten versuchen, dies nicht in einem CTE zu tun. Verwenden Sie stattdessen temporäre Tabellen und prüfen Sie, ob diese schneller sind.
Rottengeek
Nur eine Stilfrage: Ich habe noch nie jemanden gesehen, der alle Spalten- und Tabellennamen in doppelte Anführungszeichen gesetzt hat. Ist das die Praxis Ihres gesamten Unternehmens? Ich finde es definitiv unangenehm. Es ist aus meiner Sicht nicht notwendig und erhöht somit das Rauschen über dem Signal ...
ErikE
@ErikE Die obige Methode ist Teil eines riesigen Addons. Einige der Objekte werden dynamisch erstellt und hängen von der Auswahl der Endbenutzereingaben ab. So könnten zB Leerzeichen in Tabellen- oder Ansichtsnamen auftauchen. doppelte Anführungszeichen um diese lassen die Abfrage nicht abstürzen ...!
Nico
@Nico in meiner Welt, die normalerweise mit eckigen Klammern gemacht wird, mag dann [this]. Ich mag das einfach besser als doppelte Anführungszeichen, denke ich.
ErikE
@ErikE eckige Klammern sind tsql. Standard ist doppelte Anführungszeichen! Jedenfalls habe ich es so gelernt und bin irgendwie daran gewöhnt!
Nico

Antworten:

3

Ich kann Ihre Frage nach dem absolut besten Weg nicht beantworten. Aber ich kann einen anderen Weg zur Lösung des Problems anbieten , der vielleicht besser ist oder nicht. Es hat einen ziemlich flachen Ausführungsplan, und ich denke, es wird gut funktionieren. (Ich bin gespannt, also teile die Ergebnisse!)

Ich entschuldige mich dafür, dass ich meinen eigenen Syntaxstil anstelle Ihres verwendet habe - er hilft mir, Abfrage-Assistenten zu finden, wenn alles an seinem gewohnten Platz ausgerichtet ist.

Die Abfrage ist in einer SqlFiddle verfügbar . Ich habe EmpID 1 überlappt, nur um sicherzugehen, dass ich das abgedeckt habe. Wenn Sie schließlich feststellen, dass Überlappungen in Anwesenheitsdaten nicht auftreten können, können Sie die endgültige Abfrage und die Dense_RankBerechnungen entfernen .

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;

Hinweis: Die Leistung dieser Abfrage würde verbessert. Sie haben die drei Tabellen kombiniert und eine Spalte hinzugefügt, um anzugeben, wie lange es gedauert hat: Arbeit, Pause oder Abwesenheit.

Und warum all die CTEs, fragst du? Weil jeder von dem gezwungen wird, was ich mit den Daten tun muss. Es gibt ein Aggregat, oder ich muss eine WHERE-Bedingung für eine Fensterfunktion setzen oder sie in einer Klausel verwenden, in der Fensterfunktionen nicht zulässig sind.

Jetzt werde ich nachsehen, ob ich mir keine andere Strategie ausdenken kann, um dies zu erreichen. :) :)

Zur Unterhaltung füge ich hier ein "Diagramm" hinzu, das ich zur Lösung des Problems erstellt habe:

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

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

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

Die drei Sätze von Strichen (durch Leerzeichen getrennt) repräsentieren in der Reihenfolge: Anwesenheitsdaten, Abwesenheitsdaten und das gewünschte Ergebnis.

ErikE
quelle
Vielen Dank für diesen Ansatz. Ich werde es überprüfen, wenn ich wieder im Büro bin, und Ihnen Laufzeitergebnisse mit größerer Datenbank geben.
Nico
Die Laufzeit ist definitiv viel höher als beim ersten Ansatz. Ich hatte keine Zeit zu prüfen, ob weitere Indizes es noch verringern könnten. Werde so schnell wie möglich nachsehen!
Nico
Ich habe eine andere Idee, für die ich keine Zeit hatte, mich an die Arbeit zu machen. Für das, was es wert ist, gibt Ihre Abfrage falsche Ergebnisse mit überlappenden Bereichen in allen Tabellen zurück.
ErikE
Ich habe das noch einmal überprüft und sehe diese Geige, die in allen drei Tabellen vollständig überlappende Intervalle aufweist. es gibt korrekte Ergebnisse zurück, wie ich sehen kann. Können Sie einen Fall angeben, in dem falsche Ergebnisse zurückgegeben werden? Fühlen Sie sich frei, Demo-Daten in Geige anzupassen!
Nico
Okay, ich habe deinen Standpunkt verstanden. Bei sich überschneidenden Intervallen in einer Tabelle wurden die Ergebnisse verrückt. wird dies überprüfen.
Nico