web-dev-qa-db-ja.com

データが変化しないUPDATEパフォーマンス

実際にデータを変更しないUPDATEステートメントがある場合(データが既に更新された状態であるため)。更新を防ぐためにWHERE句にチェックを入れることで、パフォーマンス上の利点はありますか?

たとえば、以下の場合、UPDATE 1とUPDATE 2の実行速度に違いがあります。

CREATE TABLE MyTable (ID int PRIMARY KEY, Value int);
INSERT INTO MyTable (ID, Value)
VALUES
    (1, 1),
    (2, 2),
    (3, 3);

-- UPDATE 1
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2
    AND Value <> 2;
SELECT @@ROWCOUNT;

-- UPDATE 2
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2;
SELECT @@ROWCOUNT;

DROP TABLE MyTable;

私が尋ねる理由は、変更されていない行を含めるために行カウントが必要なので、IDが存在しない場合に挿入を行うかどうかを知っているためです。そのため、UPDATE 2フォームを使用しました。 UPDATE 1フォームの使用にパフォーマンス上の利点がある場合、どういうわけか必要な行数を取得することは可能ですか?

32
Martin Brown

実際にデータを変更しないUPDATEステートメントがある場合(データは既に更新された状態にあるため)、更新を防ぐためにwhere句にチェックを入れることでパフォーマンス上の利点はありますか?

UPDATE 1によるわずかなパフォーマンスの違いがあるため、確かにある可能性があります。

  • 実際に行を更新しない(したがって、ディスクに書き込むものは何もない、最小限のログアクティビティさえも)、および
  • 実際の更新を実行するために必要なものよりも制限の少ないロックを取得します(したがって、並行性が向上します)(最後の方の更新セクションを参照してください

ただし、どの程度の違いがあるかは、システム、スキーマ、データ、システム負荷を使用してシステムで測定する必要があります。非更新UPDATEが与える影響には、いくつかの要因があります。

  • 更新されるテーブルの競合の量
  • 更新される行の数
  • 更新中のテーブルにUPDATEトリガーがある場合(質問のコメントのマークに示されているように)。 UPDATE TableName SET Field1 = Field1の場合、更新トリガーが起動し、フィールドが更新されたことを示します( PDATE() または COLUMNS_UPDATED 関数を使用して確認した場合)。 INSERTEDテーブルとDELETEDテーブルはどちらも同じ値です。

また、次の要約セクションは、Paul Whiteの記事 非更新更新の影響 (彼の回答に関するコメントで@spaghettidbaが指摘したとおり)にあります。

SQL Serverには、永続的なデータベースに変更を加えないUPDATE操作を処理するときに、不要なログ記録やページのフラッシュを回避するための多数の最適化が含まれています。

  • クラスター化されたテーブルへの非更新更新では、クラスターキー(の一部)を形成する列が更新操作の影響を受けない限り、通常、余分なロギングとページのフラッシュが回避されます。
  • クラスターキーのいずれかの部分が同じ値に「更新」されると、データが変更されたかのように操作がログに記録され、影響を受けるページがバッファープールでダーティとしてマークされます。これは、UPDATEを削除してから挿入する操作に変換した結果です。
  • ヒープテーブルは、追加のロギングやページフラッシュを発生させるクラスターキーがないことを除いて、クラスター化テーブルと同じように動作します。これは、クラスタ化されていない主キーがヒープに存在する場合でも同じです。したがって、ヒープの更新を更新しないことで、通常、余分なロギングとフラッシュを回避できます(ただし、以下を参照)。
  • ヒープとクラスター化されたテーブルの両方で、8000バイトを超えるデータを含むLOB列が「SET column_name = column_name」以外の構文を使用して同じ値に更新される行について、追加のロギングとフラッシュが発生します。
  • データベースでいずれかのタイプの行バージョン管理分離レベルを有効にするだけで、常に追加のロギングとフラッシュが発生します。これは、更新トランザクションに有効な分離レベルに関係なく発生します。

次の2つの項目に注意してください(特に、リンクをクリックしてPaulの全文を表示しない場合)。

  1. 更新されていない更新には、まだsomeログアクティビティがあり、トランザクションが開始および終了していることを示しています。データの変更が発生しないだけです(これは、まだかなりの節約です)。

  2. 上で述べたように、システムでテストする必要があります。 Paulが使用しているのと同じリサーチクエリを使用して、同じ結果が得られるかどうかを確認します。私のシステムでは、記事に示されているものとは少し異なる結果が表示されます。書き込むダーティページはまだありませんが、ログアクティビティが少し増えます。


... IDが存在しない場合に挿入を行うかどうかがわかるように、変更されていない行を含めるには行数が必要です。 ...どういうわけか必要な行数を取得することは可能ですか?

簡単に言うと、1つの行だけを処理する場合は、次のことができます。

UPDATE MyTable
SET    Value = 2
WHERE  ID = 2
AND Value <> 2;

IF (@@ROWCOUNT = 0)
BEGIN
  IF (NOT EXISTS(
                 SELECT *
                 FROM   MyTable
                 WHERE  ID = 2 -- or Value = 2 depending on the scenario
                )
     )
  BEGIN
     INSERT INTO MyTable (ID, Value) -- or leave out ID if it is an IDENTITY
     VALUES (2, 2);
  END;
END;

複数の行の場合、OUTPUT句を使用して、その決定に必要な情報を取得できます。更新された行を正確にキャプチャすることで、アイテムを絞り込んで検索して、存在しない行を更新しないことと、更新が不要な行を更新しないことの違いを知ることができます。

次の回答で基本的な実装を示します。

xmlパラメータを使用して複数のデータをアップサートするときにマージクエリを使用しないようにする方法

その回答に示されている方法では、更新する必要がない既存の行は除外されません。その部分を追加することもできますが、MyTableにマージするデータセットを取得している場所を正確に示す必要があります。それらは一時テーブルからのものですか?テーブル値パラメーター(TVP)?


更新1:

ようやくいくつかのテストを行うことができました。トランザクションログとロックに関して私が見つけたものは次のとおりです。まず、テーブルのスキーマ:

CREATE TABLE [dbo].[Test]
(
  [ID] [int] NOT NULL CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED,
  [StringField] [varchar](500) NULL
);

次に、フィールドを既に持っている値に更新するテスト:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117

結果:

-- Transaction Log (2 entries):
Operation
----------------------------
LOP_BEGIN_XACT
LOP_COMMIT_XACT


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
8 - IX          6 - PAGE
5 - X           7 - KEY

最後に、値が変化しないために更新を除外するテスト:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117
AND    rt.StringField <> '04CF508B-B78E-4264-B9EE-E87DC4AD237A';

結果:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
7 - IU          6 - PAGE
4 - U           7 - KEY

ご覧のように、トランザクションの開始と終了をマークする2つのエントリとは対照的に、行をフィルターで除外してもトランザクションログには何も書き込まれません。そして、これらの2つのエントリがほとんどないことは事実ですが、それらはまだ何かです。

また、変更されていない行をフィルターで除外する場合、PAGEおよびKEYリソースのロックの制限が緩和されます。他のプロセスがこのテーブルと対話していない場合、それはおそらく問題ではありません(しかし、それは実際にどのくらい可能性がありますか?)。リンクされたブログ(および私のテスト)に示されているテストは、テストの一部ではないため、テーブルに競合がないことを暗黙的に想定していることに注意してください。更新を行わない更新は非常に軽量であり、フィルタリングを行うのに費用がかからないと言うことは、テストが多かれ少なかれ真空で行われているため、塩の粒で行われる必要があります。しかし、プロダクションでは、このテーブルはおそらく分離されていません。もちろん、ほんの少しのロギングとより制限されたロックが効率の低下につながらないことは十分にあり得ます。それで、この質問に答えるための最も信頼できる情報源ですか? SQLサーバー。具体的には:yourSQL Server。それはあなたのシステムにとってより良い方法を示します:-)。


更新2:

新しい値が現在の値と同じである(つまり、更新がない)操作の数が、新しい値が異なり、更新が必要な操作よりも多い場合、特に次の場合テーブルには多くの競合があります。アイデアは、単純なSELECTを最初に実行して、現在の値を取得することです。値が得られない場合は、INSERTに関する答えがあります。値がある場合は、単純なIFを実行し、必要な場合はUPDATEonlyを発行できます。

DECLARE @CurrentValue VARCHAR(500) = NULL,
        @NewValue VARCHAR(500) = '04CF508B-B78E-4264-B9EE-E87DC4AD237A',
        @ID INT = 4082117;

SELECT @CurrentValue = rt.StringField
FROM   dbo.Test rt
WHERE  rt.ID = @ID;

IF (@CurrentValue IS NULL) -- if NULL is valid, use @@ROWCOUNT = 0
BEGIN
  -- row does not exist
  INSERT INTO dbo.Test (ID, StringField)
  VALUES (@ID, @NewValue);
END;
ELSE
BEGIN
  -- row exists, so check value to see if it is different
  IF (@CurrentValue <> @NewValue)
  BEGIN
    -- value is different, so do the update
    UPDATE rt
    SET    rt.StringField = @NewValue
    FROM   dbo.Test rt
    WHERE  rt.ID = @ID;
  END;
END;

結果:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (2 Lock:Acquired events):
Mode            Type
--------------------------------------
6 - IS          5 - OBJECT
6 - IS          6 - PAGE

したがって、取得されるロックは3つではなく2つしかなく、これらのロックは両方ともIntent Sharedであり、Intent eXclusiveまたはIntent Updateではありません( Lock Compatibility )。獲得した各ロックも解放されることに注意してください。各ロックは実際には2つの操作であるため、この新しい方法は、最初に提案された方法の6つの操作ではなく、合計4つの操作になります。この操作が15 msごとに1回実行されることを考えると(およそ、O.P。によると)、1秒あたり約66回です。したがって、元の提案では1秒あたり396回のロック/ロック解除操作になりましたが、この新しい方法では、さらに軽量なロックで1秒あたり264回のロック/ロック解除操作しかできません。これは素晴らしいパフォーマンスを保証するものではありませんが、確かにテストする価値があります:-)。

24
Solomon Rutzky

少しズームアウトして、全体像を考えます。現実の世界では、更新ステートメントは本当に次のようになりますか?

UPDATE MyTable
  SET Value = 2
WHERE
     ID = 2
     AND Value <> 2;

または、次のようになります。

UPDATE Customers
  SET AddressLine1 = '123 Main St',
      AddressLine2 = 'Apt 24',
      City = 'Chicago',
      State = 'IL',
      (and a couple dozen more fields)
WHERE
     ID = 2
     AND (AddressLine1 <> '123 Main St'
     OR AddressLine2 <> 'Apt 24'
     OR City <> 'Chicago'
     OR State <> 'IL'
      (and a couple dozen more fields))

現実の世界では、テーブルには多くの列があるからです。つまり、動的文字列を構築するには、多くの複雑な動的アプリロジックを生成する必要があります。ORすべてのフィールドの前後のコンテンツを指定する必要があります。時間。

これらの更新ステートメントをすべてのテーブルに対して動的に作成し、更新されるフィールドのみを渡すと、数年前の NHibernateパラメータサイズの問題 に類似した計画キャッシュ汚染問題にすぐに直面する可能性があります。さらに悪いことに、SQLステートメントを(ストアドプロシージャのように)SQL Serverで作成すると、SQL Serverは文字列を大規模に連結する効率が非常に悪いため、貴重なCPUサイクルを消費します。

これらの複雑さのため、更新を行っているときに、このような行ごと、フィールドごとの比較を行うことは通常意味がありません。代わりにセットベースの操作を考えてください。

14
Brent Ozar

行の数が多い場合にのみ更新する必要のない行をスキップすると、パフォーマンスが向上する可能性があります(ログが少なく、ディスクに書き込むダーティページが少ない)。

あなたの場合のように単一行の更新を処理する場合、パフォーマンスの違いは完全に無視できます。すべてのケースで行を更新すると、より簡単になる場合は、それを実行します。

このトピックの詳細については、Paul Whiteによる Non Updating Updates を参照してください。

3
spaghettidba

更新と挿入を1つのステートメントに組み合わせることができます。 SQL Serverでは、 [〜#〜] merge [〜#〜] ステートメントを使用して、更新と見つからない場合の挿入の両方を実行できます。 MySQLの場合、 INSERT ON DUPLICATE KEY UPDATE を使用できます。

3
Russell Harkins

すべてのフィールドの値をチェックする代わりに、関心のある列を使用してハッシュ値を取得し、それをテーブルの行に対して格納されているハッシュと比較できませんか?

IF EXISTS (Select 1 from Table where ID =@ID AND HashValue=Sha256(column1+column2))
GOTO EXIT
ELSE
1