2011-01-06 12 views
3

私はイベントのテーブルを持っており、イベントの発生頻度(日数)を指定します。計算されたオカレンスを含む、指定された日付範囲内のイベントのすべての出現を選択したいと思います(たとえば、最初のイベントの日付が2011年1月6日で、7日ごとに発生する場合は、1月13日と1月20日が結果)。ここで 集計テーブルを使用して繰り返し日付のクエリを選択

は私のイベント表は次のようになります。

 

event_ID INT, 
event_title NVARCHAR(50), 
first_event_date DATETIME, 
occurs_every INT 
 

this articleを読んだ後、それはこの集計テーブルであるに対処するための最も効率的な方法のように思えるが、私は私の頭をラップすることができていません私が探している結果を返す方法について。

さんは次のようになり、その私がデータを持っているとしましょう:

 
event_ID | event_title | first_event_date | occurs_every 
1  | Event 1  | 1/6/2011  |  7 
2  | Event 2  | 1/8/2011  |  3 

私が探している結果は、次のようになります。

 
event_ID | event_title | event_date | 
1  | Event 1  | 1/6/2011 | 
2  | Event 2  | 1/8/2011 | 
1  | Event 1  | 1/13/2011 | 
2  | Event 2  | 1/12/2011 | 
2  | Event 2  | 1/16/2011 | 
1  | Event 1  | 1/20/2011 | 
(etc) 

任意の提案ですか?編集:私は作業のクエリを持っているが、それはかなり場しのぎようだと私はに、より多くのデータを得れば、私はパフォーマンスが心配です

:私はSQL Server 2008の

追加情報を使用していますテーブル。

参考のために、これは集計テーブルで、まず:

 

SELECT TOP 11000 
     IDENTITY(INT,1,1) AS N 
    INTO dbo.Tally 
    FROM Master.dbo.SysColumns sc1, 
     Master.dbo.SysColumns sc2 

    ALTER TABLE dbo.Tally 
    ADD CONSTRAINT PK_Tally_N 
     PRIMARY KEY CLUSTERED (N) WITH FILLFACTOR = 100 
 

は今、ここにクルージ選択クエリです:今

 

SELECT event_ID, 
     event_title, 
     first_event_date, 
     DATEADD(dd, occurs_every * (t.N - 1), [first_event_date]) AS occurrence 
FROM dbo.Events 
     CROSS JOIN dbo.Tally t 
WHERE DATEADD(dd, occurs_every * (t.N - 1), [first_event_date]) 

、これは動作します - しかし、私は、サンプルの1000行を追加しましたテーブルへのデータは本当にうんざりしていました。私はそれが私の十字結合であると仮定します。また、上記のコードは私の選択したクエリの最後の行を表示していません。これは "ORDER BY occurrence"です。

+1

どのデータベースエンジンを使用していますか?どのバージョンですか? – Lamak

+0

SQL Server 2008. – Ethan

答えて

4

まず、この投稿に返信しないように心からお詫び申し上げます。私はプリアンブルとしていくつかのコメントを書いた後、ちょうど "セージのアドバイス"の代わりに便利な答えを投稿することを完全に意図していました。そして実際の生活が起こったので、私はこの投稿を完全に失いました。

てみましょう最初の彼は、彼は彼がやった言ったように千回のイベントでそれを使用して移入されたと述べたテーブルを構築することにより、OPのポストを再訪。私は、WhileループまたはrCTEのいずれかのRBARの代わりに必要な "行の存在"を提供するために、高性能の "疑似カーソル"を使用して2015年と2016年のランダム開始日を使用してデータを少し近代化します(Recursive CTE )。サイドバーのビットとして

2005を使用している人々の全体の多くが残っていると、このための2008+技術を使用してもパフォーマンスの向上はありませんので、私は互換性のあるすべてのもの2005を保っています。

はここでテストテーブルを構築するためのコードです。詳細はコメントにあります。ここで

--==================================================================== 
--  Presets 
--==================================================================== 
--===== Declare and prepopulate some obviously named variables 
DECLARE @StartDate  DATETIME 
     ,@EndDate  DATETIME 
     ,@Days   INT 
     ,@Events  INT 
     ,@MaxEventGap INT 
; 
SELECT @StartDate  = '2015-01-01' --Inclusive date 
     ,@EndDate  = '2017-01-01' --Exclusive date 
     ,@Days   = DATEDIFF(dd,@StartDate,@EndDate) 
     ,@Events  = 1000 
     ,@MaxEventGap = 30 --Note that 1 day will be the next day 
; 
--==================================================================== 
--  Create the Test Table 
--==================================================================== 
--===== If the test table already exists, drop it to make reruns of 
    -- this demo easier. I also use a Temp Table so that we don't 
    -- accidenttly screw up a real table. 
    IF OBJECT_ID('tempdb..#Events','U') IS NOT NULL 
     DROP TABLE #Events 
; 
--===== Build the test table. 
    -- I'm following what the OP did so that anyone with a case 
    -- sensitive server won't have a problem. 
CREATE TABLE #Events 
     (
     event_ID   INT, 
     event_title   NVARCHAR(50), 
     first_event_date DATETIME, 
     occurs_every  INT 
     ) 
; 
--==================================================================== 
--  Populate the Test Table 
--==================================================================== 
--===== Build @Events number of events using the previously defined 
    -- start date and number of days as limits for the random dates. 
    -- To make life a little easier, I'm using a CTE with a 
    -- "pseudo-cursor" to form most of the data and then an 
    -- external INSERT so that I can name the event after the 
    -- event_ID. 
    WITH cteGenData AS 
     (
     SELECT TOP (@Events) 
       event_ID   = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) 
       ,first_event_date = DATEADD(dd, ABS(CHECKSUM(NEWID())) % @Days, @StartDate) 
       ,occurs_every  = ABS(CHECKSUM(NEWID())) % 30 + 1 
      FROM  sys.all_columns ac1 --Has at least 4000 rows in it for most editions 
      CROSS JOIN sys.all_columns ac2 --Just in case it doesn't for Express ;-) 
     ) 
INSERT INTO #Events 
     (event_ID, event_title, first_event_date, occurs_every) 
SELECT event_ID 
     ,event_title = 'Event #' + CAST(event_id AS VARCHAR(10)) 
     ,first_event_date 
     ,occurs_every 
    FROM cteGenData 
; 
--===== Let's see the first 10 rows 
SELECT TOP 10 * 
    FROM #Events 
    ORDER BY event_ID 
; 

最初の10行がfirst_even_datetとoccurs_everyの値があるため、私は拘束されたランダムデータを生成するのに使用される方法ではかなり異なるものになることを理解した上でどのように見えるかです。

event_ID event_title first_event_date  occurs_every 
-------- ----------- ----------------------- ------------ 
1  Event #1 2016-10-12 00:00:00.000 10 
2  Event #2 2015-04-25 00:00:00.000 28 
3  Event #3 2015-11-08 00:00:00.000 4 
4  Event #4 2016-02-16 00:00:00.000 25 
5  Event #5 2016-06-11 00:00:00.000 15 
6  Event #6 2016-04-29 00:00:00.000 14 
7  Event #7 2016-04-16 00:00:00.000 9 
8  Event #8 2015-03-29 00:00:00.000 2 
9  Event #9 2016-02-14 00:00:00.000 29 
10  Event #10 2016-01-23 00:00:00.000 8 

OP実験を複製するには、必ずタリーテーブルが必要です。ここにそのコードがあります。既にある場合は、パフォーマンス上の理由から、一意のクラスタ化インデックス(通常はPKの形式)が必要であることを確認してください。私は廃止された "syscolumns"ビューを使用しないように、コードの "疑似カーソル"部分に行ソーステーブルを近代化しました。

--===== Create a Tally Table with enough sequential numbers 
    -- for more than 30 years worth of dates. 
SELECT TOP 11000 
     IDENTITY(INT,1,1) AS N 
    INTO dbo.Tally 
    FROM  sys.all_columns sc1 
    CROSS JOIN sys.all_columns sc2 
; 
--===== Add the quintessential Unique Clustered Index as the PK. 
    ALTER TABLE dbo.Tally 
    ADD CONSTRAINT PK_Tally_N 
     PRIMARY KEY CLUSTERED (N) WITH FILLFACTOR = 100 
; 

私たちは準備ができています。 OPのコードの一部がフォーラムで飲み込まれましたが、元の投稿の編集を使用して回復できました。私が作成したばかりのデータと一致するように「終了日」を変更したことを除いて、実際にはこのように見えます(それが私が作成した唯一の変更です)。コードにはスカラーまたは複数ステートメントのUDFが含まれていないため、統計情報を有効にして何が起こっているのか説明しようとしました。

ここで言及した変更でOPのコードです。

SET STATISTICS TIME,IO ON 
; 
SELECT event_id, 
     event_title, 
     first_event_date, 
     DATEADD(dd, occurs_every * (t.N - 1), [first_event_date]) AS Occurrence 
    FROM #Events 
    CROSS JOIN dbo.Tally t 
    WHERE t.N <= DATEDIFF(dd,first_event_date,'2017-03-01')/occurs_every + 1 
    ORDER BY Occurrence 
; 
SET STATISTICS TIME,IO OFF 
; 

ここにOPコードを実行した際の統計情報があります。すべてのスクロールについては申し訳ありませんが、長い行です。

(61766 row(s) affected) 
Table 'Worktable'. Scan count 4, logical reads 118440, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Tally'. Scan count 4, logical reads 80, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table '#Events_____________________________________________________________________________________________________________00000000001F'. Scan count 5, logical reads 7, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 

SQL Server Execution Times: 
    CPU time = 4196 ms, elapsed time = 1751 ms. 

明らかに、このパフォーマンスは、While LoopまたはrCTEでさえ打ち消すことができる吸い上げ音を作り出しています。何が問題ですか?

以下の実行計画で強調表示されている矢印をチェックすると、非SARGable(SARG = "Search ARGument"およびSARGableでないために1100万の実際の行が含まれています。 11,000行の集計表と1,000行の#Events表の間で完全なCROSS JOINが発生したことを示しています。そしてそれらは実績のある行であり、推測された行ではありません。

Non-SARGable Query scanned Tally Table 1,000 times for total of 11 million rows

タリー表の「N」の欄は、式で使用され、全体タリー表#Eventsテーブルのすべての行の結果としてスキャンされなければならないためです。これは、Tally Tablesが遅いコードを生成するという一般的なエラーです。

だから、どのように我々はそれを修正しますか?各行の日付を計算するためにt.Nを使用するのではなく、日付の差をとって日数で割って、t.Nと何が起こるかを見分けるために必要な出現回数を把握しましょう。以下のコードで変更したのは、WHERE句の基準で、tをルックアップすることだけでした。N SARGable(インデックスを使用してシークを開始および停止し、その後に範囲スキャンを実行できる)

SET STATISTICS TIME,IO ON 
; 
SELECT event_id, 
     event_title, 
     first_event_date, 
     DATEADD(dd, occurs_every * (t.N - 1), [first_event_date]) AS Occurrence 
    FROM #Events 
    CROSS JOIN dbo.Tally t 
    WHERE t.N <= DATEDIFF(dd,first_event_date,'2017-03-01')/occurs_every + 1 
    ORDER BY Occurrence 
; 
SET STATISTICS TIME,IO OFF 
; 

新しい実行プランの外観は次のとおりです。 61,766行の実際の行(すべてキャッシュ内)は、1億1,000万行とはまったく異なります。

enter image description here ここでは、計算上の天の小さなスライスの統計はどのように見えますか?

(61766 row(s) affected) 
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table '#Events_____________________________________________________________________________________________________________00000000001F'. Scan count 5, logical reads 7, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 
Table 'Tally'. Scan count 1000, logical reads 3011, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 

SQL Server Execution Times: 
    CPU time = 78 ms, elapsed time = 528 ms. 
  • CPU時間は52.79倍または5279パーセント減少しました。
  • 経過時間が2.32倍または232%減少しました。
  • 合計は38.27倍または3827パーセント変更コードの

合計... WHERE句の1行減少読み込みます。

Itzik Ben-GanのインラインカスケーディングCTE(rCTEではありません)のいずれかを使用すると、読み込みの合計数をわずか7に減らすことができます。

結論は、タリーテーブルの使用はパフォーマンスの万能薬ですが、他のものと同様に正しく使用する必要があることです。 SARGable WHERE句を書くなど、ベストプラクティスを使用して、インデックスを正しく取得する必要があります。

また、私はこのことで非常に後悔しています。私はそれが将来誰かを助けることを願っています。私はまた、このスレッドのrCTEの例を書き直して、どれほど悪いのかを示す時間がないことをお詫びします。あなたがrCTEsがとても悪く、SQLServerCentral.comのメンバーシップを気にしない理由に興味があるなら、ここにその件に関する記事があります。私はここにすべてを掲示するつもりだが、そうするには時間がかかりすぎる。

Hidden RBAR: Counting with Recursive CTE's

1

これは、Oracleを使用する1つの方法です(これは、連続番号を生成するサブクエリを変更することで他のエンジンに切り替えることができます。以下を参照)。このクエリの背後にあるアイデアは、窓サイズ(日付間の日数)までの乗数の連続リスト(例えば、0,1,2,3 ...、n)を生成することである。これは、サブクエリが返すものです。これを使用してイベントテーブルとの結合を行い、結果を要求された日付範囲に制限します。

SELECT t.event_id, t.event_title, t.event_date + t.occurs_every*x.r event_date 
FROM tally_table t CROSS JOIN (
SELECT rownum-1 r FROM DUAL 
     connect by level <= (date '2011-1-20' - date '2011-1-6') + 1 
) x 
WHERE t.event_date + t.occurs_every*x.r <= date '2011-1-20' 
ORDER BY t.event_date + t.occurs_every*x.r, t.event_id; 

クエリのtally_tableは、質問で指定したテーブルです。

9

SQL Server 2008では、再帰的なCTEを使用できます。

DECLARE @StartDate DATE, @EndDate DATE 
SET @StartDate = '20110106' 
SET @EndDate = '20110228'; 


WITH DateTable AS 
(
    SELECT Event_id, event_title, event_date, occurs_every 
    FROM tally_table 
    UNION ALL 
    SELECT event_ID, event_title, DATEADD(DAY,occurs_every,event_date), occurs_every 
    FROM DateTable 
    WHERE DATEADD(DAY,occurs_every,event_date) BETWEEN @StartDate AND @EndDate 
) 
SELECT Event_id, event_title, event_date 
FROM DateTable 
WHERE event_date BETWEEN @StartDate AND @EndDate 
ORDER BY event_date 

日付範囲でフィルタリングして無限ループにならないように覚えておく必要があります。または、MAXRECURSIONのヒントを使用して結果を制限してください(デフォルトでは、この値は100です)。

+1

例を更新していただきありがとうございます。初めてのことでは分かりませんでした。これはめちゃくちゃ速く、集計テーブルを一切使わずに私の問題を解決しているようです。 – Ethan

+0

@Ethanええ、私はテーブル名を台無しにしていたので、あなたはそれを理解できないだけでなく、間違っていました。それがうれしかった。 – Lamak

+0

いいえ!これをしないでください。それは再帰的なCTEを使用して "カウント"しますが、一般的にパフォーマンスとリソースの使用方法についてはWhileループより悪いです。 1桁の数字を効果的に生成する方法はたくさんありますが、これはその1つではありません。 –