Qual è il sovraccarico di aggiornare tutte le colonne, anche quelle che non sono cambiate [chiuso]


17

Quando si tratta di aggiornare una riga, molti strumenti ORM emettono un'istruzione UPDATE che imposta ogni colonna associata a quella particolare entità .

Il vantaggio è che puoi facilmente raggruppare le istruzioni di aggiornamento poiché l' UPDATEistruzione è la stessa, indipendentemente dall'attributo dell'entità che modifichi. Inoltre, puoi anche utilizzare la cache delle istruzioni lato server e lato client.

Quindi, se carico un'entità e imposto solo una singola proprietà:

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

Tutte le colonne verranno modificate:

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

Ora, supponendo che anche sulla titleproprietà sia presente un indice , il DB non dovrebbe rendersi conto che il valore non è cambiato comunque?

In questo articolo , Markus Winand afferma:

L'aggiornamento su tutte le colonne mostra lo stesso modello che abbiamo già osservato nelle sezioni precedenti: il tempo di risposta aumenta con ogni indice aggiuntivo.

Mi chiedo perché sia ​​questo sovraccarico poiché il database carica la pagina di dati associata dal disco in memoria e quindi può capire se un valore di colonna deve essere modificato o meno.

Anche per gli indici, non riequilibrare nulla poiché i valori dell'indice non cambiano per le colonne che non sono cambiate, ma sono state incluse nell'aggiornamento.

È necessario che anche gli indici B + Tree associati alle colonne ridondanti invariate debbano essere esplorati, solo perché il database si renda conto che il valore foglia è sempre lo stesso?

Naturalmente, alcuni strumenti ORM consentono di AGGIORNARE solo le proprietà modificate:

UPDATE post
SET    score = 12,
WHERE  id = 1

Ma questo tipo di UPDATE potrebbe non trarre sempre vantaggio dagli aggiornamenti batch o dalla memorizzazione nella cache delle istruzioni quando vengono modificate proprietà diverse per righe diverse.


1
Se il database PostgreSQL fosse (o alcuni altri che utilizzano MVCC ), una UPDATEè praticamente equivalente a un DELETE+ INSERT(perché in realtà crea un nuovo V ersione della riga). Il sovraccarico è elevato e cresce con il numero di indici , specialmente se molte delle colonne che li compongono sono effettivamente aggiornate e l' albero (o qualsiasi altra cosa) usato per rappresentare l'indice necessita di un cambiamento significativo. Non è il numero di colonne che viene aggiornato ciò che è rilevante, ma se si aggiorna una parte di colonna di un indice.
joanolo,

@joanolo Questo deve essere vero solo per l'implementazione di MVCC da parte di postgres. MySQL, Oracle (e altri) eseguono un aggiornamento e trasferiscono le colonne modificate nello spazio UNDO.
Morgan Tocker,

2
Vorrei sottolineare che un buon ORM dovrebbe tenere traccia delle colonne che devono essere aggiornate e ottimizzare l'istruzione inviata al database. È rilevante, anche se solo per la quantità di dati trasmessi al DB, specialmente se alcune delle colonne sono testi lunghi o BLOB .
joanolo,

1
Domanda che ne discute per SQL Server dba.stackexchange.com/q/114360/3690
Martin Smith,

2
Quale DBMS stai usando?
a_horse_with_no_name,

Risposte:


12

So che sei principalmente preoccupato UPDATEe soprattutto delle prestazioni, ma come collega manutentore di "ORM", lascia che ti dia un'altra prospettiva sul problema della distinzione tra valori "modificati" , "null" e "predefiniti" , che sono tre cose diverse in SQL, ma probabilmente solo una cosa in Java e nella maggior parte degli ORM:

Traducendo la tua logica in INSERTdichiarazioni

Le tue argomentazioni a favore della batchability e della cache delle dichiarazioni sono vere allo stesso modo per le INSERTdichiarazioni che per le UPDATEdichiarazioni. Ma nel caso di INSERTdichiarazioni, omettere una colonna dall'istruzione ha una semantica diversa rispetto a UPDATE. Significa candidarsi DEFAULT. I seguenti due sono semanticamente equivalenti:

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

Questo non è vero per UPDATE, dove i primi due sono semanticamente equivalenti e il terzo ha un significato completamente diverso:

-- 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 maggior parte delle API client del database, incluso JDBC e, di conseguenza, JPA, non consente di associare DEFAULTun'espressione a una variabile di bind, soprattutto perché i server non lo consentono neanche. Se si desidera riutilizzare la stessa istruzione SQL per i suddetti motivi di batchability e cacheability delle istruzioni, utilizzare la seguente istruzione in entrambi i casi (supponendo che (a, b, c)siano presenti tutte le colonne t):

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

E poiché cnon è impostato, probabilmente assoceresti Java nullalla terza variabile di bind, poiché molti ORM non sono in grado di distinguere tra NULLe DEFAULT( jOOQ , ad esempio essendo un'eccezione qui). Vedono solo Java nulle non sanno se ciò significhi NULL(come nel valore sconosciuto) o DEFAULT(come nel valore non inizializzato).

In molti casi, questa distinzione non ha importanza, ma nel caso in cui la colonna c stia utilizzando una delle seguenti funzionalità, l'affermazione è semplicemente errata :

  • Ha una DEFAULTclausola
  • Potrebbe essere generato da un trigger

Torna alle UPDATEdichiarazioni

Mentre quanto sopra è vero per tutti i database, posso assicurarti che il problema del trigger è vero anche per il database Oracle. Considera il seguente 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;

Quando esegui quanto sopra, vedrai il seguente output:

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

Come puoi vedere, l'istruzione che aggiorna sempre tutte le colonne attiverà sempre il trigger per tutte le colonne, mentre le istruzioni che aggiornano solo le colonne che sono state modificate genereranno solo quei trigger che sono in attesa di tali cambiamenti specifici.

In altre parole:

L'attuale comportamento di Hibernate che stai descrivendo è incompleto e potrebbe persino essere considerato errato in presenza di trigger (e probabilmente di altri strumenti).

Personalmente ritengo che l'argomento di ottimizzazione della cache delle query sia sopravvalutato nel caso di SQL dinamico. Certo, ci saranno alcune query in più in tale cache e un po 'più di analisi da fare, ma questo di solito non è un problema per le UPDATEdichiarazioni dinamiche , molto meno di per SELECT.

Il batching è certamente un problema, ma a mio avviso, un singolo aggiornamento non dovrebbe essere normalizzato per aggiornare tutte le colonne solo perché c'è una leggera possibilità che l'istruzione sia accettabile. È probabile che l'ORM possa raccogliere sotto-lotti di istruzioni identiche consecutive e raggruppare quelli invece del "intero lotto" (nel caso in cui l'ORM sia persino in grado di tracciare la differenza tra "cambiato" , "null" e "predefinito"


Il DEFAULTcaso d'uso può essere risolto da @DynamicInsert. La situazione di TRIGGER può anche essere affrontata usando controlli come WHEN (NEW.b <> OLD.b)o semplicemente passare a @DynamicUpdate.
Vlad Mihalcea,

Sì, le cose possono essere risolte, ma originariamente chiedevi delle prestazioni e la tua soluzione alternativa aumenta ulteriormente le spese generali.
Lukas Eder,

Penso che Morgan l'abbia detto meglio: è complicato .
Vlad Mihalcea,

Penso che sia piuttosto semplice. Dal punto di vista del framework, ci sono più argomenti a favore del default su SQL dinamico. Dal punto di vista dell'utente, sì, è complicato.
Lukas Eder,

9

Penso che la risposta sia: è complicata . Ho provato a scrivere una prova rapida usando una longtextcolonna in MySQL, ma la risposta è un po 'inconcludente. Prova prima:

# 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

Quindi c'è una piccola differenza di tempo tra lento + valore modificato e lento + nessun valore modificato. Così ho deciso di guardare un'altra metrica, che era le pagine scritte:

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)

Quindi sembra che il tempo sia aumentato perché ci deve essere un confronto per confermare che il valore stesso non è stato modificato, che nel caso di un longtext 1G richiede tempo (perché è suddiviso su più pagine). Ma la modifica stessa non sembra sfocare nel registro di ripetizione.

Ho il sospetto che se i valori sono colonne regolari che sono in-page il confronto aggiunge solo un piccolo overhead. E supponendo che si applichi la stessa ottimizzazione, questi non sono operativi quando si tratta dell'aggiornamento.

Risposta più lunga

In realtà penso che l'ORM non dovrebbe eliminare le colonne che sono state modificate ( ma non modificate ), poiché questa ottimizzazione ha strani effetti collaterali.

Considera quanto segue nello pseudo codice:

# 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

Il risultato se l'ORM dovesse "ottimizzare" la modifica senza modifiche:

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

Il risultato se l'ORM ha inviato tutte le modifiche al server:

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

Il test-case qui si basa repeatable-readsull'isolamento (impostazione predefinita di MySQL), ma esiste anche una finestra temporale per l' read-committedisolamento in cui si verifica la lettura di session2 prima del commit di session1.

Per dirla in altro modo: l'ottimizzazione è sicura solo se si emette un SELECT .. FOR UPDATEper leggere le righe seguite da un UPDATE. SELECT .. FOR UPDATEnon utilizza MVCC e legge sempre l'ultima versione delle righe.


Modifica: assicurarsi che il set di dati del test case fosse al 100% in memoria. Risultati di temporizzazione regolati.


Grazie per la spiegazione. Anche questa è la mia intuizione. Penso che il DB controllerà sia la riga nella pagina di dati sia tutti gli indici associati. Se la colonna è molto grande o ci sono tonnellate di indici coinvolti, l'overhead potrebbe diventare evidente. Ma per la maggior parte delle situazioni, quando si utilizzano tipi di colonna compatti e tutti gli indici necessari, immagino che l'overhead potrebbe essere meno che non trarre vantaggio dalla memorizzazione nella cache delle istruzioni o avere una probabilità inferiore di raggruppare le istruzioni.
Vlad Mihalcea,

1
@VladMihalcea attenzione che la risposta riguarda MySQL. Le conclusioni potrebbero non essere le stesse in diversi DBMS.
ypercubeᵀᴹ

@ypercube Ne sono consapevole. Tutto dipende dall'RDBMS.
Vlad Mihalcea,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.