총 방문수 계산


12

중복되는 일을 처리하여 고객의 방문수를 계산 해야하는 쿼리를 작성하려고합니다. itemID 2009 시작 날짜가 23 일이고 종료 날짜가 26 일이라고 가정하면 항목 20010은이 날짜 사이에 있으며이 구매 날짜를 총 수에 추가하지 않습니다.

시나리오 예 :

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 일 방문해야합니다.

입력 테이블 :

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'  

나는 지금까지 시도했다 :

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      

답변:


5

이 첫 번째 쿼리는 서로 다른 시작 날짜 및 종료 날짜 범위를 겹치지 않게 만듭니다.

노트 :

  • 샘플 ( id=0)이 Ypercube ( id=1) 의 샘플과 혼합되었습니다.
  • 이 솔루션은 각 ID 또는 엄청난 수의 ID에 대해 엄청난 양의 데이터로 확장 할 수 없습니다. 이것은 숫자 테이블이 필요 없다는 장점이 있습니다. 큰 데이터 집합을 사용하면 숫자 테이블이 더 나은 성능을 제공 할 가능성이 높습니다.

질문:

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;

산출:

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

이 시작 날짜와 종료 날짜를 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
...

출력 (중복 포함)은 다음과 같습니다.

  • id 0의 경우 1, 4 및 2 (샘플 => SUM=7)
  • id 1의 경우 3, 2 및 5 (Ypercube 샘플 => SUM=10)

그런 다음 모든 것을 a 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;

산출:

id  Days
0   7
1   10

2 개의 다른 ID와 함께 사용되는 데이터 :

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

포장 시간 간격에 대한 많은 질문 과 기사가 있습니다. 예를 들어, Itzik Ben-Gan의 패킹 간격 .

주어진 사용자에 대한 간격을 포장 할 수 있습니다. 패킹 된 후에는 겹치지 않으므로 패킹 된 간격의 기간을 간단히 요약 할 수 있습니다.


간격이 시간이없는 날짜 인 경우 Calendar표를 사용 합니다. 이 표에는 단순히 수십 년 동안의 날짜 목록이 있습니다. 캘린더 테이블이없는 경우 간단히 캘린더 테이블을 만드십시오.

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

이러한 테이블을 채우는 방법 에는 여러 가지 가 있습니다 .

예를 들어 1900-01-01의 100K 행 (~ 270 년) :

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

숫자 테이블이 왜 "가치"있는가?를 참조하십시오 .

당신은 일단 Calendar테이블을 여기에 그것을 사용하는 방법이다.

각각의 원래 행이 함께 결합되어 Calendar사이의 기간이 있기 때문에 여러 행으로 반환하는 테이블 StartDate및이 EndDate.

그런 다음 별개의 날짜를 계산하여 겹치는 날짜를 제거합니다.

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
;

결과

TotalCount
7

7

나는 a NumbersCalendar테이블이 매우 유용하고 Calendar 테이블 로이 문제를 많이 단순화 할 수 있다고 강력히 동의 합니다.

Itzik에 의해 링크 된 게시물의 답변 중 일부와 마찬가지로 다른 솔루션을 제안 할 것입니다 (캘린더 테이블이나 창 집계가 필요하지 않음). 모든 경우에 가장 효율적이지는 않지만 모든 경우에 가장 좋지 않을 수도 있지만 테스트에 해를 끼치 지 않는다고 생각합니다.

다른 간격과 겹치지 않는 시작 및 종료 날짜를 먼저 찾은 다음 행 번호를 지정하기 위해 두 행 (별도의 시작 및 종료 날짜)에 배치하고 마지막으로 첫 번째 시작 날짜와 첫 번째 종료 날짜를 일치시킵니다. , 2 등, 2 등 등 :

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 ;

on (CustID, StartDate, EndDate)및 on 두 인덱스 (CustID, EndDate, StartDate)는 쿼리 성능을 향상시키는 데 유용합니다.

캘린더 (아마도 유일한 것)에 비해 장점은 datetime값 으로 작업 하고 "패킹 된 간격"의 길이를 다른 정밀도, 더 큰 (주, 년) 또는 더 작은 (시간, 분 또는 초) 계산할 수 있다는 점 입니다. 밀리 초 등) 및 날짜 만 계산합니다. 분 또는 초 정밀도의 달력 테이블은 상당히 크며 (크로스) 큰 테이블에 조인하는 것은 매우 흥미로운 경험이지만 아마도 가장 효율적인 것은 아닙니다.

(Vladimir Baranov 덕분에) : 다른 방법의 성능은 데이터 분포에 따라 달라질 수 있기 때문에 성능을 적절히 비교하는 것이 다소 어렵습니다. 1) 간격이 얼마나 오래 걸리는가-간격이 짧을수록 더 많은 달력 테이블이 더 나은 성능을 발휘합니다. 간격이 길면 많은 중간 행이 생성되기 때문입니다. . Itzik 솔루션의 성능은 그에 달려 있다고 생각합니다. 데이터를 왜곡하는 다른 방법이있을 수 있으며 다양한 방법의 효율성이 어떻게 영향을 받는지 말하기 어렵습니다.


1
사본 2 개가 보입니다. 반 세미 조인을 반으로 계산하면 3이 될 수 있습니다.;)
ypercubeᵀᴹ

1
@wBob 성능 테스트를 한 경우 답변에 추가하십시오. 나는 그들과 다른 많은 사람들을 보게되어 기쁘다. 그것이 사이트의 작동 방식입니다 ..
ypercubeᵀᴹ

3
@wBob 너무 강력 할 필요는 없습니다. 아무도 성능에 대한 우려를 표명하지 않았습니다. 관심이 있으시면 직접 테스트를 실행 해보십시오. 답이 얼마나 복잡한 지에 대한 주관적인 측정이 공감대를위한 이유가 아닙니다. 다른 답변을 내리는 대신 자체 테스트를 수행하고 자신의 답변을 확장하는 것은 어떻습니까? 원하는 경우 자신의 답변을 공감할만한 가치가있는 것으로 만들되, 다른 합법적 인 답변은 공감하지 마십시오.
Monkpit

1
@Monkpit의 전투는 없습니다. 완벽하게 유효한 이유와 성능에 대한 심각한 대화.
wBob

2
@wBob, 다른 방법의 성능은 데이터 분포에 따라 달라질 수 있기 때문에 성능을 적절히 비교하는 것이 다소 어렵습니다. 1) 간격이 얼마나 오래 걸리는가-간격이 짧을수록 더 많은 달력 테이블이 더 나은 성능을 발휘합니다. 간격이 길면 많은 중간 행이 생성되기 때문입니다. . Itzik 솔루션의 성능은 그에 달려 있다고 생각합니다. 데이터를 왜곡하는 다른 방법이있을 수 있습니다.
블라디미르 바라 노프

2

캘린더 테이블을 사용하면 다음과 같이 간단합니다.

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

테스트 장비

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
잘 작동하지만 다음과 같은 나쁜 습관을 읽어야합니다 . 잘못 처리 한 날짜 / 범위 쿼리 : 요약 2. DATETIME, SMALLDATETIME, DATETIME2 및 DATETIMEOFFSET에 대한 범위 쿼리대해서는 피하십시오 .
Julien Vavasseur
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.