web-dev-qa-db-ja.com

変更されていないものも含めて、すべての列を更新するオーバーヘッドは何ですか

行の更新に関しては、多くの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は値がいずれにしても変更されていないことを認識すべきではありませんか?

この記事 で、Markus Winandは言います:

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

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

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

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

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

UPDATE post
SET    score = 12,
WHERE  id = 1

ただし、このタイプのUPDATEは、行ごとに異なるプロパティが変更された場合に、バッチ更新やステートメントキャッシングのメリットが必ずしも得られるとは限りません。

18
Vlad Mihalcea

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が設定されていないので、おそらくJava nullを3番目のバインド変数にバインドします。多くのORMもNULLDEFAULTを区別できないためです( jOOQ 、 Java nullのみが表示され、これがNULL(不明な値の場合)とDEFAULT(初期化されていない値の場合)のどちらを意味するのかがわかりません。

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

  • 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"

12
Lukas Eder

答えは-それは複雑ですだと思います。 MySQLでlongtext列を使用して簡単な証明を書こうとしましたが、答えは少し決定的ではありません。最初に証明:

# 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)

したがって、値自体が変更されていないことを確認するための比較が必要なため、時間が増加しているように見えます。1Gロングテキストの場合、(多くのページに分割されるため)時間がかかります。しかし、変更自体は、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分離には、セッション2の読み取りがセッション1コミットの前に発生する時間ウィンドウも存在します。

言い換えると、UPDATEが後に続く行を読み取るためにSELECT .. FOR UPDATEを発行する場合にのみ、最適化は安全です。 SELECT .. FOR UPDATEはMVCCを使用せず、常に最新バージョンの行を読み取ります。


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

9
Morgan Tocker