Quel est le surcoût de la mise à jour de toutes les colonnes, même celles qui n'ont pas changé [fermé]

17

Lorsqu'il s'agit de mettre à jour une ligne, de nombreux outils ORM émettent une instruction UPDATE qui définit chaque colonne associée à cette entité particulière .

L'avantage est que vous pouvez facilement regrouper les instructions de mise à jour car l' UPDATEinstruction est la même quel que soit l'attribut d'entité que vous modifiez. De plus, vous pouvez même utiliser la mise en cache des instructions côté serveur et côté client.

Donc, si je charge une entité et que je ne définis qu'une seule propriété:

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

Toutes les colonnes vont être modifiées:

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

Maintenant, en supposant que nous ayons également un index sur la titlepropriété, la base de données ne devrait-elle pas se rendre compte que la valeur n'a pas changé de toute façon?

Dans cet article , Markus Winand dit:

La mise à jour sur toutes les colonnes montre le même schéma que nous avons déjà observé dans les sections précédentes: le temps de réponse augmente avec chaque index supplémentaire.

Je me demande pourquoi cette surcharge est due au fait que la base de données charge la page de données associée du disque dans la mémoire et qu'elle peut donc déterminer si une valeur de colonne doit être modifiée ou non.

Même pour les index, il ne rééquilibre rien car les valeurs d'index ne changent pas pour les colonnes qui n'ont pas changé, mais elles ont été incluses dans la MISE À JOUR.

Est-ce que les index B + Tree associés aux colonnes inchangées redondantes doivent également être parcourus, uniquement pour que la base de données se rende compte que la valeur de la feuille est toujours la même?

Bien sûr, certains outils ORM vous permettent de METTRE À JOUR uniquement les propriétés modifiées:

UPDATE post
SET    score = 12,
WHERE  id = 1

Mais ce type de MISE À JOUR peut ne pas toujours bénéficier des mises à jour par lots ou de la mise en cache des instructions lorsque différentes propriétés sont modifiées pour différentes lignes.

Vlad Mihalcea
la source
1
Si la base de données PostgreSQL étaient (ou quelques autres qui utilisent MVCC ), un UPDATEest pratiquement équivalent à un DELETE+ INSERT(parce que vous créez en fait une nouvelle V ersion de la ligne). Les frais généraux sont élevés et augmentent avec le nombre d' index , surtout si la plupart des colonnes qui les composent sont réellement mises à jour, et l' arborescence (ou autre) utilisée pour représenter l'index a besoin d'un changement significatif. Ce n'est pas le nombre de colonnes mises à jour qui importe, mais la mise à jour d'une partie de colonne d'un index.
joanolo
@joanolo Cela ne doit être vrai que pour l'implémentation postgres de MVCC. MySQL, Oracle (et autres) effectuent une mise à jour sur place et déplacent les colonnes modifiées dans l'espace UNDO.
Morgan Tocker
2
Je dois souligner qu'un bon ORM doit suivre les colonnes à mettre à jour et optimiser la déclaration envoyée à la base de données. C'est pertinent, ne serait-ce que pour la quantité de données transmises à la base de données, surtout si certaines des colonnes sont des textes longs ou des BLOB .
joanolo
1
Question sur ce sujet pour SQL Server dba.stackexchange.com/q/114360/3690
Martin Smith
2
Quel SGBD utilisez-vous?
a_horse_with_no_name

Réponses:

12

Je sais que vous êtes principalement préoccupé UPDATEet surtout par les performances, mais en tant que responsable de la maintenance "ORM", permettez-moi de vous donner une autre perspective sur le problème de la distinction entre les valeurs "modifiées" , "nulles" et "par défaut" , qui sont trois choses différentes en SQL, mais peut-être une seule chose en Java et dans la plupart des ORM:

Traduire votre justification en INSERTdéclarations

Vos arguments en faveur de la batchabilité et de la mise en cache des instructions sont valables de la même manière pour INSERT instructions que pour les UPDATEinstructions. Mais dans le cas d' INSERTinstructions, l'omission d'une colonne de l'instruction a une sémantique différente de celle de UPDATE. Cela signifie appliquer DEFAULT. Les deux suivants sont sémantiquement équivalents:

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

Ce n'est pas vrai pour UPDATE, où les deux premiers sont sémantiquement équivalents, et le troisième a une signification entièrement différente:

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

La plupart des API clientes de base de données, y compris JDBC, et par conséquent, JPA, ne permettent pas de lier un DEFAULT expression à une variable de liaison - principalement parce que les serveurs ne le permettent pas non plus. Si vous souhaitez réutiliser la même instruction SQL pour les raisons de batchability et de mise en cache des instructions susmentionnées, vous devez utiliser l'instruction suivante dans les deux cas (en supposant que (a, b, c)toutes les colonnes figurent dans t):

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

Et comme ce cn'est pas défini, vous lieriez probablement Java nullà la troisième variable de liaison, car de nombreux ORM ne peuvent pas non plus faire la distinction entre NULLet DEFAULT( jOOQ , par exemple étant une exception ici). Ils ne voient que Java nullet ne savent pas si cela signifie NULL(comme dans la valeur inconnue) ou DEFAULT(comme dans la valeur non initialisée).

Dans de nombreux cas, cette distinction n'a pas d'importance, mais si votre colonne c utilise l'une des fonctionnalités suivantes, l'instruction est tout simplement erronée :

  • Il a une DEFAULTclause
  • Il peut être généré par un déclencheur

Retour aux UPDATEdéclarations

Bien que ce qui précède soit vrai pour toutes les bases de données, je peux vous assurer que le problème de déclenchement est également vrai pour la base de données Oracle. Considérez le SQL suivant:

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;

Lorsque vous exécutez ce qui précède, vous verrez la sortie suivante:

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

Comme vous pouvez le voir, l'instruction qui met toujours à jour toutes les colonnes déclenche toujours le déclencheur pour toutes les colonnes, tandis que les instructions qui mettent à jour uniquement les colonnes qui ont changé ne déclenchent que les déclencheurs qui écoutent ces modifications spécifiques.

En d'autres termes:

Le comportement actuel d'Hibernate que vous décrivez est incomplet et pourrait même être considéré comme incorrect en présence de déclencheurs (et probablement d'autres outils).

Personnellement, je pense que votre argument d'optimisation du cache de requête est surfait dans le cas de SQL dynamique. Bien sûr, il y aura quelques requêtes supplémentaires dans un tel cache et un peu plus de travail d'analyse à effectuer, mais ce n'est généralement pas un problème pour les UPDATEinstructions dynamiques , beaucoup moins que pour SELECT.

Le traitement par lots est certainement un problème, mais à mon avis, une seule mise à jour ne devrait pas être normalisée pour mettre à jour toutes les colonnes simplement parce qu'il y a une légère possibilité que l'instruction soit groupable. Il y a de fortes chances que l'ORM puisse collecter des sous-lots d'instructions identiques consécutives et les regrouper au lieu du "lot entier" (dans le cas où l'ORM est même capable de suivre la différence entre "modifié" , "nul" et "par défaut"

Lukas Eder
la source
Le DEFAULTcas d'utilisation peut être résolu par @DynamicInsert. La situation TRIGGER peut également être résolue à l'aide de contrôles comme WHEN (NEW.b <> OLD.b)ou simplement basculer vers @DynamicUpdate.
Vlad Mihalcea
Oui, les choses peuvent être résolues, mais vous posiez à l'origine des questions sur les performances et votre solution de contournement ajoute encore plus de surcharge.
Lukas Eder
Je pense que Morgan l'a mieux dit: c'est compliqué .
Vlad Mihalcea
Je pense que c'est plutôt simple. Du point de vue du framework, il y a plus d'arguments en faveur du passage par défaut au SQL dynamique. Du point de vue de l'utilisateur, oui, c'est compliqué.
Lukas Eder
9

Je pense que la réponse est - c'est compliqué . J'ai essayé d'écrire une preuve rapide en utilisant une longtextcolonne dans MySQL, mais la réponse n'est pas concluante. Preuve d'abord:

# 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

Il y a donc une petite différence de temps entre la valeur lente + modifiée et la valeur lente + aucune valeur modifiée. J'ai donc décidé de regarder une autre métrique, qui était des pages écrites:

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)

Il semble donc que le temps a augmenté car il doit y avoir une comparaison pour confirmer que la valeur elle-même n'a pas été modifiée, ce qui dans le cas d'un texte long 1G prend du temps (car il est divisé sur plusieurs pages). Mais la modification elle-même ne semble pas parcourir le journal de rétablissement.

Je soupçonne que si les valeurs sont des colonnes régulières sur la page, la comparaison n'ajoute qu'une petite surcharge. Et en supposant que la même optimisation s'applique, ce ne sont pas des opérations en ce qui concerne la mise à jour.

Réponse plus longue

En fait, je pense que l'ORM ne devrait pas éliminer les colonnes qui ont été modifiées ( mais pas changées ), car cette optimisation a d'étranges effets secondaires.

Tenez compte des éléments suivants dans le pseudo-code:

# 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

Le résultat si l'ORM devait "Optimiser" la modification sans changement:

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

Résultat si l'ORM a envoyé toutes les modifications au serveur:

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

Le cas de test repose ici sur l' repeatable-readisolement (MySQL par défaut), mais une fenêtre de temps existe également pour l' read-committedisolement où la lecture de session2 a lieu avant la validation de session1.

En d'autres termes: l'optimisation n'est sûre que si vous émettez un SELECT .. FOR UPDATEpour lire les lignes suivi d'un UPDATE. SELECT .. FOR UPDATEn'utilise pas MVCC et lit toujours la dernière version des lignes.


Modifier: Assurez-vous que l'ensemble de données du scénario de test était à 100% en mémoire. Résultats de synchronisation ajustés.

Morgan Tocker
la source
Merci pour l'explication. C'est aussi mon intuition. Je pense que la base de données vérifiera à la fois la ligne de la page de données et tous les index associés. Si la colonne est très grande ou s'il y a des tonnes d'index impliqués, la surcharge pourrait devenir perceptible. Mais pour la plupart des situations, lorsque vous utilisez des types de colonnes compactes et autant d'index que nécessaire, je suppose que la surcharge pourrait être moins que de ne pas bénéficier de la mise en cache des instructions ou d'avoir moins de chances de regrouper l'instruction.
Vlad Mihalcea
1
@VladMihalcea attention, la réponse concerne MySQL. Les conclusions peuvent ne pas être les mêmes dans différents SGBD.
ypercubeᵀᴹ
@ypercube J'en suis conscient. Tout dépend du SGBDR.
Vlad Mihalcea