web-dev-qa-db-ja.com

このトリッキーなSQL自己結合を最適化する方法は?

テーブルで複雑な自己結合を実行したい。これは理論的には非常に効率的に実行できることを知っていますが(以下を参照)、SQL(Microsoft SQL Server)でこれを実行するのに問題があります。

私の質問は:

SQLでこれを効率的に行うにはどうすればよいですか?最適なソリューション、または同様の高速なものを推測できるようにするには、どのような情報を提供する必要がありますか?

入力:

イベントの表があります。各イベントは特定のアイテムに属し、2つのタイプのいずれかを持ちます。このテーブルは中間テーブルなので、自由にこのテーブルを好きなように使用し、任意のインデックスを作成します。また、オフライン処理にのみ使用されるため、新しいデータは後で追加されません。

テーブルには何億もの行があります。 type = 0のエントリとtype = 1のエントリはほぼ同じ頻度で表示され、入力データは特定のルールに従って作成されているため、次のことが当てはまると見なされるため、ほぼ均等に分散されます。データの場合:_type=0_のイベントが発生するたびに、関係するアイテムのカウンターが増加し、_type=1_のイベントが発生するたびに、再び減少します。カウンターは常に0から3(両端を含む)の間にあります。

現在、表は次のようになっていますが、変更を提案することもできます。

_select
    a.item
    ,case when a.<some_condition> then 1 else 0 end as event_type
    ,row_number() over(partition by a.item order by a.date asc) as sequence_id -- this makes the order clearer and deals with duplicate dates in a manner that is acceptable for these purposes
    ,<...> as counter_after_event -- this lies in [1;3] if event_type=0, and in [0;2] if event_type=1
from <original_source_table> a;
_

私はいくつかのタイプのインデックスとそれらのインデックスの列のいくつかの順序を試しましたが、以下のタスクは速くなりません:

タスク:

「type = 0の各イベントについて、同じ項目に関係するtype = 1の時系列的に次のイベントを見つけます。(_item, sequence_id_for_type_0, sequence_id_for_type_1_)のタプルを取得します。」

これを取得するクエリは次のようになります。

_select
    a.item
    ,a.sequence_id as sequence_id_for_type_0
    ,min(b.sequence_id) as sequence_id_for_type_1
from <input_table> a
inner join <input_table> b
    on a.item = b.item
    and a.sequence_id < b.sequence_id
    and a.event_type = 0
    and b.event_type = 1
group by a.item, a.sequence_id;
_

各アイテムは個別に検討できます。単一のアイテムの場合、sequence_idで並べられたinput_tableのエントリは、次のようになります。エントリが角括弧で囲まれた後のカウンターの値:

sequence_id = 1、type = 0(1)

sequence_id = 2、type = 1(0)

sequence_id = 3、type = 0(2)-一度に複数のsequence_id = 1によるカウンターの増加/減少が可能です

sequence_id = 4、type = 0(3)

sequence_id = 5、type = 1(1)-カウンタが最大値に達したため、このエントリはtype = 1である必要があります

sequence_id = 6、type = 1(0)

sequence_id = 7、type = 0(1)

この例では、このアイテムに対して次の出力ペアが必要です。

sequence_id_for_type_0 = 1、sequence_id_for_type_1 = 2

sequence_id_for_type_0 = 3、sequence_id_for_type_1 = 5

sequence_id_for_type_0 = 4、sequence_id_for_type_1 = 5

理論的な解決策:

理論的には、この問題はすぐに解決できます。input_table(item, sequence_id, event_type)にBツリーインデックスを作成します。木を横断します。 _event_type=0_が含まれるすべてのノードについて、_event_type=1_が含まれる次のノードが見つかるまで先読みしますが、アイテムが変更された場合は先読みをキャンセルします。

先読みで一致が見つかった場合は、それから(_item, sequence_id_for_type_0, sequence_id_for_type_1_)タプルを作成できます。これにより、O(n*log(n)*k)の理論上のランタイムが得られます。ここで、kは必要な先読みの最大数です。これは、上記のカウンターの説明のために非常に制限されています(最大で次のtype = 1が発生する前に、アイテムに対して4つの連続するtype = 0イベントが発生します。

残念ながら、SQLにこの後者の事実を伝える方法がわかりません。kは非常に小さいため、これが最適なソリューションです。

また、インデックスが複数のマシン間で分割されている場合、必要な最大通信は、隣接するマシンのペアごとに1エントリです。つまり、最後の先読みがインデックスの独自の部分を超えたことを隣接マシンに通知します。

代わりにSQLが行うこと:

インデックスの設定方法に関係なく、基本的なソリューションアプローチは常に同じです。

Input_tableは、並行して2回スキャンされます。インデックスに応じて、代わりにSQLに1つのindex_scanと1つのindex_seekを実行させることができますが、インデックスのツリー構造で先読みを使用するだけでよいことを理解することはできません(または少なくともSQLがそれを認識している場合)これを行うことができる場合、Microsoft SQL Serverが生成するクエリプランはこれを示しません)。

_group by_は、他のグループと同じ素朴な方法で処理されます。ハッシュテーブルが作成されます(キーとして(a.item、a.sequence_id)が使用されます)。これにより、クエリに許容できないオーバーヘッドが追加されます。 速度を上げる方法はありますか?

追加クレジット:

必須ではないが役立つであろうこのタスクの高度なバージョンがあります:上記のカウンターを明示的にしてください。 (_item, sequence_id_for_type_0, sequence_id_for_type_1, counter_value_at_type_1_entry_)のタプルを知りたいと思います。

これにより、理論的にはタスクの難易度が最大3倍に増加します(カウンターは0、1、または2のいずれかにしかデクリメントできないため)。理論的な解決策はほとんど変わりません。ただし、SQLクエリの_group by_句には、テーブルbのエントリが含まれています。

_select
    a.item
    ,a.sequence_id as sequence_id_for_type_0
    ,min(b.sequence_id) as sequence_id_for_type_1
    ,b.counter_after_event as counter_after_type_1_event
from <input_table> a
inner join <input_table> b
    on a.item = b.item
    and a.sequence_id < b.sequence_id
    and a.event_type = 0
    and b.event_type = 1
group by a.item, a.sequence_id, b.counter_after_event;
_

更新

これをlead()関数で解決してみましたが、次のような構成で元の問題を解決できます。

_-- only three values need to be checked, because the counter ensures that we don't have to look at more than 3 following items before getting a type=1 event
,case when lead(a.event_type, 1, -1) over(partition by a.item order by a.sequence_id) = 1
        then lead(a.sequence_id, 1, null) over(partition by a.item order by a.sequence_id)
    when lead(a.event_type, 2, -1) over(partition by a.item order by a.sequence_id) = 1
        then lead(a.sequence_id, 2, null) over(partition by a.item order by a.sequence_id)
    when lead(a.event_type, 3, -1) over(partition by a.item order by a.sequence_id) = 1
        then lead(a.sequence_id, 3, null) over(partition by a.item order by a.sequence_id)
    else null
    end as sequence_id_for_type_1
_

残念ながら、現在このタスクは延長されており、「追加クレジット」タスクの解決策を探しています。特に、counter_after_type_1_event = 0を使用して最初のイベントを見つけることを最も重視しています。このタスクでは、特定のカウンター値に到達するtype = 1を取得するまでイベント数に上限を設定できないため、上記のトリックを使用できません。

4
Florian Dietz

[〜#〜] lead [〜#〜] および [〜#〜] lag [〜#〜]Analytic Functions の使用を検討する必要がありますソートのための適切なインデックス。それらの1つの自己結合クエリを変更しました。経過時間が10分の1に短縮されました。

5
Dmitriy Dokshin

私は実際には例には従いませんが、以下に基づいています。

Type = 0のイベントが発生するたびに、関係するアイテムのカウンターが増加し、type = 1のイベントが発生するたびに、再び減少します。カウンターは常に0と3の間にある必要があります。

Event_typeを-1と1に変更した方が簡単です

select item, min(seq) 
from ( select a.item
            , row_number()           over(partition by a.item order by a.date asc) as seq
            , sum(-2*event_type + 1) over(partition by a.item order by a.date asc) as counter 
         from <original_source_table> a 
     ) tt 
where counter < 0 or counter > 3 
group by item;

上記の違反を見つけます
あなたが良いものだけを欲しければ、フロップをし、グループをドロップしてください。
質問から、このカウンターで何をしたいか明確ではありません
そして、あなたはタスクのカウンターに対処していないようです

タスクは

select *
  from ( select a.item
              , row_number()           over(partition by a.item order by a.date asc) as seq
              , sum(-2*event_type + 1) over(partition by a.item order by a.date asc) as counter
              , lead(event_type)       over(partition by a.item order by a.date asc) as lead_event_type 
           from <original_source_table> a 
       ) tt 
 where event_type 0 lead_event_type = 1;

理論的には、なぜ各行を先読みするのですか?
行を読み取り、バッファに0を保持するだけです
1に到達したら、1つの情報でバッファを更新し、バッファを書き出します
新しいアイテムに到達したら、バッファを破棄します
これは簡単なシングルパス操作です
これは、CLRを使用するC#の12行すべてです。

1
paparazzo