最初の1億個の正の整数を文字列に変換するにはどうすればよいですか?


13

これは、実際の問題から少し逸脱しています。コンテキストの提供が役立つ場合、このデータの生成は、文字列の処理方法のパフォーマンステスト、カーソル内で何らかの操作を適用する必要がある文字列の生成、または機密データの一意の匿名名の置換の生成に役立ちます。SQL Server内でデータを効率的に生成する方法に興味があるだけです。このデータを生成する必要がある理由を尋ねないでください。

ある程度正式な定義から始めようと思います。文字列は、A〜Zの大文字のみで構成される場合、シリーズに含まれます。シリーズの最初の用語は「A」です。シリーズは、最初に長さ、2番目に一般的なアルファベット順でソートされたすべての有効な文字列で構成されます。文字列がという列のテーブルにあるSTRING_COL場合、順序はT-SQLでとして定義できますORDER BY LEN(STRING_COL) ASC, STRING_COL ASC

あまり正式ではない定義を行うには、Excelのアルファベット順の列ヘッダーを見てください。シリーズは同じパターンです。整数を基数26の数値に変換する方法を検討してください。

1-> A、2-> B、3-> C、...、25-> Y、26-> Z、27-> AA、28-> AB、...

「A」は10を基数とする0とは異なる動作をするため、類推は完全ではありません。以下に、選択した値の表を示します。

╔════════════╦════════╗
 ROW_NUMBER  STRING 
╠════════════╬════════╣
          1  A      
          2  B      
         25  Y      
         26  Z      
         27  AA     
         28  AB     
         51  AY     
         52  AZ     
         53  BA     
         54  BB     
      18278  ZZZ    
      18279  AAAA   
     475253  ZZZY   
     475254  ZZZZ   
     475255  AAAAA  
  100000000  HJUNYV 
╚════════════╩════════╝

目標は、SELECT上記で定義した順序で最初の100000000文字列を返すクエリを作成することです。テーブルに保存するのではなく、結果セットを破棄してSSMSでクエリを実行してテストを行いました。

結果セットを破棄

理想的には、クエリは合理的に効率的です。ここでは、シリアルクエリのCPU時間とパラレルクエリの経過時間として効率を定義しています。文書化されていない任意のトリックを使用できます。未定義または非保証の振る舞いに依存することも同様に大丈夫ですが、あなたの答えでそれを呼び出すならば、それは高く評価されるでしょう。

上記のデータセットを効率的に生成する方法は何ですか?Martin Smithは、CLRストアドプロシージャは、非常に多くの行を処理するオーバーヘッドがあるため、おそらく適切なアプローチではないと指摘しました。

回答:


7

あなたのソリューションは私のラップトップで35秒間実行されます。次のコードには26秒かかります(一時テーブルの作成とデータの追加を含む)。

一時テーブル

DROP TABLE IF EXISTS #T1, #T2, #T3, #T4;

CREATE TABLE #T1 (string varchar(6) NOT NULL PRIMARY KEY);
CREATE TABLE #T2 (string varchar(6) NOT NULL PRIMARY KEY);
CREATE TABLE #T3 (string varchar(6) NOT NULL PRIMARY KEY);
CREATE TABLE #T4 (string varchar(6) NOT NULL PRIMARY KEY);

INSERT #T1 (string)
VALUES
    ('A'), ('B'), ('C'), ('D'), ('E'), ('F'), ('G'),
    ('H'), ('I'), ('J'), ('K'), ('L'), ('M'), ('N'),
    ('O'), ('P'), ('Q'), ('R'), ('S'), ('T'), ('U'),
    ('V'), ('W'), ('X'), ('Y'), ('Z');

INSERT #T2 (string)
SELECT T1a.string + T1b.string
FROM #T1 AS T1a, #T1 AS T1b;

INSERT #T3 (string)
SELECT #T2.string + #T1.string
FROM #T2, #T1;

INSERT #T4 (string)
SELECT #T3.string + #T1.string
FROM #T3, #T1;

そこでのアイデアは、最大4文字の順序付けられた組み合わせを事前に設定することです。

メインコード

SELECT TOP (100000000)
    UA.string + UA.string2
FROM
(
    SELECT U.Size, U.string, string2 = '' FROM 
    (
        SELECT Size = 1, string FROM #T1
        UNION ALL
        SELECT Size = 2, string FROM #T2
        UNION ALL
        SELECT Size = 3, string FROM #T3
        UNION ALL
        SELECT Size = 4, string FROM #T4
    ) AS U
    UNION ALL
    SELECT Size = 5, #T1.string, string2 = #T4.string
    FROM #T1, #T4
    UNION ALL
    SELECT Size = 6, #T2.string, #T4.string
    FROM #T2, #T4
) AS UA
ORDER BY 
    UA.Size, 
    UA.string, 
    UA.string2
OPTION (NO_PERFORMANCE_SPOOL, MAXDOP 1);

これは、事前に計算された4つのテーブルの単純な順序保存ユニオン*であり、必要に応じて5文字と6文字の文字列が導出されます。接頭辞を接尾辞から分離すると、ソートが回避されます。

実行計画

1億行


*上記のSQLには、順序保持ユニオンを直接指定するものは何もありません。オプティマイザーは、最上位の順序付けなど、SQLクエリ仕様に一致するプロパティを持つ物理演算子を選択します。ここでは、ソートを回避するために、マージ結合物理演算子によって実装される連結を選択します。

保証は、実行計画がクエリのセマンティックと最上位の順序を仕様別に提供することです。マージ結合連結が順序を保持することを知っていると、クエリ作成者は実行計画を予測できますが、オプティマイザは期待が有効な場合にのみ配信します。


6

回答を投稿して開始します。私が最初に考えたのは、ネストされたループ結合の順序を維持する性質と、文字ごとに1行のいくつかのヘルパーテーブルを利用できることです。トリッキーな部分は、結果が長さで並べられ、重複を避けるような方法でループすることでした。「」と一緒に、すべての26件の大文字が含まれてCTEを接合する際、クロスたとえば、あなたが生成を終わることができ'A' + '' + 'A'そして'' + 'A' + 'A'もちろん、同じ文字列です。

最初の決定は、ヘルパーデータを保存する場所でした。一時テーブルを使用してみましたが、データが単一のページに収まる場合でも、これはパフォーマンスに驚くほど悪い影響を与えました。一時テーブルには以下のデータが含まれていました。

SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'

CTEを使用した場合と比較すると、クエリはクラスター化されたテーブルでは3倍、ヒープでは4倍長くかかりました。問題は、データがディスク上にあることだとは思わない。単一ページとしてメモリに読み込まれ、プラン全体でメモリで処理される必要があります。おそらく、SQL Serverは、通常の行ストアページに格納されているデータよりも、Constant Scanオペレーターからのデータをより効率的に処理できます。

興味深いことに、SQL Serverは、順序付けられたデータを持つ単一ページのtempdbテーブルからの順序付けされた結果をテーブルスプールに入れることを選択します。

悪いスプーン

SQL Serverは、クロス結合の内部テーブルの結果をテーブルスプールに入れることがよくあります。オプティマイザーはこの領域で少し作業が必要だと思います。NO_PERFORMANCE_SPOOLパフォーマンスヒットを回避するために、クエリを実行しました。

CTEを使用してヘルパーデータを保存する場合の問題の1つは、データの順序が保証されていないことです。オプティマイザーがそれを順序付けしないことを選択する理由は考えられません。すべてのテストで、データはCTEを記述した順序で処理されました。

一定のスキャン順序

ただし、特にパフォーマンスの大きなオーバーヘッドなしでそれを行う方法がある場合は、チャンスをとらないことをお勧めします。余分なTOP演算子を追加することで、派生テーブルのデータを並べ替えることができます。例えば:

(SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR)

クエリへの追加により、結果が正しい順序で返されることが保証されます。すべての種類がパフォーマンスに大きな悪影響を与えると予想しました。クエリオプティマイザーは、推定コストに基づいてこれも予想していました。

高価な種類

非常に驚くべきことに、明示的な順序付けの有無にかかわらず、CPU時間または実行時間の統計的に有意な差を観察できませんでした。どちらかといえば、クエリはORDER BY!この動作の説明はありません。

問題の厄介な部分は、適切な場所に空白文字を挿入する方法を見つけ出すことでした。前に述べたように、単純なCROSS JOIN結果は重複データになります。100000000番目の文字列の長さは6文字であることがわかっています。

26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 = 914654 <100000000

だが

26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321272406> 100000000

したがって、CTEに6回参加するだけで済みます。CTEに6回参加し、各CTEから1つの文字を取得し、それらをすべて連結するとします。左端の文字が空白ではないと仮定します。後続の文字のいずれかが空白の場合、文字列の長さが6文字未満であるため、重複していることを意味します。したがって、最初の非空白文字を見つけ、それ以降のすべての文字も空白ではないことを要求することにより、重複を防ぐことができます。FLAG列をCT​​Eの1つに割り当て、WHERE句にチェックを追加することで、これを追跡することにしました。これは、クエリを見るとより明確になるはずです。最終的なクエリは次のとおりです。

WITH FIRST_CHAR (CHR) AS
(
    SELECT 'A'
    UNION ALL SELECT 'B'
    UNION ALL SELECT 'C'
    UNION ALL SELECT 'D'
    UNION ALL SELECT 'E'
    UNION ALL SELECT 'F'
    UNION ALL SELECT 'G'
    UNION ALL SELECT 'H'
    UNION ALL SELECT 'I'
    UNION ALL SELECT 'J'
    UNION ALL SELECT 'K'
    UNION ALL SELECT 'L'
    UNION ALL SELECT 'M'
    UNION ALL SELECT 'N'
    UNION ALL SELECT 'O'
    UNION ALL SELECT 'P'
    UNION ALL SELECT 'Q'
    UNION ALL SELECT 'R'
    UNION ALL SELECT 'S'
    UNION ALL SELECT 'T'
    UNION ALL SELECT 'U'
    UNION ALL SELECT 'V'
    UNION ALL SELECT 'W'
    UNION ALL SELECT 'X'
    UNION ALL SELECT 'Y'
    UNION ALL SELECT 'Z'
)
, ALL_CHAR (CHR, FLAG) AS
(
    SELECT '', 0 CHR
    UNION ALL SELECT 'A', 1
    UNION ALL SELECT 'B', 1
    UNION ALL SELECT 'C', 1
    UNION ALL SELECT 'D', 1
    UNION ALL SELECT 'E', 1
    UNION ALL SELECT 'F', 1
    UNION ALL SELECT 'G', 1
    UNION ALL SELECT 'H', 1
    UNION ALL SELECT 'I', 1
    UNION ALL SELECT 'J', 1
    UNION ALL SELECT 'K', 1
    UNION ALL SELECT 'L', 1
    UNION ALL SELECT 'M', 1
    UNION ALL SELECT 'N', 1
    UNION ALL SELECT 'O', 1
    UNION ALL SELECT 'P', 1
    UNION ALL SELECT 'Q', 1
    UNION ALL SELECT 'R', 1
    UNION ALL SELECT 'S', 1
    UNION ALL SELECT 'T', 1
    UNION ALL SELECT 'U', 1
    UNION ALL SELECT 'V', 1
    UNION ALL SELECT 'W', 1
    UNION ALL SELECT 'X', 1
    UNION ALL SELECT 'Y', 1
    UNION ALL SELECT 'Z', 1
)
SELECT TOP (100000000)
d6.CHR + d5.CHR + d4.CHR + d3.CHR + d2.CHR + d1.CHR
FROM (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d6
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d5
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d4
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d3
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d2
CROSS JOIN (SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR) d1
WHERE (d2.FLAG + d3.FLAG + d4.FLAG + d5.FLAG + d6.FLAG) =
    CASE 
    WHEN d6.FLAG = 1 THEN 5
    WHEN d5.FLAG = 1 THEN 4
    WHEN d4.FLAG = 1 THEN 3
    WHEN d3.FLAG = 1 THEN 2
    WHEN d2.FLAG = 1 THEN 1
    ELSE 0 END
OPTION (MAXDOP 1, FORCE ORDER, LOOP JOIN, NO_PERFORMANCE_SPOOL);

CTEは上記のとおりです。ALL_CHAR空白文字の行が含まれているため、5回結合されます。文字列の最後の文字は空白にしないでください。そのため、別個のCTEが定義されていFIRST_CHARます。余分なフラグ列は、ALL_CHAR上記の重複を防ぐために使用されます。このチェックを行うためのより効率的な方法があるかもしれませんが、それを行うための間違いなくより非効率的な方法があります。私が一つの試みLEN()POWER()現在のバージョンよりも6倍も遅いクエリの実行を作りました。

MAXDOP 1そしてFORCE ORDERヒントは、順番がクエリに保存されていることを確認するために不可欠です。注釈付きの推定プランは、結合が現在の順序になっている理由を確認するのに役立つ場合があります。

注釈付き

クエリプランはしばしば右から左に読み取られますが、行リクエストは左から右に発生します。理想的には、SQL Serverはd1常時スキャン演算子から1億行を正確に要求します。左から右に移動すると、各演算子から要求される行が少なくなると予想されます。これは実際の実行計画で見ることができます。さらに、以下はSQL Sentry Plan Explorerのスクリーンショットです。

冒険者

d1から正確に1億行を得たのは良いことです。d2とd3の間の行の比率は、ほぼ正確に27:1(165336 * 27 = 4464072)であることに注意してください。これは、クロス結合がどのように機能するかを考えると意味があります。d1とd2の間の行の比率は22.4で、これは無駄な作業を表しています。余分な行は(文字列の中央にある空白文字のため)重複からのものであり、フィルタリングを行うネストされたループ結合演算子を通過しないと考えています。

aはSQL Serverのループ結合としてのみ実装できるLOOP JOINため、このヒントは技術的には不要CROSS JOINです。これNO_PERFORMANCE_SPOOLは、不要なテーブルスプールを防ぐためです。スプールヒントを省略すると、クエリが私のマシンで3倍長くかかります。

最終クエリのCPU時間は約17秒で、合計経過時間は18秒です。それは、SSMSを介してクエリを実行し、結果セットを破棄するときでした。私はデータを生成する他の方法を見ることに非常に興味があります。


2

最大217,180,147,158(8文字)までの特定の番号の文字列コードを取得するために最適化されたソリューションがあります。しかし、私はあなたの時間を打ち負かすことはできません。

私のマシンでは、SQL Server 2014を使用すると、クエリに18秒かかりますが、私のクエリでは3分46秒かかります。2014はNO_PERFORMANCE_SPOOLヒントをサポートしていないため、両方のクエリは文書化されていないトレースフラグ8690を使用します。

コードは次のとおりです。

/* precompute offsets and powers to simplify final query */
CREATE TABLE #ExponentsLookup (
    offset          BIGINT NOT NULL,
    offset_end      BIGINT NOT NULL,
    position        INTEGER NOT NULL,
    divisor         BIGINT NOT NULL,
    shifts          BIGINT NOT NULL,
    chars           INTEGER NOT NULL,
    PRIMARY KEY(offset, offset_end, position)
);

WITH base_26_multiples AS ( 
    SELECT  number  AS exponent,
            CAST(POWER(26.0, number) AS BIGINT) AS multiple
    FROM    master.dbo.spt_values
    WHERE   [type] = 'P'
            AND number < 8
),
num_offsets AS (
    SELECT  *,
            -- The maximum posible value is 217180147159 - 1
            LEAD(offset, 1, 217180147159) OVER(
                ORDER BY exponent
            ) AS offset_end
    FROM    (
                SELECT  exponent,
                        SUM(multiple) OVER(
                            ORDER BY exponent
                        ) AS offset
                FROM    base_26_multiples
            ) x
)
INSERT INTO #ExponentsLookup(offset, offset_end, position, divisor, shifts, chars)
SELECT  ofst.offset, ofst.offset_end,
        dgt.number AS position,
        CAST(POWER(26.0, dgt.number) AS BIGINT)     AS divisor,
        CAST(POWER(256.0, dgt.number) AS BIGINT)    AS shifts,
        ofst.exponent + 1                           AS chars
FROM    num_offsets ofst
        LEFT JOIN master.dbo.spt_values dgt --> as many rows as resulting chars in string
            ON [type] = 'P'
            AND dgt.number <= ofst.exponent;

/*  Test the cases in table example */
SELECT  /*  1.- Get the base 26 digit and then shift it to align it to 8 bit boundaries
            2.- Sum the resulting values
            3.- Bias the value with a reference that represent the string 'AAAAAAAA'
            4.- Take the required chars */
        ref.[row_number],
        REVERSE(SUBSTRING(REVERSE(CAST(SUM((((ref.[row_number] - ofst.offset) / ofst.divisor) % 26) * ofst.shifts) +
            CAST(CAST('AAAAAAAA' AS BINARY(8)) AS BIGINT) AS BINARY(8))),
            1, MAX(ofst.chars))) AS string
FROM    (
            VALUES(1),(2),(25),(26),(27),(28),(51),(52),(53),(54),
            (18278),(18279),(475253),(475254),(475255),
            (100000000), (CAST(217180147158 AS BIGINT))
        ) ref([row_number])
        LEFT JOIN #ExponentsLookup ofst
            ON ofst.offset <= ref.[row_number]
            AND ofst.offset_end > ref.[row_number]
GROUP BY
        ref.[row_number]
ORDER BY
        ref.[row_number];

/*  Test with huge set  */
WITH numbers AS (
    SELECT  TOP(100000000)
            ROW_NUMBER() OVER(
                ORDER BY x1.number
            ) AS [row_number]
    FROM    master.dbo.spt_values x1
            CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 676) x2
            CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 676) x3
    WHERE   x1.number < 219
)
SELECT  /*  1.- Get the base 26 digit and then shift it to align it to 8 bit boundaries
            2.- Sum the resulting values
            3.- Bias the value with a reference that represent the string 'AAAAAAAA'
            4.- Take the required chars */
        ref.[row_number],
        REVERSE(SUBSTRING(REVERSE(CAST(SUM((((ref.[row_number] - ofst.offset) / ofst.divisor) % 26) * ofst.shifts) +
            CAST(CAST('AAAAAAAA' AS BINARY(8)) AS BIGINT) AS BINARY(8))),
            1, MAX(ofst.chars))) AS string
FROM    numbers ref
        LEFT JOIN #ExponentsLookup ofst
            ON ofst.offset <= ref.[row_number]
            AND ofst.offset_end > ref.[row_number]
GROUP BY
        ref.[row_number]
ORDER BY
        ref.[row_number]
OPTION (QUERYTRACEON 8690);

ここでのコツは、異なる順列が始まる場所を事前に計算することです。

  1. 単一の文字を出力する必要がある場合、26 ^ 0から始まる26 ^ 1個の順列があります。
  2. 2文字を出力する必要がある場合、26 ^ 0 + 26 ^ 1で始まる26 ^ 2個の順列があります。
  3. 3文字を出力する必要がある場合、26 ^ 0 + 26 ^ 1 + 26 ^ 2で始まる26 ^ 3の順列があります
  4. n文字分繰り返す

使用されるもう1つのトリックは、連結を試みるのではなく、単にsumを使用して正しい値を取得することです。これを実現するために、単純に数字を基数26から基数256にオフセットし、各桁に「A」のASCII値を追加します。したがって、探している文字列のバイナリ表現を取得します。その後、いくつかの文字列操作によりプロセスが完了します。


-1

さて、ここに私の最新のスクリプトがあります。

ループなし、再帰なし。

6文字でのみ動作します

最大の欠点は、1,00,00,000で約22分かかることです

今回は私のスクリプトは非常に短いです。

SET NoCount on

declare @z int=26
declare @start int=@z+1 
declare @MaxLimit int=10000000

SELECT TOP (@MaxLimit) IDENTITY(int,1,1) AS N
    INTO NumbersTest1
    FROM     master.dbo.spt_values x1   
   CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 500) x2
            CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 500) x3
    WHERE   x1.number < 219
ALTER TABLE NumbersTest1 ADD CONSTRAINT PK_NumbersTest1 PRIMARY KEY CLUSTERED (N)


select N, strCol from NumbersTest1
cross apply
(
select 
case when IntCol6>0 then  char((IntCol6%@z)+64) else '' end 
+case when IntCol5=0 then 'Z' else isnull(char(IntCol5+64),'') end 
+case when IntCol4=0 then 'Z' else isnull(char(IntCol4+64),'') end 
+case when IntCol3=0 then 'Z' else isnull(char(IntCol3+64),'') end 
+case when IntCol2=0 then 'Z' else isnull(char(IntCol2+64),'') end 
+case when IntCol1=0 then 'Z' else isnull(char(IntCol1+64),'') end strCol
from
(
select  IntCol1,IntCol2,IntCol3,IntCol4
,case when IntCol5>0 then  IntCol5%@z else null end IntCol5

,case when IntCol5/@z>0 and  IntCol5%@z=0 then  IntCol5/@z-1 
when IntCol5/@z>0 then IntCol5/@z
else null end IntCol6
from
(
select IntCol1,IntCol2,IntCol3
,case when IntCol4>0 then  IntCol4%@z else null end IntCol4

,case when IntCol4/@z>0 and  IntCol4%@z=0 then  IntCol4/@z-1 
when IntCol4/@z>0 then IntCol4/@z
else null end IntCol5
from
(
select IntCol1,IntCol2
,case when IntCol3>0 then  IntCol3%@z else null end IntCol3
,case when IntCol3/@z>0 and  IntCol3%@z=0 then  IntCol3/@z-1 
when IntCol3/@z>0 then IntCol3/@z
else null end IntCol4

from
(
select IntCol1
,case when IntCol2>0 then  IntCol2%@z else null end IntCol2
,case when IntCol2/@z>0 and  IntCol2%@z=0 then  IntCol2/@z-1 
when IntCol2/@z>0 then IntCol2/@z
else null end IntCol3

from
(
select case when N>0 then N%@z else null end IntCol1
,case when N%@z=0 and  (N/@z)>1 then (N/@z)-1 else  (N/@z) end IntCol2 

)Lv2
)Lv3
)Lv4
)Lv5
)LV6

)ca

DROP TABLE NumbersTest1

派生テーブルは、400000文字を超えるコードである単一の計算スカラーに変換されるようです。その計算には多くのオーバーヘッドがあると思います。次のようなものを試してください:dbfiddle.uk/… そのコンポーネントを自由に回答に統合してください。
ジョーオブビッシュ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.