¿Cuál es la sobrecarga de actualizar todas las columnas, incluso las que no han cambiado [cerrado]

17

Cuando se trata de actualizar una fila, muchas herramientas ORM emiten una instrucción UPDATE que establece cada columna asociada a esa entidad en particular .

La ventaja es que puede procesar fácilmente las declaraciones de actualización, ya que la UPDATEdeclaración es la misma sin importar qué atributo de entidad cambie. Además, incluso puede usar el almacenamiento en caché de instrucciones del lado del servidor y del lado del cliente también.

Entonces, si cargo una entidad y solo establezco una sola propiedad:

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

Todas las columnas serán cambiadas:

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

Ahora, suponiendo que también tengamos un índice en la titlepropiedad, ¿no debería el DB darse cuenta de que el valor no ha cambiado de todos modos?

En este artículo , Markus Winand dice:

La actualización en todas las columnas muestra el mismo patrón que ya hemos observado en las secciones anteriores: el tiempo de respuesta crece con cada índice adicional.

Me pregunto por qué es esta sobrecarga, ya que la base de datos carga la página de datos asociada del disco en la memoria y, por lo tanto, puede determinar si un valor de columna debe cambiarse o no.

Incluso para los índices, no es necesario reequilibrar nada, ya que los valores del índice no cambian para las columnas que no han cambiado, pero se incluyeron en la ACTUALIZACIÓN.

¿Es necesario navegar también por los índices B + Tree asociados a las columnas redundantes sin cambios, solo para que la base de datos se dé cuenta de que el valor de la hoja sigue siendo el mismo?

Por supuesto, algunas herramientas ORM le permiten ACTUALIZAR solo las propiedades modificadas:

UPDATE post
SET    score = 12,
WHERE  id = 1

Pero este tipo de ACTUALIZACIÓN no siempre se beneficia de las actualizaciones por lotes o el almacenamiento en caché de instrucciones cuando se cambian diferentes propiedades para diferentes filas.

Vlad Mihalcea
fuente
1
Si la base de datos PostgreSQL eran (o algunos otros que utilizan MVCC ), una UPDATEes prácticamente equivalente a una DELETE+ INSERT(porque en realidad se crea un nuevo V ersión de la fila). La sobrecarga es alta y crece con el número de índices , especialmente si muchas de las columnas que las componen se actualizan realmente, y el árbol (o lo que sea) utilizado para representar el índice necesita un cambio significativo. No es el número de columnas que se actualizan lo que es relevante, sino si actualiza una parte de una columna de un índice.
joanolo
@joanolo Esto solo debe ser cierto para la implementación de MVCC de postgres. MySQL, Oracle (y otros) realizan una actualización en el lugar y reubican las columnas cambiadas al espacio UNDO.
Morgan Tocker
2
Debo señalar que un buen ORM debe rastrear qué columnas necesitan actualizarse y optimizar la declaración enviada a la base de datos. Es relevante, aunque solo sea por la cantidad de datos transmitidos a la base de datos, especialmente si algunas de las columnas son textos largos o BLOB .
joanolo
1
Pregunta sobre esto para SQL Server dba.stackexchange.com/q/114360/3690
Martin Smith
2
¿Qué DBMS estás usando?
a_horse_with_no_name

Respuestas:

12

Sé que está más preocupado UPDATEy sobre todo por el rendimiento, pero como compañero de mantenimiento de "ORM", permítame darle otra perspectiva sobre el problema de distinguir entre "cambiado" , valores "nulos" y "predeterminados" , que son tres cosas diferentes en SQL, pero posiblemente solo una cosa en Java y en la mayoría de los ORM:

Traducir su justificación a INSERTdeclaraciones

Sus argumentos a favor de la capacidad de batchability y la capacidad de almacenamiento en caché de declaraciones son verdaderas de la misma manera para INSERTdeclaraciones que para UPDATEdeclaraciones. Pero en el caso de las INSERTdeclaraciones, omitir una columna de la declaración tiene una semántica diferente que en UPDATE. Significa aplicar DEFAULT. Los dos siguientes son semánticamente equivalentes:

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

Esto no es cierto para UPDATE, donde los dos primeros son semánticamente equivalentes, y el tercero tiene un significado completamente diferente:

-- 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 mayoría de las API de cliente de base de datos, incluido JDBC y, en consecuencia, JPA, no permiten vincular una DEFAULTexpresión a una variable de vinculación , principalmente porque los servidores tampoco lo permiten. Si desea volver a utilizar la misma instrucción SQL por los motivos de capacidad de batchability y de capacidad de almacenamiento de información mencionados anteriormente, usaría la siguiente instrucción en ambos casos (suponiendo que (a, b, c)estén todas las columnas t):

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

Y dado cque no está configurado, probablemente vinculará Java nulla la tercera variable de vinculación, porque muchos ORM tampoco pueden distinguir entre NULLy DEFAULT( jOOQ , por ejemplo, es una excepción aquí). Solo ven Java nully no saben si esto significa NULL(como en el valor desconocido) o DEFAULT(como en el valor no inicializado).

En muchos casos, esta distinción no importa, pero en caso de que su columna c esté usando alguna de las siguientes características, la afirmación es simplemente incorrecta :

  • Tiene una DEFAULTcláusula
  • Puede ser generado por un disparador

Volver a las UPDATEdeclaraciones

Si bien lo anterior es cierto para todas las bases de datos, puedo asegurarle que el problema de activación también es cierto para la base de datos Oracle. Considere el siguiente 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;

Cuando ejecute lo anterior, verá el siguiente resultado:

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

Como puede ver, la declaración que siempre actualiza todas las columnas siempre activará el activador para todas las columnas, mientras que las instrucciones que actualizan solo las columnas que han cambiado activarán solo los activadores que están escuchando dichos cambios específicos.

En otras palabras:

El comportamiento actual de Hibernate que está describiendo es incompleto e incluso podría considerarse incorrecto en presencia de desencadenantes (y probablemente otras herramientas).

Personalmente, creo que su argumento de optimización de caché de consulta está sobrevalorado en el caso de SQL dinámico. Claro, habrá algunas consultas más en ese caché, y un poco más de trabajo de análisis por hacer, pero esto generalmente no es un problema para las UPDATEdeclaraciones dinámicas , mucho menos que para SELECT.

El procesamiento por lotes es ciertamente un problema, pero en mi opinión, una única actualización no debería normalizarse para actualizar todas las columnas solo porque hay una pequeña posibilidad de que la declaración sea procesable. Lo más probable es que el ORM pueda recopilar subgrupos de sentencias idénticas consecutivas y agruparlas en lugar del "lote completo" (en caso de que el ORM sea capaz de rastrear la diferencia entre "cambiado" , "nulo" y "predeterminado"

Lukas Eder
fuente
El DEFAULTcaso de uso puede ser abordado por @DynamicInsert. La situación de TRIGGER también se puede abordar mediante controles como WHEN (NEW.b <> OLD.b)o simplemente cambiar a @DynamicUpdate.
Vlad Mihalcea
Sí, las cosas pueden abordarse, pero originalmente estaba preguntando sobre el rendimiento y su solución agrega aún más sobrecarga.
Lukas Eder
Creo que Morgan lo dijo mejor: es complicado .
Vlad Mihalcea
Creo que es bastante simple. Desde una perspectiva de marco, hay más argumentos a favor de la configuración predeterminada de SQL dinámico. Desde la perspectiva del usuario, sí, es complicado.
Lukas Eder
9

Creo que la respuesta es: es complicado . Traté de escribir una prueba rápida usando una longtextcolumna en MySQL, pero la respuesta no es concluyente. Prueba primero:

# 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

Por lo tanto, hay una pequeña diferencia de tiempo entre el valor lento + cambiado y el valor lento + sin cambio. Así que decidí mirar otra métrica, que eran páginas escritas:

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)

Por lo tanto, parece que el tiempo aumentó porque tiene que haber una comparación para confirmar que el valor en sí no se ha modificado, lo que en el caso de un texto largo de 1G lleva tiempo (porque está dividido en muchas páginas). Pero la modificación en sí no parece agitar el registro de rehacer.

Sospecho que si los valores son columnas regulares que están en la página, la comparación agrega solo un poco de sobrecarga. Y suponiendo que se aplique la misma optimización, estas no son operaciones cuando se trata de la actualización.

Respuesta larga

De hecho, creo que el ORM no debería eliminar las columnas que se han modificado ( pero no cambiado ), ya que esta optimización tiene efectos secundarios extraños.

Considere lo siguiente en pseudocódigo:

# 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

El resultado si el ORM fuera a "Optimizar" la modificación sin cambios:

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

El resultado si el ORM envió todas las modificaciones al servidor:

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

El caso de prueba aquí se basa en el repeatable-readaislamiento (valor predeterminado de MySQL), pero también existe una ventana de tiempo para el read-committedaislamiento donde la lectura de sesión2 ocurre antes de la confirmación de sesión1.

En otras palabras: la optimización solo es segura si emite un SELECT .. FOR UPDATEpara leer las filas seguidas de un UPDATE. SELECT .. FOR UPDATEno usa MVCC y siempre lee la última versión de las filas.


Editar: se aseguró de que el conjunto de datos del caso de prueba estuviera 100% en la memoria. Resultados de tiempo ajustados.

Morgan Tocker
fuente
Gracias por la explicación. Esa es mi intuición también. Creo que el DB verificará tanto la fila en la página de datos como todos los índices asociados. Si la columna es muy grande o hay toneladas de índices involucrados, la sobrecarga podría ser notable. Pero para la mayoría de las situaciones, cuando se utilizan tipos de columnas compactas y tantos índices como sea necesario, supongo que la sobrecarga podría ser menor que no beneficiarse del almacenamiento en caché de la declaración o tener una menor probabilidad de agrupar la declaración.
Vlad Mihalcea
1
@VladMihalcea tenga cuidado con que la respuesta es sobre MySQL. Las conclusiones pueden no ser las mismas en diferentes DBMS.
ypercubeᵀᴹ
@ypercube Soy consciente de eso. Todo depende del RDBMS.
Vlad Mihalcea