次のユーザー履歴テーブルには、特定のユーザーがWebサイトにアクセスした日ごとに1つのレコード(24時間のUTC期間内)が含まれています。何千ものレコードがありますが、ユーザーあたり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クエリであり、パフォーマンスが良好です。これにより、1日も逃すことなく(n)日間、どのユーザーIDがWebサイトにアクセスしたかがわかります。
言い換えれば、-このテーブルに連続する(前日、または後)の日付を持つ(n)レコードを持っているユーザーの数?シーケンスから欠落している日がある場合、シーケンスは壊れており、1から再開する必要があります。ここでは、ギャップのない連続した日数を達成したユーザーを探しています。
このクエリと 特定のスタックオーバーフローバッジ の間の類似点は、もちろんまったく偶然です。
答えは明らかに:
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のソリューションも優れています であり、間違いなくさらに高速です(!)。ぜひチェックしてみてください!
どうですか(前のステートメントがセミコロンで終わっていることを確認してください):
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)のインデックスが非常に役立ちます
編集済み:オフセットは予約語であることが判明したため、代わりにTheOffsetを使用しました。
編集:COUNT(*)を使用するという提案は非常に有効です-そもそもそうすべきだったのですが、実際には考えていませんでした。以前は、代わりにdatediff(day、min(CreationDate)、max(CreationDate))を使用していました。
ロブ
テーブルスキーマを変更できる場合は、LongestStreak
で終わる連続する日数を設定した列にCreationDate
をテーブルに追加することをお勧めします。ログイン時にテーブルを更新するのは簡単です(すでに行っていることと同様に、当日の行が存在しない場合は、前日の行が存在するかどうかを確認します。trueの場合は、LongestStreak
を新しい行に追加します。それ以外の場合は、1に設定します。)
この列を追加すると、クエリは明白になります。
if exists(select * from table
where LongestStreak >= 30 and UserId = @UserId)
-- award the Woot badge.
以下の行に沿ったいくつかの見事に表現力のある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);
}
}
}
N日間にわたって継続するには、n行が必要であるという事実を利用できるようです。
したがって、次のようなもの:
SELECT users.UserId, count(1) as cnt
FROM users
WHERE users.CreationDate > now() - INTERVAL 30 DAY
GROUP BY UserId
HAVING cnt = 30
単一のSQLクエリでこれを行うのは、私には非常に複雑に思えます。この答えを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日あたり最大1つのレコードがあるという質問で述べられている制約に依存しています。
Joe Celkoは、SQL for Smartiesでこれに関する完全な章を持っています(RunsおよびSequencesと呼びます)。家に本を持っていないので、仕事に着いたら…答えます。 (履歴テーブルがdbo.UserHistoryと呼ばれ、日数が@Daysであると仮定)
別のリードは 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
上記はWAY HARDERである可能性が高いですが、「実行」の定義が他にある場合は、脳のくすぐりとして残されます日付だけ。
これがあなたにとって非常に重要である場合、このイベントを調達し、この情報を提供するためにテーブルを運転してください。クレイジーなクエリでマシンを殺す必要はありません。
再帰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
単純な数学プロパティを使用して、誰が連続してサイトにアクセスしたかを特定しました。このプロパティは、最初のアクセスと最後の時間の日差がアクセステーブルログのレコード数と等しくなければならないということです。
以下は、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);
このようなもの?
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
_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番目のソリューションの日付処理を改善しました。
これはあなたが望むことをするはずですが、効率をテストするのに十分なデータがありません。複雑なCONVERT/FLOORは、日時フィールドから時間部分を取り除くことです。 SQL Server 2008を使用している場合は、CAST(x.CreationDate AS DATE)を使用できます。
DECLARE @Range as INT SET @Range = 10 SELECT DISTINCT UserId、CONVERT(DATETIME、FLOOR(CONLOT(FLOAT、a.CreationDate))) FROM tblUserLogin a WHERE EXISTS (SELECT 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)))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、 [UserId] [int] NULL、 [CreationDate] [datetime] NULL )ON [PRIMARY]
ビルのクエリを少し調整します。グループ化する前に日付を切り捨てて、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
EDITEDは、convert(char(10)、CreationDate、101)の代わりにDATEADD(dd、DATEDIFF(dd、0、CreationDate)、0)を使用します。
@IDisposable以前はdatepartの使用を検討していましたが、構文を調べるのが面倒なので、代わりにconvertを使用することを考えました。大きな影響があったことを知っています。ありがとうございます。今私は知っている。
次のようなスキーマを想定しています。
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
スペンサーはほとんどそれをしましたが、これは動作するコードでなければなりません:
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
私の頭の上の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の変換が必要ですが、いくつかのアイデアが得られたと思います。
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()[email protected] and
[CreationDate]<getdate()-t.Tally
group by [UserId],t.Tally
having count(*)>=@ContinousDays
delete #tallyTable