126

次のユーザー履歴テーブルには、特定のユーザーが Web サイトにアクセスした日 (24 時間の UTC 期間) の 1 つのレコードが含まれています。何千ものレコードがありますが、ユーザーごとに 1 日あたり 1 つのレコードしかありません。ユーザーがその日に Web サイトにアクセスしなかった場合、レコードは生成されません。

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) 日間連続して Web サイトにアクセスしたかを 1 日も逃さずに教えてくれます。

言い換えると、このテーブルに連続した (前日または翌日の) 日付を持つ (n) レコードを持つユーザーは何人ですか? シーケンスに欠落している日がある場合、シーケンスは壊れており、1 から再開する必要があります。ここで連続日数を隙間なく達成しているユーザーを募集しています。

もちろん、このクエリと特定のスタック オーバーフロー バッジとの類似点はまったくの偶然です.. :)

4

19 に答える 19

149

どうですか(そして、前のステートメントがセミコロンで終わっていることを確認してください):

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 がある場合、欠落した日数により、これら 2 つのリスト間のオフセットがわずかに大きくなるという考えです。したがって、一貫したオフセットを持つ範囲を探しています。

この最後に「ORDER BY NumConsecutiveDays DESC」を使用するか、しきい値に「HAVING count(*) > 14」と言うことができます...

私はこれをテストしていませんが、頭のてっぺんから書いているだけです。うまくいけば、SQL2005 以降で動作します。

...そして、tablename(UserID, CreationDate) のインデックスが非常に役立ちます

編集: Offset は予約語であることが判明したため、代わりに TheOffset を使用しました。

編集: COUNT(*) を使用するという提案は非常に有効です。そもそもそうすべきだったのですが、あまり考えていませんでした。以前は、代わりに datediff(day, min(CreationDate), max(CreationDate)) を使用していました。

ロブ

于 2009-07-24T07:37:48.473 に答える
70

答えは明らかに次のとおりです。

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 のソリューションも優れており、間違いなくさらに高速です (!)。こちらも是非チェックしてみてください!

于 2009-07-24T06:54:02.553 に答える
18

テーブル スキーマを変更できる場合は、テーブルに列LongestStreakを追加することをお勧めします。この列は、CreationDate. ログイン時にテーブルを更新するのは簡単です (すでに行っていることと同様に、当日の行が存在しない場合は、前日の行が存在するかどうかを確認します。真の場合はLongestStreak、それ以外の場合は、1 に設定します)。

この列を追加すると、クエリが明確になります。

if exists(select * from table
          where LongestStreak >= 30 and UserId = @UserId)
   -- award the Woot badge.
于 2009-07-24T06:35:45.960 に答える
5

n 日間連続するためには n 行が必要であるという事実を利用できるようです。

次のようなものです:

SELECT users.UserId, count(1) as cnt
FROM users
WHERE users.CreationDate > now() - INTERVAL 30 DAY
GROUP BY UserId
HAVING cnt = 30
于 2009-07-24T06:47:10.697 に答える
4

単一の SQL クエリでこれを行うのは、私には非常に複雑に思えます。この答えを 2 つの部分に分けて考えてみましょう。

  1. これまでにすべきことと今から始めるべきこと:
    毎日 cron ジョブを実行して、そのユーザーが今日ログインしたかどうかをチェックし、ログインしている場合はカウンターをインクリメントし、ログインしていない場合は 0 に設定します。
  2. 今すべきこと:
    - このテーブルを、あなたの Web サイトを実行していないサーバーにエクスポートし、しばらくは必要としません。;)
    - ユーザー、次に日付で並べ替えます。
    - 順番にそれを通過し、カウンターを維持します...
于 2009-07-24T06:37:22.353 に答える
3

Joe Celko は、SQL for Smarties でこれに関する完全な章を持っています (Runs and Sequences と呼んでいます)。その本は家にないので、仕事に着いたら… 実際にこれに答えます。(履歴テーブルが dbo.UserHistory と呼ばれ、日数が @Days であると仮定します)

もう 1 つの手掛かりは、実行に関する SQL チームのブログです。

私が持っていた他のアイデアは、ここで作業するのに便利な 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

上記はおそらく必要以上に難しいですが、単なる日付以外の「実行」の定義がある場合の脳のくすぐりとして残されています.

于 2009-07-24T08:14:12.610 に答える
3

いくつかの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 日あたり最大 1 つのレコードがあるという質問に記載されている制約に依存しています。

于 2012-01-05T21:09:52.767 に答える
3

再帰的な 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
于 2009-07-24T07:06:28.037 に答える
2

これがあなたにとって非常に重要な場合は、このイベントをソースにして、この情報を提供するテーブルを作成してください。クレイジーなクエリでマシンを強制終了する必要はありません。

于 2009-07-24T06:55:30.057 に答える
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)は日付の時間部分を削除するため、深夜に開始します。

creationdateまた、列とuserid列にインデックスが付けられていると仮定します。

これでは、すべてのユーザーとその連続日数がわかるわけではないことに気づきました。ただし、選択した日付から設定された日数でどのユーザーがアクセスしたかがわかります。

改訂されたソリューション:

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

これを確認すると、すべてのユーザーとすべての日付が照会されます。これはスペンサーの最初の(ジョーク?)ソリューションに基づいていますが、私のものは機能します。

更新:2番目のソリューションの日付処理を改善しました。

于 2009-07-24T09:03:06.240 に答える
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);
于 2009-07-24T07:13:23.577 に答える
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
于 2009-07-24T07:13:56.227 に答える
0

ビルのクエリを少し調整します。1日に1回のログインのみをカウントするには、グループ化する前に日付を切り捨てる必要がある場合があります...

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を使用することを検討していましたが、構文を検索するのが面倒だったため、代わりにiduseconvertを使用することにしました。私はそれが大きな影響を与えたことを知っていますありがとう!今私は知っている。

于 2009-07-24T07:04:11.630 に答える
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
于 2009-07-24T08:29:13.393 に答える
0

スペンサーはほぼそれを行いましたが、これは動作するコードである必要があります:

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
于 2009-07-24T07:11:02.503 に答える
0

これはあなたが望むことをするはずですが、効率をテストするのに十分なデータがありません. 複雑な CONVERT/FLOOR は、日時フィールドから時間部分を取り除くことです。SQL Server 2008 を使用している場合は、CAST(x.CreationDate AS DATE) を使用できます。

DECLARE @Range を INT として
SET @範囲 = 10

SELECT DISTINCT UserId, CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, a.CreationDate)))
  FROM tblUserLogin a
存在する場所
   (1を選択
      FROM tblUserLogin b
     WHERE a.userId = b.userId
       AND (SELECT COUNT(DISTINCT(CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, CreationDate)))))
              FROM tblUserLogin c
             WHERE 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)) )+@Range-1) = @Range)

作成スクリプト

CREATE TABLE [dbo].[tblUserLogin](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [ユーザー ID] [int] NULL、
    [作成日] [日時] NULL
) オン [プライマリ]
于 2009-07-24T06:52:54.600 に答える
0

集計表を使ったものはいかがですか?よりアルゴリズム的なアプローチに従い、実行計画は簡単です。テーブルをスキャンする 1 から 'MaxDaysBehind' までの数値を集計テーブルに入力します (つまり、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
于 2009-07-24T07:44:13.060 に答える
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 の変換が必要ですが、いくつかのアイデアが得られると思います。

于 2009-07-24T07:25:06.143 に答える