최소 연속 액세스 일수를 결정하는 SQL?


125

다음 사용자 기록 테이블에는 지정된 사용자가 웹 사이트에 액세스 한 매일 (24 시간 UTC 기간)에 대한 하나의 레코드 가 포함되어 있습니다 . 수천 개의 레코드가 있지만 사용자 당 하루에 하나의 레코드 만 있습니다. 사용자가 그날 웹 사이트에 접속하지 않은 경우 기록이 생성되지 않습니다.

ID UserId CreationDate
------ ------ ------------
750997 12 2009-07-07 18 : 42 : 20.723
750998 15 2009-07-07 18 : 42 : 20.927
751000 19 2009-07-07 18 : 42 : 22.283

내가 찾고있는 것은 좋은 성능을 가진 이 테이블에 대한 SQL 쿼리로 , 어느 사용자 ID가 하루를 놓치지 않고 (n) 일 동안 웹 사이트에 액세스했는지 알려줍니다.

즉, 이 테이블에 순차 (전일 또는 후일) 날짜가있는 레코드가있는 사용자 수는 몇 명 입니까? 시퀀스에서 누락 된 날짜가 있으면 시퀀스가 ​​중단되고 1시에 다시 시작해야합니다. 여기에서 공백없이 연속적인 일수를 달성 한 사용자를 찾고 있습니다.

이 쿼리와 특정 Stack Overflow 배지 사이의 유사점 은 물론 순전히 우연입니다 .. :)


멤버십 28 (<30) 일 후에 매니아 배지를 받았습니다. 비밀.
Kirill V. Lyadvinsky

3
날짜가 UTC로 저장되어 있습니까? 그렇다면 CA 거주자가 하루 오전 8시에 사이트를 방문한 다음 다음 날 오후 8시에 사이트를 방문하면 어떻게됩니까? 태평양 시간대에서 연속으로 방문하더라도 DB는 UTC로 시간을 저장하기 때문에 DB에 기록되지 않습니다.
Guy

제프 / 재 러드 - 당신은 체크 아웃 할 수 meta.stackexchange.com/questions/865/...에게 주십시오?
Rob Farley

답변:


69

대답은 분명히 다음과 같습니다.

SELECT DISTINCT UserId
FROM UserHistory uh1
WHERE (
       SELECT COUNT(*) 
       FROM UserHistory uh2 
       WHERE uh2.CreationDate 
       BETWEEN uh1.CreationDate AND DATEADD(d, @days, uh1.CreationDate)
      ) = @days OR UserId = 52551

편집하다:

여기 내 진지한 대답이 있습니다.

DECLARE @days int
DECLARE @seconds bigint
SET @days = 30
SET @seconds = (@days * 24 * 60 * 60) - 1
SELECT DISTINCT UserId
FROM (
    SELECT uh1.UserId, Count(uh1.Id) as Conseq
    FROM UserHistory uh1
    INNER JOIN UserHistory uh2 ON uh2.CreationDate 
        BETWEEN uh1.CreationDate AND 
            DATEADD(s, @seconds, DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate), 0))
        AND uh1.UserId = uh2.UserId
    GROUP BY uh1.Id, uh1.UserId
    ) as Tbl
WHERE Conseq >= @days

편집하다:

[Jeff Atwood] 이것은 훌륭하고 빠른 솔루션이며 받아 들일 가치가 있지만 Rob Farley의 솔루션도 훌륭하고 틀림없이 더 빠릅니다 (!). 꼭 확인 해주세요!


@Artem : 그게 처음에 생각했던 것이지만 생각해 보면 (UserId, CreationDate)에 대한 인덱스가 있으면 레코드가 인덱스에 연속적으로 표시되고 잘 수행 될 것입니다.
Mehrdad Afshari

이 항목에 대해 찬성하면 50 만 행에서 15 초 이내에 결과를 다시 얻습니다.
Jim T

4
DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0)를 사용하여 이러한 모든 테스트에서 CreateionDate를 일 단위로 줄이십시오 (오른쪽에만 있거나 SARG를 종료 함). 이것은 0에서 제공된 날짜를 빼서 작동합니다. SQL Server는 1900-01-01 00:00:00으로 해석하고 일 수를 제공합니다. 이 값은 0 날짜에 다시 추가되어 시간이 잘린 동일한 날짜를 생성합니다.
는 IDisposable

1
내가 말할 수있는 것은 IDisposable의 변경없이 계산이 잘못되었다는 것 입니다. 나는 개인적으로 데이터를 직접 검증했습니다. 일일 격차와 일부 사용자 텐데 잘못 배지를 얻을.
Jeff Atwood

3
이 검색어는 23 : 59 : 59.5에 발생하는 방문을 놓칠 가능성이 있습니다. ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)'아직 31 일이 지나지 않음'을 의미 하는로 변경하는 것은 어떻습니까? 또한 @seconds 계산을 건너 뛸 수 있습니다.
Rob Farley

147

(그리고 이전 문장이 세미콜론으로 끝나는 지 확인하세요) :

WITH numberedrows
     AS (SELECT ROW_NUMBER() OVER (PARTITION BY UserID 
                                       ORDER BY CreationDate)
                - DATEDIFF(day,'19000101',CreationDate) AS TheOffset,
                CreationDate,
                UserID
         FROM   tablename)
SELECT MIN(CreationDate),
       MAX(CreationDate),
       COUNT(*) AS NumConsecutiveDays,
       UserID
FROM   numberedrows
GROUP  BY UserID,
          TheOffset  

날짜 목록 (숫자)과 row_number가 있으면 누락 된 날짜가이 두 목록 사이의 오프셋을 약간 더 크게 만든다는 아이디어입니다. 그래서 우리는 일관된 오프셋을 가진 범위를 찾고 있습니다.

이 끝에 "ORDER BY NumConsecutiveDays DESC"를 사용하거나 임계 값에 대해 "HAVING count (*)> 14"라고 말할 수 있습니다.

나는 이것을 테스트하지 않았습니다-단지 내 머리 꼭대기에 씁니다. SQL2005 이상에서 작동하기를 바랍니다.

... 그리고 tablename (UserID, CreationDate)의 인덱스에 의해 많은 도움이 될 것입니다.

편집 됨 : Offset이 예약어 인 것으로 밝혀 졌으므로 대신 TheOffset을 사용했습니다.

수정 됨 : COUNT (*)를 사용하라는 제안은 매우 유효합니다. 처음에는 그렇게 했어야했지만 실제로는 생각하지 않았습니다. 이전에는 대신 datediff (day, min (CreationDate), max (CreationDate))를 사용했습니다.

Rob


1
오 당신은 또한 추가해야합니다; 와 -와 전>
믈라덴 Prajdic

2
Mladen-아니요, 세미콜론으로 이전 문을 끝내야합니다. ;) Jeff-Ok, 대신 [Offset]을 입력합니다. 오프셋은 예약어 인 것 같습니다. 내가 말했듯이 나는 그것을 테스트하지 않았습니다.
Rob Farley

1
이것은 자주 보이는 문제이기 때문에 자신을 반복합니다. DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0)를 사용하여 이러한 모든 테스트에서 CreateionDate를 일 단위로 줄이십시오 (오른쪽에만 있거나 SARG를 종료 함). 이것은 0에서 제공된 날짜를 빼서 작동합니다. SQL Server는 1900-01-01 00:00:00으로 해석하고 일 수를 제공합니다. 이 값은 0 날짜에 다시 추가되어 시간이 잘린 동일한 날짜를 생성합니다.
IDisposable

1
IDisposable-그래, 나도 그렇게 자주한다. 여기서하는 일에 대해 걱정하지 않았습니다. int로 캐스팅하는 것보다 빠르지는 않지만 시간, 월 등을 계산할 수있는 유연성이 있습니다.
Rob Farley

1
방금 DENSE_RANK ()로이 문제를 해결하는 방법에 대한 블로그 게시물을 작성했습니다. tinyurl.com/denserank
Rob Farley

18

당신이 테이블 스키마를 변경할 수 있다면, 나는 열을 추가 좋을 것 LongestStreak당신이 결말 연속 일수로 설정하려는 테이블에 CreationDate. 그것은 당신이 행이 현재의 날이 존재하지 않는 경우 모든 행이 전날이있는 경우, 당신은 확인할 것이다, 이미하고있는 일에 (유사한 로그인 시간에 테이블을 업데이트하는 것은 쉽다. true의 경우, 당신은 증가 것이다 LongestStreak의를 새 행, 그렇지 않으면 1로 설정합니다.)

이 열을 추가하면 쿼리가 명확 해집니다.

if exists(select * from table
          where LongestStreak >= 30 and UserId = @UserId)
   -- award the Woot badge.

1
+1 나는 비슷한 생각을 가진, 그러나 그렇지 않으면 0 전날에 대한 기록이있는 경우 하나 일 것입니다 비트 필드 (IsConsecutive)와
프레드릭 Mörk

7
우리는이에 대한 스키마를 변경하지 않을거야
제프 앳 우드

그리고 IsConsecutive는 UserHistory 테이블에 정의 된 계산 열일 수 있습니다. 행이 삽입 될 때 생성되는 구체화 된 (저장된) 계산 된 열로 만들 수도 있습니다. IFF (만약 경우에만) 항상 시간순으로 행을 삽입합니다.
는 IDisposable

(NOBODY가 SELECT *를 수행하기 때문에이 계산 된 열을 추가해도 열이 참조되지 않는 한 쿼리 계획에 영향을주지 않는다는 것을 알고 있습니다 ... 맞습니까?!?)
IDisposable

3
확실히 유효한 해결책이지만 내가 요청한 것이 아닙니다. 나는 "엄지 손가락을 옆으로".. 그것을 줄 그래서
제프 앳 우드

6

다음 행을 따라 멋지게 표현 된 SQL :

select
        userId,
    dbo.MaxConsecutiveDates(CreationDate) as blah
from
    dbo.Logins
group by
    userId

다음과 같은 사용자 정의 집계 함수 가 있다고 가정합니다 (버그가 있음을 유의하십시오).

using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Runtime.InteropServices;

namespace SqlServerProject1
{
    [StructLayout(LayoutKind.Sequential)]
    [Serializable]
    internal struct MaxConsecutiveState
    {
        public int CurrentSequentialDays;
        public int MaxSequentialDays;
        public SqlDateTime LastDate;
    }

    [Serializable]
    [SqlUserDefinedAggregate(
        Format.Native,
        IsInvariantToNulls = true, //optimizer property
        IsInvariantToDuplicates = false, //optimizer property
        IsInvariantToOrder = false) //optimizer property
    ]
    [StructLayout(LayoutKind.Sequential)]
    public class MaxConsecutiveDates
    {
        /// <summary>
        /// The variable that holds the intermediate result of the concatenation
        /// </summary>
        private MaxConsecutiveState _intermediateResult;

        /// <summary>
        /// Initialize the internal data structures
        /// </summary>
        public void Init()
        {
            _intermediateResult = new MaxConsecutiveState { LastDate = SqlDateTime.MinValue, CurrentSequentialDays = 0, MaxSequentialDays = 0 };
        }

        /// <summary>
        /// Accumulate the next value, not if the value is null
        /// </summary>
        /// <param name="value"></param>
        public void Accumulate(SqlDateTime value)
        {
            if (value.IsNull)
            {
                return;
            }
            int sequentialDays = _intermediateResult.CurrentSequentialDays;
            int maxSequentialDays = _intermediateResult.MaxSequentialDays;
            DateTime currentDate = value.Value.Date;
            if (currentDate.AddDays(-1).Equals(new DateTime(_intermediateResult.LastDate.TimeTicks)))
                sequentialDays++;
            else
            {
                maxSequentialDays = Math.Max(sequentialDays, maxSequentialDays);
                sequentialDays = 1;
            }
            _intermediateResult = new MaxConsecutiveState
                                      {
                                          CurrentSequentialDays = sequentialDays,
                                          LastDate = currentDate,
                                          MaxSequentialDays = maxSequentialDays
                                      };
        }

        /// <summary>
        /// Merge the partially computed aggregate with this aggregate.
        /// </summary>
        /// <param name="other"></param>
        public void Merge(MaxConsecutiveDates other)
        {
            // add stuff for two separate calculations
        }

        /// <summary>
        /// Called at the end of aggregation, to return the results of the aggregation.
        /// </summary>
        /// <returns></returns>
        public SqlInt32 Terminate()
        {
            int max = Math.Max((int) ((sbyte) _intermediateResult.CurrentSequentialDays), (sbyte) _intermediateResult.MaxSequentialDays);
            return new SqlInt32(max);
        }
    }
}

4

n 일 동안 연속하려면 n 개의 행이 있어야한다는 사실을 활용할 수있는 것 같습니다.

그래서 다음과 같습니다.

SELECT users.UserId, count(1) as cnt
FROM users
WHERE users.CreationDate > now() - INTERVAL 30 DAY
GROUP BY UserId
HAVING cnt = 30

예, 우리는 할 수있는 게이트 확실히 기록, 수로 ...하지만 우리는 매일 간격의 많은 몇 년에 걸쳐 방문 120 일을 수 있기 때문에 아니라, 몇 가지 가능성을 제거하는 것이
제프 앳 우드를

1
좋습니다.하지만이 페이지의 수상 내역을 확인한 후에는 하루에 한 번만 실행하면됩니다. 나는 그 경우 위와 같은 것이 트릭을 할 것이라고 생각합니다. 따라 잡으려면 BETWEEN을 사용하여 WHERE 절을 슬라이딩 창으로 바꾸면됩니다.
Bill

1
태스크의 각 실행은 상태 비 저장 및 독립형입니다. 그것은 문제의 테이블 이외의 이전 실행에 대한 지식이 없습니다
제프 앳 우드

3

단일 SQL 쿼리로이 작업을 수행하는 것은 나에게 지나치게 복잡해 보입니다. 이 답변을 두 부분으로 나누겠습니다.

  1. 지금까지해야했고 지금 시작해야 할
    작업 : 오늘 로그인 한 모든 사용자를 확인하는 매일 크론 작업을 실행 한 다음 카운터가있는 경우 카운터를 증가 시키거나 그렇지 않은 경우 0으로 설정합니다.
  2. 지금해야 할 일 :
    -이 테이블을 웹 사이트를 실행하지 않고 잠시 동안 필요하지 않은 서버로 내 보냅니다. ;)
    -사용자별로 정렬 한 다음 날짜로 정렬합니다.
    -순차적으로 진행하고 카운터 유지 ...

우리는 쿼리 앤 루프에 코드를 작성할 수 있습니다. 그것은 .. dary입니다. 나는 현재 SQL에 대한 유일한 방법이 궁금합니다.
Jeff Atwood

2

이것이 당신에게 매우 중요하다면,이 이벤트를 소싱하고이 정보를 제공하는 테이블을 구동하십시오. 모든 미친 쿼리로 기계를 죽일 필요가 없습니다.


2

재귀 CTE (SQL Server 2005 이상)를 사용할 수 있습니다.

WITH recur_date AS (
        SELECT t.userid,
               t.creationDate,
               DATEADD(day, 1, t.created) 'nextDay',
               1 'level' 
          FROM TABLE t
         UNION ALL
        SELECT t.userid,
               t.creationDate,
               DATEADD(day, 1, t.created) 'nextDay',
               rd.level + 1 'level'
          FROM TABLE t
          JOIN recur_date rd on t.creationDate = rd.nextDay AND t.userid = rd.userid)
   SELECT t.*
    FROM recur_date t
   WHERE t.level = @numDays
ORDER BY t.userid

2

Joe Celko는 SQL for Smarties (실행 및 시퀀스라고 함)에서 이에 대한 전체 장을 보유하고 있습니다. 집에 그 책이 없어서 일하러 가면 ... 실제로 대답하겠습니다. (기록 테이블이 dbo.UserHistory이고 일 수가 @Days라고 가정)

또 다른 리드는 SQL Team의 블로그에서 실행 중입니다.

내가 가지고 있지만 여기에서 작업하기 편리한 SQL 서버가없는 다른 아이디어는 다음과 같이 분할 된 ROW_NUMBER가있는 CTE를 사용하는 것입니다.

WITH Runs
AS
  (SELECT UserID
         , CreationDate
         , ROW_NUMBER() OVER(PARTITION BY UserId
                             ORDER BY CreationDate)
           - ROW_NUMBER() OVER(PARTITION BY UserId, NoBreak
                               ORDER BY CreationDate) AS RunNumber
  FROM
     (SELECT UH.UserID
           , UH.CreationDate
           , ISNULL((SELECT TOP 1 1 
              FROM dbo.UserHistory AS Prior 
              WHERE Prior.UserId = UH.UserId 
              AND Prior.CreationDate
                  BETWEEN DATEADD(dd, DATEDIFF(dd, 0, UH.CreationDate), -1)
                  AND DATEADD(dd, DATEDIFF(dd, 0, UH.CreationDate), 0)), 0) AS NoBreak
      FROM dbo.UserHistory AS UH) AS Consecutive
)
SELECT UserID, MIN(CreationDate) AS RunStart, MAX(CreationDate) AS RunEnd
FROM Runs
GROUP BY UserID, RunNumber
HAVING DATEDIFF(dd, MIN(CreationDate), MAX(CreationDate)) >= @Days

위의 내용은 필요한 것보다 훨씬 더 강할 수 있지만, 날짜 이외의 "달리기"에 대한 다른 정의가있을 때 뇌가 간지럼으로 남습니다.


2

몇 가지 SQL Server 2012 옵션 (아래에서 N = 100이라고 가정).

;WITH T(UserID, NRowsPrevious)
     AS (SELECT UserID,
                DATEDIFF(DAY, 
                        LAG(CreationDate, 100) 
                            OVER 
                                (PARTITION BY UserID 
                                     ORDER BY CreationDate), 
                         CreationDate)
         FROM   UserHistory)
SELECT DISTINCT UserID
FROM   T
WHERE  NRowsPrevious = 100 

내 샘플 데이터로 다음이 더 효율적으로 작동했지만

;WITH U
         AS (SELECT DISTINCT UserId
             FROM   UserHistory) /*Ideally replace with Users table*/
    SELECT UserId
    FROM   U
           CROSS APPLY (SELECT TOP 1 *
                        FROM   (SELECT 
                                       DATEDIFF(DAY, 
                                                LAG(CreationDate, 100) 
                                                  OVER 
                                                   (ORDER BY CreationDate), 
                                                 CreationDate)
                                FROM   UserHistory UH
                                WHERE  U.UserId = UH.UserID) T(NRowsPrevious)
                        WHERE  NRowsPrevious = 100) O

둘 다 사용자 당 하루에 최대 하나의 레코드가 있다는 질문에 명시된 제약 조건에 의존합니다.


1

이 같은?

select distinct userid
from table t1, table t2
where t1.UserId = t2.UserId 
  AND trunc(t1.CreationDate) = trunc(t2.CreationDate) + n
  AND (
    select count(*)
    from table t3
    where t1.UserId  = t3.UserId
      and CreationDate between trunc(t1.CreationDate) and trunc(t1.CreationDate)+n
   ) = n

1

나는 사이트에 연속적으로 액세스 한 사람을 식별하기 위해 간단한 수학 속성을 사용했습니다. 이 속성은 액세스 테이블 로그의 레코드 수와 동일한 첫 번째 액세스와 마지막 시간 간의 일 차이가 있어야한다는 것입니다.

다음은 Oracle DB에서 테스트 한 SQL 스크립트입니다 (다른 DB에서도 작동해야 함).

-- show basic understand of the math properties 
  select    ceil(max (creation_date) - min (creation_date))
              max_min_days_diff,
           count ( * ) real_day_count
    from   user_access_log
group by   user_id;


-- select all users that have consecutively accessed the site 
  select   user_id
    from   user_access_log
group by   user_id
  having       ceil(max (creation_date) - min (creation_date))
           / count ( * ) = 1;



-- get the count of all users that have consecutively accessed the site 
  select   count(user_id) user_count
    from   user_access_log
group by   user_id
  having   ceil(max (creation_date) - min (creation_date))
           / count ( * ) = 1;

테이블 준비 스크립트 :

-- create table 
create table user_access_log (id           number, user_id      number, creation_date date);


-- insert seed data 
insert into user_access_log (id, user_id, creation_date)
  values   (1, 12, sysdate);

insert into user_access_log (id, user_id, creation_date)
  values   (2, 12, sysdate + 1);

insert into user_access_log (id, user_id, creation_date)
  values   (3, 12, sysdate + 2);

insert into user_access_log (id, user_id, creation_date)
  values   (4, 16, sysdate);

insert into user_access_log (id, user_id, creation_date)
  values   (5, 16, sysdate + 1);

insert into user_access_log (id, user_id, creation_date)
  values   (6, 16, sysdate + 5);

1
declare @startdate as datetime, @days as int
set @startdate = cast('11 Jan 2009' as datetime) -- The startdate
set @days = 5 -- The number of consecutive days

SELECT userid
      ,count(1) as [Number of Consecutive Days]
FROM UserHistory
WHERE creationdate >= @startdate
AND creationdate < dateadd(dd, @days, cast(convert(char(11), @startdate, 113)  as datetime))
GROUP BY userid
HAVING count(1) >= @days

이 문 cast(convert(char(11), @startdate, 113) as datetime)은 날짜의 시간 부분을 제거하므로 자정에 시작됩니다.

또한 creationdateuserid열이 인덱싱 되었다고 가정 합니다.

나는 이것이 모든 사용자와 총 연속 일수를 알려주지 않는다는 것을 깨달았습니다. 그러나 선택한 날짜로부터 일정 기간 동안 방문한 사용자를 알려줍니다.

수정 된 솔루션 :

declare @days as int
set @days = 30
select t1.userid
from UserHistory t1
where (select count(1) 
       from UserHistory t3 
       where t3.userid = t1.userid
       and t3.creationdate >= DATEADD(dd, DATEDIFF(dd, 0, t1.creationdate), 0) 
       and t3.creationdate < DATEADD(dd, DATEDIFF(dd, 0, t1.creationdate) + @days, 0) 
       group by t3.userid
) >= @days
group by t1.userid

나는 이것을 확인했고 모든 사용자와 모든 날짜에 대해 쿼리합니다. Spencer의 첫 번째 (농담?) 솔루션을 기반으로 하지만 내 것이 작동합니다.

업데이트 : 두 번째 솔루션에서 날짜 처리를 개선했습니다.


가까운, 그러나 우리는 그 어떤 (N) 일 동안 작동하지 고정 된 시작 날짜에 뭔가가 필요
제프 앳 우드

0

이것은 당신이 원하는 것을해야하지만 효율성을 테스트하기에 충분한 데이터가 없습니다. 복잡한 CONVERT / FLOOR 항목은 datetime 필드에서 시간 부분을 제거하는 것입니다. SQL Server 2008을 사용하는 경우 CAST (x.CreationDate AS DATE)를 사용할 수 있습니다.

@Range를 INT로 선언
SET @ 범위 = 10

SELECT DISTINCT UserId, CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)))
  tblUserLogin에서
존재하는 곳
   (선택 1 
      tblUserLogin b에서 
     여기서 a.userId = b.userId 
       AND (SELECT COUNT (DISTINCT (CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, CreationDate)))))) 
              tblUserLogin c에서 
             어디 c.userid = b.userid 
               AND CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) BETWEEN CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) 및 CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) ) + @ 범위 -1) = @ 범위)

생성 스크립트

테이블 만들기 [dbo]. [tblUserLogin] (
    [Id] [int] IDENTITY (1,1) NOT NULL,
    [UserId] [int] NULL,
    [CreationDate] [datetime] NULL
) [기본] 사용

꽤 잔인합니다. 406,624 개 행에서 26 초.
Jeff Atwood

배지 수여를 위해 얼마나 자주 확인하십니까? 하루에 한 번뿐이라면 느린 기간의 26 초 히트는 그다지 나쁘지 않은 것 같습니다. 하지만 테이블이 커지면 성능이 저하됩니다. 질문을 다시 읽은 후 하루에 하나의 기록 만 있기 때문에 시간이 관련이 없을 수 있습니다.
Dave Barker

0

Spencer가 거의 해냈지만 다음은 작동하는 코드 여야합니다.

SELECT DISTINCT UserId
FROM History h1
WHERE (
    SELECT COUNT(*) 
    FROM History
    WHERE UserId = h1.UserId AND CreationDate BETWEEN h1.CreationDate AND DATEADD(d, @n-1, h1.CreationDate)
) >= @n

0

내 머릿속에서 MySQLish :

SELECT start.UserId
FROM UserHistory AS start
  LEFT OUTER JOIN UserHistory AS pre_start ON pre_start.UserId=start.UserId
    AND DATE(pre_start.CreationDate)=DATE_SUB(DATE(start.CreationDate), INTERVAL 1 DAY)
  LEFT OUTER JOIN UserHistory AS subsequent ON subsequent.UserId=start.UserId
    AND DATE(subsequent.CreationDate)<=DATE_ADD(DATE(start.CreationDate), INTERVAL 30 DAY)
WHERE pre_start.Id IS NULL
GROUP BY start.Id
HAVING COUNT(subsequent.Id)=30

테스트되지 않았으며 거의 ​​확실히 MSSQL에 대한 변환이 필요하지만 몇 가지 아이디어를 제공한다고 생각합니다.


0

Tally 테이블을 사용하는 것은 어떻습니까? 보다 알고리즘적인 접근 방식을 따르며 실행 계획은 간단합니다. 테이블을 스캔하려는 1부터 'MaxDaysBehind'까지의 숫자로 tallyTable을 채 웁니다 (예 : 90은 3 개월 뒤를 찾습니다 등).

declare @ContinousDays int
set @ContinousDays = 30  -- select those that have 30 consecutive days

create table #tallyTable (Tally int)
insert into #tallyTable values (1)
...
insert into #tallyTable values (90) -- insert numbers for as many days behind as you want to scan

select [UserId],count(*),t.Tally from HistoryTable 
join #tallyTable as t on t.Tally>0
where [CreationDate]> getdate()-@ContinousDays-t.Tally and 
      [CreationDate]<getdate()-t.Tally 
group by [UserId],t.Tally 
having count(*)>=@ContinousDays

delete #tallyTable

0

Bill의 쿼리를 약간 수정했습니다. 하루에 한 번의 로그인 만 계산하려면 그룹화하기 전에 날짜를 잘라야 할 수 있습니다.

SELECT UserId from History 
WHERE CreationDate > ( now() - n )
GROUP BY UserId, 
DATEADD(dd, DATEDIFF(dd, 0, CreationDate), 0) AS TruncatedCreationDate  
HAVING COUNT(TruncatedCreationDate) >= n

convert (char (10), CreationDate, 101) 대신 DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0)를 사용하도록 편집되었습니다.

@IDisposable 이전에 datepart를 사용하려고했지만 구문을 찾기에는 너무 게으 르기 때문에 대신 변환을 사용한다고 생각했습니다. 나는 그것이 중대한 영향을 미쳤다는 것을 알고 있습니다. 감사합니다! 이제 알아.


SQL DATETIME을 날짜 전용으로 자르는 것은 DATEADD (dd, DATEDIFF (dd, 0, UH.CreationDate), 0)를 사용하여 가장 잘 수행됩니다.
IDisposable

(위의 방법은 0 (예 : 1900-01-01 00 : 00 : 00.000) 사이의 전체 요일 차이를 취한 다음 전체 요일의 차이를 다시 0 (예 : 1900-01-01 00:00:00)으로 더하여 작동합니다. . 그러면 DATETIME의 시간 부분이 삭제됩니다.)
IDisposable

0

다음과 같은 스키마를 가정합니다.

create table dba.visits
(
    id  integer not null,
    user_id integer not null,
    creation_date date not null
);

이렇게하면 간격이있는 날짜 시퀀스에서 연속 범위가 추출됩니다.

select l.creation_date  as start_d, -- Get first date in contiguous range
    (
        select min(a.creation_date ) as creation_date 
        from "DBA"."visits" a 
            left outer join "DBA"."visits" b on 
                   a.creation_date = dateadd(day, -1, b.creation_date ) and 
                   a.user_id  = b.user_id 
            where b.creation_date  is null and
                  a.creation_date  >= l.creation_date  and
                  a.user_id  = l.user_id 
    ) as end_d -- Get last date in contiguous range
from  "DBA"."visits" l
    left outer join "DBA"."visits" r on 
        r.creation_date  = dateadd(day, -1, l.creation_date ) and 
        r.user_id  = l.user_id 
    where r.creation_date  is null
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.