Hitung Total Kunjungan


12

Saya mencoba menulis kueri di mana saya harus menghitung jumlah kunjungan untuk pelanggan dengan mengurus hari yang tumpang tindih. Misalkan untuk tanggal mulai itemID 2009 adalah tanggal 23 dan tanggal berakhir adalah tanggal 26 maka dari itu item 20010 adalah antara hari-hari ini kami tidak akan menambahkan tanggal pembelian ini ke jumlah total kami.

Skenario Contoh:

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 harus 7 VisitDays

Tabel Input:

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'  

Saya sudah mencoba sejauh ini:

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      

Jawaban:


5

Kueri pertama ini menciptakan rentang Tanggal Mulai dan Tanggal Berakhir yang berbeda tanpa tumpang tindih.

catatan:

  • Sampel Anda ( id=0) dicampur dengan sampel dari Ypercube ( id=1)
  • Solusi ini mungkin tidak skala dengan baik dengan sejumlah besar data untuk setiap id atau jumlah id yang sangat besar Ini memiliki keuntungan karena tidak memerlukan tabel angka. Dengan dataset besar, tabel angka kemungkinan besar akan memberikan kinerja yang lebih baik.

Pertanyaan:

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;

Keluaran:

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

Jika Anda menggunakan Tanggal Mulai dan Tanggal Berakhir ini dengan DATEDIFF:

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

Output (dengan duplikat) adalah:

  • 1, 4 dan 2 untuk id 0 (sampel Anda => SUM=7)
  • 3, 2 dan 5 untuk id 1 (Sampel Ypercube => SUM=10)

Maka Anda hanya perlu menggabungkan semuanya dengan a SUMdan 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;

Keluaran:

id  Days
0   7
1   10

Data digunakan dengan 2 id berbeda:

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

Ada banyak pertanyaan dan artikel tentang interval waktu pengepakan. Misalnya, Interval Pengepakan oleh Itzik Ben-Gan.

Anda dapat mengemas interval Anda untuk pengguna yang diberikan. Setelah dikemas, tidak akan ada tumpang tindih, jadi Anda bisa meringkas durasi interval yang dikemas.


Jika interval Anda adalah tanggal tanpa waktu, saya akan menggunakan Calendartabel. Tabel ini hanya memiliki daftar tanggal selama beberapa dekade. Jika Anda tidak memiliki tabel Kalender, cukup buat satu:

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

Ada banyak cara untuk mengisi tabel seperti itu .

Misalnya, 100 ribu baris (~ 270 tahun) dari 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);

Lihat juga Mengapa tabel angka "tidak ternilai"?

Setelah Anda memiliki Calendartabel, berikut adalah cara menggunakannya.

Setiap baris asli digabungkan dengan Calendartabel untuk mengembalikan baris sebanyak ada tanggal antara StartDatedan EndDate.

Lalu kami menghitung tanggal yang berbeda, yang menghilangkan tanggal yang tumpang tindih.

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
;

Hasil

TotalCount
7

7

Saya sangat setuju bahwa tabel Numbersa dan Calendarsangat berguna dan jika masalah ini dapat disederhanakan banyak dengan tabel Kalender.

Saya akan menyarankan solusi lain (yang tidak membutuhkan tabel kalender atau agregat berjendela - seperti beberapa jawaban dari posting tertaut oleh Itzik lakukan). Ini mungkin bukan yang paling efisien dalam semua kasus (atau mungkin yang terburuk dalam semua kasus!) Tapi saya pikir itu tidak berbahaya untuk diuji.

Ini bekerja dengan pertama-tama menemukan tanggal mulai dan berakhir yang tidak tumpang tindih dengan interval lain, kemudian menempatkannya dalam dua baris (secara terpisah tanggal mulai dan berakhir) untuk menetapkan nomor baris dan akhirnya mencocokkan tanggal mulai pertama dengan tanggal akhir pertama , yang ke-2 dengan ke-2, dll .:

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 ;

Dua indeks, terus (CustID, StartDate, EndDate)dan terus(CustID, EndDate, StartDate) akan berguna untuk meningkatkan kinerja kueri.

Kelebihan dari Kalender (mungkin satu-satunya) adalah kalender dapat dengan mudah disesuaikan untuk digunakan datetime nilai nilai dan menghitung panjang "interval waktu" dalam presisi yang berbeda, lebih besar (minggu, tahun) atau lebih kecil (jam, menit atau detik, milidetik, dll) dan tidak hanya menghitung tanggal. Tabel Kalender dengan ketelitian menit atau detik akan cukup besar dan (silang) menyatukannya ke meja besar akan menjadi pengalaman yang cukup menarik tetapi mungkin bukan yang paling efisien.

(terima kasih kepada Vladimir Baranov): Agak sulit untuk memiliki perbandingan kinerja yang tepat, karena kinerja metode yang berbeda kemungkinan akan tergantung pada distribusi data. 1) berapa lama interval - semakin pendek interval, tabel Kalender akan lebih baik, karena interval panjang akan menghasilkan banyak baris menengah 2) seberapa sering interval tumpang tindih - sebagian besar interval tidak tumpang tindih vs sebagian besar interval yang mencakup rentang yang sama . Saya pikir kinerja solusi Itzik tergantung pada itu. Mungkin ada cara lain untuk memiringkan data dan sulit untuk mengatakan bagaimana efisiensi berbagai metode akan terpengaruh.


1
Saya melihat 2 salinan. Atau mungkin 3 jika kita menghitung anti-semijoins sebagai 2 setengah;)
ypercubeᵀᴹ

1
@ wBob jika Anda membuat tes kinerja, tambahkan di jawaban Anda. Saya akan senang melihat mereka dan pasti banyak orang lain. Begitulah cara kerjanya ..
ypercubeᵀᴹ

3
@ wBob Tidak perlu bersikap agresif - tidak ada yang menyatakan kekhawatiran tentang kinerja. Jika Anda memiliki masalah sendiri, Anda dapat menjalankan tes Anda sendiri. Pengukuran subjektif Anda tentang seberapa rumit suatu jawaban bukanlah alasan untuk downvote. Bagaimana kalau Anda melakukan tes Anda sendiri dan memperluas jawaban Anda sendiri, daripada menjatuhkan jawaban lain? Jadikan jawaban Anda sendiri lebih layak untuk upvotes jika Anda mau, tetapi jangan downvote jawaban sah lainnya.
Monkpit

1
lol tidak ada pertempuran di sini @Monkpit. Alasan yang benar-benar valid dan percakapan serius tentang kinerja.
wBob

2
@ wBob, agak sulit untuk memiliki perbandingan kinerja yang tepat, karena kinerja metode yang berbeda kemungkinan akan tergantung pada distribusi data. 1) berapa lama interval - semakin pendek interval, tabel Kalender akan lebih baik, karena interval panjang akan menghasilkan banyak baris menengah 2) seberapa sering interval tumpang tindih - kebanyakan interval tidak tumpang tindih vs sebagian besar interval yang mencakup rentang yang sama . Saya pikir kinerja solusi Itzik tergantung pada itu. Mungkin ada cara lain untuk mengubah data, ini hanya beberapa yang terlintas dalam pikiran.
Vladimir Baranov

2

Saya pikir ini akan langsung dengan tabel kalender, misalnya sesuatu seperti ini:

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

Alat uji

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
Meskipun berfungsi dengan baik, Anda harus membaca kebiasaan buruk ini untuk menendang: kueri tanggal / rentang penanganan yang salah : Ringkasan 2. hindari ANTARA untuk kueri rentang terhadap DATETIME, SMALLDATETIME, DATETIME2, dan DATETIMEOFFSET;
Julien Vavasseur
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.