最初に、最後のコメントからの応答が遅れたことをおologiesびします。
件名は、行数が少ないため、再帰的CTE(以降、rCTE)を使用すると十分に速く実行されるというコメントで出てきました。そのように見えるかもしれませんが、真実から遠く離れることはできません。
タリーテーブルとタリー機能を構築する
テストを開始する前に、適切なクラスター化インデックスとItzik Ben-Ganスタイルの集計関数を使用して物理集計表を作成する必要があります。また、TempDBでこれらすべてを実行して、誰かのグッズを誤って削除しないようにします。
Tallyテーブルを作成するためのコードと、Itzikのすばらしいコードの現在の製品バージョンです。
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
--===== Create/Recreate a Physical Tally Table
IF OBJECT_ID('dbo.Tally','U') IS NOT NULL
DROP TABLE dbo.Tally
;
-- Note that the ISNULL makes a NOT NULL column
SELECT TOP 1000001
N = ISNULL(ROW_NUMBER() OVER (ORDER BY (SELECT NULL))-1,0)
INTO dbo.Tally
FROM sys.all_columns ac1
CROSS JOIN sys.all_columns ac2
;
ALTER TABLE dbo.Tally
ADD CONSTRAINT PK_Tally PRIMARY KEY CLUSTERED (N)
;
--===== Create/Recreate a Tally Function
IF OBJECT_ID('dbo.fnTally','IF') IS NOT NULL
DROP FUNCTION dbo.fnTally
;
GO
CREATE FUNCTION [dbo].[fnTally]
/**********************************************************************************************************************
Purpose:
Return a column of BIGINTs from @ZeroOrOne up to and including @MaxN with a max value of 1 Trillion.
As a performance note, it takes about 00:02:10 (hh:mm:ss) to generate 1 Billion numbers to a throw-away variable.
Usage:
--===== Syntax example (Returns BIGINT)
SELECT t.N
FROM dbo.fnTally(@ZeroOrOne,@MaxN) t
;
Notes:
1. Based on Itzik Ben-Gan's cascading CTE (cCTE) method for creating a "readless" Tally Table source of BIGINTs.
Refer to the following URLs for how it works and introduction for how it replaces certain loops.
http://www.sqlservercentral.com/articles/T-SQL/62867/
http://sqlmag.com/sql-server/virtual-auxiliary-table-numbers
2. To start a sequence at 0, @ZeroOrOne must be 0 or NULL. Any other value that's convertable to the BIT data-type
will cause the sequence to start at 1.
3. If @ZeroOrOne = 1 and @MaxN = 0, no rows will be returned.
5. If @MaxN is negative or NULL, a "TOP" error will be returned.
6. @MaxN must be a positive number from >= the value of @ZeroOrOne up to and including 1 Billion. If a larger
number is used, the function will silently truncate after 1 Billion. If you actually need a sequence with
that many values, you should consider using a different tool. ;-)
7. There will be a substantial reduction in performance if "N" is sorted in descending order. If a descending
sort is required, use code similar to the following. Performance will decrease by about 27% but it's still
very fast especially compared with just doing a simple descending sort on "N", which is about 20 times slower.
If @ZeroOrOne is a 0, in this case, remove the "+1" from the code.
DECLARE @MaxN BIGINT;
SELECT @MaxN = 1000;
SELECT DescendingN = @MaxN-N+1
FROM dbo.fnTally(1,@MaxN);
8. There is no performance penalty for sorting "N" in ascending order because the output is explicity sorted by
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
Revision History:
Rev 00 - Unknown - Jeff Moden
- Initial creation with error handling for @MaxN.
Rev 01 - 09 Feb 2013 - Jeff Moden
- Modified to start at 0 or 1.
Rev 02 - 16 May 2013 - Jeff Moden
- Removed error handling for @MaxN because of exceptional cases.
Rev 03 - 22 Apr 2015 - Jeff Moden
- Modify to handle 1 Trillion rows for experimental purposes.
**********************************************************************************************************************/
(@ZeroOrOne BIT, @MaxN BIGINT)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN WITH
E1(N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1) --10E1 or 10 rows
, E4(N) AS (SELECT 1 FROM E1 a, E1 b, E1 c, E1 d) --10E4 or 10 Thousand rows
,E12(N) AS (SELECT 1 FROM E4 a, E4 b, E4 c) --10E12 or 1 Trillion rows
SELECT N = 0 WHERE ISNULL(@ZeroOrOne,0)= 0 --Conditionally start at 0.
UNION ALL
SELECT TOP(@MaxN) N = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E12 -- Values from 1 to @MaxN
;
GO
ちなみに、数百万行のタリーテーブルを作成し、クラスター化インデックスを約1秒ほどで追加したことに注目してください。rCTEで試してみて、どれくらい時間がかかるかを確認してください!;-)
いくつかのテストデータを作成する
また、いくつかのテストデータも必要です。はい、rCTEを含め、テストするすべての機能がわずか12行で1ミリ秒以下で実行されることに同意しますが、これは多くの人が陥るtrapです。このトラップについては後で詳しく説明しますが、ここでは、各関数の呼び出しを40,000回シミュレートします。これは、私のショップの特定の関数が1日8時間に呼び出される回数です。大規模なオンライン小売業でこのような関数が何回呼び出されるか想像してみてください。
そこで、ランダムな日付で40,000行を作成するコードを以下に示します。各行には、追跡のためだけに行番号があります。ここでは重要ではないので、時間を丸1時間とすることはしませんでした。
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
--===== Create/Recreate a Test Date table
IF OBJECT_ID('dbo.TestDate','U') IS NOT NULL
DROP TABLE dbo.TestDate
;
DECLARE @StartDate DATETIME
,@EndDate DATETIME
,@Rows INT
;
SELECT @StartDate = '2010' --Inclusive
,@EndDate = '2020' --Exclusive
,@Rows = 40000 --Enough to simulate an 8 hour day where I work
;
SELECT RowNum = IDENTITY(INT,1,1)
,SomeDateTime = RAND(CHECKSUM(NEWID()))*DATEDIFF(dd,@StartDate,@EndDate)+@StartDate
INTO dbo.TestDate
FROM dbo.fnTally(1,@Rows)
;
12行の時間を実行するためのいくつかの機能を構築する
次に、rCTEコードを関数に変換し、他の3つの関数を作成しました。これらはすべて、高性能iTVF(インラインテーブル値関数)として作成されています。iTVFにはScalarやmTVF(Multi-statement Table Valued Functions)のようにBEGINがないため、いつでもわかります。
これらの4つの関数を作成するコードを以下に示します...それらを使用する方法にちなんで命名しました。
--===== CREATE THE iTVFs
--===== Do this in a nice, safe place that everyone has
USE tempdb
;
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.OriginalrCTE','IF') IS NOT NULL
DROP FUNCTION dbo.OriginalrCTE
;
GO
CREATE FUNCTION dbo.OriginalrCTE
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
WITH Dates AS
(
SELECT DATEPART(HOUR,DATEADD(HOUR,-1,@Date)) [Hour],
DATEADD(HOUR,-1,@Date) [Date], 1 Num
UNION ALL
SELECT DATEPART(HOUR,DATEADD(HOUR,-1,[Date])),
DATEADD(HOUR,-1,[Date]), Num+1
FROM Dates
WHERE Num <= 11
)
SELECT [Hour], [Date]
FROM Dates
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.MicroTally','IF') IS NOT NULL
DROP FUNCTION dbo.MicroTally
;
GO
CREATE FUNCTION dbo.MicroTally
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,t.N,@Date))
,[DATE] = DATEADD(HOUR,t.N,@Date)
FROM (VALUES (-1),(-2),(-3),(-4),(-5),(-6),(-7),(-8),(-9),(-10),(-11),(-12))t(N)
;
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.PhysicalTally','IF') IS NOT NULL
DROP FUNCTION dbo.PhysicalTally
;
GO
CREATE FUNCTION dbo.PhysicalTally
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,-t.N,@Date))
,[DATE] = DATEADD(HOUR,-t.N,@Date)
FROM dbo.Tally t
WHERE N BETWEEN 1 AND 12
;
GO
-----------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.TallyFunction','IF') IS NOT NULL
DROP FUNCTION dbo.TallyFunction
;
GO
CREATE FUNCTION dbo.TallyFunction
(@Date DATETIME)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT [Hour] = DATEPART(HOUR,DATEADD(HOUR,-t.N,@Date))
,[DATE] = DATEADD(HOUR,-t.N,@Date)
FROM dbo.fnTally(1,12) t
;
GO
機能をテストするためのテストハーネスの構築
最後になりましたが、テストハーネスが必要です。ベースラインチェックを行ってから、各機能を同じ方法でテストします。
テストハーネスのコードは次のとおりです...
PRINT '--========== Baseline Select =================================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = RowNum
,@Date = SomeDateTime
FROM dbo.TestDate
CROSS APPLY dbo.fnTally(1,12);
SET STATISTICS TIME,IO OFF;
GO
PRINT '--========== Orginal Recursive CTE ===========================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.OriginalrCTE(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT '--========== Dedicated Micro-Tally Table =====================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.MicroTally(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT'--========== Physical Tally Table =============================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.PhysicalTally(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
PRINT'--========== Tally Function ===================================';
DECLARE @Hour INT, @Date DATETIME
;
SET STATISTICS TIME,IO ON;
SELECT @Hour = fn.[Hour]
,@Date = fn.[Date]
FROM dbo.TestDate td
CROSS APPLY dbo.TallyFunction(td.SomeDateTime) fn;
SET STATISTICS TIME,IO OFF;
GO
上記のテストハーネスで注目すべきことは、すべての出力を「スローアウェイ」変数に分流していることです。これは、ディスクや画面への出力結果を歪めることなく、パフォーマンス測定を可能な限り純粋に保つことを試みることです。
設定統計に関する注意事項
また、テスターになるための注意事項... ScalarまたはmTVF関数のいずれかをテストする場合は、SET STATISTICSを使用しないでください。このテストのようなiTVF機能でのみ安全に使用できます。SET STATISTICSは、SCALAR関数を実際に実行しない場合よりも数百倍遅く実行することが証明されています。ええ、私は別の風車を傾けようとしていますが、それは全体の「より長い記事の長さの投稿」であり、そのための時間はありません。SQLServerCentral.comでそれについてすべて話している記事がありますが、ここにリンクを投稿しても意味がありません。誰かがそれについて形を崩してしまうからです。
テスト結果
そこで、6GBのRAMを搭載した小さなi5ラップトップでテストハーネスを実行したときのテスト結果を次に示します。
--========== Baseline Select =================================
Table 'Worktable'. Scan count 1, logical reads 82309, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, 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 = 203 ms, elapsed time = 206 ms.
--========== Orginal Recursive CTE ===========================
Table 'Worktable'. Scan count 40001, logical reads 2960000, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, 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 = 4258 ms, elapsed time = 4415 ms.
--========== Dedicated Micro-Tally Table =====================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, 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 = 234 ms, elapsed time = 235 ms.
--========== Physical Tally Table =============================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Tally'. Scan count 1, logical reads 3, 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 = 250 ms, elapsed time = 252 ms.
--========== Tally Function ===================================
Table 'Worktable'. Scan count 1, logical reads 81989, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TestDate'. Scan count 1, logical reads 105, 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 = 250 ms, elapsed time = 253 ms.
データのみを選択する「BASELINE SELECT」(各行が12回作成され、同じ収益のボリュームをシミュレートしました)が、約1/5秒で正しく入力されました。それ以外のすべては、約4分の1秒で到着しました。まあ、その血まみれのrCTE機能以外はすべて。4秒と1/4秒、または16倍長くなりました(1,600%遅くなりました)。
論理読み取り(メモリIO)を見てください。rCTEはなんと2,960,000(ほぼ300万回の読み取り)を消費しましたが、他の機能は約82,100しか消費しませんでした。つまり、rCTEは他のどの機能よりも34.3倍以上のメモリIOを消費しました。
締めくくり
まとめましょう。この「小さな」12行の処理を行うrCTEメソッドは、他のどの機能よりも16時間(1,600%)多いCPU(および期間)と34.3時間(3,430%)多いメモリIOを使用しました。
へえ...あなたの考えを知っています。「大したことだ!それはただ一つの機能だ」
ええ、同意しましたが、他にいくつの機能がありますか?関数以外の場所は他にいくつありますか?また、実行ごとに12行以上で動作するものはありますか?そして、メソッドを探している人がそのrCTEコードをもっと大きな何かのためにコピーする可能性はありますか?
さて、鈍い時間です。行数や使用量が制限されていると思われるからといって、パフォーマンスが問題になっているコードを正当化することはまったく意味がありません。おそらく数百万ドルでMPPボックスを購入する場合を除き(そのようなマシンで動作させるためにコードを書き換える費用は言うまでもありません)、コードを16倍高速で実行するマシンを購入することはできません(SSDが勝ちます私たちがそれをテストしたとき、これらはすべて高速メモリにありました)。パフォーマンスはコード内にあります。優れたパフォーマンスは優れたコードにあります。
すべてのコードが「ちょうど」16倍速く実行されたと想像できますか?
行数が少ない場合や使用率が低い場合でも、不良またはパフォーマンスに問題のあるコードを正当化しないでください。そうした場合、CPUとディスクを十分に冷却するために、私が傾いていると非難された風車の1つを借りる必要があるかもしれません。;-)
「TALLY」という言葉の言葉
ええ…そうですね。意味的に言えば、集計表には「タリー」ではなく数字が含まれています。主題に関する私の元の記事(技術の元の記事ではなかったが、最初の記事でした)で、私はそれを「タリー」と呼びました。ループの代わりに「カウント」するために使用され、何かを「カウント」するために何かを「集計」します。;-)それをあなたが何と呼ぶか...ナンバーテーブル、タリーテーブル、シーケンステーブル、何でも。気にしません。私にとって、「タリー」はより完全な意味であり、優れた怠DBなDBAであるため、7文字ではなく5文字(2文字は同一)しか含まれておらず、ほとんどの人にとっては簡単です。また、テーブルの命名規則に従っている「単数形」です。;-) それ' また、60年代の本のページを含む記事がそれを呼んだもの。私は常に「集計表」と呼んでいますが、私や他の誰かが何を意味するのかはまだ知っています。また、ペストのようなハンガリー記法は避けますが、関数を「fnTally」と呼び、「まあ、もしあなたが示したeff-en Tally関数を使用すれば、パフォーマンスの問題はないでしょう」と言うことができます。人事違反。;-) 実際にはHR違反ではありません。;-) 実際にはHR違反ではありません。;-)
私が心配しているのは、パフォーマンスに問題のあるrCTEや他の形式のHidden RBARに頼る代わりに、それを適切に使用することを学ぶ人々です。