web-dev-qa-db-ja.com

大きなテーブルでの結合の最適化

2億5000万件のレコードを持つテーブルにアクセスしているクエリから、さらにパフォーマンスを引き出そうとしています。実際の(推定ではない)実行プランを読んだところ、最初のボトルネックは次のようなクエリです。

_select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
where
    a.added between @start and @end;
_

関連するテーブルとインデックスの定義については、下を参照してください。

実行計画は、ネストされたループが#smalltableで使用されていること、およびhugetableに対するインデックススキャンが480回(#smalltableの各行に対して)実行されていることを示しています。これは私には逆に思えるので、代わりにマージ結合を使用するように強制しようとしました:

_select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a with(index = ix_hugetable)
    inner merge join
    #smalltable b with(index(1)) on a.fk = b.pk
where
    a.added between @start and @end;
_

問題のインデックス(完全な定義については以下を参照)は、列fk(結合述語)、added(where句で使用)&idをカバーします=(役に立たない)昇順で、を含みます。

ただし、これを実行すると、クエリが2分30秒から9分以上に吹き飛ばされます。ヒントによって、各テーブルに対して1回のパスのみを実行するより効率的な結合が強制されることを期待していましたが、明らかにそうではありませんでした。

どんなガイダンスも大歓迎です。必要に応じて追加情報が提供されます。

アップデート(2011/06/02)

テーブルのインデックスを再編成したことで、大幅なパフォーマンスの向上が見られましたが、巨大なテーブルのデータを集計することに関して、新たな障害に直面しました。結果は月ごとの要約で、現在は次のようになります。

_select
    b.stuff,
    datediff(month, 0, a.added),
    count(a.value),
    sum(case when a.value > 0 else 1 end) -- this triples the running time!
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
group by
    b.stuff,
    datediff(month, 0, a.added);
_

現在、hugetableにはクラスター化インデックスpk_hugetable (added, fk)(主キー)と、逆方向の非クラスター化インデックスix_hugetable (fk, added)があります。

上記の4列目がない場合、オプティマイザーは以前と同じようにネストされたループ結合を使用し、#smalltableを外部入力として使用し、非クラスター化インデックスシークを内部ループとして使用します(再度480回実行)。気になるのは、推定行数(12,958.4)と実際の行数(74,668,468)の違いです。これらのシークの相対コストは45%です。ただし、実行時間は1分未満です。

4列目では、実行時間は4分に急上昇します。今回(2実行)は同じ相対コスト(45%)でクラスター化インデックスを検索し、ハッシュ一致(30%)を介して集計し、#smalltable(0%)でハッシュ結合を実行します。

私の次の行動方針がわかりません。私の懸念は、日付範囲検索も結合述部も保証されていないこと、または結果セットを大幅に削減する可能性が高いことです。ほとんどの場合、日付範囲はレコードの10〜15%だけをトリミングし、fkの内部結合は20〜30%を除外する可能性があります。


ウィルAの要求に応じて、_sp_spaceused_の結果:

_name      | rows      | reserved    | data        | index_size  | unused
hugetable | 261774373 | 93552920 KB | 18373816 KB | 75167432 KB | 11672 KB
_

#smalltableは次のように定義されます:

_create table #endpoints (
    pk uniqueidentifier primary key clustered,
    stuff varchar(6) null
);
_

一方dbo.hugetableは次のように定義されます:

_create table dbo.hugetable (
    id uniqueidentifier not null,
    fk uniqueidentifier not null,
    added datetime not null,
    value decimal(13, 3) not null,

    constraint pk_hugetable primary key clustered (
        fk asc,
        added asc,
        id asc
    )
    with (
        pad_index = off, statistics_norecompute = off,
        ignore_dup_key = off, allow_row_locks = on,
        allow_page_locks = on
    )
    on [primary]
)
on [primary];
_

次のインデックスが定義されています:

_create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc, id asc
) include(value) with (
    pad_index = off, statistics_norecompute = off,
    sort_in_tempdb = off, ignore_dup_key = off,
    drop_existing = off, online = off,
    allow_row_locks = on, allow_page_locks = on
)
on [primary];
_

idフィールドは冗長であり、allすべてのテーブルにGUIDがあり、例外はないことを主張した以前のDBAによるアーティファクト。

10
Quick Joe Smith

あなたの_ix_hugetable_は次の理由でまったく役に立たないように見えます:

  • it isクラスタ化インデックス(PK)
  • クラスター化インデックスはすべての非キー列をINCLUDEするため、INCLUDEは違いを生じません(最低リーフの非キー値= INCLUDEd =クラスター化インデックスとは)

さらに:-追加またはfkを最初にする必要があります-IDを最初にする=あまり使用しない

クラスタ化されたキーを_(added, fk, id)_に変更して、_ix_hugetable_を削除してみてください。すでに_(fk, added, id)_を試しました。他に何もなければ、多くのディスク容量とインデックスのメンテナンスを節約できます

別のオプションは、テーブルの順序と方法を使用してJOIN/INDEXヒントを使用せずにFORCE ORDERヒントを試すことです。オプティマイザーのオプションを削除するため、個人的にJOIN/INDEXヒントを使用しないようにします。何年も前に、SQLグルを使用したセミナーで、巨大なテーブルに小さなテーブルを結合する場合にFORCE ORDERヒントが役立つと言われました:7年後のYMMV ...

ああ、そしてDBAがどこに住んでいるか教えてください。パーカッションの調整を手配できるようにします。

6月2日更新後の編集

4番目の列は非クラスター化インデックスの一部ではないため、クラスター化インデックスを使用します。

NCインデックスを変更して値列を含めるようにして、クラスター化インデックスの値列にアクセスする必要がないようにします。

_create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc
) include(value)
_

注:値がnullにできない場合、意味的にはCOUNT(*)と同じです。ただし、SUMの場合は存在ではなく実際の値が必要です。

例として、COUNT(value)COUNT(DISTINCT value)に変更した場合なしインデックスを変更すると、値を値ではなく値として処理する必要があるため、クエリが再度中断されます。存在として。

クエリには3つの列が必要です:追加、fk、値。最初の2つはフィルタリング/結合され、キー列も同様です。値は使用されるだけなので、含めることができます。カバリングインデックスの古典的な使用。

5
gbn

hugetable列だけにaddedのインデックスを定義します。

DBは、マルチパート(マルチカラム)インデックスを使用します。これは、カラムリストの左から数えて、カラムリストの右端のみを使用します。クエリは最初のクエリのwhere句でfkを指定していないため、インデックスは無視されます。

2
Bohemian

実行計画は、ネストされたループが#smalltableで使用されていること、およびhugetableに対するインデックススキャンが480回(#smalltableの各行に対して)実行されていることを示しています。

これは、ループジョインが正しい選択であると想定して、クエリオプティマイザーが使用すると予想される順序です。別の方法として、2億5000万回ループし、毎回#tempテーブルへのルックアップを実行します。これには数時間/日かかる可能性があります。

MERGE結合で使用するように強制しているインデックスはかなり 2億5000万行* '各行のサイズ'-小さくない最小数GB 。 sp_spaceusedの出力「数GB」から判断すると、控えめな表現かもしれません。MERGE結合では、非常にI/Oが集中するインデックスをトロールする必要があります。

2
Will A

インデックスが正しくありません。 indexes dos and donts を参照してください。

現状では、唯一の有用なインデックスは、小さなテーブルの主キーのものです。したがって、唯一の合理的な計画は、小さなテーブルのシーケンススキャンを行い、混乱を巨大なテーブルと入れ子にしてループすることです。

hugetable(added, fk)にクラスター化インデックスを追加してみてください。これにより、プランナーは巨大なテーブルから該当する行を探し出し、ネストしたループまたは小さなテーブルにマージ結合する必要があります。

1