Berechnen Sie die Gesamtzahl der Besuche

12

Ich versuche eine Anfrage zu schreiben, bei der ich die Anzahl der Besuche für einen Kunden berechnen muss, indem ich auf überlappende Tage achte. Angenommen, für itemID 2009 ist das Startdatum der 23. und das Enddatum der 26. Tag. Daher wird bei item 20010 zwischen diesen Tagen das Kaufdatum nicht zu unserer Gesamtzahl hinzugefügt.

Beispielszenario:

Item ID Start Date   End Date   Number of days     Number of days Candidate for visit count
20009   2015-01-23  2015-01-26     4                      4
20010   2015-01-24  2015-01-24     1                      0
20011   2015-01-23  2015-01-26     4                      0
20012   2015-01-23  2015-01-27     5                      1
20013   2015-01-23  2015-01-27     5                      0
20014   2015-01-29  2015-01-30     2                      2

OutPut sollte 7 VisitDays sein

Eingabetabelle:

CREATE TABLE #Items    
(
CustID INT,
ItemID INT,
StartDate DATETIME,
EndDate DATETIME
)           


INSERT INTO #Items
SELECT 11205, 20009, '2015-01-23',  '2015-01-26'  
UNION ALL 
SELECT 11205, 20010, '2015-01-24',  '2015-01-24'    
UNION ALL  
SELECT 11205, 20011, '2015-01-23',  '2015-01-26' 
UNION ALL  
SELECT 11205, 20012, '2015-01-23',  '2015-01-27'  
UNION ALL  
SELECT 11205, 20012, '2015-01-23',  '2015-01-27'   
UNION ALL  
SELECT 11205, 20012, '2015-01-28',  '2015-01-29'  

Ich habe bisher versucht:

CREATE TABLE #VisitsTable
    (
      StartDate DATETIME,
      EndDate DATETIME
    )

INSERT  INTO #VisitsTable
        SELECT DISTINCT
                StartDate,
                EndDate
        FROM    #Items items
        WHERE   CustID = 11205
        ORDER BY StartDate ASC

IF EXISTS (SELECT TOP 1 1 FROM #VisitsTable) 
BEGIN 


SELECT  ISNULL(SUM(VisitDays),1)
FROM    ( SELECT DISTINCT
                    abc.StartDate,
                    abc.EndDate,
                    DATEDIFF(DD, abc.StartDate, abc.EndDate) + 1 VisitDays
          FROM      #VisitsTable abc
                    INNER JOIN #VisitsTable bc ON bc.StartDate NOT BETWEEN abc.StartDate AND abc.EndDate      
        ) Visits

END



--DROP TABLE #Items 
--DROP TABLE #VisitsTable      
AA.SC
quelle

Antworten:

5

Diese erste Abfrage erstellt unterschiedliche Bereiche für Start- und Enddatum ohne Überschneidungen.

Hinweis:

  • Ihre Probe ( id=0) wird mit einer Probe von Ypercube ( id=1) gemischt
  • Diese Lösung lässt sich mit einer großen Datenmenge für jede ID oder eine große Anzahl von IDs möglicherweise nicht gut skalieren. Dies hat den Vorteil, dass keine Nummerntabelle erforderlich ist. Bei einem großen Datensatz bietet eine Nummerntabelle mit hoher Wahrscheinlichkeit eine bessere Leistung.

Abfrage:

SELECT DISTINCT its.id
    , Start_Date = its.Start_Date 
    , End_Date = COALESCE(DATEADD(day, -1, itmax.End_Date), CASE WHEN itmin.Start_Date > its.End_Date THEN itmin.Start_Date ELSE its.End_Date END)
    --, x1=itmax.End_Date, x2=itmin.Start_Date, x3=its.End_Date
FROM @Items its
OUTER APPLY (
    SELECT Start_Date = MAX(End_Date) FROM @Items std
    WHERE std.Item_ID <> its.Item_ID AND std.Start_Date < its.Start_Date AND std.End_Date > its.Start_Date
) itmin
OUTER APPLY (
    SELECT End_Date = MIN(Start_Date) FROM @Items std
    WHERE std.Item_ID <> its.Item_ID+1000 AND std.Start_Date > its.Start_Date AND std.Start_Date < its.End_Date
) itmax;

Ausgabe:

id  | Start_Date                    | End_Date                      
0   | 2015-01-23 00:00:00.0000000   | 2015-01-23 00:00:00.0000000   => 1
0   | 2015-01-24 00:00:00.0000000   | 2015-01-27 00:00:00.0000000   => 4
0   | 2015-01-29 00:00:00.0000000   | 2015-01-30 00:00:00.0000000   => 2
1   | 2016-01-20 00:00:00.0000000   | 2016-01-22 00:00:00.0000000   => 3
1   | 2016-01-23 00:00:00.0000000   | 2016-01-24 00:00:00.0000000   => 2
1   | 2016-01-25 00:00:00.0000000   | 2016-01-29 00:00:00.0000000   => 5

Wenn Sie dieses Start- und Enddatum mit DATEDIFF verwenden:

SELECT DATEDIFF(day
    , its.Start_Date 
    , End_Date = COALESCE(DATEADD(day, -1, itmax.End_Date), CASE WHEN itmin.Start_Date > its.End_Date THEN itmin.Start_Date ELSE its.End_Date END)
) + 1
...

Ausgabe (mit Duplikaten) ist:

  • 1, 4 und 2 für id 0 (Ihre Probe => SUM=7)
  • 3, 2 und 5 für id 1 (Ypercube sample => SUM=10)

Sie müssen dann nur noch alles mit einem SUMund zusammenfügen GROUP BY:

SELECT id 
    , Days = SUM(
        DATEDIFF(day, Start_Date, End_Date)+1
    )
FROM (
    SELECT DISTINCT its.id
         , Start_Date = its.Start_Date 
        , End_Date = COALESCE(DATEADD(day, -1, itmax.End_Date), CASE WHEN itmin.Start_Date > its.End_Date THEN itmin.Start_Date ELSE its.End_Date END)
    FROM @Items its
    OUTER APPLY (
        SELECT Start_Date = MAX(End_Date) FROM @Items std
        WHERE std.Item_ID <> its.Item_ID AND std.Start_Date < its.Start_Date AND std.End_Date > its.Start_Date
    ) itmin
    OUTER APPLY (
        SELECT End_Date = MIN(Start_Date) FROM @Items std
        WHERE std.Item_ID <> its.Item_ID AND std.Start_Date > its.Start_Date AND std.Start_Date < its.End_Date
    ) itmax
) as d
GROUP BY id;

Ausgabe:

id  Days
0   7
1   10

Daten verwendet mit 2 verschiedenen IDs:

INSERT INTO @Items
    (id, Item_ID, Start_Date, End_Date)
VALUES 
    (0, 20009, '2015-01-23', '2015-01-26'),
    (0, 20010, '2015-01-24', '2015-01-24'),
    (0, 20011, '2015-01-23', '2015-01-26'),
    (0, 20012, '2015-01-23', '2015-01-27'),
    (0, 20013, '2015-01-23', '2015-01-27'),
    (0, 20014, '2015-01-29', '2015-01-30'),

    (1, 20009, '2016-01-20', '2016-01-24'),
    (1, 20010, '2016-01-23', '2016-01-26'),
    (1, 20011, '2016-01-25', '2016-01-29')
Julien Vavasseur
quelle
8

Es gibt viele Fragen und Artikel zu Verpackungsintervallen. Zum Beispiel Verpackungsintervalle von Itzik Ben-Gan.

Sie können Ihre Intervalle für den angegebenen Benutzer packen. Einmal gepackt, gibt es keine Überlappungen, sodass Sie einfach die Dauer gepackter Intervalle zusammenfassen können.


Wenn Ihre Intervalle Daten ohne Zeiten sind, würde ich eine CalendarTabelle verwenden. Diese Tabelle enthält lediglich eine Liste von Daten für mehrere Jahrzehnte. Wenn Sie keine Kalendertabelle haben, erstellen Sie einfach eine:

CREATE TABLE [dbo].[Calendar](
    [dt] [date] NOT NULL,
CONSTRAINT [PK_Calendar] PRIMARY KEY CLUSTERED 
(
    [dt] ASC
));

Es gibt viele Möglichkeiten, eine solche Tabelle zu füllen .

Zum Beispiel 100K-Zeilen (~ 270 Jahre) von 1900-01-01:

INSERT INTO dbo.Calendar (dt)
SELECT TOP (100000) 
    DATEADD(day, ROW_NUMBER() OVER (ORDER BY s1.[object_id])-1, '19000101') AS dt
FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2
OPTION (MAXDOP 1);

Siehe auch Warum sind Zahlentabellen "von unschätzbarem Wert"?

Sobald Sie einen CalendarTisch haben, erfahren Sie , wie Sie ihn verwenden.

Jede ursprüngliche Zeile wird mit der CalendarTabelle verknüpft, um so viele Zeilen zurückzugeben, wie Daten zwischen StartDateund liegenEndDate .

Dann zählen wir verschiedene Daten, wodurch überlappende Daten entfernt werden.

SELECT COUNT(DISTINCT CA.dt) AS TotalCount
FROM
    #Items AS T
    CROSS APPLY
    (
        SELECT dbo.Calendar.dt
        FROM dbo.Calendar
        WHERE
            dbo.Calendar.dt >= T.StartDate
            AND dbo.Calendar.dt <= T.EndDate
    ) AS CA
WHERE T.CustID = 11205
;

Ergebnis

TotalCount
7
Vladimir Baranov
quelle
7

Ich bin der festen Meinung, dass a Numbersund aCalendar Tabelle sehr nützlich sind und wenn sich dieses Problem mit einer Kalendertabelle stark vereinfachen lässt.

Ich werde jedoch eine andere Lösung vorschlagen (für die weder eine Kalendertabelle noch Fensteraggregate erforderlich sind - wie es einige Antworten aus dem verlinkten Beitrag von Itzik tun). Es ist vielleicht nicht in allen Fällen die effizienteste (oder in allen Fällen die schlechteste!), Aber ich denke nicht, dass es schadet, zu testen.

Es funktioniert, indem zuerst Start- und Enddatum gefunden werden, die sich nicht mit anderen Intervallen überschneiden, diese dann in zwei Zeilen (getrennt das Start- und Enddatum) gestellt werden, um ihnen Zeilennummern zuzuweisen, und schließlich das 1. Startdatum mit dem 1. Enddatum abgeglichen werden , der 2. mit dem 2. usw .:

WITH 
  start_dates AS
    ( SELECT CustID, StartDate,
             Rn = ROW_NUMBER() OVER (PARTITION BY CustID 
                                     ORDER BY StartDate)
      FROM items AS i
      WHERE NOT EXISTS
            ( SELECT *
              FROM Items AS j
              WHERE j.CustID = i.CustID
                AND j.StartDate < i.StartDate AND i.StartDate <= j.EndDate 
            )
      GROUP BY CustID, StartDate
    ),
  end_dates AS
    ( SELECT CustID, EndDate,
             Rn = ROW_NUMBER() OVER (PARTITION BY CustID 
                                     ORDER BY EndDate) 
      FROM items AS i
      WHERE NOT EXISTS
            ( SELECT *
              FROM Items AS j
              WHERE j.CustID = i.CustID
                AND j.StartDate <= i.EndDate AND i.EndDate < j.EndDate 
            )
      GROUP BY CustID, EndDate
    )
SELECT s.CustID, 
       Result = SUM( DATEDIFF(day, s.StartDate, e.EndDate) + 1 )
FROM start_dates AS s
  JOIN end_dates AS e
    ON  s.CustID = e.CustID
    AND s.Rn = e.Rn 
GROUP BY s.CustID ;

Zwei Indizes (on (CustID, StartDate, EndDate)und on) (CustID, EndDate, StartDate)sind nützlich, um die Leistung der Abfrage zu verbessern.

Ein Vorteil gegenüber dem Kalender (vielleicht dem einzigen) ist, dass er leicht an die Arbeit angepasst werden kann datetime Werten und die Zählung der Länge der "gepackten Intervalle" mit unterschiedlicher Genauigkeit , und zwar größer (Wochen, Jahre) oder kleiner (Stunden, Minuten oder Sekunden). Millisekunden usw.) und zählt nicht nur Daten. Ein Kalendertisch mit einer Genauigkeit von Minuten oder Sekunden wäre ziemlich groß, und (Kreuz-) Verbinden mit einem großen Tisch wäre eine interessante Erfahrung, aber möglicherweise nicht die effizienteste.

(danke an Vladimir Baranov): Es ist ziemlich schwierig, einen korrekten Leistungsvergleich zu haben, da die Leistung verschiedener Methoden wahrscheinlich von der Datenverteilung abhängen würde. 1) Wie lang sind die Intervalle? Je kürzer die Intervalle, desto besser die Leistung der Kalendertabelle, da bei langen Intervallen viele Zwischenzeilen entstehen. 2) Wie oft überlappen sich Intervalle? Meist nicht überlappende Intervalle im Vergleich zu den meisten Intervallen, die denselben Bereich abdecken . Ich denke, dass die Leistung von Itziks Lösung davon abhängt. Es könnte andere Möglichkeiten geben, die Daten zu verzerren, und es ist schwer zu sagen, wie sich die Effizienz der verschiedenen Methoden auswirken würde.

ypercubeᵀᴹ
quelle
1
Ich sehe 2 Exemplare. Oder vielleicht 3, wenn wir die Antisemijoins als 2 Hälften zählen;)
ypercubeᵀᴹ
1
@wBob Wenn Sie Leistungstests durchgeführt haben, fügen Sie diese bitte in Ihre Antwort ein. Ich würde mich freuen, sie und sicherlich viele andere zu sehen. So funktioniert die Seite ..
ypercubeᵀᴹ
3
@wBob Man muss nicht so kämpferisch sein - niemand äußerte Bedenken hinsichtlich der Leistung. Wenn Sie Ihre eigenen Bedenken haben, können Sie gerne Ihre eigenen Tests durchführen. Ihre subjektive Einschätzung, wie kompliziert eine Antwort ist, ist kein Grund für eine Ablehnung. Wie wäre es, wenn Sie Ihre eigenen Tests durchführen und Ihre eigene Antwort erweitern, anstatt eine andere Antwort zu notieren? Erhöhen Sie die Wertigkeit Ihrer eigenen Antwort, wenn Sie möchten, stimmen Sie jedoch andere legitime Antworten nicht ab.
Monkpit
1
lol kein Kampf hier @Monkpit. Tadellose Gründe und ein ernstes Gespräch über die Leistung.
wBob
2
@wBob, es ist ziemlich schwierig, die Leistung richtig zu vergleichen, da die Leistung verschiedener Methoden wahrscheinlich von der Datenverteilung abhängt. 1) Wie lang sind die Intervalle? Je kürzer die Intervalle, desto besser die Leistung der Kalendertabelle, da bei langen Intervallen viele Zwischenzeilen entstehen. 2) Wie oft überlappen sich Intervalle? Meist nicht überlappende Intervalle im Vergleich zu den meisten Intervallen, die denselben Bereich abdecken . Ich denke, dass die Leistung von Itziks Lösung davon abhängt. Es gibt auch andere Möglichkeiten, die Daten zu verzerren. Dies sind nur einige, die mir in den Sinn kommen.
Vladimir Baranov
2

Ich denke, das wäre mit einer Kalendertabelle einfach, zB so etwas:

SELECT i.CustID, COUNT( DISTINCT c.calendarDate ) days
FROM #Items i
    INNER JOIN calendar.main c ON c.calendarDate Between i.StartDate And i.EndDate
GROUP BY i.CustID

Prüfstand

USE tempdb
GO

-- Cutdown calendar script
IF OBJECT_ID('dbo.calendar') IS NULL
BEGIN

    CREATE TABLE dbo.calendar (
        calendarId      INT IDENTITY(1,1) NOT NULL,
        calendarDate    DATE NOT NULL,

        CONSTRAINT PK_calendar__main PRIMARY KEY ( calendarDate ASC ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
        CONSTRAINT UK_calendar__main UNIQUE NONCLUSTERED ( calendarId ASC ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
    ) ON [PRIMARY]
END
GO


-- Populate calendar table once only
IF NOT EXISTS ( SELECT * FROM dbo.calendar )
BEGIN

    -- Populate calendar table
    WITH cte AS
    (
    SELECT 0 x
    UNION ALL
    SELECT x + 1
    FROM cte
    WHERE x < 11323 -- Do from year 1 Jan 2000 until 31 Dec 2030 (extend if required)
    )
    INSERT INTO dbo.calendar ( calendarDate )
    SELECT
        calendarDate
    FROM
        (
        SELECT 
            DATEADD( day, x, '1 Jan 2010' ) calendarDate,
            DATEADD( month, -7, DATEADD( day, x, '1 Jan 2010' ) ) academicDate
        FROM cte
        ) x
    WHERE calendarDate < '1 Jan 2031'
    OPTION ( MAXRECURSION 0 )

    ALTER INDEX ALL ON dbo.calendar REBUILD

END
GO





IF OBJECT_ID('tempdb..Items') IS NOT NULL DROP TABLE Items
GO

CREATE TABLE dbo.Items
    (
    CustID INT NOT NULL,
    ItemID INT NOT NULL,
    StartDate DATE NOT NULL,
    EndDate DATE NOT NULL,

    INDEX _cdx_Items CLUSTERED ( CustID, StartDate, EndDate )
    )
GO

INSERT INTO Items ( CustID, ItemID, StartDate, EndDate )
SELECT 11205, 20009, '2015-01-23',  '2015-01-26'  
UNION ALL 
SELECT 11205, 20010, '2015-01-24',  '2015-01-24'    
UNION ALL  
SELECT 11205, 20011, '2015-01-23',  '2015-01-26' 
UNION ALL  
SELECT 11205, 20012, '2015-01-23',  '2015-01-27'  
UNION ALL  
SELECT 11205, 20012, '2015-01-23',  '2015-01-27'   
UNION ALL  
SELECT 11205, 20012, '2015-01-28',  '2015-01-29'
GO


-- Scale up : )
;WITH cte AS (
SELECT TOP 1000000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT INTO Items ( CustID, ItemID, StartDate, EndDate )
SELECT 11206 + rn % 999, 20012 + rn, DATEADD( day, rn % 333, '1 Jan 2015' ), DATEADD( day, ( rn % 333 ) + rn % 7, '1 Jan 2015' )
FROM cte
GO
--:exit



-- My query: Pros: simple, one copy of items, easy to understand and maintain.  Scales well to 1 million + rows.
-- Cons: requires calendar table.  Others?
SELECT i.CustID, COUNT( DISTINCT c.calendarDate ) days
FROM dbo.Items i
    INNER JOIN dbo.calendar c ON c.calendarDate Between i.StartDate And i.EndDate
GROUP BY i.CustID
--ORDER BY i.CustID
GO


-- Vladimir query: Pros: Effectively same as above
-- Cons: I wouldn't use CROSS APPLY where it's not necessary.  Fortunately optimizer simplifies avoiding RBAR (I think).
-- Point of style maybe, but in terms of queries being self-documenting I prefer number 1.
SELECT T.CustID, COUNT( DISTINCT CA.calendarDate ) AS TotalCount
FROM
    Items AS T
    CROSS APPLY
    (
        SELECT c.calendarDate
        FROM dbo.calendar c
        WHERE
            c.calendarDate >= T.StartDate
            AND c.calendarDate <= T.EndDate
    ) AS CA
GROUP BY T.CustID
--ORDER BY T.CustID
--WHERE T.CustID = 11205
GO


/*  WARNING!! This is commented out as it can't compete in the scale test.  Will finish at scale 100, 1,000, 10,000, eventually.  I got 38 mins for 10,0000.  Pegs CPU.  

-- Julian:  Pros; does not require calendar table.
-- Cons: over-complicated (eg versus Query 1 in terms of number of lines of code, clauses etc); three copies of dbo.Items table (we have already shown
-- this query is possible with one); does not scale (even at 100,000 rows query ran for 38 minutes on my test rig versus sub-second for first two queries).  <<-- this is serious.
-- Indexing could help.
SELECT DISTINCT
    CustID,
     StartDate = CASE WHEN itmin.StartDate < its.StartDate THEN itmin.StartDate ELSE its.StartDate END
    , EndDate = CASE WHEN itmax.EndDate > its.EndDate THEN itmax.EndDate ELSE its.EndDate END
FROM Items its
OUTER APPLY (
    SELECT StartDate = MIN(StartDate) FROM Items std
    WHERE std.ItemID <> its.ItemID AND (
        (std.StartDate <= its.StartDate AND std.EndDate >= its.StartDate)
        OR (std.StartDate >= its.StartDate AND std.StartDate <= its.EndDate)
    )
) itmin
OUTER APPLY (
    SELECT EndDate = MAX(EndDate) FROM Items std
    WHERE std.ItemID <> its.ItemID AND (
        (std.EndDate >= its.StartDate AND std.EndDate <= its.EndDate)
        OR (std.StartDate <= its.EndDate AND std.EndDate >= its.EndDate)
    )
) itmax
GO
*/

-- ypercube:  Pros; does not require calendar table.
-- Cons: over-complicated (eg versus Query 1 in terms of number of lines of code, clauses etc); four copies of dbo.Items table (we have already shown
-- this query is possible with one); does not scale well; at 1,000,000 rows query ran for 2:20 minutes on my test rig versus sub-second for first two queries.
WITH 
  start_dates AS
    ( SELECT CustID, StartDate,
             Rn = ROW_NUMBER() OVER (PARTITION BY CustID 
                                     ORDER BY StartDate)
      FROM items AS i
      WHERE NOT EXISTS
            ( SELECT *
              FROM Items AS j
              WHERE j.CustID = i.CustID
                AND j.StartDate < i.StartDate AND i.StartDate <= j.EndDate 
            )
      GROUP BY CustID, StartDate
    ),
  end_dates AS
    ( SELECT CustID, EndDate,
             Rn = ROW_NUMBER() OVER (PARTITION BY CustID 
                                     ORDER BY EndDate) 
      FROM items AS i
      WHERE NOT EXISTS
            ( SELECT *
              FROM Items AS j
              WHERE j.CustID = i.CustID
                AND j.StartDate <= i.EndDate AND i.EndDate < j.EndDate 
            )
      GROUP BY CustID, EndDate
    )
SELECT s.CustID, 
       Result = SUM( DATEDIFF(day, s.StartDate, e.EndDate) + 1 )
FROM start_dates AS s
  JOIN end_dates AS e
    ON  s.CustID = e.CustID
    AND s.Rn = e.Rn 
GROUP BY s.CustID ;
wBob
quelle
2
Obwohl dies in Ordnung ist, sollten Sie die folgenden schlechten Angewohnheiten lesen, um zu kicken: Misshandlung von Datums- / Bereichsabfragen : Zusammenfassung 2. Vermeiden Sie ZWISCHEN bei Bereichsabfragen für DATETIME, SMALLDATETIME, DATETIME2 und DATETIMEOFFSET.
Julien Vavasseur