web-dev-qa-db-fra.com

Quelle est la surcharge de mise à jour de toutes les colonnes, même celles qui n'ont pas changé

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'instruction UPDATE 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 propriété title, 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 puisque 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.

18
Vlad Mihalcea

Je sais que vous êtes principalement préoccupé par UPDATE et surtout par les performances, mais en tant que collègue responsable de "ORM", permettez-moi de vous donner une autre perspective sur le problème de la distinction entre " changé ", " null ", et " default " valeurs, qui sont trois choses différentes dans SQL, mais probablement une seule chose dans Java et dans la plupart des ORM:

Traduire votre justification en déclarations INSERT

Vos arguments en faveur de la batchabilité et de la mise en cache des instructions sont valables de la même manière pour les instructions INSERT que pour les instructions UPDATE. Mais dans le cas des instructions INSERT, 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 une expression DEFAULT à 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) sont toutes les colonnes de t):

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

Et comme c n'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 NULL et DEFAULT ( jOOQ , par exemple étant une exception ici). Ils ne voient que Java null et don ') Je ne sais 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 fausse :

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

Retour aux instructions UPDATE

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 surévalué dans le cas de SQL dynamique. Bien sûr, il y aura quelques requêtes de plus 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 instructions dynamiques UPDATE, 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 juste 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" (au cas où l'ORM serait même capable de suivre la différence entre "modifié" , "null" , et "default"

12
Lukas Eder

Je pense que la réponse est - c'est compliqué. J'ai essayé d'écrire une preuve rapide en utilisant une colonne longtext 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

Je pense en fait que l'ORM ne doit pas éliminer les colonnes qui ont été modifiées ( mais pas changé ), car cette optimisation a un côté étrange -effets.

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"

Le 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 repeatable-read isolation (MySQL par défaut), mais une fenêtre de temps existe également pour read-committed isolation 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 UPDATE pour lire les lignes suivies d'un UPDATE. SELECT .. FOR UPDATE n'utilise pas MVCC et lit toujours la dernière version des lignes.


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

9
Morgan Tocker