web-dev-qa-db-ja.com

JOINを使用してテーブルを効率的に更新する

世帯の詳細が記載されたテーブルと、世帯に関連するすべての人物の詳細が記載されたテーブルがあります。世帯テーブルには、2つの列を使用して定義された主キーがあります-[tempId,n]。 personテーブルには、3つの列を使用して定義された主キーがあります[tempId,n,sporder]

主キーのクラスター化インデックスによって指定された並べ替えを使用して、各世帯[HHID]および各個人[PERID]レコードに一意のIDを生成しました(以下のスニペットはPERIDを生成するためのものです):

 ALTER TABLE dbo.persons
 ADD PERID INT IDENTITY
 CONSTRAINT [UQ dbo.persons HHID] UNIQUE;

今、私の次のステップは、各人を対応する世帯に関連付けることです。 [PERID][HHID]にマップします。 2つのテーブル間の横断歩道は、2つの列に基づいています[tempId,n]。このために、次の内部結合ステートメントがあります。

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n;

私は合計で1928783世帯の記録と5239842人の記録を持っています。現在、実行時間は非常に長くなっています。

今、私の質問:

  1. このクエリをさらに最適化することは可能ですか?より一般的には、結合クエリを最適化するための経験則は何ですか?
  2. より良い実行時間で私が望む結果を達成できる別のクエリ構成はありますか?

SQLPerformance.comへのスクリプト全体に対して、SQL Server 2008によって生成された 実行プランをアップロード を生成しました

8
sriramn

私はテーブルの定義がこれに近いと確信しています:

CREATE TABLE dbo.households
(
    tempId  integer NOT NULL,
    n       integer NOT NULL,
    HHID    integer IDENTITY NOT NULL,

    CONSTRAINT [UQ dbo.households HHID] 
        UNIQUE NONCLUSTERED (HHID),

    CONSTRAINT [PK dbo.households tempId, n]
    PRIMARY KEY CLUSTERED (tempId, n)
);

CREATE TABLE dbo.persons
(
    tempId  integer NOT NULL,
    sporder integer NOT NULL,
    n       integer NOT NULL,
    PERID   integer IDENTITY NOT NULL,
    HHID    integer NOT NULL,

    CONSTRAINT [UQ dbo.persons HHID]
        UNIQUE NONCLUSTERED (PERID),

    CONSTRAINT [PK dbo.persons tempId, n, sporder]
        PRIMARY KEY CLUSTERED (tempId, n, sporder)
);

これらのテーブルやデータの統計情報はありませんが、少なくとも次のようにするとテーブルのカーディナリティが正しく設定されます(ページ数は推測です)。

UPDATE STATISTICS dbo.persons 
WITH 
    ROWCOUNT = 5239842, 
    PAGECOUNT = 100000;

UPDATE STATISTICS dbo.households 
WITH 
    ROWCOUNT = 1928783, 
    PAGECOUNT = 25000;

クエリプラン分析

あなたが今持っているクエリは:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n;

これはかなり非効率的な計画を生成します:

Default plan

この計画の主な問題は、ハッシュ結合とソートです。どちらもメモリの許可が必要です(ハッシュ結合はハッシュテーブルを作成する必要があり、ソートはソートの進行中に行を格納するためのスペースが必要です)。プランエクスプローラーは、このクエリが765 MB許可されたことを示しています

Memory Grant

これは、1つのクエリ専用に使用するかなりのサーバーメモリです。さらに、このメモリの付与は、行数とサイズの見積もりに基づいて実行が開始される前に修正されます。

実行時にメモリが不足していることが判明した場合、ハッシュやソートの少なくとも一部のデータが物理tempdbディスクに書き込まれます。これは「流出」と呼ばれ、処理が非常に遅くなることがあります。これらの流出(SQL Server 2008の場合)は、プロファイラーイベント ハッシュ警告 および ソート警告 を使用して追跡できます。

ハッシュテーブルのビルド入力の見積もりは非常に優れています。

Hash Input

ソート入力の推定値はあまり正確ではありません。

Sort Input

チェックにはプロファイラーを使用する必要がありますが、この場合、並べ替えがtempdbに漏れるのではないかと思います。ハッシュテーブルが流出する可能性もありますが、それほど明確ではありません。

このクエリ用に予約されたメモリは、同時に実行されるため、ハッシュテーブルとソートの間で分割されることに注意してください。 Memory Fractionsプランプロパティは、各操作で使用されることが予想されるメモリ許可の相対的な量を示します。

なぜソートしてハッシュするのですか?

並べ替えはクエリオプティマイザーによって導入され、クラスター化されたキーの順序で行がクラスター化インデックス更新演算子に到達するようにします。これにより、テーブルへの順次アクセスが促進されます。これは、多くの場合、ランダムアクセスよりもはるかに効率的です。

ハッシュ結合は、入力が(とにかく最初の概算で)同様のサイズであるため、それほど明白ではありません。ハッシュ結合は、1つの入力(ハッシュテーブルを構築する入力)が比較的小さい場合に最適です。

この場合、オプティマイザの原価計算モデルは、ハッシュ結合が3つのオプション(ハッシュ、マージ、ネストされたループ)の中で安価であると判断します。

パフォーマンスの向上

コストモデルは常に正しいとは限りません。特にスレッド数が増えると、並列マージ結合のコストを過大評価する傾向があります。クエリヒントを使用してマージ結合を強制できます。

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (MERGE JOIN);

これにより、それほど多くのメモリを必要としないプランが生成されます(マージ結合にはハッシュテーブルが必要ないため)。

Merge Plan

マージ結合では結合キーの順序(tempId、n)しか保持されないが、クラスター化されたキーは(tempId、n、sporder)であるため、問題のあるソートはまだ残っています。マージ結合プランは、ハッシュ結合プランと同じように機能します。

ネストされたループ結合

ネストされたループ結合を試すこともできます:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (LOOP JOIN);

このクエリの計画は次のとおりです。

Serial Nested Loops Plan

このクエリプランは、オプティマイザーのコストモデルでは最悪と見なされますが、非常に望ましい機能がいくつかあります。まず、ネストされたループ結合は、メモリの付与を必要としません。次に、Personsテーブルのキーの順序を保持できるため、明示的な並べ替えが不要になります。このプランは比較的うまく機能し、おそらく十分に優れているかもしれません。

ネストされた並列ループ

ネストされたループ計画の大きな欠点は、単一のスレッドで実行されることです。このクエリは並列処理の恩恵を受ける可能性がありますが、オプティマイザはここでそれを行うことに利点がないと判断します。これも必ずしも正しいとは限りません。残念ながら、並列プランを取得するための組み込みのクエリヒントはありませんが、文書化されていない方法があります。

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n
OPTION (LOOP JOIN, QUERYTRACEON 8649);

QUERYTRACEONヒントでトレースフラグ8649を有効にすると、次の計画が作成されます。

Parallel Nested Loops Plan

これで、並べ替えを回避し、結合に追加のメモリを必要とせず、並列処理を効果的に使用する計画ができました。このクエリは、他のクエリよりもはるかに優れたパフォーマンスを発揮するはずです。

私の記事の並列処理に関する詳細 並列クエリ実行プランの強制

19
Paul White 9

クエリプランを見ると、実際の問題は結合自体ではなく、実際の更新プロセスである可能性があります。

私が見ることができるものから、あなたはあなたのデータベースのすべての個人レコードを更新し、インデックスを更新している可能性があります(これが他のどのインデックスを持っているのかわからないので、それが要因であるかどうかわかりません)

これが1回限りのタスクである場合、インデックスを無効にし、更新を実行してインデックスを再構築できますか?

データを入力したら、where句をクエリに追加して、更新が必要なレコードのみを更新できます。

1
TomGough