Toplam Ziyareti Hesapla


12

Çakışan günlere dikkat ederek bir müşteri için ziyaret sayısını hesaplamak zorunda bir sorgu yazmaya çalışıyorum. İtemID 2009 başlangıç ​​tarihi 23 ve bitiş tarihi 26'dır, bu nedenle 20010 öğesi bu günler arasındadır, bu satın alma tarihini toplam sayımıza eklemeyeceğiz.

Örnek Senaryo:

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 7 Ziyaret Günü olmalıdır

Giriş Tablosu:

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'  

Şimdiye kadar denedim:

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      

Yanıtlar:


5

Bu ilk sorgu, çakışma olmadan farklı Başlangıç ​​Tarihi ve Bitiş Tarihi aralıkları oluşturur.

Not:

  • Numuneniz ( id=0) Ypercube ( id=1) ' den bir örnekle karıştırılmıştır.
  • Bu çözüm, her kimlik veya çok sayıda kimlik için büyük miktarda veriyle iyi ölçeklenmeyebilir. Bunun bir sayı tablosu gerektirmeme avantajı vardır. Büyük veri kümesinde, bir sayı tablosu büyük olasılıkla daha iyi performans verecektir.

Sorgu:

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;

Çıktı:

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

Bu Başlangıç ​​Tarihini ve Bitiş Tarihini DATEDIFF ile kullanırsanız:

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
...

Çıktı (kopyalarla birlikte):

  • 0 kimliği için 1, 4 ve 2 (örneğiniz => SUM=7)
  • İd 1 için 3, 2 ve 5 (Ypercube örneği => SUM=10)

O zaman her şeyi bir SUMve ile birleştirmeniz yeterlidir 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;

Çıktı:

id  Days
0   7
1   10

2 farklı kimlikle kullanılan veriler:

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

8

Bir sürü vardır sorulara zaman aralıklarını ambalaj konusunda ve makaleler. Örneğin, Itzik Ben-Gan'ın Ambalaj Aralıkları .

Belirli bir kullanıcı için aralıklarınızı toplayabilirsiniz. Paketlendiğinde, çakışma olmayacaktır, böylece paketlenmiş aralıkların sürelerini özetleyebilirsiniz.


Aralıklarınız zamansız tarihlerse, bir Calendartablo kullanırdım . Bu tablonun birkaç on yıllık tarih listesi vardır. Takvim tablonuz yoksa bir tane oluşturun:

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

Böyle bir tabloyu doldurmanın birçok yolu vardır .

Örneğin, 1900-01-01'den 100K satır (~ 270 yıl):

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

Ayrıca bkz. Sayı tabloları neden "paha biçilmez"?

Bir Calendarmasanız olduğunda, nasıl kullanacağınız aşağıda açıklanmıştır.

Her orijinal satır ve ile Calendararasındaki tarihler kadar satır döndürmek için tabloyla birleştirilir .StartDateEndDate

Ardından, çakışan tarihleri ​​kaldıran farklı tarihleri ​​sayarız.

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
;

Sonuç

TotalCount
7

7

A Numbersve bir Calendartablonun çok faydalı olduğunu ve bu sorunun bir Takvim tablosu ile çok basitleştirilebileceğini kesinlikle kabul ediyorum .

Yine de başka bir çözüm önereceğim (bu bir takvim tablosuna veya pencereli toplamalara gerek yok - Itzik tarafından bağlantılı yazıdan bazı cevaplar gibi). Her durumda en verimli olmayabilir (veya her durumda en kötü olabilir!) Ama test etmenin zararlı olduğunu düşünmüyorum.

İlk olarak diğer aralıklarla örtüşmeyen başlangıç ​​ve bitiş tarihlerini bularak çalışır, ardından satır numaralarını atamak için iki satıra (başlangıç ​​ve bitiş tarihlerini ayrı ayrı) koyar ve son olarak 1. başlangıç ​​tarihini 1. bitiş tarihiyle eşleştirir. , 2. ile 2., vb .:

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 ;

Açık (CustID, StartDate, EndDate)ve açık (CustID, EndDate, StartDate)olmak üzere iki dizin, sorgunun performansını artırmak için yararlı olacaktır.

Takvim'e (belki de tek) göre bir avantaj, datetimedeğerlerle çalışmak ve "paketlenmiş aralıkların" uzunluğunu farklı hassasiyette, daha büyük (haftalar, yıllar) veya daha küçük (saat, dakika veya saniye) saymak için kolayca uyarlanabilmesidir. milisaniye, vb.) ve yalnızca tarihleri ​​saymaz. Dakika veya saniye hassasiyetli bir Takvim tablosu oldukça büyük olurdu ve (çapraz) büyük bir masaya katılmak oldukça ilginç bir deneyim olurdu, ancak muhtemelen en verimli olanı değil.

(Vladimir Baranov sayesinde): Performansın düzgün bir şekilde karşılaştırılmasını sağlamak oldukça zordur, çünkü farklı yöntemlerin performansı muhtemelen veri dağıtımına bağlı olacaktır. 1) aralıklar ne kadar sürer - aralıklar ne kadar kısa olursa Takvim tablosu o kadar iyi performans gösterir, çünkü uzun aralıklar çok fazla ara satır oluşturur. . Bence Itzik'in çözümünün performansı buna bağlı. Verileri eğriltmenin başka yolları olabilir ve çeşitli yöntemlerin verimliliğinin nasıl etkileneceğini söylemek zor.


1
2 kopya görüyorum. Veya anti-semijoins'i 2 yarı olarak sayarsak belki 3;)
ypercubeᵀᴹ

1
@ wBob Performans testleri yaptıysanız, lütfen bunları yanıtınıza ekleyin. Onları ve kesinlikle başkalarını gördüğüme sevinirim. Bu site böyle çalışır ..
ypercubeᵀᴹ

3
@wBob Bu kadar kavgacı olmaya gerek yok - hiç kimse performansla ilgili endişelerini dile getirmedi. Kendi endişeleriniz varsa, kendi testlerinizi yapabilirsiniz. Bir cevabın ne kadar karmaşık olduğuna dair öznel ölçümünüz, bir aşağı oy için bir neden değildir. Başka bir cevap vermek yerine kendi testlerinizi yapmaya ve kendi cevabınızı genişletmeye ne dersiniz? İsterseniz kendi yanıtınızı daha yüksek oylara layık yapın, ancak diğer meşru cevapları küçümsemeyin.
Monkpit

1
lol savaş yok burada @Monkpit. Mükemmel geçerli nedenler ve performans hakkında ciddi bir konuşma.
wBob

2
@wBob, performansın düzgün bir karşılaştırmasını yapmak oldukça zordur, çünkü farklı yöntemlerin performansı büyük olasılıkla veri dağıtımına bağlı olacaktır. 1) aralıklar ne kadar sürer - aralıklar ne kadar kısa olursa Takvim tablosu o kadar iyi performans gösterir, çünkü uzun aralıklar çok fazla ara satır oluşturur. . Bence Itzik'in çözümünün performansı buna bağlı. Verileri çarpıtmanın başka yolları olabilir, bunlar akla gelen birkaç şeydir.
Vladimir Baranov

2

Bunun bir takvim tablosu ile basit olacağını düşünüyorum, örneğin böyle bir şey:

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

Test donanımı

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 ;

2
Her ne kadar iyi çalışıyor olsa da, başlamak için bu Kötü alışkanlıkları okumalısınız : yanlış kullanım tarihi / aralık sorguları : Özet 2. DATETIME, SMALLDATETIME, DATETIME2 ve DATETIMEOFFSET'e karşı aralık sorguları için BETWEEN'den kaçının;
Julien Vavasseur
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.