web-dev-qa-db-fra.com

Comment mettre à jour plus de 10 millions de lignes dans une seule table MySQL aussi rapidement que possible?

Utilisation de 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 + sont d'environ 10 Go. Le serveur a 32 Go RAM et exécute Cent OS 7 x64.

J'ai une grande table qui contient environ 10 millions + d'enregistrements.

J'obtiens un fichier de vidage mis à jour d'un serveur distant toutes les 24 heures. Le fichier est au format csv. Je n'ai aucun contrôle sur ce format. Le fichier fait ~ 750 Mo. 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 dans le fichier et le mettre à jour dans la base de données.

Quelle est la meilleure façon de réaliser quelque chose comme ça?

Je dois le faire quotidiennement.

Actuellement, Flow est comme ceci:

  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 -40 minutes pour terminer et en faisant cela, il y a d'autres mises à jour en cours qui me donnent

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

Mise à jour 1

chargement des données dans une nouvelle table à l'aide de LOAD DATA LOCAL INFILE. Dans MyISAM, il a fallu 38.93 sec tandis que dans InnoDB, il a fallu 7 min 5.21 sec. Ensuite, 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)

Update 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 du tableau seront mises à jour par le fichier, mais parfois elles peuvent atteindre 25%.
  • Il existe des index sur les champs en cours de mise à jour. Il y a 12 index sur la table 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 toute la table, car plus tard, je dois 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 une mise à jour rapide et sans verrouillage.
  • Le tableau 2 est MyISAM. Il s'agit de la table nouvellement créée à partir du fichier csv en utilisant un fichier de données de chargement. La taille du fichier MYI est de 452 Mo. Le tableau 2 est indexé sur la colonne field1.
  • 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 table content en utilisant les données de content_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 la machine de test., mais maintenant j'ai fait 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 vaut mieux 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 mettrai à jour une fois le benchmark effectué.

35
AMB

Sur la base de 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 un tableau à une vitesse très élevée.

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

Exemple de tableau

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

Instruction d'importation à exécuter à partir de 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 colonne.

Après IGNORE, nous spécifions les colonnes (sauter 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 suffire pour vous aider à démarrer.

17
Craig Efrein

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

ASPECT # 1: Rejoindre la taille du tampon

Selon toute vraisemblance, votre join_buffer_size est probablement trop faible.

Selon la documentation MySQL sur Comment MySQL utilise le cache de tampon de jointure

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

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

Vous avez 10 millions de lignes multipliées par 4 octets pour chaque clé. C'est environ 40M.

Essayez de l'augmenter dans la session à 42M (un peu plus grand que 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 fait l'affaire, ajoutez-le à my.cnf

[mysqld]
join_buffer_size = 42M

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

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

ASPECT # 2: Rejoindre l'opération

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

Selon la documentation MySQL sur Bloquer les jointures d'accès par boucle imbriquée et par clé

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

Pour que BKA soit utilisé, le drapeau batched_key_access de la variable système optimizer_switch doit être activé. BKA utilise MRR, le drapeau mrr doit donc également être activé. Actuellement, l'estimation des coûts du 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: Écriture des mises à jour sur le disque (FACULTATIF)

La plupart oublient d'augmenter innodb_write_io_threads pour écrire des pages sales hors du pool de tampons plus rapidement.

[mysqld]
innodb_write_io_threads = 16

Vous devrez redémarrer MySQL pour cette modification

ESSAIE !!!

16
RolandoMySQLDBA

Vous avez dit:

  • Les mises à jour affectent 6-25% de votre table
  • Vous souhaitez le faire le plus rapidement possible (<1h)
  • sans verrouillage
  • il n'a pas besoin d'être en une seule transaction
  • pourtant (dans le commentaire de la réponse de Rick James), vous vous inquiétez des conditions de course

Beaucoup de ces déclarations peuvent être contradictoires. Par exemple, de grandes mises à jour sans verrouiller la table. Ou en évitant les conditions de course sans utiliser une seule transaction géante.

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


Éviter les conditions de course

Si vous pouvez ajouter un horodatage mis à jour à votre table, vous pouvez résoudre les conditions de course tout en évitant de consigner un demi-million de mises à jour dans un transaction unique.

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

Vous évitez les conditions de concurrence (lors de la mise à jour ligne par ligne) en effectuant une vérification qu'une mise à jour ultérieure ne s'est pas déjà produite (UPDATE ... WHERE pk = [pk] AND updated < [batchfile date])

Et, ce qui est important, cela vous permet d'exécuter des mises à jour parallèles .


Exécution aussi rapide que possible —Paralléliser

Avec ce contrôle d'horodatage maintenant en place:

  1. Divisez votre fichier de commandes en morceaux de taille raisonnable (disons 50 000 lignes/fichier)
  2. En parallèle, faire lire un script dans chaque fichier et sortir un fichier avec 50 000 instructions UPDATE.
  3. En parallèle, une fois (2) terminé, demandez à mysql d'exécuter chaque fichier sql.

(par exemple, dans bash regardez split et xargs -P pour savoir comment exécuter facilement une commande de plusieurs façons en parallèle. Le degré de parallélisme dépend du nombre de threads que vous êtes prêt à consacrer à la mise à jour )

3
Peter Dixon-Moses
  1. CREATE TABLE qui correspond au CSV
  2. LOAD DATA dans ce tableau
  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 elle verrouillera toujours toutes les lignes du tableau pendant une durée non triviale. Si ce temps de verrouillage est plus important que la durée totale du processus, alors, ...

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

  1. CREATE TABLE qui correspond au CSV; aucun index sauf ce qui est nécessaire dans le JOIN dans le UPDATE. Si unique, faites-le PRIMARY KEY.
  2. LOAD DATA dans ce tableau
  3. copiez 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 supprimés.
L'étape 5 est "instantanée".

3
Rick James

Les mises à jour importantes sont liées aux E/S. 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 gardez, eh bien, les données statiques, et l'autre assets_dynamic qui stockera les uploaders, downloaders et 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 poids léger et agile assets_dynamic selon votre mise à jour 4 (ie 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 met à jour l'impact ~ 6 millions de lignes, en 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 souhaitez utiliser l'un de ces champs comme attribut).
1
user3127882

Pour que UPDATE s'exécute rapidement, vous devez

INDEX(uploaders, downloaders, verified)

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

Cela facilitera la possibilité pour UPDATE 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 SIGNED ou les deux INT UNSIGNED).

0
Rick James