Was kostet die Aktualisierung aller Spalten, auch derjenigen, die sich nicht geändert haben? [Closed]


17

Beim Aktualisieren einer Zeile geben viele ORM-Tools eine UPDATE-Anweisung aus, mit der jede Spalte festgelegt wird, die dieser bestimmten Entität zugeordnet ist .

Der Vorteil ist, dass Sie die Aktualisierungsanweisungen problemlos stapeln können, da die UPDATEAnweisung unabhängig davon, welches Entitätsattribut Sie ändern, identisch ist. Darüber hinaus können Sie auch serverseitiges und clientseitiges Anweisungs-Caching verwenden.

Wenn ich also eine Entität lade und nur eine einzelne Eigenschaft festlege:

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

Alle Spalten werden geändert:

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

Sollte titledie DB nun, vorausgesetzt, wir haben auch einen Index für die Eigenschaft, nicht erkennen, dass sich der Wert ohnehin nicht geändert hat?

In diesem Artikel sagt Markus Winand:

Die Aktualisierung aller Spalten zeigt dasselbe Muster, das wir bereits in den vorherigen Abschnitten beobachtet haben: Die Antwortzeit wächst mit jedem zusätzlichen Index.

Ich frage mich, warum das so ist, da die Datenbank die zugehörige Datenseite von der Festplatte in den Speicher lädt und so herausfinden kann, ob ein Spaltenwert geändert werden muss oder nicht.

Selbst für Indizes wird kein Neuausgleich durchgeführt, da sich die Indexwerte für die Spalten, die sich nicht geändert haben, nicht ändern, sie jedoch im UPDATE enthalten waren.

Müssen die B + Tree-Indizes, die den redundanten unveränderten Spalten zugeordnet sind, ebenfalls navigiert werden, nur damit die Datenbank erkennt, dass der Blattwert immer noch derselbe ist?

Natürlich können Sie mit einigen ORM-Tools nur die geänderten Eigenschaften AKTUALISIEREN:

UPDATE post
SET    score = 12,
WHERE  id = 1

Diese Art von UPDATE profitiert jedoch möglicherweise nicht immer von Stapelaktualisierungen oder Anweisungs-Caching, wenn verschiedene Eigenschaften für verschiedene Zeilen geändert werden.


1
Wenn die Datenbank PostgreSQL waren (oder einige andere , dass die Verwendung MVCC ), eine UPDATEpraktisch gleichbedeutend mit einem DELETE+ INSERT(weil Sie tatsächlich eine neue erstellen V ersion der Reihe). Der Overhead ist hoch und wächst mit der Anzahl der Indizes , insbesondere wenn viele der Spalten, aus denen sie bestehen, tatsächlich aktualisiert werden und der Baum (oder was auch immer), der zur Darstellung des Index verwendet wird, eine erhebliche Änderung erfordert. Es ist nicht die Anzahl der Spalten, die aktualisiert werden, sondern ob Sie einen Spaltenteil eines Indexes aktualisieren.
Joanolo

@joanolo Dies muss nur für die Implementierung von MVCC durch postgres zutreffen. MySQL, Oracle (und andere) führen ein Update durch und verschieben geänderte Spalten in den UNDO-Bereich.
Morgan Tocker

2
Ich sollte darauf hinweisen, dass ein guter ORM nachverfolgen sollte, welche Spalten aktualisiert werden müssen, und die an die Datenbank gesendete Anweisung optimieren sollte. Es ist relevant, wenn auch nur für die Menge der Daten an die DB übertragen werden , besonders , wenn einige der Säulen sind lange Texte oder BLOBs .
Joanolo


2
Welches DBMS verwenden Sie?
a_horse_with_no_name

Antworten:


12

Ich weiß, dass Sie sich UPDATEhauptsächlich um die Leistung und vor allem um die Leistung kümmern. Lassen Sie mich jedoch als Mitbetreuer von ORM eine weitere Perspektive auf das Problem der Unterscheidung zwischen den Werten "changed" , "null" und "default" geben drei verschiedene Dinge in SQL, aber möglicherweise nur eines in Java und in den meisten ORMs:

Übersetzen Sie Ihre Begründung in INSERTAussagen

Ihre Argumente für die Stapelbarkeit und die Zwischenspeicherbarkeit von Anweisungen gelten in gleicher Weise für INSERTAnweisungen wie für UPDATEAnweisungen. Bei INSERTAnweisungen hat das Weglassen einer Spalte in der Anweisung eine andere Semantik als in UPDATE. Es bedeutet, sich zu bewerben DEFAULT. Die folgenden zwei sind semantisch äquivalent:

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

Dies trifft nicht zu UPDATE, wenn die ersten beiden semantisch äquivalent sind und die dritte eine ganz andere Bedeutung hat:

-- 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;

Die meisten Datenbank-Client-APIs, einschließlich JDBC und folglich JPA, lassen das Binden eines DEFAULTAusdrucks an eine Bindungsvariable nicht zu - hauptsächlich, weil die Server dies ebenfalls nicht zulassen. Wenn Sie dieselbe SQL-Anweisung aus den oben genannten Gründen der Stapelbarkeit und der Zwischenspeicherbarkeit von Anweisungen wiederverwenden möchten, verwenden Sie in beiden Fällen die folgende Anweisung (vorausgesetzt, (a, b, c)alle Spalten sind in t):

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

Und da dies cnicht festgelegt ist, würden Sie Java wahrscheinlich nullan die dritte Bindungsvariable binden, da viele ORMs auch nicht zwischen NULLund unterscheiden können DEFAULT( JOOQ ist hier beispielsweise eine Ausnahme). Sie sehen nur Java nullund wissen nicht, ob dies NULL(wie im unbekannten Wert) oder DEFAULT(wie im nicht initialisierten Wert) bedeutet.

In vielen Fällen spielt diese Unterscheidung keine Rolle. Wenn Ihre Spalte c jedoch eine der folgenden Funktionen verwendet, ist die Aussage einfach falsch :

  • Es hat eine DEFAULTKlausel
  • Es kann durch einen Trigger generiert werden

Zurück zu den UPDATEAussagen

Obwohl das oben Gesagte für alle Datenbanken zutrifft, kann ich Ihnen versichern, dass das Auslöserproblem auch für die Oracle-Datenbank zutrifft. Betrachten Sie das folgende 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;

Wenn Sie den obigen Befehl ausführen, wird die folgende Ausgabe angezeigt:

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

Wie Sie sehen, löst die Anweisung, die immer alle Spalten aktualisiert, immer den Auslöser für alle Spalten aus, während die Anweisungen, die nur geänderte Spalten aktualisieren, nur die Auslöser auslösen, die auf solche spezifischen Änderungen warten.

Mit anderen Worten:

Das aktuelle Verhalten von Hibernate, das Sie beschreiben, ist unvollständig und kann bei Vorhandensein von Triggern (und wahrscheinlich anderen Tools) sogar als falsch angesehen werden.

Ich persönlich denke, dass Ihr Argument zur Optimierung des Abfrage-Cache im Fall von dynamischem SQL überbewertet ist. Sicher, in einem solchen Cache gibt es ein paar mehr Abfragen und ein bisschen mehr Parsing-Arbeit, aber dies ist normalerweise kein Problem für dynamische UPDATEAnweisungen, viel weniger als für SELECT.

Batching ist sicherlich ein Problem, aber meiner Meinung nach sollte ein einzelnes Update nicht normalisiert werden, um alle Spalten zu aktualisieren, nur weil die Möglichkeit gering ist, dass die Anweisung Batch-fähig ist. Möglicherweise kann der ORM Untergruppen aufeinanderfolgender identischer Anweisungen sammeln und diese anstelle der "gesamten Gruppe" stapeln (falls der ORM sogar in der Lage ist, den Unterschied zwischen "geändert" , "null" und "Standard" zu verfolgen ).


Der DEFAULTAnwendungsfall kann durch adressiert werden @DynamicInsert. Die TRIGGER-Situation kann auch mit Checks wie WHEN (NEW.b <> OLD.b)oder einfach mit Switch angesprochen werden @DynamicUpdate.
Vlad Mihalcea

Ja, Dinge können angesprochen werden, aber Sie haben ursprünglich nach der Leistung gefragt, und Ihre Problemumgehung trägt noch mehr zum Overhead bei.
Lukas Eder

Ich denke, Morgan hat es am besten gesagt: Es ist kompliziert .
Vlad Mihalcea

Ich denke es ist ziemlich einfach. Aus Framework-Sicht sprechen mehr Argumente für die Standardeinstellung von dynamischem SQL. Aus der Sicht der Benutzer ist das kompliziert.
Lukas Eder

9

Ich denke die Antwort ist - es ist kompliziert . Ich habe versucht, einen schnellen Beweis mit einer longtextSpalte in MySQL zu schreiben , aber die Antwort ist ein wenig unschlüssig. Beweis zuerst:

# 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

Es gibt also einen kleinen Zeitunterschied zwischen langsam + geändertem Wert und langsam + keinem geänderten Wert. Deshalb habe ich mich für eine andere Metrik entschieden, die Seiten umfasst:

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)

Es sieht also so aus, als hätte sich die Zeit erhöht, da ein Vergleich durchgeführt werden muss, um zu bestätigen, dass der Wert selbst nicht geändert wurde. Dies nimmt im Fall eines 1-G-Langtexts Zeit in Anspruch (da er auf mehrere Seiten aufgeteilt ist). Die Änderung selbst scheint jedoch das Redo-Log nicht zu durchlaufen.

Ich vermute, dass der Vergleich, wenn es sich bei den Werten um reguläre Spalten handelt, die sich in der Seite befinden, nur einen geringen Mehraufwand verursacht. Unter der Annahme, dass die gleiche Optimierung angewendet wird, handelt es sich bei dem Update um No-Ops.

Längere Antwort

Ich denke eigentlich, das ORM sollte keine Spalten entfernen , die modifiziert ( aber nicht verändert ) wurden, da diese Optimierung seltsame Nebenwirkungen hat.

Beachten Sie im Pseudocode Folgendes:

# 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

Das Ergebnis, wenn der ORM die Änderung ohne Änderung "optimieren" würde:

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

Das Ergebnis, wenn der ORM alle Änderungen an den Server gesendet hat:

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

Der Testfall basiert hier auf der repeatable-readIsolation (MySQL-Standard), es gibt jedoch auch ein Zeitfenster für die read-committedIsolation, in dem der Lesevorgang für Sitzung2 vor dem Festschreiben von Sitzung1 erfolgt.

Anders ausgedrückt: Die Optimierung ist nur dann sicher, wenn Sie ein ausgeben SELECT .. FOR UPDATE, um die Zeilen gefolgt von einem zu lesen UPDATE. SELECT .. FOR UPDATEverwendet MVCC nicht und liest immer die neueste Version der Zeilen.


Bearbeiten: Es wurde sichergestellt, dass sich der Testfalldatensatz zu 100% im Speicher befindet. Angepasste Timing-Ergebnisse.


Danke für die Erklärung. Das ist auch meine Intuition. Ich denke, dass die DB sowohl die Zeile in der Datenseite als auch alle zugehörigen Indizes überprüft. Wenn die Spalte sehr groß ist oder es sich um eine Vielzahl von Indizes handelt, kann der Overhead spürbar werden. Aber in den meisten Situationen, in denen kompakte Spaltentypen und genau so viele Indizes wie erforderlich verwendet werden, kann der Overhead vermutlich geringer sein, als wenn das Anweisungs-Caching keine Vorteile bringt oder die Wahrscheinlichkeit geringer ist, dass die Anweisung im Stapel verarbeitet wird.
Vlad Mihalcea

1
@VladMihalcea hüte dich davor, dass die Antwort über MySQL ist. Die Schlussfolgerungen in verschiedenen DBMS sind möglicherweise nicht identisch.
ypercubeᵀᴹ

@ypercube Mir ist das bewusst. Es hängt alles vom RDBMS ab.
Vlad Mihalcea
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.