Qual é a sobrecarga de atualizar todas as colunas, mesmo as que não foram alteradas?

17

Quando se trata de atualizar uma linha, muitas ferramentas ORM emitem uma instrução UPDATE que define todas as colunas associadas a essa entidade específica .

A vantagem é que você pode facilmente agrupar as instruções de atualização, pois a UPDATEinstrução é a mesma, independentemente do atributo de entidade que você altera. Além disso, você também pode usar o cache de instruções do servidor e do lado do cliente.

Portanto, se eu carregar uma entidade e definir apenas uma única propriedade:

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

Todas as colunas serão alteradas:

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

Agora, supondo que também tenhamos um índice na titlepropriedade, o banco de dados não deve perceber que o valor não mudou de qualquer maneira?

No presente artigo , Markus Winand diz:

A atualização em todas as colunas mostra o mesmo padrão que já observamos nas seções anteriores: o tempo de resposta aumenta com cada índice adicional.

Gostaria de saber por que essa sobrecarga, pois o banco de dados carrega a página de dados associada do disco na memória e, assim, pode descobrir se um valor da coluna precisa ser alterado ou não.

Mesmo para índices, não é necessário reequilibrar nada, pois os valores do índice não são alterados para as colunas que não foram alteradas, mas elas foram incluídas no UPDATE.

Será que os índices B + Tree associados às colunas redundantes inalteradas também precisam ser navegados, apenas para o banco de dados perceber que o valor da folha ainda é o mesmo?

Obviamente, algumas ferramentas ORM permitem que você atualize apenas as propriedades alteradas:

UPDATE post
SET    score = 12,
WHERE  id = 1

Mas esse tipo de UPDATE nem sempre pode se beneficiar de atualizações em lote ou cache de instruções quando propriedades diferentes são alteradas para linhas diferentes.

Vlad Mihalcea
fonte
1
Se o banco de dados foram PostgreSQL (ou alguns outros que usam MVCC ), um UPDATEé praticamente equivalente a um DELETE+ INSERT(porque você realmente criar uma nova V ersão da linha). A sobrecarga é alta e aumenta com o número de índices , especialmente se muitas das colunas que as compõem forem realmente atualizadas e a árvore (ou qualquer outra coisa) usada para representar o índice precisar de uma alteração significativa. Não é o número de colunas atualizadas que é relevante, mas se você atualiza uma parte da coluna de um índice.
Joanolo
@joanolo Isso só precisa ser verdade para a implementação do MVCC no postgres. MySQL, Oracle (e outros) fazem uma atualização no local e realocam as colunas alteradas para o espaço UNDO.
Morgan Tocker
2
Devo salientar que um bom ORM deve rastrear quais colunas precisam ser atualizadas e otimizar a instrução enviada ao banco de dados. É relevante, apenas para a quantidade de dados transmitidos ao banco de dados, especialmente se algumas das colunas forem textos longos ou BLOBs .
Joanolo
1
Pergunta discutindo isso para o SQL Server dba.stackexchange.com/q/114360/3690
Martin Smith
2
Qual DBMS você está usando?
a_horse_with_no_name

Respostas:

12

Sei que você se preocupa UPDATEprincipalmente com o desempenho, mas como colega de manutenção de "ORM", deixe-me dar outra perspectiva sobre o problema de distinguir entre valores "alterados" , "nulos" e "padrão" , que são três coisas diferentes no SQL, mas possivelmente apenas uma coisa no Java e na maioria dos ORMs:

Traduzindo sua justificativa para INSERTdeclarações

Seus argumentos a favor da capacidade de armazenamento e cache de instruções são verdadeiros da mesma maneira para INSERTinstruções e para UPDATEinstruções. Mas, no caso de INSERTdeclarações, a omissão de uma coluna da declaração tem uma semântica diferente da que em UPDATE. Significa aplicar DEFAULT. Os dois seguintes são semanticamente equivalentes:

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

Isso não é verdade para UPDATE, onde os dois primeiros são semanticamente equivalentes e o terceiro tem um significado totalmente 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;

A maioria das APIs de clientes de banco de dados, incluindo JDBC e, consequentemente, JPA, não permite vincular uma DEFAULTexpressão a uma variável de ligação - principalmente porque os servidores também não permitem isso. Se você deseja reutilizar a mesma instrução SQL pelos motivos mencionados de capacidade de manipulação e armazenamento em cache, use a seguinte instrução nos dois casos (supondo que (a, b, c)todas as colunas estejam t):

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

E como cnão está definido, você provavelmente ligaria o Java nullà terceira variável de ligação, porque muitos ORMs também não podem distinguir entre NULLe DEFAULT( jOOQ , por exemplo, sendo uma exceção aqui). Eles apenas veem Java nulle não sabem se isso significa NULL(como no valor desconhecido) ou DEFAULT(como no valor não inicializado).

Em muitos casos, essa distinção não importa, mas, se sua coluna c estiver usando algum dos seguintes recursos, a instrução estará simplesmente errada :

  • Tem uma DEFAULTcláusula
  • Pode ser gerado por um gatilho

Voltar para UPDATEdeclarações

Embora o acima seja verdadeiro para todos os bancos de dados, posso garantir que o problema do acionador também é verdadeiro para o banco de dados Oracle. Considere o seguinte 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;

Ao executar o procedimento acima, você verá a seguinte saída:

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 você pode ver, a instrução que sempre atualiza todas as colunas sempre aciona o gatilho para todas as colunas, enquanto as instruções que atualizam apenas as colunas que foram alteradas acionam apenas os gatilhos que estão ouvindo essas alterações específicas.

Em outras palavras:

O comportamento atual do Hibernate que você está descrevendo é incompleto e pode até ser considerado errado na presença de gatilhos (e provavelmente outras ferramentas).

Pessoalmente, acho que seu argumento de otimização de cache de consulta é superestimado no caso de SQL dinâmico. Claro, haverá mais algumas consultas nesse cache e um pouco mais de trabalho de análise a ser feito, mas isso geralmente não é um problema para UPDATEinstruções dinâmicas , muito menos do que para SELECT.

O lote é certamente um problema, mas, na minha opinião, uma única atualização não deve ser normalizada para atualizar todas as colunas apenas porque existe uma pequena possibilidade de a declaração ser recuperável. Provavelmente, o ORM pode coletar sub-lotes de instruções idênticas consecutivas e colocá-las em lote em vez do "lote inteiro" (caso o ORM seja capaz de rastrear a diferença entre "alterado" , "nulo" e "padrão"

Lukas Eder
fonte
O DEFAULTcaso de uso pode ser tratado por @DynamicInsert. A situação do TRIGGER também pode ser resolvida usando cheques como WHEN (NEW.b <> OLD.b)ou apenas mudar para @DynamicUpdate.
Vlad Mihalcea
Sim, as coisas podem ser resolvidas, mas você estava originalmente perguntando sobre desempenho e sua solução alternativa acrescenta ainda mais sobrecarga.
Lukas Eder
Acho que Morgan disse o melhor: é complicado .
18717 Vlad Vladinhalea em 19/06
Eu acho que é bastante simples. Da perspectiva da estrutura, há mais argumentos a favor da padronização do SQL dinâmico. Do ponto de vista do usuário, sim, é complicado.
Lukas Eder
9

Eu acho que a resposta é - é complicado . Tentei escrever uma prova rápida usando uma longtextcoluna no MySQL, mas a resposta é um pouco inconclusiva. Prova primeiro:

# 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

Portanto, há uma pequena diferença de tempo entre lento + valor alterado e lento + sem valor alterado. Então decidi olhar para outra métrica, que era 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)

Portanto, parece que o tempo aumentou, pois é necessário comparar para confirmar que o valor em si não foi modificado, o que, no caso de um texto longo de 1G, leva tempo (porque está dividido em várias páginas). Mas a modificação em si não parece agitar o log de refazer.

Eu suspeito que, se os valores são colunas regulares que estão na página, a comparação adiciona apenas um pouco de sobrecarga. E supondo que a mesma otimização se aplique, estas não são operacionais quando se trata da atualização.

Resposta mais longa

Na verdade, acho que o ORM não deve eliminar colunas que foram modificadas ( mas não alteradas ), pois essa otimização tem efeitos colaterais estranhos.

Considere o seguinte no pseudo-có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

O resultado se o ORM "otimizar" a modificação sem alteração:

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

O resultado se o ORM enviou todas as modificações ao servidor:

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

O caso de teste aqui depende do repeatable-readisolamento (padrão do MySQL), mas também existe uma janela de tempo para o read-committedisolamento em que a leitura da sessão2 ocorre antes do commit da sessão1.

Dito de outra forma: a otimização só é segura se você emitir a SELECT .. FOR UPDATEpara ler as linhas seguidas de uma UPDATE. SELECT .. FOR UPDATEnão usa MVCC e sempre lê a versão mais recente das linhas.


Edit: Verifique se o conjunto de dados do caso de teste estava 100% na memória. Resultados de tempo ajustados.

Morgan Tocker
fonte
Obrigada pelo esclarecimento. Essa é a minha intuição também. Eu acho que o banco de dados irá verificar tanto a linha na página de dados e todos os índices associados. Se a coluna for muito grande ou houver vários índices envolvidos, a sobrecarga poderá se tornar perceptível. Mas para a maioria das situações, ao usar tipos de colunas compactos e apenas os índices necessários, acho que a sobrecarga pode ser menor do que não se beneficiar do cache de instruções ou ter uma menor chance de gerar lotes da instrução.
Vlad Mihalcea
1
@VladMihalcea cuidado que a resposta é sobre o MySQL. As conclusões podem não ser as mesmas em diferentes SGBD.
ypercubeᵀᴹ
@ypercube Estou ciente disso. Tudo depende do RDBMS.
Vlad Mihalcea