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.
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:
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 :
DEFAULT
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.
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"
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.