Comment mettre à jour plus de 10 millions de lignes de la table unique MySQL le plus rapidement possible?


32

Utiliser MySQL 5.6 avec le moteur de stockage InnoDB pour la plupart des tables. La taille du pool de mémoire tampon InnoDB est de 15 Go et les index Innodb DB + d'environ 10 Go. Le serveur dispose de 32 Go de RAM et exécute Cent OS 7 x64.

J'ai une grande table qui contient environ 10 millions de disques.

Je reçois un fichier de vidage mis à jour d'un serveur distant toutes les 24 heures. Le fichier est au format csv. Je n'ai pas de contrôle sur ce format. Le fichier est ~ 750 MB. J'ai essayé d'insérer des données dans une table MyISAM ligne par ligne et cela a pris 35 minutes.

Je dois prendre seulement 3 valeurs par ligne sur 10-12 du fichier et le mettre à jour dans la base de données.

Quel est le meilleur moyen de réaliser quelque chose comme ça?

J'ai besoin de faire ça tous les jours.

Actuellement, Flow est comme ça:

  1. mysqli_begin_transaction
  2. Lire le fichier de vidage ligne par ligne
  3. Mettez à jour chaque enregistrement ligne par ligne.
  4. mysqli_commit

Les opérations ci-dessus prennent environ 30 à 40 minutes et, ce faisant, d’autres mises à jour sont en cours, ce qui me permet

Délai d'attente de verrouillage dépassé; essayez de redémarrer la transaction

Mise à jour 1

Chargement des données dans une nouvelle table avec LOAD DATA LOCAL INFILE. Dans MyISAM, il a 38.93 secfallu 7 minutes à 5,21 secondes pour se rendre à InnoDB. Puis j'ai fait:

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)

Mise à jour 2

même mise à jour avec requête de jointure

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)

Clarifications des questions dans les commentaires:

  • Environ 6% des lignes de la table seront mises à jour par le fichier, mais cela peut parfois atteindre 25%.
  • Il y a des index sur les champs en cours de mise à jour. La table contient 12 index et 8 index incluent les champs de mise à jour.
  • Il n'est pas nécessaire de faire la mise à jour en une seule transaction. Cela peut prendre du temps mais pas plus de 24 heures. Je cherche à le faire en 1 heure sans verrouiller l'ensemble de la table, car je devrai ensuite mettre à jour l'index sphinx, qui dépend de cette table. Peu importe si les étapes durent plus longtemps tant que la base de données est disponible pour d'autres tâches.
  • Je pourrais modifier le format CSV dans une étape de prétraitement. La seule chose qui compte est la mise à jour rapide et sans verrouillage.
  • Le tableau 2 est MyISAM. C'est la table nouvellement créée à partir d'un fichier csv utilisant load data infile. La taille du fichier MYI est de 452 Mo. Le tableau 2 est indexé sur la colonne champ1.
  • MYD de la table MyISAM est de 663 Mo.

Mise à jour 3:

voici plus de détails sur les deux tables.

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

et voici la requête de mise à jour qui met à jour la contenttable en utilisant les données decontent_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

mise à jour 4:

Tous les tests ci-dessus ont été effectués sur une machine de test., mais maintenant, j'ai effectué les mêmes tests sur la machine de production et les requêtes sont très rapides.

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

Je m'excuse pour mon erreur. Il est préférable d’utiliser join au lieu de chaque mise à jour d’enregistrement. Maintenant, j'essaye d'améliorer mpre en utilisant l'index suggéré par rick_james, je le mettrai à jour une fois le benchmark terminé.


Avez-vous un composite INDEX(field2, field3, field4) (dans n'importe quel ordre)? S'il vous plaît nous montrer SHOW CREATE TABLE.
Rick James

1
Les index 12 et 8 font partie intégrante de votre problème. MyISAM est une autre partie sérieuse. InnoDB ou TokuDB fonctionnent beaucoup mieux avec plusieurs index.
Rick James

Vous avez deux différents UPDATEs . Dites-nous exactement à quoi ressemble une déclaration simple pour la mise à jour de la table à partir des données CSV. Ensuite, nous pourrons peut-être vous aider à concevoir une technique qui réponde à vos exigences.
Rick James

@ RickJames, il n'y en a qu'un update, et s'il vous plaît vérifier la question mise à jour., Merci
AMB

Réponses:


17

D'après mon expérience, j'utiliserais LOAD DATA INFILE pour importer votre fichier CSV.

L'instruction LOAD DATA INFILE lit les lignes d'un fichier texte dans une table à une vitesse très élevée.

Exemple trouvé sur Internet Exemple de chargement de données . J'ai testé cet exemple sur ma boîte et j'ai bien travaillé

Exemple de table

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

Exemple de fichier 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

Déclaration d'importation à exécuter depuis la 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');

Résultat

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 |
+----+---------+---------+---------+------------+

IGNORE ignore simplement la première ligne qui sont des en-têtes de colonnes.

Après IGNORE, nous spécifions les colonnes (en ignorant la colonne2) à importer, qui correspondent à l'un des critères de votre question.

Voici un autre exemple directement d'Oracle: Exemple LOAD DATA INFILE

Cela devrait être suffisant pour vous aider à démarrer.


Je pourrais utiliser des données de chargement pour charger des données dans une table temporaire, puis utiliser d'autres requêtes pour la mettre à jour dans une table principale., merci
AMB

14

À la lumière de toutes les choses mentionnées, il semble que le goulot d'étranglement est le joint lui-même.

ASPECT # 1: Taille du tampon de jointure

Probablement , votre join_buffer_size est probablement trop basse.

Selon la documentation de MySQL sur la manière dont MySQL utilise le cache de tampon de jointure

Nous ne stockons que les colonnes utilisées dans le tampon de jointure, pas les lignes entières.

Ceci étant, faites en sorte que les clés du tampon de jointure restent dans la RAM.

Vous avez 10 millions de lignes fois 4 octets pour chaque clé. C'est à peu près 40M.

Essayez d'augmenter la séance à 42M (un peu plus de 40M)

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;

Si cela vous convient, ajoutez-le à my.cnf

[mysqld]
join_buffer_size = 42M

Le redémarrage de mysqld n'est pas nécessaire pour les nouvelles connexions. Il suffit de courir

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

ASPECT # 2: Opération de jointure

Vous pouvez manipuler le style de l'opération de jointure en modifiant l'optimiseur.

Selon la documentation MySQL sur les blocages de jointures imbriquées en boucle imbriquée et en lot

Lorsque BKA est utilisé, la valeur join_buffer_size définit la taille du lot de clés dans chaque requête adressée au moteur de stockage. Plus la mémoire tampon est grande, plus l'accès séquentiel sera grand au tableau de droite d'une opération de jointure, ce qui peut améliorer considérablement les performances.

Pour que BKA soit utilisé, l'indicateur batched_key_access de la variable système optimizer_switch doit être définie sur on. BKA utilise MRR, le drapeau mrr doit donc également être activé. Actuellement, l’estimation des coûts pour le MRR est trop pessimiste. Par conséquent, il est également nécessaire que mrr_cost_based soit désactivé pour que BKA soit utilisé.

Cette même page recommande de faire ceci:

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

ASPECT # 3: Ecriture des mises à jour sur le disque (FACULTATIF)

La plupart oublient d’augmenter innodb_write_io_threads pour écrire plus rapidement les pages non conformes du pool de mémoire tampon.

[mysqld]
innodb_write_io_threads = 16

Vous devrez redémarrer MySQL pour ce changement

ESSAIE !!!


Agréable! +1 pour la pointe de tampon de jointure ajustable. Si vous devez vous joindre, rejoignez en mémoire. Bon conseil!
Peter Dixon-Moses

3
  1. CREATE TABLE qui correspond au CSV
  2. LOAD DATA dans cette table
  3. UPDATE real_table JOIN csv_table ON ... SET ..., ..., ...;
  4. DROP TABLE csv_table;

L'étape 3 sera beaucoup plus rapide que ligne par ligne, mais toutes les lignes de la table seront verrouillées pour une durée non négligeable. Si ce temps de verrouillage est plus important que la durée totale du processus, ...

Si rien d'autre n'écrit sur la table, alors ...

  1. CREATE TABLEqui correspond au CSV; pas d'index sauf ce qui est nécessaire JOINdans le UPDATE. Si unique, faites-lePRIMARY KEY .
  2. LOAD DATA dans cette table
  3. copier le real_tableà 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;

L'étape 3 est plus rapide que la mise à jour, surtout si les index inutiles sont laissés.
L'étape 5 est "instantanée".


Par exemple, en secondes, après l’étape 3, nous passons à l’étape 4, puis les nouvelles données sont insérées dans la table real_table et nous manquerons donc ces données dans la table new_table? Quelle est la solution de contournement pour cela? merci
AMB

Voir ce que pt-online-schema-digest; il prend en charge de telles questions via un TRIGGER.
Rick James

Vous n'avez probablement pas besoin d'index sur la table LOAD DATA. L'ajout d'index inutiles est coûteux (à temps).
Rick James

Sur la base des dernières informations, je me penche vers le fichier CSV en cours de chargement dans une table MyISAM avec juste un AUTO_INCREMENT, puis tronquant des rangées de 1K à la fois en fonction du PK. Mais il me faut voir toutes les exigences et le schéma de la table avant d'essayer de préciser les détails.
Rick James

J'ai défini le hachage en tant que PRIMARY index, mais bien que découper dans 50k à l'aide d'une requête d'ordre prenne plus de temps, serait-il préférable que je crée une incrémentation automatique? et le définir comme PRIMARY index?
AMB

3

Vous avez dit:

  • Les mises à jour affectent 6 à 25% de votre table
  • Vous voulez le faire aussi vite que possible (<1h)
  • sans verrouillage
  • il ne doit pas être dans une seule transaction
  • pourtant (dans le commentaire sur la réponse de Rick James), vous exprimez votre inquiétude à propos des conditions de course

Beaucoup de ces déclarations peuvent être contradictoires. Par exemple, les mises à jour volumineuses ne verrouillent pas la table. Ou éviter les conditions de course sans utiliser une transaction géante.

De plus, comme votre table est fortement indexée, les insertions et les mises à jour peuvent être lentes.


Eviter les conditions de course

Si vous pouvez ajouter un horodatage mis à jour à votre table, vous pouvez résoudre le problème des conditions de concurrence tout en évitant de consigner un demi-million de mises à jour en une seule transaction.

Cela vous libère pour effectuer des mises à jour ligne par ligne (comme vous le faites actuellement), mais avec des lots de transactions autocommit ou plus raisonnables.

Vous évitez les situations de concurrence (lors de la mise à jour ligne par ligne) en vérifiant qu’une mise à jour ultérieure n’a pas déjà eu lieu (UPDATE ... WHERE pk = [pk] AND updated < [batchfile date] )

Et surtout, cela vous permet d'exécuter des mises à jour parallèles .


Courir aussi vite que possible —Parallélisation

Avec cette vérification d'horodatage maintenant en place:

  1. Divisez votre fichier de commandes en plusieurs morceaux de taille raisonnable (par exemple, 50 000 lignes / fichiers)
  2. En parallèle, faites lire un script dans chaque fichier et créez un fichier contenant 50 000 instructions UPDATE.
  3. En parallèle, une fois (2) terminé, mysqlexécutez chaque fichier SQL.

(par exemple bashregarder splitet xargs -Pdes façons de facilement exécuter une commande en parallèle de nombreuses façons. Le degré de parallélisme dépend du nombre de threads que vous êtes prêt à consacrer à la mise à jour )


Gardez à l'esprit que "ligne par ligne" sera probablement 10 fois plus lent que de faire des choses par lots d'au moins 100.
Rick James

Dans ce cas, vous devrez le comparer pour en être sûr. En mettant à jour 6 à 25% d'une table (avec 8 index impliqués dans les colonnes mises à jour), je m'interroge sur la possibilité que la maintenance de l'index devienne le goulot d'étranglement.
Peter Dixon-Moses

Je veux dire, dans certains cas, il peut être plus rapide de supprimer des index, de mettre à jour en masse et de les recréer après ... mais OP ne veut pas de temps mort.
Peter Dixon-Moses

1

Les mises à jour volumineuses sont liées aux entrées / sorties. Je voudrais suggerer:

  1. Créez une table distincte qui stockera vos 3 champs fréquemment mis à jour. Appelons une table assets_static où vous conservez, ainsi, des données statiques, et l’autre assets_dynamic qui stockera les téléchargeurs, les téléchargeurs et les vérifiés.
  2. Si vous le pouvez, utilisez le moteur MEMORY pour la table assets_dynamic . (sauvegarde sur disque après chaque mise à jour).
  3. Mettez à jour votre asset_dynamic léger et agile conformément à votre mise à jour 4 (c'est-à-dire LOAD INFILE ... INTO temp; UPDATE assets_dynamic a JOIN temp b sur a.id = b.id SET [ce qui doit être mis à jour]. Cela devrait prendre moins d'une minute (sur notre système, assets_dynamic a 95 millions de lignes et les mises à jour ont un impact sur environ 6 millions de lignes, sur un peu plus de 40 secondes.)
  4. Lorsque vous exécutez l'indexeur de Sphinx, JOIN assets_static et assets_dynamic (en supposant que vous souhaitiez utiliser l'un de ces champs en tant qu'attribut).

0

Pour le UPDATEfaire vite, il faut

INDEX(uploaders, downloaders, verified)

Cela peut être sur l'une ou l'autre table. Les trois champs peuvent être dans n'importe quel ordre.

Cela facilitera la UPDATEpossibilité de faire correspondre rapidement les lignes entre les deux tables.

Et rendre les types de données identiques dans les deux tables (les deux INT SIGNEDou les deux INT UNSIGNED).


cela a en fait ralenti la mise à jour.
AMB

Hmmm ... S'il vous plaît fournir EXPLAIN UPDATE ...;.
Rick James
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.