これは私のC#ホームワールドでのちょっとしたタスクですが、まだSQLで作成しておらず、セットベース(カーソルなし)で解決したいと思っています。結果セットは、このようなクエリから取得する必要があります。
SELECT SomeId, MyDate,
dbo.udfLastHitRecursive(param1, param2, MyDate) as 'Qualifying'
FROM T
どのように機能するか
これら3つのパラメーターをUDFに送信します。
UDFは内部でparamsを使用して、関連する90日以内の関連する行をビューからフェッチします。
UDFは「MyDate」を走査し、合計計算に含める必要がある場合は1を返します。
そうでない場合は、0を返します。ここでは「適格」と名付けられています。
dfが行うこと
行を日付順にリストします。行間の日数を計算します。結果セットの最初の行のデフォルトはHit = 1です。差が最大90の場合、ギャップの合計が90日になるまで次の行に進みます(90日が経過する必要があります)到達したら、Hitを1に設定し、ギャップを0にリセットします。結果から行を省略することもできます。
|(column by udf, which not work yet)
Date Calc_date MaxDiff | Qualifying
2014-01-01 11:00 2014-01-01 0 | 1
2014-01-03 10:00 2014-01-01 2 | 0
2014-01-04 09:30 2014-01-03 1 | 0
2014-04-01 10:00 2014-01-04 87 | 0
2014-05-01 11:00 2014-04-01 30 | 1
上の表のMaxDiff列は、前の行の日付とのギャップです。これまでの私の試みの問題は、上のサンプルの最後から2番目の行を無視できないことです。
[編集]
コメントに従って、タグを追加し、今コンパイルしたudfも貼り付けます。ただし、これは単なるプレースホルダーであり、有用な結果は得られません。
;WITH cte (someid, otherkey, mydate, cost) AS
(
SELECT someid, otherkey, mydate, cost
FROM dbo.vGetVisits
WHERE someid = @someid AND VisitCode = 3 AND otherkey = @otherkey
AND CONVERT(Date,mydate) = @VisitDate
UNION ALL
SELECT top 1 e.someid, e.otherkey, e.mydate, e.cost
FROM dbo.vGetVisits AS E
WHERE CONVERT(date, e.mydate)
BETWEEN DateAdd(dd,-90,CONVERT(Date,@VisitDate)) AND CONVERT(Date,@VisitDate)
AND e.someid = @someid AND e.VisitCode = 3 AND e.otherkey = @otherkey
AND CONVERT(Date,e.mydate) = @VisitDate
order by e.mydate
)
必要なものにより近い別のクエリを個別に定義していますが、ウィンドウ化された列で計算できないという事実でブロックされています。 MyDateでLAG()を使用してほぼ同じ出力を生成する類似の1つも試し、datediffで囲みました。
SELECT
t.Mydate, t.VisitCode, t.Cost, t.SomeId, t.otherkey, t.MaxDiff, t.DateDiff
FROM
(
SELECT *,
MaxDiff = LAST_VALUE(Diff.Diff) OVER (
ORDER BY Diff.Mydate ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
FROM
(
SELECT *,
Diff = ISNULL(DATEDIFF(DAY, LAST_VALUE(r.Mydate) OVER (
ORDER BY r.Mydate ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING),
r.Mydate),0),
DateDiff = ISNULL(LAST_VALUE(r.Mydate) OVER (
ORDER BY r.Mydate ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING),
r.Mydate)
FROM dbo.vGetVisits AS r
WHERE r.VisitCode = 3 AND r.SomeId = @SomeID AND r.otherkey = @otherkey
) AS Diff
) AS t
WHERE t.VisitCode = 3 AND t.SomeId = @SomeId AND t.otherkey = @otherkey
AND t.Diff <= 90
ORDER BY
t.Mydate ASC;
質問を読んだとき、必要な基本的な再帰アルゴリズムは次のとおりです。
これは、再帰的な共通テーブル式を使用して比較的簡単に実装できます。
たとえば、次のサンプルデータ(質問に基づく)を使用します。
DECLARE @T AS table (TheDate datetime PRIMARY KEY);
INSERT @T (TheDate)
VALUES
('2014-01-01 11:00'),
('2014-01-03 10:00'),
('2014-01-04 09:30'),
('2014-04-01 10:00'),
('2014-05-01 11:00'),
('2014-07-01 09:00'),
('2014-07-31 08:00');
再帰的なコードは次のとおりです。
WITH CTE AS
(
-- Anchor:
-- Start with the earliest date in the table
SELECT TOP (1)
T.TheDate
FROM @T AS T
ORDER BY
T.TheDate
UNION ALL
-- Recursive part
SELECT
SQ1.TheDate
FROM
(
-- Recursively find the earliest date that is
-- more than 90 days after the "current" date
-- and set the new date as "current".
-- ROW_NUMBER + rn = 1 is a trick to get
-- TOP in the recursive part of the CTE
SELECT
T.TheDate,
rn = ROW_NUMBER() OVER (
ORDER BY T.TheDate)
FROM CTE
JOIN @T AS T
ON T.TheDate > DATEADD(DAY, 90, CTE.TheDate)
) AS SQ1
WHERE
SQ1.rn = 1
)
SELECT
CTE.TheDate
FROM CTE
OPTION (MAXRECURSION 0);
結果は次のとおりです。
╔═════════════════════════╗
║ TheDate ║
╠═════════════════════════╣
║ 2014-01-01 11:00:00.000 ║
║ 2014-05-01 11:00:00.000 ║
║ 2014-07-31 08:00:00.000 ║
╚═════════════════════════╝
TheDate
を主キーとするインデックスを使用すると、実行プランは非常に効率的です。
これを関数にラップして、質問で述べたビューに対して直接実行することもできますが、私の本能はそれに対して反対です。通常、ビューから一時テーブルに行を選択し、一時テーブルに適切なインデックスを提供してから、上記のロジックを適用すると、パフォーマンスが向上します。詳細はビューの詳細によって異なりますが、これは私の一般的な経験です。
完全を期すために(そしてypercubeの答えによって促されます)、このタイプの問題に対する他の主な解決策(T-SQLが適切な順序付けされたセット関数を取得するまで)はSQLCLRカーソル( ここで私の答えを参照 =テクニックの例)。これは、T-SQLカーソルよりもmuchのパフォーマンスが高く、.NET言語のスキルを持ち、本番環境でSQLCLRを実行する機能を持つユーザーに便利です。 。コストの大部分がソートであるため、このシナリオでは再帰的なソリューションよりも多くのことを提供しない可能性がありますが、言及する価値があります。
これはisSQL Server 2014の質問なので、ネイティブにコンパイルされたストアドプロシージャバージョンの「カーソル」を追加することもできます。
いくつかのデータを含むソーステーブル:
_create table T
(
TheDate datetime primary key
);
go
insert into T(TheDate) values
('2014-01-01 11:00'),
('2014-01-03 10:00'),
('2014-01-04 09:30'),
('2014-04-01 10:00'),
('2014-05-01 11:00'),
('2014-07-01 09:00'),
('2014-07-31 08:00');
_
ストアドプロシージャのパラメーターであるテーブルタイプ。 _bucket_count
_を適切に調整 。
_create type TType as table
(
ID int not null primary key nonclustered hash with (bucket_count = 16),
TheDate datetime not null
) with (memory_optimized = on);
_
そして、テーブル値パラメーターをループして_@R
_の行を収集するストアドプロシージャ。
_create procedure dbo.GetDates
@T dbo.TType readonly
with native_compilation, schemabinding, execute as owner
as
begin atomic with (transaction isolation level = snapshot, language = N'us_english', delayed_durability = on)
declare @R dbo.TType;
declare @ID int = 0;
declare @RowsLeft bit = 1;
declare @CurDate datetime = '1901-01-01';
declare @LastDate datetime = '1901-01-01';
while @RowsLeft = 1
begin
set @ID += 1;
select @CurDate = T.TheDate
from @T as T
where T.ID = @ID
if @@rowcount = 1
begin
if datediff(day, @LastDate, @CurDate) > 90
begin
insert into @R(ID, TheDate) values(@ID, @CurDate);
set @LastDate = @CurDate;
end;
end
else
begin
set @RowsLeft = 0;
end
end;
select R.TheDate
from @R as R;
end
_
ネイティブにコンパイルされたストアドプロシージャへのパラメーターとして使用されるメモリ最適化テーブル変数を埋め、プロシージャを呼び出すためのコード。
_declare @T dbo.TType;
insert into @T(ID, TheDate)
select row_number() over(order by T.TheDate),
T.TheDate
from T;
exec dbo.GetDates @T;
_
結果:
_TheDate
-----------------------
2014-07-31 08:00:00.000
2014-01-01 11:00:00.000
2014-05-01 11:00:00.000
_
更新:
何らかの理由でテーブルのすべての行にアクセスする必要がない場合は、Paul Whiteによる再帰CTEに実装されている「次の日付へのジャンプ」バージョンと同等のことができます。
データ型はID列を必要としないため、ハッシュインデックスを使用しないでください。
_create type TType as table
(
TheDate datetime not null primary key nonclustered
) with (memory_optimized = on);
_
そして、ストアドプロシージャはselect top(1) ..
を使用して次の値を見つけます。
_create procedure dbo.GetDates
@T dbo.TType readonly
with native_compilation, schemabinding, execute as owner
as
begin atomic with (transaction isolation level = snapshot, language = N'us_english', delayed_durability = on)
declare @R dbo.TType;
declare @RowsLeft bit = 1;
declare @CurDate datetime = '1901-01-01';
while @RowsLeft = 1
begin
select top(1) @CurDate = T.TheDate
from @T as T
where T.TheDate > dateadd(day, 90, @CurDate)
order by T.TheDate;
if @@rowcount = 1
begin
insert into @R(TheDate) values(@CurDate);
end
else
begin
set @RowsLeft = 0;
end
end;
select R.TheDate
from @R as R;
end
_
カーソルを使用するソリューション。
(最初に、いくつかの必要なテーブルと変数):
-- a table to hold the results
DECLARE @cd TABLE
( TheDate datetime PRIMARY KEY,
Qualify INT NOT NULL
);
-- some variables
DECLARE
@TheDate DATETIME,
@diff INT,
@Qualify INT = 0,
@PreviousCheckDate DATETIME = '1900-01-01 00:00:00' ;
実際のカーソル:
-- declare the cursor
DECLARE c CURSOR
LOCAL STATIC FORWARD_ONLY READ_ONLY
FOR
SELECT TheDate
FROM T
ORDER BY TheDate ;
-- using the cursor to fill the @cd table
OPEN c ;
FETCH NEXT FROM c INTO @TheDate ;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @diff = DATEDIFF(day, @PreviousCheckDate, @Thedate) ;
SET @Qualify = CASE WHEN @diff > 90 THEN 1 ELSE 0 END ;
INSERT @cd (TheDate, Qualify)
SELECT @TheDate, @Qualify ;
SET @PreviousCheckDate =
CASE WHEN @diff > 90
THEN @TheDate
ELSE @PreviousCheckDate END ;
FETCH NEXT FROM c INTO @TheDate ;
END
CLOSE c;
DEALLOCATE c;
そして結果を得る:
-- get the results
SELECT TheDate, Qualify
FROM @cd
-- WHERE Qualify = 1 -- optional, to see only the qualifying rows
ORDER BY TheDate ;
SQLFiddleでテスト
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[vGetVisits]') AND type in (N'U'))
DROP TABLE [dbo].[vGetVisits]
GO
CREATE TABLE [dbo].[vGetVisits](
[id] [int] NOT NULL,
[mydate] [datetime] NOT NULL,
CONSTRAINT [PK_vGetVisits] PRIMARY KEY CLUSTERED
(
[id] ASC
)
)
GO
INSERT INTO [dbo].[vGetVisits]([id], [mydate])
VALUES
(1, '2014-01-01 11:00'),
(2, '2014-01-03 10:00'),
(3, '2014-01-04 09:30'),
(4, '2014-04-01 10:00'),
(5, '2014-05-01 11:00'),
(6, '2014-07-01 09:00'),
(7, '2014-07-31 08:00');
GO
-- Clean up
IF OBJECT_ID (N'dbo.udfLastHitRecursive', N'FN') IS NOT NULL
DROP FUNCTION udfLastHitRecursive;
GO
-- Actual Function
CREATE FUNCTION dbo.udfLastHitRecursive
( @MyDate datetime)
RETURNS TINYINT
AS
BEGIN
-- Your returned value 1 or 0
DECLARE @Returned_Value TINYINT;
SET @Returned_Value=0;
-- Prepare gaps table to be used.
WITH gaps AS
(
-- Select Date and MaxDiff from the original table
SELECT
CONVERT(Date,mydate) AS [date]
, DATEDIFF(day,ISNULL(LAG(mydate, 1) OVER (ORDER BY mydate), mydate) , mydate) AS [MaxDiff]
FROM dbo.vGetVisits
)
SELECT @Returned_Value=
(SELECT DISTINCT -- DISTINCT in case we have same date but different time
CASE WHEN
(
-- It is a first entry
[date]=(SELECT MIN(CONVERT(Date,mydate)) FROM dbo.vGetVisits))
OR
/*
--Gap between last qualifying date and entered is greater than 90
Calculate Running sum upto and including required date
and find a remainder of division by 91.
*/
((SELECT SUM(t1.MaxDiff)
FROM (SELECT [MaxDiff] FROM gaps WHERE [date]<=t2.[date]
) t1
)%91 -
/*
ISNULL added to include first value that always returns NULL
Calculate Running sum upto and NOT including required date
and find a remainder of division by 91
*/
ISNULL((SELECT SUM(t1.MaxDiff)
FROM (SELECT [MaxDiff] FROM gaps WHERE [date]<t2.[date]
) t1
)%91, 0) -- End ISNULL
<0 )
/* End Running sum upto and including required date */
OR
-- Gap between two nearest dates is greater than 90
((SELECT SUM(t1.MaxDiff)
FROM (SELECT [MaxDiff] FROM gaps WHERE [date]<=t2.[date]
) t1
) - ISNULL((SELECT SUM(t1.MaxDiff)
FROM (SELECT [MaxDiff] FROM gaps WHERE [date]<t2.[date]
) t1
), 0) > 90)
THEN 1
ELSE 0
END
AS [Qualifying]
FROM gaps t2
WHERE [date]=CONVERT(Date,@MyDate))
-- What is neccesary to return when entered date is not in dbo.vGetVisits?
RETURN @Returned_Value
END
GO
SELECT
dbo.udfLastHitRecursive(mydate) AS [Qualifying]
, [id]
, mydate
FROM dbo.vGetVisits
ORDER BY mydate
結果
SQL Serverで実行中の合計を計算する方法もご覧ください
更新:以下のパフォーマンステストの結果を参照してください。
「90日のギャップ」を見つけるために使用されるロジックが異なるため、ypercubeと私のソリューションをそのままにしておくと、Paul Whiteのソリューションに異なる結果が返される場合があります。これは、 [〜#〜] datediff [〜#〜] と [〜#〜] dateadd [〜#〜] それぞれ機能します。
例えば:
SELECT DATEADD(DAY, 90, '2014-01-01 00:00:00.000')
「2014-04-01 00:00:00.000」を返します。これは、「2014-04-01 01:00:00.000」が90日のギャップを超えていることを意味します
だが
SELECT DATEDIFF(DAY, '2014-01-01 00:00:00.000', '2014-04-01 01:00:00.000')
まだギャップ内にあることを意味する「90」を返します。
小売業者の例を考えてみましょう。この場合、「2014-01-01 23:59:59:999」の日付「2014-01-01」までに販売された生鮮商品を販売することは問題ありません。したがって、この場合の値DATEDIFF(DAY、...)は問題ありません。
別の例は、診察を待っている患者です。 「2014-01-01 00:00:00:000」に到着し、「2014-01-01 23:59:59:999」に出発する人にとっては、0(zero) =実際の待機がほぼ24時間であったにもかかわらずDATEDIFFが使用された場合の日数「2014-01-01 23:59:59」に来て、「2014-01-02 00:00:01」に退院する患者が待機したDATEDIFFが使用されている場合は1日。
しかし、私は余談です。
私はDATEDIFFソリューションを残し、パフォーマンスもテストしましたが、実際には独自のリーグに属しているはずです。
また、大きなデータセットの場合、同じ日の値を回避することは不可能であることが指摘されました。したがって、2年間のデータにまたがる1,300万件のレコードがあるとすると、数日間は複数のレコードが存在することになります。これらのレコードは、myおよびypercubeのDATEDIFFソリューションで最も早い機会に除外されます。 ypercubeがこれを気にしないことを願っています。
ソリューションは次の表でテストされました
CREATE TABLE [dbo].[vGetVisits](
[id] [int] NOT NULL,
[mydate] [datetime] NOT NULL,
)
2つの異なるクラスター化インデックス(この場合はmydate):
CREATE CLUSTERED INDEX CI_mydate on vGetVisits(mydate)
GO
テーブルは次の方法で入力されました
SET NOCOUNT ON
GO
INSERT INTO dbo.vGetVisits(id, mydate)
VALUES (1, '01/01/1800')
GO
DECLARE @i bigint
SET @i=2
DECLARE @MaxRows bigint
SET @MaxRows=13001
WHILE @i<@MaxRows
BEGIN
INSERT INTO dbo.vGetVisits(id, mydate)
VALUES (@i, DATEADD(day,FLOOR(Rand()*(3)),(SELECT MAX(mydate) FROM dbo.vGetVisits)))
SET @i=@i+1
END
数百万行の場合、INSERTが変更され、0〜20分のエントリがランダムに追加されました。
すべてのソリューションは次のコードに慎重にまとめられました
SET NOCOUNT ON
GO
DECLARE @StartDate DATETIME
SET @StartDate = GETDATE()
--- Code goes here
PRINT 'Total milliseconds: ' + CONVERT(varchar, DATEDIFF(ms, @StartDate, GETDATE()))
テストされた実際のコード(順不同):
YpercubeのDATEDIFFソリューション(YPC、DATEDIFF)
DECLARE @cd TABLE
( TheDate datetime PRIMARY KEY,
Qualify INT NOT NULL
);
DECLARE
@TheDate DATETIME,
@Qualify INT = 0,
@PreviousCheckDate DATETIME = '1799-01-01 00:00:00'
DECLARE c CURSOR
LOCAL STATIC FORWARD_ONLY READ_ONLY
FOR
SELECT
mydate
FROM
(SELECT
RowNum = ROW_NUMBER() OVER(PARTITION BY cast(mydate as date) ORDER BY mydate)
, mydate
FROM
dbo.vGetVisits) Actions
WHERE
RowNum = 1
ORDER BY
mydate;
OPEN c ;
FETCH NEXT FROM c INTO @TheDate ;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @Qualify = CASE WHEN DATEDIFF(day, @PreviousCheckDate, @Thedate) > 90 THEN 1 ELSE 0 END ;
IF @Qualify=1
BEGIN
INSERT @cd (TheDate, Qualify)
SELECT @TheDate, @Qualify ;
SET @PreviousCheckDate=@TheDate
END
FETCH NEXT FROM c INTO @TheDate ;
END
CLOSE c;
DEALLOCATE c;
SELECT TheDate
FROM @cd
ORDER BY TheDate ;
YpercubeのDATEADDソリューション(YPC、DATEADD)
DECLARE @cd TABLE
( TheDate datetime PRIMARY KEY,
Qualify INT NOT NULL
);
DECLARE
@TheDate DATETIME,
@Next_Date DATETIME,
@Interesting_Date DATETIME,
@Qualify INT = 0
DECLARE c CURSOR
LOCAL STATIC FORWARD_ONLY READ_ONLY
FOR
SELECT
[mydate]
FROM [test].[dbo].[vGetVisits]
ORDER BY mydate
;
OPEN c ;
FETCH NEXT FROM c INTO @TheDate ;
SET @Interesting_Date=@TheDate
INSERT @cd (TheDate, Qualify)
SELECT @TheDate, @Qualify ;
WHILE @@FETCH_STATUS = 0
BEGIN
IF @TheDate>DATEADD(DAY, 90, @Interesting_Date)
BEGIN
INSERT @cd (TheDate, Qualify)
SELECT @TheDate, @Qualify ;
SET @Interesting_Date=@TheDate;
END
FETCH NEXT FROM c INTO @TheDate;
END
CLOSE c;
DEALLOCATE c;
SELECT TheDate
FROM @cd
ORDER BY TheDate ;
ポールホワイトの解([〜#〜] pw [〜#〜])
;WITH CTE AS
(
SELECT TOP (1)
T.[mydate]
FROM dbo.vGetVisits AS T
ORDER BY
T.[mydate]
UNION ALL
SELECT
SQ1.[mydate]
FROM
(
SELECT
T.[mydate],
rn = ROW_NUMBER() OVER (
ORDER BY T.[mydate])
FROM CTE
JOIN dbo.vGetVisits AS T
ON T.[mydate] > DATEADD(DAY, 90, CTE.[mydate])
) AS SQ1
WHERE
SQ1.rn = 1
)
SELECT
CTE.[mydate]
FROM CTE
OPTION (MAXRECURSION 0);
私のDATEADDソリューション(PN、DATEADD)
DECLARE @cd TABLE
( TheDate datetime PRIMARY KEY
);
DECLARE @TheDate DATETIME
SET @TheDate=(SELECT MIN(mydate) as mydate FROM [dbo].[vGetVisits])
WHILE (@TheDate IS NOT NULL)
BEGIN
INSERT @cd (TheDate) SELECT @TheDate;
SET @TheDate=(
SELECT MIN(mydate) as mydate
FROM [dbo].[vGetVisits]
WHERE mydate>DATEADD(DAY, 90, @TheDate)
)
END
SELECT TheDate
FROM @cd
ORDER BY TheDate ;
私のDATEDIFFソリューション(PN、DATEDIFF)
DECLARE @MinDate DATETIME;
SET @MinDate=(SELECT MIN(mydate) FROM dbo.vGetVisits);
;WITH gaps AS
(
SELECT
t1.[date]
, t1.[MaxDiff]
, SUM(t1.[MaxDiff]) OVER (ORDER BY t1.[date]) AS [Running Total]
FROM
(
SELECT
mydate AS [date]
, DATEDIFF(day,LAG(mydate, 1, mydate) OVER (ORDER BY mydate) , mydate) AS [MaxDiff]
FROM
(SELECT
RowNum = ROW_NUMBER() OVER(PARTITION BY cast(mydate as date) ORDER BY mydate)
, mydate
FROM dbo.vGetVisits
) Actions
WHERE RowNum = 1
) t1
)
SELECT [date]
FROM gaps t2
WHERE
( ([Running Total])%91 - ([Running Total]- [MaxDiff])%91 <0 )
OR
( [MaxDiff] > 90)
OR
([date]=@MinDate)
ORDER BY [date]
私はSQL Server 2012を使用しています。MikaelErikssonに謝罪しますが、彼のコードはここではテストされません。 DATADIFFとDATEADDを使用した彼のソリューションでは、一部のデータセットで異なる値が返されることを期待しています。
そして実際の結果は:
わかりました、何かを逃したのですか、それとも再帰をスキップして自分自身に戻って参加しないのですか?日付が主キーである場合、それは一意である必要があり、次の行へのオフセットを計算する予定の場合は、発生順になります。
DECLARE @T AS TABLE
(
TheDate DATETIME PRIMARY KEY
);
INSERT @T
(TheDate)
VALUES ('2014-01-01 11:00'),
('2014-01-03 10:00'),
('2014-01-04 09:30'),
('2014-04-01 10:00'),
('2014-05-01 11:00'),
('2014-07-01 09:00'),
('2014-07-31 08:00');
SELECT [T1].[TheDate] [first],
[T2].[TheDate] [next],
Datediff(day, [T1].[TheDate], [T2].[TheDate])[offset],
( CASE
WHEN Datediff(day, [T1].[TheDate], [T2].[TheDate]) >= 30 THEN 1
ELSE 0
END ) [qualify]
FROM @T[T1]
LEFT JOIN @T[T2]
ON [T2].[TheDate] = (SELECT Min([TheDate])
FROM @T
WHERE [TheDate] > [T1].[TheDate])
収量
私が重要な何かを完全に逃したのでなければ...