特定の数式に基づいて欠落している最小の要素を特定する


8

数百万行のテーブルから欠落している要素を見つけることができる必要があり、BINARY(64)列の主キーがあります(これは計算に使用する入力値です)。これらの値はほとんど順番に挿入されますが、削除された以前の値を再利用したい場合があります。削除されたレコードをIsDeleted列で変更することはできません。現在の行の前に何百万もの値がある行が挿入される場合があるためです。つまり、サンプルデータは次のようになります。

KeyCol : BINARY(64)
0x..000000000001
0x..000000000002
0x..FFFFFFFFFFFF

したがって、0x000000000002との間にすべての欠損値を挿入すること0xFFFFFFFFFFFFは実行不可能であり、使用される時間とスペースの量は望ましくありません。基本的に、アルゴリズムを実行する0x000000000003と、最初の開始点であるが返されることが期待されます。

私はC#でバイナリ検索アルゴリズムを考え出しました。これは、位置の各値についてデータベースにクエリを実行し、iその値が予期されているかどうかをテストします。コンテキストについては、私のひどいアルゴリズム:https : //codereview.stackexchange.com/questions/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula

このアルゴリズムは、たとえば、100,000,000アイテムのテーブルで26-27 SQLクエリを実行します。(それほど多くはないように見えますが、非常に頻繁に発生します。)現在、このテーブルには約50,000,000行あり、パフォーマンスが顕著になりつつあります。

私の最初の代替の考えは、これをストアドプロシージャに変換することですが、それには独自のハードルがあります。(私はBINARY(64) + BINARY(64)アルゴリズムや他の多くのものを書かなければなりません。)これは苦痛ですが、実行不可能ではありません。また、に基づく変換アルゴリズムの実装を検討しましたROW_NUMBERが、これについては非常に悪い直感があります。(A BIGINTはこれらの値に対して十分な大きさではありません。)

私はのためにアップしてる他の Iのように、提案本当にこれは、可能な限り迅速にする必要があります。C#クエリで選択された唯一の列に値するのはであり、他のKeyColはこの部分には関係ありません。


また、価値のあるものとして、適切なレコードをフェッチする現在のクエリは次の行に沿っています。

SELECT [KeyCol]
  FROM [Table]
  ORDER BY [KeyCol] ASC
  OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY

<VALUE>アルゴリズムによって提供されるインデックスはどこにありますか。私もまだBIGINT問題を抱えていませんが、問題はOFFSETありません。(現在、50,000,000行しかないということは、その値を超えるインデックスを要求しないことを意味しますが、ある時点でBIGINT範囲を超えることになります。)

いくつかの追加データ:

  • 削除から、gap:sequential比率は約1:20です。
  • テーブルの最後の35,000行には、> BIGINTの最大値があります。

もう少し明確化... 1探している)とは対照的に、なぜあなたは「最小の」利用可能バイナリが必要なのか任意の利用可能バイナリ?2)今後、特にこのルックアップを非常に頻繁に行う必要があるという観点deleteから、現在利用可能なバイナリを別のテーブル(たとえばcreate table available_for_reuse(id binary64))にダンプするトリガーをテーブルに置く可能性はありますか?
markp-fuso 2017

利用可能な最小値は、それを「好み」を持っている@markp、URL短縮サービスと同様にそれを考えると、次たくないもはや誰かが手動のようなものを指定することができますので、値をmynameisebrownあなたが得るだろう意味しているがmynameisebrowo、これを使用abc利用可能な場合は必要ありません。
Der Kommissar 2017

クエリはどのようなものをselect t1.keycol+1 as aa from t as t1 where not exists (select 1 from t as t2 where t2.keycol = t1.keycol+1) order by keycol fetch first 1 rows only提供しますか?
Lennart 2017

@Lennart必要なものではありません。使用する必要SELECT TOP 1 ([T1].[KeyCol] + 1) AS [AA] FROM [SearchTestTableProper] AS [T1] WHERE NOT EXISTS (SELECT 1 FROM [SearchTestTableProper] AS [T2] WHERE [T2].[KeyCol] = [T1].[KeyCol] + 1) ORDER BY [KeyCol]があり、常に戻ります1
Der Kommissar 2017

それが何らかのキャストエラーであるのかと思いますが、1を返すべきではありません。selectt1.keycol from ... returnは何ですか?
Lennart 2017

回答:


6

ジョーは、私が1時間タイプしたばかりのほとんどのポイントにすでに当たっています。

  • 非常に疑わしいKeyCol値< bigintmax(9.2e18)を使い尽くすbigintことになるので、検索を制限する限り、(必要に応じて)変換を(必要に応じて)問題にしないでください。KeyCol <= 0x00..007FFFFFFFFFFFFFFF
  • 私は常に「効率的に」ギャップを見つけるクエリを考えることができません。幸運にも検索の最初の方でギャップを見つけるかもしれませんし、検索にかなりの方法でギャップを見つけるために大金を払うこともできます
  • クエリを並列化する方法について簡単に考えましたが、その考えをすぐに破棄しました(DBAとして、プロセスが100%のCPU使用率でデータサーバーを定期的に停止していることを知りたくありません...特に複数の同時に実行中のこのコピー); noooo ...並列化は問題外です

じゃあ何をすればいいの?

(繰り返しのある、CPU集約型の総当たり)検索のアイデアを1分間保留して、全体像を見てみましょう。

  • 平均して、この検索の1つのインスタンスは、単一の値を見つけるためだけに数百万のインデックスキーをスキャンする必要があります(そして、かなりのCPU、DBキャッシュのスラッシング、および回転する砂時計を見るユーザーが必要です)。
  • cpu-usage / cache-thrashing / spinning-hour-glassを... 1日に何回の検索が予想されるかを掛けます。
  • 一般的に言って、この検索のインスタンスは同じ(数百万の)インデックスキーのセットをスキャンする必要があることに注意してください。ことだLOTな最小限の利益のために繰り返し活動の

私が提案したいのは、データモデルへのいくつかの追加です...

  • 一連の「使用可能」KeyCol値を追跡する新しいテーブル。例:available_for_use(KeyCol binary(64) not null primary key)
  • このテーブルで維持するレコードの数は、たとえば、おそらく1か月分の活動に十分かどうかを判断する次第です。
  • テーブルを定期的(毎週?)に新しいKeyCol値のバッチで「追加」することができます(おそらく「追加」のストアドプロシージャを作成しますか?)[たとえば、Joeのselect/top/row_number()クエリを更新してtop 100000]
  • 値が少なくなり始めavailable_for_use た場合に備えて、使用可能なエントリの数を追跡する監視プロセスを設定できます
  • メインテーブルから行が削除されるたびに、削除されたKeyCol値を新しいテーブルに配置する> main_table <の新しい(または変更された)DELETEトリガーavailable_for_use
  • KeyCol列の更新を許可する場合、> main_table <で新規/変更されたUPDATEトリガーを使用して、新しいテーブルもavailable_for_use更新されたままにします
  • 新しいKeyCol値を「検索」するときが来たらselect min(KeyCol) from available_for_use(明らかに、a)同時実行の問題をコード化する必要があります-プロセスの2つのコピーが同じものを取得しないようにしてくださいmin(KeyCol)b)あなた' min(KeyCol)テーブルから削除する必要があります。これは、おそらくストアドプロシージャとして比較的簡単にコーディングでき、必要に応じて別のQ&Aで対処できます)
  • 最悪のシナリオでは、select min(KeyCol)プロセスで使用可能な行が見つからない場合、「トップオフ」プロシージャを開始して、行の新しいバッチを生成できます。

データモデルへのこれらの提案された変更により:

  • 過剰なCPUサイクルの多くを排除します[DBAに感謝します]
  • これらの反復的なインデックススキャンとキャッシュスラッシングをすべて排除します[DBAに感謝します]
  • ユーザーは、回転する砂時計を見る必要がなくなりました(ただし、机から離れるという言い訳を失うことを好まないかもしれません)。
  • available_for_useテーブルのサイズを監視して、新しい値が不足しないようにする方法はたくさんあります

はい、提案されたavailable_for_useテーブルは、事前に生成された「次のキー」値のテーブルにすぎません。はい、「次の」値を取得する際に競合が発生する可能性がありますが、競合はa)適切なテーブル/インデックス/クエリの設計を通じて簡単に対処され、b)オーバーヘッドと比較してマイナー/短命になります/繰り返されるブルートフォースのインデックス検索の現在のアイデアによる遅延。


これは実際に私がチャットで考えていたものに似ています。ジョーのクエリは比較的速く実行されるため、おそらく15〜20分ごとに実行されると思います(テストデータが不自然なライブサーバーでは、最悪のケースは4.5秒でした。 0.25秒)、私は1日分のキーを取得でき、nキー(少なくとも10または20 )を取得できます。ここで答えを本当に感謝します、あなたは考えを書面に入れます!:)
Der Kommissar 2017

ああ、利用可能なKeyCol値の中間キャッシュを提供できるアプリケーション/ミドルウェアサーバーがある場合...そう、それも機能します:-)データモデルの変更の必要性を明らかに排除しますeh
markp-fuso

正確には、Webアプリケーション自体に静的キャッシュを構築することも考えていますが、唯一の問題は分散されていることです(そのため、サーバー間でキャッシュを同期する必要があります)。つまり、SQLまたはミドルウェアの実装は多くなります。優先。:)
Der Kommissar 2017

hmmmm ...分散KeyColマネージャー、およびアプリの2つ(またはそれ以上)の同時インスタンスが同じKeyCol値を使用しようとした場合の潜在的なPK違反のコーディングの必要性... 残念 ...単一のミドルウェアサーバーまたはdb中心のソリューション
markp-fuso 2017

8

この質問にはいくつかの課題があります。SQL Serverのインデックスは、それぞれ数個の論理読み取りで次のことを非常に効率的に実行できます。

  • 行が存在することを確認してください
  • 行が存在しないことを確認してください
  • ある時点で始まる次の行を見つける
  • ある時点で始まる前の行を見つける

ただし、これらを使用してインデックスのN番目の行を見つけることはできません。そのためには、テーブルとして格納されている独自のインデックスをロールするか、インデックスの最初のN行をスキャンする必要があります。C#コードは、配列のN番目の要素を効率的に見つけることができるという事実に大きく依存していますが、ここではそれを行うことはできません。データモデルを変更しないと、アルゴリズムはT-SQLには使用できないと思います。

2番目の課題は、BINARYデータ型の制限に関するものです。私の知る限り、通常の方法で加算、減算、または除算を実行することはできません。をに変換できますBINARY(64)が、BIGINT変換エラーはスローされませんが、動作は定義されていません

データ型とバイナリデータ型の間の変換は、SQL Serverのバージョン間で同じであるとは限りません。

さらに、変換エラーがないことは、ここでは多少問題になります。可能な最大BIGINT値よりも大きいものはすべて変換できますが、間違った結果が得られます。

現在、9223372036854775807より大きい値があることは事実です。ただし、常に1から始めて最小の最小値を検索している場合、テーブルに9223372036854775807を超える行がない限り、それらの大きな値は関係ありません。その時点でのテーブルが約2000エクサバイトになるため、これはありそうにありません。質問に答えるために、非常に大きな値を検索する必要がないと仮定します。また、データ型の変換は避けられないようです。

テストデータについては、5,000万個の連続した整数に相当する整数を、20個の値ごとに1つの値のギャップを持つさらに5,000万個の整数とともにテーブルに挿入しました。また、signedに適切に適合しない単一の値を挿入しましたBIGINT

CREATE TABLE dbo.BINARY_PROBLEMS (
    KeyCol BINARY(64) NOT NULL
);

INSERT INTO dbo.BINARY_PROBLEMS WITH (TABLOCK)
SELECT CAST(SUM(OFFSET) OVER (ORDER BY (SELECT NULL) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BINARY(64))
FROM
(
    SELECT 1 + CASE WHEN t.RN > 50000000 THEN
        CASE WHEN ABS(CHECKSUM(NewId()) % 20)  = 10 THEN 1 ELSE 0 END
    ELSE 0 END OFFSET
    FROM
    (
        SELECT TOP (100000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
        FROM master..spt_values t1
        CROSS JOIN master..spt_values t2
        CROSS JOIN master..spt_values t3
    ) t
) tt
OPTION (MAXDOP 1);

CREATE UNIQUE CLUSTERED INDEX CI_BINARY_PROBLEMS ON dbo.BINARY_PROBLEMS (KeyCol);

-- add a value too large for BIGINT
INSERT INTO dbo.BINARY_PROBLEMS
SELECT CAST(0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000 AS BINARY(64));

このコードを私のマシンで実行するには数分かかりました。表の前半には、パフォーマンスの悪い例を表すギャップがないようにしました。問題を解決するために使用したコードは、インデックスを順番にスキャンするため、最初のギャップがテーブルの早い段階であると、非常に速く終了します。その前に、データが正しいことを確認しましょう。

SELECT TOP (2) KeyColBigInt
FROM
(
    SELECT KeyCol
    , CAST(KeyCol AS BIGINT) KeyColBigInt
    FROM dbo.BINARY_PROBLEMS
) t
ORDER By KeyCol DESC;

結果は、変換後の最大値BIGINTが102500672であることを示唆しています。

╔══════════════════════╗
     KeyColBigInt     
╠══════════════════════╣
 -9223372036854775808 
            102500672 
╚══════════════════════╝

予想どおりBIGINTに適合する値を持つ1億行があります。

SELECT COUNT(*) 
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF;

この問題への1つのアプローチは、インデックスを順番にスキャンし、行の値が期待ROW_NUMBER()値と一致しなくなったらすぐに終了することです。最初の行を取得するためにテーブル全体をスキャンする必要はありません。最初のギャップまでの行のみをスキャンする必要があります。クエリプランを取得する可能性が高いコードを記述する方法の1つを次に示します。

SELECT TOP (1) KeyCol
FROM
(
    SELECT KeyCol
    , CAST(KeyCol AS BIGINT) KeyColBigInt
    , ROW_NUMBER() OVER (ORDER BY KeyCol) RN
    FROM dbo.BINARY_PROBLEMS
    WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF
) t
WHERE KeyColBigInt <> RN
ORDER BY KeyCol;

この回答に収まらない理由により、このクエリはSQL Serverによって逐次実行されることが多く、SQL Serverは最初の一致が見つかるまでにスキャンする必要がある行数を過小評価することがよくあります。私のマシンでは、SQL Serverは最初の一致を見つける前にインデックスから50000022行をスキャンします。クエリの実行には11秒かかります。これはギャップを超えた最初の値を返すことに注意してください。正確にどの行が必要かは明確ではありませんが、多くの問題なく、ニーズに合わせてクエリを変更できるはずです。ここでは何の計画は次のようになります。

シリアルプラン

私の他の唯一のアイデアは、クエリに並列処理を使用するようにSQL Serverをいじめることでした。CPUが4つあるので、データを4つの範囲に分割し、それらの範囲でシークを行います。各CPUには範囲が割り当てられます。範囲を計算するために、最大値を取得し、データが均等に分散されていると想定しました。よりスマートになりたい場合は、列の値のサンプリングされた統計ヒストグラムを見て、そのように範囲を構築できます。以下のコードは、トレースフラグ8649を含む、プロダクションにとって安全ではない多くの文書化されていないトリックに依存しています。

SELECT TOP 1 ca.KeyCol
FROM (
    SELECT 1 bucket_min_value, 25625168 bucket_max_value
    UNION ALL
    SELECT 25625169, 51250336
    UNION ALL
    SELECT 51250337, 76875504
    UNION ALL
    SELECT 76875505, 102500672
) buckets
CROSS APPLY (
    SELECT TOP 1 t.KeyCol
    FROM
    (
        SELECT KeyCol
        , CAST(KeyCol AS BIGINT) KeyColBigInt
        , buckets.bucket_min_value - 1 + ROW_NUMBER() OVER (ORDER BY KeyCol) RN
        FROM dbo.BINARY_PROBLEMS
        WHERE KeyCol >= CAST(buckets.bucket_min_value AS BINARY(64)) AND KeyCol <=  CAST(buckets.bucket_max_value AS BINARY(64))
    ) t
    WHERE t.KeyColBigInt <> t.RN
    ORDER BY t.KeyCol
) ca
ORDER BY ca.KeyCol
OPTION (QUERYTRACEON 8649);

並列ネストループパターンは次のようになります。

並行計画

全体として、クエリはテーブル内のより多くの行をスキャンするため、以前よりも多くの処理を実行します。ただし、デスクトップでは7秒で実行されます。実際のサーバーでの並列化が向上する可能性があります。こちらが実際のプランへのリンクです。

私は本当にこの問題を解決する良い方法を考えることができません。SQLの外部で計算を行うか、データモデルを変更するのが最善の策です。


「SQLではうまくいかない」という最良の答えがあったとしても、少なくとも次にどこに移動すればよいかがわかります。:)
Der Kommissar 2017

1

これはおそらくうまくいかない答えですが、とにかく追加します。

BINARY(64)は列挙可能ですが、アイテムの後継者を決定するためのサポートは不十分です。BIGINTがドメインに対して小さすぎるように見えるため、SQLサーバーで最大のNUMBER型であると思われるDECIMAL(38,0)の使用を検討する場合があります。

CREATE TABLE SearchTestTableProper
( keycol decimal(38,0) not null primary key );

INSERT INTO SearchTestTableProper (keycol)
VALUES (1),(2),(3),(12);

探している数を作成できるため、最初のギャップを見つけるのは簡単です。

select top 1 t1.keycol+1 
from SearchTestTableProper t1 
where not exists (
    select 1 
    from SearchTestTableProper t2 
    where t2.keycol = t1.keycol + 1
)
order by t1.keycol;

pkインデックスに対するネストされたループ結合は、最初に利用可能なアイテムを見つけるのに十分です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.