質問

特定の種類の SQL クエリでは、補助的な数値テーブルが非常に便利です。これは、特定のタスクに必要な数の行を含むテーブルとして作成することも、各クエリで必要な行数を返すユーザー定義関数として作成することもできます。

このような関数を作成する最適な方法は何でしょうか?

役に立ちましたか?

解決

へー…古い投稿への返信が大変遅くなり申し訳ありません。そして、そう、このスレッドで最も人気のある答え (当時、14 の異なるメソッドへのリンクを含む Recursive CTE の答え) が、うーん...パフォーマンスはせいぜい挑戦されます。

まず、14 の異なるソリューションを掲載した記事は、その場で Numbers/Tally テーブルを作成するさまざまな方法を確認するのに適していますが、記事と引用されたスレッドで指摘されているように、 とても 重要な引用...

「効率とパフォーマンスに関する提案はしばしば主観的です。クエリの使用方法に関係なく、物理的実装によりクエリの効率が決定されます。したがって、偏ったガイドラインに依存するのではなく、クエリをテストし、どちらがより良いパフォーマンスを発揮するかを判断することが不可欠です。」

皮肉なことに、記事自体には多くの主観的な記述や、次のような「偏ったガイドライン」が含まれています。 「再帰的 CTE は数値リストを生成できます かなり効率的に" そして "これは 効率的な方法 Itzik Ben-Gen によるニュースグループ投稿からの WHILE ループの使用について」 (彼は比較目的だけで投稿したと確信しています)。さあ皆さん...Itzik の良い名前を言及するだけで、一部の哀れな怠け者が実際にその恐ろしい方法を使用するようになるかもしれません。著者は、自分が説いていることを実践し、特にスケーラビリティに直面してそのようなばかばかしいほど間違った発言をする前に、少しのパフォーマンス テストを行うべきです。

コードが何をするか、または誰かが「好き」であるかについて主観的な主張をする前に、実際にテストを行ってみるという考えで、独自のテストを実行できるコードをいくつか紹介します。テストを実行している SPID のプロファイラーをセットアップし、自分で確認してください...番号 1000000 を「お気に入り」の番号として「検索と置換」を実行して確認してください...

--===== Test for 1000000 rows ==================================
GO
--===== Traditional RECURSIVE CTE method
   WITH Tally (N) AS 
        ( 
         SELECT 1 UNION ALL 
         SELECT 1 + N FROM Tally WHERE N < 1000000 
        ) 
 SELECT N 
   INTO #Tally1 
   FROM Tally 
 OPTION (MAXRECURSION 0);
GO
--===== Traditional WHILE LOOP method
 CREATE TABLE #Tally2 (N INT);
    SET NOCOUNT ON;
DECLARE @Index INT;
    SET @Index = 1;
  WHILE @Index <= 1000000 
  BEGIN 
         INSERT #Tally2 (N) 
         VALUES (@Index);
            SET @Index = @Index + 1;
    END;
GO
--===== Traditional CROSS JOIN table method
 SELECT TOP (1000000)
        ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS N
   INTO #Tally3
   FROM Master.sys.All_Columns ac1
  CROSS JOIN Master.sys.ALL_Columns ac2;
GO
--===== Itzik's CROSS JOINED CTE method
   WITH E00(N) AS (SELECT 1 UNION ALL SELECT 1),
        E02(N) AS (SELECT 1 FROM E00 a, E00 b),
        E04(N) AS (SELECT 1 FROM E02 a, E02 b),
        E08(N) AS (SELECT 1 FROM E04 a, E04 b),
        E16(N) AS (SELECT 1 FROM E08 a, E08 b),
        E32(N) AS (SELECT 1 FROM E16 a, E16 b),
   cteTally(N) AS (SELECT ROW_NUMBER() OVER (ORDER BY N) FROM E32)
 SELECT N
   INTO #Tally4
   FROM cteTally
  WHERE N <= 1000000;
GO
--===== Housekeeping
   DROP TABLE #Tally1, #Tally2, #Tally3, #Tally4;
GO

その作業中に、SQL Profiler から取得した値 100、1000、10000、100000、および 1000000 の数値を次に示します。

SPID TextData                                 Dur(ms) CPU   Reads   Writes
---- ---------------------------------------- ------- ----- ------- ------
  51 --===== Test for 100 rows ==============       8     0       0      0
  51 --===== Traditional RECURSIVE CTE method      16     0     868      0
  51 --===== Traditional WHILE LOOP method CR      73    16     175      2
  51 --===== Traditional CROSS JOIN table met      11     0      80      0
  51 --===== Itzik's CROSS JOINED CTE method        6     0      63      0
  51 --===== Housekeeping   DROP TABLE #Tally      35    31     401      0

  51 --===== Test for 1000 rows =============       0     0       0      0
  51 --===== Traditional RECURSIVE CTE method      47    47    8074      0
  51 --===== Traditional WHILE LOOP method CR      80    78    1085      0
  51 --===== Traditional CROSS JOIN table met       5     0      98      0
  51 --===== Itzik's CROSS JOINED CTE method        2     0      83      0
  51 --===== Housekeeping   DROP TABLE #Tally       6    15     426      0

  51 --===== Test for 10000 rows ============       0     0       0      0
  51 --===== Traditional RECURSIVE CTE method     434   344   80230     10
  51 --===== Traditional WHILE LOOP method CR     671   563   10240      9
  51 --===== Traditional CROSS JOIN table met      25    31     302     15
  51 --===== Itzik's CROSS JOINED CTE method       24     0     192     15
  51 --===== Housekeeping   DROP TABLE #Tally       7    15     531      0

  51 --===== Test for 100000 rows ===========       0     0       0      0
  51 --===== Traditional RECURSIVE CTE method    4143  3813  800260    154
  51 --===== Traditional WHILE LOOP method CR    5820  5547  101380    161
  51 --===== Traditional CROSS JOIN table met     160   140     479    211
  51 --===== Itzik's CROSS JOINED CTE method      153   141     276    204
  51 --===== Housekeeping   DROP TABLE #Tally      10    15     761      0

  51 --===== Test for 1000000 rows ==========       0     0       0      0
  51 --===== Traditional RECURSIVE CTE method   41349 37437 8001048   1601
  51 --===== Traditional WHILE LOOP method CR   59138 56141 1012785   1682
  51 --===== Traditional CROSS JOIN table met    1224  1219    2429   2101
  51 --===== Itzik's CROSS JOINED CTE method     1448  1328    1217   2095
  51 --===== Housekeeping   DROP TABLE #Tally       8     0     415      0

ご覧のように、 再帰的 CTE メソッドは、継続時間と CPU に関して While ループに次いで 2 番目に最悪であり、論理読み取りの形式でのメモリ負荷が While ループの 8 倍になります。. 。これは強力な RBAR であり、while ループを避ける必要があるのと同じように、単一行の計算では絶対に避けるべきです。 再帰が非常に価値のある場所はありますが、これはその 1 つではありません.

補足として、Mr.デニーはまさにその通りです...適切なサイズの永続的な Numbers テーブルまたは Tally テーブルを使用すると、ほとんどの場合に使用できます。正しいサイズとはどういう意味ですか?そうですね、ほとんどの人は Tally テーブルを使用して日付を生成したり、VARCHAR(8000) で分割を行ったりします。「N」に正しいクラスター化インデックスを使用して 11,000 行の集計テーブルを作成すると、30 年分以上の日付を作成するのに十分な行が得られます (私は住宅ローンを扱う仕事が多いので、30 年は私にとって重要な数字です) )、VARCHAR(8000) 分割を処理するには十分です。なぜ「適切なサイズ」がそれほど重要なのでしょうか?Tally テーブルが頻繁に使用される場合、キャッシュに簡単に収まるため、メモリにまったく負担をかけずに非常に高速になります。

最後になりますが、永続的な Tally テーブルを作成する場合、1) 作成されるのは 1 回だけで、2) 行数が 11,000 行程度である場合、永続的な Tally テーブルを作成するのにどの方法を使用するかはあまり重要ではないことは誰もが知っています。表に示すように、すべてのメソッドは「十分に」実行されます。 では、なぜ私がどの方法を使用するかについて憤慨するのでしょうか?

答えは、それ以上の知識がなく、ただ自分の仕事を終わらせる必要がある哀れな男性/女性が、再帰的 CTE 手法のようなものを見て、建築よりもはるかに大規模で、より頻繁に使用される何かにそれを使用することを決心する可能性があるということです。永続的な集計テーブルを作成しようとしています それらの人々、コードが実行されるサーバー、およびそれらのサーバー上のデータを所有する企業を保護します。. 。うん...それはそれほど大きなことなのです。それは他の人たちにとっても同様であるはずです。「十分に良い」のではなく、物事を行うための正しい方法を教えてください。投稿や書籍から何かを投稿したり使用したりする前に、いくつかのテストを行ってください...特にこのようなことを行うには再帰的 CTE が最適であると考えている場合は、あなたが救った命が実際にはあなた自身のものになる可能性があります。;-)

聞いてくれてありがとう...

他のヒント

最も最適な機能は、関数の代わりにテーブルを使用することです。関数を使用すると、特に返される値が非常に広範囲にわたる場合、返されるデータの値を作成するために余分な CPU 負荷が発生します。

この記事 14 の異なる解決策がそれぞれについて説明されています。重要な点は次のとおりです。

効率とパフォーマンスに関する提案はしばしば主観的です。クエリの使用方法に関係なく、物理的実装によりクエリの効率が決定されます。したがって、偏ったガイドラインに依存するのではなく、クエリをテストし、どちらがより良いパフォーマンスを発揮するかを判断することが不可欠です。

個人的に気に入ったのは:

WITH Nbrs ( n ) AS (
    SELECT 1 UNION ALL
    SELECT 1 + n FROM Nbrs WHERE n < 500 )
SELECT n FROM Nbrs
OPTION ( MAXRECURSION 500 )

このビューは非常に高速で、すべての肯定的な内容が含まれています int 価値観。

CREATE VIEW dbo.Numbers
WITH SCHEMABINDING
AS
    WITH Int1(z) AS (SELECT 0 UNION ALL SELECT 0)
    , Int2(z) AS (SELECT 0 FROM Int1 a CROSS JOIN Int1 b)
    , Int4(z) AS (SELECT 0 FROM Int2 a CROSS JOIN Int2 b)
    , Int8(z) AS (SELECT 0 FROM Int4 a CROSS JOIN Int4 b)
    , Int16(z) AS (SELECT 0 FROM Int8 a CROSS JOIN Int8 b)
    , Int32(z) AS (SELECT TOP 2147483647 0 FROM Int16 a CROSS JOIN Int16 b)
    SELECT ROW_NUMBER() OVER (ORDER BY z) AS n
    FROM Int32
GO

使用する SQL Server 2016+ 使用できる数値テーブルを生成するには OPENJSON :

-- range from 0 to @max - 1
DECLARE @max INT = 40000;

SELECT rn = CAST([key] AS INT) 
FROM OPENJSON(CONCAT('[1', REPLICATE(CAST(',1' AS VARCHAR(MAX)),@max-1),']'));

LiveDemo


から得られたアイデア OPENJSON を使用して一連の数値を生成するにはどうすればよいでしょうか?

編集:以下のコンラッドのコメントを参照してください。

Jeff Modenの答えは素晴らしいです...しかし、Postgres では、E32 行を削除しないと Itzik メソッドが失敗することがわかりました。

postgres でわずかに高速 (40 ミリ秒対 100 ミリ秒) は、私が見つけた別の方法です。 ここ postgres に適応:

WITH 
    E00 (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 ),
    E01 (N) AS (SELECT a.N FROM E00 a CROSS JOIN E00 b),
    E02 (N) AS (SELECT a.N FROM E01 a CROSS JOIN E01 b ),
    E03 (N) AS (SELECT a.N FROM E02 a CROSS JOIN E02 b 
        LIMIT 11000  -- end record  11,000 good for 30 yrs dates
    ), -- max is 100,000,000, starts slowing e.g. 1 million 1.5 secs, 2 mil 2.5 secs, 3 mill 4 secs
    Tally (N) as (SELECT row_number() OVER (ORDER BY a.N) FROM E03 a)

SELECT N
FROM Tally

SQL Server から Postgres の世界に移行しているので、そのプラットフォームで集計テーブルを実行するより良い方法を見逃していたかもしれません...整数()?順序()?

まだずっと後ですが、少し異なる「従来の」CTE (行のボリュームを取得するためにベーステーブルには触れません) を提供したいと思います。

--===== Hans CROSS JOINED CTE method
WITH Numbers_CTE (Digit)
AS
(SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9)
SELECT HundredThousand.Digit * 100000 + TenThousand.Digit * 10000 + Thousand.Digit * 1000 + Hundred.Digit * 100 + Ten.Digit * 10 + One.Digit AS Number
INTO #Tally5
FROM Numbers_CTE AS One CROSS JOIN Numbers_CTE AS Ten CROSS JOIN Numbers_CTE AS Hundred CROSS JOIN Numbers_CTE AS Thousand CROSS JOIN Numbers_CTE AS TenThousand CROSS JOIN Numbers_CTE AS HundredThousand

この CTE は、Itzik の CTE よりも多くの READ を実行しますが、従来の CTE よりは少ないです。ただし、他のクエリよりも一貫して WRITES の実行回数が少なくなります。ご存知のとおり、書き込みは常に読み取りよりもはるかに高価です。

所要時間はコア数 (MAXDOP) に大きく依存しますが、私の 8 コアでは、他のクエリよりも一貫して高速に実行されます (ミリ秒単位での所要時間が短くなります)。

使っています:

Microsoft SQL Server 2012 - 11.0.5058.0 (X64) 
May 14 2014 18:34:29 
Copyright (c) Microsoft Corporation
Enterprise Edition (64-bit) on Windows NT 6.3 <X64> (Build 9600: )

Windows Server 2012 R2、32 GB、Xeon X3450 @2.67Ghz、4 コア HT 対応。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top