Come aggiornare più di 10 milioni di righe nella singola tabella MySQL il più velocemente possibile?


32

Utilizzo di MySQL 5.6 con motore di archiviazione InnoDB per la maggior parte delle tabelle. La dimensione del pool di buffer InnoDB è di 15 GB e gli indici Innodb DB + sono di circa 10 GB. Il server ha 32 GB di RAM ed esegue Cent OS 7 x64.

Ho un grande tavolo che contiene circa 10 milioni di record.

Ricevo un file di dump aggiornato da un server remoto ogni 24 ore. Il file è in formato CSV. Non ho il controllo su quel formato. Il file è ~ 750 MB. Ho provato a inserire i dati in una tabella MyISAM riga per riga e ci sono voluti 35 minuti.

Ho bisogno di prendere solo 3 valori per riga su 10-12 dal file e aggiornarlo nel database.

Qual è il modo migliore per ottenere qualcosa del genere?

Devo farlo ogni giorno.

Attualmente Flow è così:

  1. mysqli_begin_transaction
  2. Leggi il file di dump riga per riga
  3. Aggiorna ogni record riga per riga.
  4. mysqli_commit

Il completamento delle operazioni sopra richiede circa 30-40 minuti e, mentre lo faccio, ci sono altri aggiornamenti in corso che mi danno

Timeout attesa blocco superato; prova a riavviare la transazione

Aggiornamento 1

caricamento dei dati in una nuova tabella utilizzando LOAD DATA LOCAL INFILE. In MyISAM ci sono voluti 38.93 secmentre in InnoDB ci sono voluti 7 minuti 5,21 sec. Quindi ho fatto:

UPDATE table1 t1, table2 t2
SET 
t1.field1 = t2.field1,
t1.field2 = t2.field2,
t1.field3 = t2.field3
WHERE t1.field10 = t2.field10

Query OK, 434914 rows affected (22 hours 14 min 47.55 sec)

Aggiornamento 2

stesso aggiornamento con query di join

UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4

(14 hours 56 min 46.85 sec)

Chiarimenti da domande nei commenti:

  • Circa il 6% delle righe nella tabella verrà aggiornato dal file, ma a volte può arrivare fino al 25%.
  • Ci sono indici sui campi in fase di aggiornamento. Ci sono 12 indici sulla tabella e 8 indici includono i campi di aggiornamento.
  • Non è necessario eseguire l'aggiornamento in una transazione. Può richiedere del tempo ma non più di 24 ore. Sto cercando di farlo in 1 ora senza bloccare l'intera tabella, poiché in seguito devo aggiornare l'indice della sfinge che dipende da questa tabella. Non importa se i passaggi richiedono una durata maggiore purché il database sia disponibile per altre attività.
  • Potrei modificare il formato CSV in una fase di preelaborazione. L'unica cosa che conta è l'aggiornamento rapido e senza blocco.
  • La tabella 2 è MyISAM. È la tabella appena creata dal file CSV che utilizza il file di caricamento dei dati. La dimensione del file MYI è 452 MB. La tabella 2 è indicizzata nella colonna field1.
  • MYD della tabella MyISAM è 663 MB.

Aggiornamento 3:

ecco maggiori dettagli su entrambe le tabelle.

CREATE TABLE `content` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `og_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `keywords` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `files_count` smallint(5) unsigned NOT NULL DEFAULT '0',
  `more_files` smallint(5) unsigned NOT NULL DEFAULT '0',
  `files` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '0',
  `category` smallint(3) unsigned NOT NULL DEFAULT '600',
  `size` bigint(19) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) NOT NULL DEFAULT '0',
  `completed` int(11) NOT NULL DEFAULT '0',
  `uploaders` int(11) NOT NULL DEFAULT '0',
  `creation_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `upload_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `last_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `vote_up` int(11) unsigned NOT NULL DEFAULT '0',
  `vote_down` int(11) unsigned NOT NULL DEFAULT '0',
  `comments_count` int(11) NOT NULL DEFAULT '0',
  `imdb` int(8) unsigned NOT NULL DEFAULT '0',
  `video_sample` tinyint(1) NOT NULL DEFAULT '0',
  `video_quality` tinyint(2) NOT NULL DEFAULT '0',
  `audio_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `subtitle_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `uploader` int(11) unsigned NOT NULL DEFAULT '0',
  `anonymous` tinyint(1) NOT NULL DEFAULT '0',
  `enabled` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `tfile_size` int(11) unsigned NOT NULL DEFAULT '0',
  `scrape_source` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `record_num` int(11) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`record_num`),
  UNIQUE KEY `hash` (`hash`),
  KEY `uploaders` (`uploaders`),
  KEY `tfile_size` (`tfile_size`),
  KEY `enabled_category_upload_date_verified_` (`enabled`,`category`,`upload_date`,`verified`),
  KEY `enabled_upload_date_verified_` (`enabled`,`upload_date`,`verified`),
  KEY `enabled_category_verified_` (`enabled`,`category`,`verified`),
  KEY `enabled_verified_` (`enabled`,`verified`),
  KEY `enabled_uploader_` (`enabled`,`uploader`),
  KEY `anonymous_uploader_` (`anonymous`,`uploader`),
  KEY `enabled_uploaders_upload_date_` (`enabled`,`uploaders`,`upload_date`),
  KEY `enabled_verified_category` (`enabled`,`verified`,`category`),
  KEY `verified_enabled_category` (`verified`,`enabled`,`category`)
) ENGINE=InnoDB AUTO_INCREMENT=7551163 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=FIXED


CREATE TABLE `content_csv_dump_temp` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `category_id` int(11) unsigned NOT NULL DEFAULT '0',
  `uploaders` int(11) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) unsigned NOT NULL DEFAULT '0',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`hash`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

e qui è la query di aggiornamento che aggiorna la contenttabella utilizzando i dati dacontent_csv_dump_temp

UPDATE content a JOIN content_csv_dump_temp b 
ON a.hash = b.hash 
SET 
a.uploaders = b.uploaders,
a.downloaders = b.downloaders,
a.verified = b.verified

aggiornamento 4:

tutti i test sopra riportati sono stati eseguiti sulla macchina di prova, ma ora ho fatto gli stessi test sulla macchina di produzione e le query sono molto veloci.

mysql> UPDATE content_test a JOIN content_csv_dump_temp b
    -> ON a.hash = b.hash
    -> SET
    -> a.uploaders = b.uploaders,
    -> a.downloaders = b.downloaders,
    -> a.verified = b.verified;
Query OK, 2673528 rows affected (7 min 50.42 sec)
Rows matched: 7044818  Changed: 2673528  Warnings: 0

mi scuso per il mio errore. È meglio usare join invece di ogni aggiornamento del record. ora sto cercando di migliorare mpre usando l'indice suggerito da rick_james, aggiornerò una volta eseguito il benchmarking.


Hai un composito INDEX(field2, field3, field4) (in qualsiasi ordine)? Per favore, mostracelo SHOW CREATE TABLE.
Rick James,

1
12 e 8 indici sono una parte seria del tuo problema. MyISAM è un'altra parte seria. InnoDB o TokuDB funzionano molto meglio con più indici.
Rick James,

Ne hai due diversi UPDATEs . Per favore, dicci esattamente come appare l'istruzione semplice per l'aggiornamento della tabella dai dati CSV. Quindi potremmo essere in grado di aiutarti a ideare una tecnica che soddisfi le tue esigenze.
Rick James,

@RickJames ce n'è solo uno update, e per favore controlla la domanda aggiornata., Grazie
AMB

Risposte:


17

In base alla mia esperienza, utilizzerei LOAD DATA INFILE per importare il tuo file CSV.

L'istruzione LOAD DATA INFILE legge le righe da un file di testo in una tabella a una velocità molto elevata.

Esempio che ho trovato su Internet Esempio di caricamento dei dati . Ho provato questo esempio sulla mia scatola e ha funzionato bene

Tabella di esempio

CREATE TABLE example (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `Column2` varchar(14) NOT NULL,
  `Column3` varchar(14) NOT NULL,
  `Column4` varchar(14) NOT NULL,
  `Column5` DATE NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB

Esempio di file CSV

# more /tmp/example.csv
Column1,Column2,Column3,Column4,Column5
1,A,Foo,sdsdsd,4/13/2013
2,B,Bar,sdsa,4/12/2013
3,C,Foo,wewqe,3/12/2013
4,D,Bar,asdsad,2/1/2013
5,E,FOObar,wewqe,5/1/2013

Dichiarazione di importazione da eseguire dalla console MySQL

LOAD DATA LOCAL INFILE '/tmp/example.csv'
    -> INTO TABLE example
    -> FIELDS TERMINATED BY ','
    -> LINES TERMINATED BY '\n'
    -> IGNORE 1 LINES
    -> (id, Column3,Column4, @Column5)
    -> set
    -> Column5 = str_to_date(@Column5, '%m/%d/%Y');

Risultato

MySQL [testcsv]> select * from example;
+----+---------+---------+---------+------------+
| Id | Column2 | Column3 | Column4 | Column5    |
+----+---------+---------+---------+------------+
|  1 |         | Column2 | Column3 | 0000-00-00 |
|  2 |         | B       | Bar     | 0000-00-00 |
|  3 |         | C       | Foo     | 0000-00-00 |
|  4 |         | D       | Bar     | 0000-00-00 |
|  5 |         | E       | FOObar  | 0000-00-00 |
+----+---------+---------+---------+------------+

IGNORA ignora semplicemente la prima riga che sono le intestazioni di colonna.

Dopo IGNORA, stiamo specificando le colonne (ignorando la colonna2), da importare, che corrisponde a uno dei criteri della tua domanda.

Ecco un altro esempio direttamente da Oracle: esempio LOAD DATA INFILE

Questo dovrebbe essere sufficiente per iniziare.


potrei usare i dati di caricamento per caricare i dati nella tabella temporanea e quindi utilizzare altre query per aggiornarli nella tabella principale., grazie
AMB

14

Alla luce di tutte le cose menzionate, sembra che il collo di bottiglia sia l'unione stessa.

ASPETTO 1: Unisci dimensioni buffer

Con ogni probabilità, il tuo join_buffer_size è probabilmente troppo basso.

Secondo la documentazione MySQL su come MySQL utilizza la cache del buffer di join

Conserviamo solo le colonne utilizzate nel buffer di join, non le intere righe.

In questo caso, le chiavi del buffer di join rimangono nella RAM.

Hai 10 milioni di righe per 4 byte per ogni chiave. Sono circa 40 milioni.

Prova a aumentarlo nella sessione a 42 M (un po 'più grande di 40 M)

SET join_buffer_size = 1024 * 1024 * 42;
UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4;

In questo caso, procedi ad aggiungerlo a my.cnf

[mysqld]
join_buffer_size = 42M

Riavvio di mysqld non richiesto per le nuove connessioni. Corri

mysql> SET GLOBAL join_buffer_size = 1024 * 1024 * 42;

ASPETTO 2: Operazione di join

È possibile manipolare lo stile dell'operazione di join modificando l'ottimizzatore

Secondo la documentazione MySQL su Block Nested-Loop e Batched Key Access Join

Quando viene utilizzato BKA, il valore di join_buffer_size definisce la dimensione del batch di chiavi in ​​ciascuna richiesta al motore di archiviazione. Maggiore è il buffer, maggiore sarà l'accesso sequenziale alla tabella di destra di un'operazione di join, che può migliorare significativamente le prestazioni.

Per utilizzare BKA, il flag batched_key_access della variabile di sistema optimizer_switch deve essere attivato. BKA usa MRR, quindi anche la bandiera mrr deve essere attiva. Attualmente, la stima dei costi per MRR è troppo pessimistica. Quindi, è anche necessario che mrr_cost_based sia disattivato per poter usare BKA.

Questa stessa pagina consiglia di farlo:

mysql> SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

ASPETTO 3: Scrivere aggiornamenti su disco (OPZIONALE)

La maggior parte dimentica di aumentare innodb_write_io_threads per scrivere più rapidamente pagine sporche dal pool di buffer.

[mysqld]
innodb_write_io_threads = 16

Dovrai riavviare MySQL per questa modifica

PROVACI !!!


Bello! +1 per la punta del buffer di join sintonizzabile. Se devi unirti, unisciti in memoria. Bel consiglio!
Peter Dixon-Moses,

3
  1. CREATE TABLE che corrisponde al CSV
  2. LOAD DATA in quel tavolo
  3. UPDATE real_table JOIN csv_table ON ... SET ..., ..., ...;
  4. DROP TABLE csv_table;

Il passaggio 3 sarà molto più veloce di riga per riga, ma bloccherà comunque tutte le righe della tabella per un periodo di tempo non banale. Se questo tempo di blocco è più importante di quanto tempo impiega l'intero processo, ...

Se nient'altro sta scrivendo sul tavolo, allora ...

  1. CREATE TABLEche corrisponde al CSV; senza indici ad eccezione di ciò che è necessario nel JOINnel UPDATE. Se unico, fallo PRIMARY KEY.
  2. LOAD DATA in quel tavolo
  3. copia il real_tablein new_table( CREATE ... SELECT)
  4. UPDATE new_table JOIN csv_table ON ... SET ..., ..., ...;
  5. RENAME TABLE real_table TO old, new_table TO real_table;
  6. DROP TABLE csv_table, old;

Il passaggio 3 è più veloce dell'aggiornamento, soprattutto se gli indici non necessari vengono lasciati fuori.
Il passaggio 5 è "istantaneo".


diciamo nel secondo esempio, dopo il passaggio 3, stiamo facendo il passaggio 4, quindi i nuovi dati vengono inseriti nella tabella reale in modo che ci manchino quei dati nella tabella nuova? che cosa è una soluzione alternativa per questo? grazie
AMB il

Vedi cosa pt-online-schema-digest; si occupa di tali problemi tramite a TRIGGER.
Rick James,

Probabilmente non è necessario alcun indice sulla tabella da LOAD DATA. L'aggiunta di indici non necessari è costosa (nel tempo).
Rick James,

Sulla base delle ultime informazioni, mi sto appoggiando al file CSV che viene caricato in una tabella MyISAM con solo una AUTO_INCREMENT, quindi a pezzi 1K righe alla volta in base al PK. Ma devo vedere tutti i requisiti e lo schema della tabella prima di provare a precisare i dettagli.
Rick James,

ho impostato l'hash come PRIMARY index, ma mentre si esegue il chunking in 50k usando la query degli ordini richiede più tempo., sarebbe meglio se creo l'incremento automatico? e impostarlo come PRIMARY index?
AMB il

3

Hai detto:

  • Gli aggiornamenti riguardano il 6-25% della tabella
  • Vuoi farlo il più velocemente possibile (<1 ora)
  • senza bloccaggio
  • non deve essere in una singola transazione
  • eppure (nel commento sulla risposta di Rick James), esprimi preoccupazione per le condizioni di gara

Molte di queste affermazioni possono essere contraddittorie. Ad esempio, aggiornamenti di grandi dimensioni senza bloccare la tabella. O evitando le condizioni di gara senza usare una transazione gigante.

Inoltre, poiché la tabella è fortemente indicizzata, sia gli inserimenti che gli aggiornamenti possono essere lenti.


Evitare le condizioni di gara

Se riesci ad aggiungere un timestamp aggiornato al tuo tavolo, puoi risolvere le condizioni di gara evitando anche di registrare mezzo milione di aggiornamenti in una singola transazione.

Ciò consente di eseguire aggiornamenti riga per riga (come avviene attualmente), ma con autocommit o batch di transazioni più ragionevoli.

Si evitano le condizioni di gara (durante l'aggiornamento riga per riga) eseguendo un controllo che non si è già verificato un aggiornamento successivo ( UPDATE ... WHERE pk = [pk] AND updated < [batchfile date])

E, soprattutto, questo ti consente di eseguire aggiornamenti paralleli .


Correre il più velocemente possibile —Parallelizzare

Con questo timestamp, controlla ora sul posto:

  1. Dividi il tuo file batch in alcuni blocchi di dimensioni ragionevoli (diciamo 50.000 righe / file)
  2. Parallelamente, leggi uno script in ciascun file e genera un file con 50.000 istruzioni UPDATE.
  3. Parallelamente, una volta terminato (2), sono stati mysqleseguiti tutti i file sql.

(ad esempio in bashsguardo splite xargs -Pdi modi per facilmente eseguire un comando per molti versi parallela. Grado di parallelismo dipende dal numero di discussioni si è disposti a dedicare al update )


Tieni presente che "riga per riga" è probabilmente 10 volte più lento rispetto a fare le cose in lotti di almeno 100.
Rick James,

Dovresti confrontarlo in questo caso per essere sicuro. Aggiornando il 6-25% di una tabella (con 8 indici coinvolti con le colonne aggiornate), mi piacerebbe avere la possibilità che la manutenzione dell'indice diventi il ​​collo di bottiglia.
Peter Dixon-Moses,

Voglio dire, in alcuni casi potrebbe essere più veloce rilasciare indici, aggiornare in blocco e ricrearli dopo ... ma OP non vuole tempi di inattività.
Peter Dixon-Moses,

1

Gli aggiornamenti di grandi dimensioni sono associati all'I / O. Suggerirei:

  1. Crea una tabella distinta che memorizzerà i tuoi 3 campi aggiornati di frequente. Chiamiamo una tabella assets_static dove conservi, beh, i dati statici e le altre assets_dynamic che memorizzeranno caricatori, downloader e verificati.
  2. Se possibile, utilizzare il motore MEMORY per la tabella assets_dynamic . (backup su disco dopo ogni aggiornamento).
  3. Aggiorna asset_dynamic leggero e agile come da aggiornamento 4 (ad esempio LOAD INFILE ... INTO temp; UPDATE assets_dynamic a JOIN temp b su a.id = b.id SET [cosa deve essere aggiornato]. Questo dovrebbe richiedere meno di un minuto (sul nostro sistema, assets_dynamic ha 95 milioni di righe e gli aggiornamenti hanno un impatto di circa 6 milioni di righe, in poco più di 40 secondi ).
  4. Quando esegui l'indicizzatore di Sphinx, JOIN assets_static e assets_dynamic (supponendo che tu voglia utilizzare uno di questi campi come attributo).

0

Per UPDATEcorrere veloce, è necessario

INDEX(uploaders, downloaders, verified)

Può essere su entrambi i tavoli. I tre campi possono essere in qualsiasi ordine.

Ciò faciliterà la UPDATEpossibilità di abbinare rapidamente le righe tra le due tabelle.

E rendere i tipi di dati uguali nelle due tabelle (entrambi INT SIGNEDo entrambi INT UNSIGNED).


questo ha effettivamente rallentato l'aggiornamento.
AMB

Hmmm ... Per favore, forniscilo EXPLAIN UPDATE ...;.
Rick James,
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.