変更されていない列も含めて、すべての列を更新するオーバーヘッドはどれくらいですか[クローズ]


17

行の更新に関して、多くのORMツールは、特定のエンティティに関連付けられているすべての列を設定するUPDATEステートメントを発行します

利点は、UPDATEどのエンティティ属性を変更しても同じステートメントであるため、更新ステートメントを簡単にバッチ処理できることです。さらに、サーバー側とクライアント側のステートメントキャッシングも使用できます。

したがって、エンティティをロードし、単一のプロパティのみを設定した場合:

Post post = entityManager.find(Post.class, 1L);
post.setScore(12);

すべての列が変更されます:

UPDATE post
SET    score = 12,
       title = 'High-Performance Java Persistence'
WHERE  id = 1

ここで、titleプロパティにもインデックスがあると仮定すると、DBは値がとにかく変わっていないことに気付かないでしょうか?

、この記事で、マルクスWinandは言います:

すべての列の更新は、前のセクションで既に確認した同じパターンを示しています。応答時間は、インデックスが追加されるたびに増加します。

データベースは関連するデータページをディスクからメモリにロードし、列の値を変更する必要があるかどうかを判断できるため、なぜこのオーバーヘッドがあるのだろうかと思います。

インデックスの場合でも、変更されていない列のインデックス値は変わらないが、UPDATEに含まれているため、何も再分散しません。

データベースがリーフ値が同じであることを認識するためだけに、冗長で変更されていない列に関連付けられたB +ツリーインデックスもナビゲートする必要があるのでしょうか?

もちろん、いくつかのORMツールでは、変更されたプロパティのみを更新できます。

UPDATE post
SET    score = 12,
WHERE  id = 1

しかし、このタイプのUPDATEは、さまざまな行のさまざまなプロパティが変更されたときに、バッチ更新またはステートメントキャッシュの恩恵を常に受け​​られるとは限りません。


1
データベースは、PostgreSQL(または使用していくつかの他のだったらMVCCを)、UPDATEと実質的に同等であるDELETE+ INSERT(あなたが実際に新しい作成するためのV行のERSIONを)。オーバーヘッドは高く、特にインデックスを構成する列の多くが実際に更新され、 インデックスを表すために使用されるツリー(またはその他)が大幅に変更される場合、インデックスの数とともに増加します。更新されるのは関連する列の数ではなく、インデックスの列部分を更新するかどうかです。
-joanolo

@joanoloこれは、postgresのMVCCの実装にのみ当てはまります。MySQL、Oracle(およびその他)は所定の場所で更新を行い、変更された列をUNDOスペースに再配置します。
モーガントッカー

2
優れたORMはどの列を更新する必要があるかを追跡し、データベースに送信されるステートメントを最適化する必要があることを指摘する必要があります。DBに送信されるデータの量のみ、特に列の一部がロングテキストまたはBLOBである場合に関連します。
-joanolo


2
どのDBMSを使用していますか?
a_horse_with_no_name

回答:


12

UPDATE主にパフォーマンスに関心があり、ほとんどがパフォーマンスに関心があることを知っていますが、仲間の "ORM"メンテナーとして、"changed""null"、および"default"の値を区別する問題について別の視点を教えてください。SQLには3つの異なる点がありますが、JavaとほとんどのORMには1つの点しかありません。

根拠をINSERT声明に翻訳する

バッチ処理可能性と文のキャッシュ可能性を支持するあなたの議論は、文に対しても同じようにINSERT文にも当てはまりますUPDATE。ただし、INSERTステートメントの場合、ステートメントから列を省略することは、とは異なるセマンティクスを持ちUPDATEます。適用することを意味しますDEFAULT。次の2つは意味的に同等です。

INSERT INTO t (a, b)    VALUES (1, 2);
INSERT INTO t (a, b, c) VALUES (1, 2, DEFAULT);

これはには当てはまりません。UPDATE最初の2つは意味的に同等であり、3つ目の意味はまったく異なります。

-- These are the same
UPDATE t SET a = 1, b = 2;
UPDATE t SET a = 1, b = 2, c = c;

-- This is different!
UPDATE t SET a = 1, b = 2, c = DEFAULT;

JDBCを含むほとんどのデータベースクライアントAPI、および結果としてJPAは、DEFAULT式をバインド変数にバインドすることを許可しません。これは主に、サーバーもこれを許可しないためです。前述のバッチ処理可能性とステートメントのキャッシュ可能性の理由で同じSQLステートメントを再使用する場合は、両方のケースで次のステートメントを使用します((a, b, c)すべての列がの場合t)。

INSERT INTO t (a, b, c) VALUES (?, ?, ?);

また、cは設定されていないnullため、Java を3番目のバインド変数にバインドすることになるでしょう。これは、多くのORMでもNULL and DEFAULTjOOQ、たとえばここでは例外)を区別できないためです。彼らはJavaのみをnull認識し、これがNULL(未知の値のように)またはDEFAULT(初期化されていない値のように)意味するかどうかを知りません。

多くの場合、この区別は重要ではありませんが、列cが次の機能のいずれかを使用している場合、ステートメントは単に間違っています。

  • それは持っているDEFAULT句を
  • トリガーによって生成される可能性があります

UPDATEステートメントに戻る

上記はすべてのデータベースに当てはまりますが、トリガーの問題はOracleデータベースにも当てはまることを保証できます。次のSQLを検討してください。

CREATE TABLE x (a INT PRIMARY KEY, b INT, c INT, d INT);

INSERT INTO x VALUES (1, 1, 1, 1);

CREATE OR REPLACE TRIGGER t
  BEFORE UPDATE OF c, d
  ON x
BEGIN
  IF updating('c') THEN
    dbms_output.put_line('Updating c');
  END IF;
  IF updating('d') THEN
    dbms_output.put_line('Updating d');
  END IF;
END;
/

SET SERVEROUTPUT ON
UPDATE x SET b = 1 WHERE a = 1;
UPDATE x SET c = 1 WHERE a = 1;
UPDATE x SET d = 1 WHERE a = 1;
UPDATE x SET b = 1, c = 1, d = 1 WHERE a = 1;

上記を実行すると、次の出力が表示されます。

table X created.
1 rows inserted.
TRIGGER T compiled
1 rows updated.
1 rows updated.
Updating c

1 rows updated.
Updating d

1 rows updated.
Updating c
Updating d

ご覧のとおり、すべての列を常に更新するステートメントは常にすべての列のトリガーを起動しますが、変更された列のみを更新するステートメントはそのような特定の変更をリッスンしているトリガーのみを起動します。

言い換えると:

説明しているHibernateの現在の動作は不完全であり、トリガー(およびおそらく他のツール)が存在する場合は間違っていると見なすこともできます。

個人的には、動的SQLの場合、クエリキャッシュ最適化の引数は過大評価されていると思います。確かに、このようなキャッシュにはさらにいくつかのクエリがあり、実行する解析作業がもう少しありますが、これは通常、動的UPDATEステートメントでは問題ではありませんSELECT

バッチ処理は確かに問題ですが、私の意見では、ステートメントがバッチ処理される可能性がわずかにあるという理由だけで、すべての列を更新するために単一の更新を正規化すべきではありません。ORMは、連続した同一ステートメントのサブバッチを収集し、「バッチ全体」の代わりにバッチ処理することができます(ORMが「changed」「null」、および「default」の違いを追跡できる場合もあります)


DEFAULTユースケースは、によって対処することができます@DynamicInsert。TRIGGER状況は、のようなチェックを使用するかWHEN (NEW.b <> OLD.b)、単にに切り替えることもできます@DynamicUpdate
ウラドミハルセア

はい、対処できますが、もともとパフォーマンスについて尋ねていたため、回避策はさらにオーバーヘッドを追加します。
ルーカスエダー

モーガンはそれを最もよく言ったと思う:それは複雑
ウラッドミハルセア

かなり簡単だと思います。フレームワークの観点からは、デフォルトを動的SQLに設定することを支持する多くの議論があります。ユーザーの観点から、はい、それは複雑です。
ルーカスエダー

9

答えは- 複雑だと思います。longtextMySQLの列を使用して簡単な証明を作成しようとしましたが、答えは少し決定的ではありません。証明最初:

# in advance:
set global max_allowed_packet=1024*1024*1024;

CREATE TABLE `t2` (
  `a` int(11) NOT NULL AUTO_INCREMENT,
  `b` char(255) NOT NULL,
  `c` LONGTEXT,
  PRIMARY KEY (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

mysql> insert into t2 (a, b, c) values (null, 'b', REPEAT('c', 1024*1024*1024));
Query OK, 1 row affected (38.81 sec)

mysql> UPDATE t2 SET b='new'; # fast
Query OK, 1 row affected (6.73 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql>  UPDATE t2 SET b='new'; # fast
Query OK, 0 rows affected (2.87 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> UPDATE t2 SET b='new'; # fast
Query OK, 0 rows affected (2.61 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> UPDATE t2 SET c= REPEAT('d', 1024*1024*1024); # slow (changed value)
Query OK, 1 row affected (22.38 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> UPDATE t2 SET c= REPEAT('d', 1024*1024*1024); # still slow (no change)
Query OK, 0 rows affected (14.06 sec)
Rows matched: 1  Changed: 0  Warnings: 0

したがって、slow +変更された値と、slow +変更されていない値の間にはわずかな時間差があります。そこで、私は別の指標を検討することにしました。

mysql> show global status like 'innodb_pages_written';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_pages_written | 198656 |
+----------------------+--------+
1 row in set (0.00 sec)

mysql> show global status like 'innodb_pages_written';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_pages_written | 198775 | <-- 119 pages changed in a "no change"
+----------------------+--------+
1 row in set (0.01 sec)

mysql> show global status like 'innodb_pages_written';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_pages_written | 322494 | <-- 123719 pages changed in a "change"!
+----------------------+--------+
1 row in set (0.00 sec)

そのため、値自体が変更されていないことを確認するために比較が必要であるため、時間が増加しているように見えます。ただし、変更自体は、REDOログ全体に行き渡っていないようです。

値がページ内の通常の列である場合、比較によってオーバーヘッドが少し増えるだけだと思います。そして、同じ最適化が適用されると仮定すると、これらは更新に関しては何もしません。

より長い答え

この最適化には奇妙な副作用あるため、実際にはORM 変更された(ただし変更されていない削除すべきではないと思います。

擬似コードでは次のことを考慮してください。

# Initial Data does not make sense
# should be either "Harvey Dent" or "Two Face"

id: 1, firstname: "Two Face", lastname: "Dent"

session1.start
session2.start

session1.firstname = "Two"
session1.lastname = "Face"
session1.save

session2.firstname = "Harvey"
session2.lastname = "Dent"
session2.save

ORMが変更なしで変更を「最適化」する場合の結果:

id: 1, firstname: "Harvey", lastname: "Face"

ORMがすべての変更をサーバーに送信した場合の結果:

id: 1, firstname: "Harvey", lastname: "Dent"

ここでのテストケースはrepeatable-read分離(MySQLのデフォルト)に依存しread-committedていますが、session1のコミット前にsession2の読み取りが発生する分離のためのタイムウィンドウも存在します。

別の言い方をすると、最適化は、を発行してSELECT .. FOR UPDATEから行を読み取り、その後にUPDATESELECT .. FOR UPDATEMVCCを使用せず、常に最新バージョンの行を読み取ります。


編集:テストケースのデータセットがメモリ内で100%であることを確認しました。タイミング結果を調整しました。


説明してくれてありがとう。それも私の直観です。DBはデータページの行と関連するすべてのインデックスの両方をチェックすると思います。列が非常に大きい場合、または大量のインデックスが関係している場合、オーバーヘッドが顕著になる可能性があります。ただし、ほとんどの状況で、コンパクトな列タイプと必要なだけのインデックスを使用する場合、オーバーヘッドはステートメントキャッシュの恩恵を受けないか、ステートメントをバッチ処理する可能性が低くなるよりも少ないと思います。
ウラッドミハルセア

1
@VladMihalceaは、答えがMySQLに関するものであることに注意してください。結論は、異なるDBMSで同じではない場合があります。
ypercubeᵀᴹ

@ypercube私はそれを知っています。それはすべてRDBMSに依存しています。
ウラッドミハルセア
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.