Tính tổng lượt truy cập


12

Tôi đang cố gắng viết một truy vấn trong đó tôi phải tính toán số lượt truy cập cho một khách hàng bằng cách chăm sóc các ngày chồng chéo. Giả sử cho ngày bắt đầu itemID 2009 là ngày 23 và ngày kết thúc là ngày 26 do đó mục 20010 là giữa những ngày này, chúng tôi sẽ không thêm ngày mua này vào tổng số của chúng tôi.

Kịch bản ví dụ:

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 phải là 7 lượt truy cập

Bảng đầu vào:

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'  

Tôi đã thử cho đến nay:

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      

Câu trả lời:


5

Truy vấn đầu tiên này tạo ra các phạm vi Ngày bắt đầu và Ngày kết thúc khác nhau mà không có sự trùng lặp.

Ghi chú:

  • Mẫu của bạn ( id=0) được trộn với một mẫu từ Ypercube ( id=1)
  • Giải pháp này có thể không mở rộng tốt với lượng dữ liệu khổng lồ cho mỗi id hoặc số lượng id lớn. Điều này có lợi thế là không yêu cầu bảng số. Với tập dữ liệu lớn, một bảng số rất có thể sẽ cho hiệu suất tốt hơn.

Truy vấn:

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;

Đầu ra:

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

Nếu bạn sử dụng các Ngày bắt đầu và Ngày kết thúc này với 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
...

Đầu ra (có trùng lặp) là:

  • 1, 4 và 2 cho id 0 (mẫu của bạn => SUM=7)
  • 3, 2 và 5 cho id 1 (mẫu Ypercube => SUM=10)

Sau đó, bạn chỉ cần đặt mọi thứ cùng với một SUMGROUP 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;

Đầu ra:

id  Days
0   7
1   10

Dữ liệu được sử dụng với 2 id khác nhau:

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

Có rất nhiều câu hỏi và bài viết về khoảng thời gian đóng gói. Ví dụ: Khoảng thời gian đóng gói của Itzik Ben-Gan.

Bạn có thể đóng gói khoảng thời gian của bạn cho người dùng nhất định. Sau khi đóng gói, sẽ không có sự chồng chéo, vì vậy bạn chỉ cần tổng hợp thời lượng của các khoảng thời gian đóng gói.


Nếu khoảng thời gian của bạn là ngày không có thời gian, tôi sẽ sử dụng Calendarbảng. Bảng này chỉ đơn giản là có một danh sách các ngày trong vài thập kỷ. Nếu bạn không có bảng Lịch, chỉ cần tạo một bảng:

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

nhiều cách để điền vào một bảng như vậy .

Ví dụ: 100K hàng (~ 270 năm) từ 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);

Xem thêm Tại sao các bảng số "vô giá"?

Một khi bạn có một Calendarbảng, đây là cách sử dụng nó.

Mỗi hàng ban đầu được nối với Calendarbảng để trả về nhiều hàng như có ngày giữa StartDateEndDate.

Sau đó, chúng tôi đếm ngày khác biệt, loại bỏ ngày chồng chéo.

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
;

Kết quả

TotalCount
7

7

Tôi hoàn toàn đồng ý rằng a Numbersvà aCalendar bảng rất hữu ích và nếu vấn đề này có thể được đơn giản hóa rất nhiều với bảng Lịch.

Tôi sẽ đề xuất một giải pháp khác mặc dù (không cần bảng lịch hoặc tổng hợp có cửa sổ - như một số câu trả lời từ bài đăng được liên kết của Itzik làm). Nó có thể không hiệu quả nhất trong mọi trường hợp (hoặc có thể là tồi tệ nhất trong mọi trường hợp!) Nhưng tôi không nghĩ rằng nó có hại khi thử nghiệm.

Nó hoạt động bằng cách tìm ngày bắt đầu và ngày kết thúc không trùng với các khoảng thời gian khác, sau đó đặt chúng thành hai hàng (riêng biệt ngày bắt đầu và ngày kết thúc) để gán cho chúng số hàng và cuối cùng khớp với ngày bắt đầu đầu tiên với ngày kết thúc đầu tiên , thứ 2 với thứ 2, v.v.:

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 ;

Hai chỉ mục, bật (CustID, StartDate, EndDate)và tắt (CustID, EndDate, StartDate)sẽ hữu ích để cải thiện hiệu năng của truy vấn.

Một lợi thế so với Lịch (có lẽ là duy nhất) là nó có thể dễ dàng thích nghi để làm việc với datetime các giá trị và đếm độ dài của "khoảng thời gian đóng gói" với độ chính xác khác nhau, lớn hơn (tuần, năm) hoặc nhỏ hơn (giờ, phút hoặc giây, mili giây, v.v.) và không chỉ đếm ngày. Một bảng Lịch có độ chính xác phút hoặc giây sẽ khá lớn và (chéo) nối nó với một bảng lớn sẽ là một trải nghiệm khá thú vị nhưng có thể không phải là hiệu quả nhất.

(cảm ơn Vladimir Baranov): Khá khó để có một so sánh đúng về hiệu suất, bởi vì hiệu suất của các phương pháp khác nhau có thể sẽ phụ thuộc vào phân phối dữ liệu. 1) khoảng thời gian là bao lâu - khoảng thời gian càng ngắn, bảng Lịch sẽ hoạt động tốt hơn, bởi vì khoảng thời gian dài sẽ tạo ra rất nhiều hàng trung gian 2) tần suất các khoảng cách chồng chéo - chủ yếu là các khoảng không chồng chéo so với hầu hết các khoảng trong cùng một phạm vi . Tôi nghĩ hiệu suất của giải pháp Itzik phụ thuộc vào điều đó. Có thể có những cách khác để làm lệch dữ liệu và thật khó để biết hiệu quả của các phương pháp khác nhau sẽ bị ảnh hưởng như thế nào.


1
Tôi thấy 2 bản. Hoặc có thể là 3 nếu chúng ta tính các chất chống bán là 2 nửa;)
ypercubeᵀᴹ 23/2/2016

1
@wBob nếu bạn đã thực hiện các bài kiểm tra hiệu suất, vui lòng thêm chúng vào câu trả lời của bạn. Tôi rất vui khi thấy họ và chắc chắn nhiều người khác. Đó là cách trang web hoạt động ..
ypercubeᵀᴹ 23/2/2016

3
@wBob Không cần thiết phải quá khích - không ai bày tỏ bất kỳ lo ngại nào về hiệu suất. Nếu bạn có mối quan tâm của riêng mình, bạn có thể chạy thử nghiệm của riêng mình. Đo lường chủ quan của bạn về mức độ phức tạp của một câu trả lời không phải là lý do cho một downvote. Làm thế nào về bạn thực hiện các bài kiểm tra của riêng bạn và mở rộng câu trả lời của riêng bạn, thay vì đưa một câu trả lời khác xuống? Làm cho câu trả lời của riêng bạn xứng đáng hơn với những câu trả lời nếu bạn muốn, nhưng đừng bỏ qua những câu trả lời hợp pháp khác.
Monkpit

1
lol không có trận chiến ở đây @Monkpit. Lý do hoàn toàn hợp lệ và một cuộc trò chuyện nghiêm túc về hiệu suất.
wBob

2
@wBob, thật khó để có một so sánh đúng về hiệu suất, bởi vì hiệu suất của các phương thức khác nhau có thể sẽ phụ thuộc vào việc phân phối dữ liệu. 1) khoảng thời gian là bao lâu - khoảng thời gian càng ngắn, bảng Lịch sẽ hoạt động tốt hơn, bởi vì khoảng thời gian dài sẽ tạo ra rất nhiều hàng trung gian 2) tần suất các khoảng cách chồng chéo - chủ yếu là các khoảng không chồng chéo so với hầu hết các khoảng trong cùng một phạm vi . Tôi nghĩ hiệu suất của giải pháp Itzik phụ thuộc vào điều đó. Có thể có những cách khác để làm lệch dữ liệu, đây chỉ là một vài cách mà bạn nghĩ đến.
Vladimir Baranov

2

Tôi nghĩ rằng điều này sẽ đơn giản với một bảng lịch, ví dụ như một cái gì đó như thế này:

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

Kiểm tra giàn khoan

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
Mặc dù nó hoạt động tốt, nhưng bạn nên đọc thói quen xấu này để xử lý: xử lý sai các truy vấn ngày / phạm vi : Tóm tắt 2. tránh GIỮA cho các truy vấn phạm vi đối với DATETIME, SMALLDATETIME, DATETIME2 và DATETIMEOFFSET;
Julien Vavasseur
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.