¿Cómo actualizar más de 10 millones de filas en la tabla única de MySQL lo más rápido posible?


32

Usando MySQL 5.6 con el motor de almacenamiento InnoDB para la mayoría de las tablas. El tamaño de la agrupación de almacenamiento intermedio de InnoDB es de 15 GB y los índices Innodb DB + son de alrededor de 10 GB. El servidor tiene 32 GB de RAM y ejecuta Cent OS 7 x64.

Tengo una gran tabla que contiene más de 10 millones de registros.

Recibo un archivo de volcado actualizado de un servidor remoto cada 24 horas. El archivo está en formato csv. No tengo control sobre ese formato. El archivo es ~ 750 MB. Intenté insertar datos en una tabla MyISAM fila por fila y me tomó 35 minutos.

Necesito tomar solo 3 valores por línea de 10-12 del archivo y actualizarlo en la base de datos.

¿Cuál es la mejor manera de lograr algo como esto?

Necesito hacer esto diariamente.

Actualmente Flow es así:

  1. mysqli_begin_transaction
  2. Leer archivo de volcado línea por línea
  3. Actualice cada registro línea por línea.
  4. mysqli_commit

Las operaciones anteriores tardan entre 30 y 40 minutos en completarse y, mientras lo hago, hay otras actualizaciones en curso que me dan

Tiempo de espera de bloqueo excedido; intente reiniciar la transacción

Actualización 1

carga de datos en una nueva tabla usando LOAD DATA LOCAL INFILE. En MyISAM tardó 38.93 secmientras que en InnoDB tardó 7 min 5.21 sec. Entonces hice:

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)

Actualización 2

misma actualización con consulta de unión

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)

Aclaraciones de las preguntas en los comentarios:

  • Alrededor del 6% de las filas de la tabla serán actualizadas por el archivo, pero a veces puede llegar hasta el 25%.
  • Hay índices en los campos que se actualizan. Hay 12 índices en la tabla y 8 índices incluyen los campos de actualización.
  • No es necesario hacer la actualización en una transacción. Puede llevar tiempo, pero no más de 24 horas. Estoy tratando de hacerlo en 1 hora sin bloquear toda la tabla, ya que luego tengo que actualizar el índice de esfinge que depende de esta tabla. No importa si los pasos duran más, siempre que la base de datos esté disponible para otras tareas.
  • Podría modificar el formato csv en un paso de preproceso. Lo único que importa es una actualización rápida y sin bloqueo.
  • La tabla 2 es MyISAM. Es la tabla recién creada del archivo csv que utiliza el archivo de datos de carga. El tamaño del archivo MYI es de 452 MB. La tabla 2 está indexada en la columna campo1.
  • MYD de la tabla MyISAM es 663MB.

Actualización 3:

Aquí hay más detalles sobre ambas tablas.

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

y aquí está la consulta de actualización que actualiza la contenttabla usando datos 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

actualización 4:

todas las pruebas anteriores se realizaron en la máquina de prueba, pero ahora hice las mismas pruebas en la máquina de producción y las consultas son muy rápidas.

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

Pido disculpas por mi error. Es mejor usar join en lugar de cada actualización de registro. ahora estoy tratando de mejorar mpre usando el índice sugerido por rick_james, se actualizará una vez que se realice el benchmarking.


¿Tiene un compuesto INDEX(field2, field3, field4) (en cualquier orden)? Por favor muéstranos SHOW CREATE TABLE.
Rick James

1
Los índices 12 y 8 son una parte grave de su problema. MyISAM es otra parte seria. InnoDB o TokuDB funcionan mucho mejor con múltiples índices.
Rick James

Tienes dos diferentes UPDATEs . Díganos exactamente cómo se ve la declaración directa para actualizar la tabla a partir de los datos csv. Entonces podremos ayudarlo a diseñar una técnica que cumpla con sus requisitos.
Rick James

@ RickJames solo hay uno update, y por favor revise la pregunta actualizada. Gracias
AMB

Respuestas:


17

Según mi experiencia, usaría LOAD DATA INFILE para importar su archivo CSV.

La instrucción LOAD DATA INFILE lee las filas de un archivo de texto en una tabla a una velocidad muy alta.

Ejemplo que encontré en Internet Ejemplo de carga de datos . Probé este ejemplo en mi caja y funcionó bien

Tabla de ejemplo

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

Archivo CSV de ejemplo

# 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

Declaración de importación que se ejecutará desde la consola 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');

Resultado

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 simplemente ignora la primera línea que son los encabezados de columna.

Después de IGNORE, estamos especificando las columnas (omitiendo la columna2) para importar, que coincide con uno de los criterios en su pregunta.

Aquí hay otro ejemplo directamente de Oracle: ejemplo LOAD DATA INFILE

Esto debería ser suficiente para comenzar.


podría usar datos de carga para cargar datos en la tabla temporal y luego usar otras consultas para actualizarlo en la tabla principal., gracias
AMB

14

A la luz de todas las cosas mencionadas, parece que el cuello de botella es la unión en sí.

ASPECTO # 1: Tamaño del búfer de unión

Con toda probabilidad, su join_buffer_size es probablemente demasiado bajo.

De acuerdo con la documentación de MySQL sobre cómo MySQL utiliza la caché de búfer de unión

Solo almacenamos las columnas usadas en el búfer de unión, no las filas completas.

Siendo este el caso, haga que las claves del búfer de unión permanezcan en la RAM.

Tiene 10 millones de filas por 4 bytes para cada clave. Eso es alrededor de 40 millones.

Intenta aumentarlo en la sesión a 42M (un poco más grande 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 esto funciona, agregue esto a my.cnf

[mysqld]
join_buffer_size = 42M

No es necesario reiniciar mysqld para nuevas conexiones. Solo corre

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

ASPECTO # 2: Operación de unión

Puede manipular el estilo de la operación de combinación modificando el optimizador

De acuerdo con la documentación de MySQL sobre las uniones de acceso de bloque anidado y de bloque bloqueado

Cuando se usa BKA, el valor de join_buffer_size define qué tan grande es el lote de claves en cada solicitud al motor de almacenamiento. Cuanto más grande sea el búfer, más acceso secuencial será a la tabla de la derecha de una operación de unión, lo que puede mejorar significativamente el rendimiento.

Para usar BKA, el indicador batched_key_access de la variable del sistema optimizer_switch debe estar activado. BKA usa MRR, por lo que el indicador mrr también debe estar activado. Actualmente, la estimación de costos para MRR es demasiado pesimista. Por lo tanto, también es necesario que mrr_cost_based esté desactivado para que se use BKA.

Esta misma página recomienda hacer esto:

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

ASPECTO # 3: Escribir actualizaciones en el disco (OPCIONAL)

La mayoría se olvida de aumentar innodb_write_io_threads para escribir páginas sucias fuera del grupo de búferes más rápido.

[mysqld]
innodb_write_io_threads = 16

Tendrá que reiniciar MySQL para este cambio

DARLE UNA OPORTUNIDAD !!!


¡Agradable! +1 para la punta del búfer de unión ajustable. Si tienes que unirte, únete en la memoria. ¡Buen consejo!
Peter Dixon-Moses

3
  1. CREATE TABLE que coincide con el CSV
  2. LOAD DATA en esa mesa
  3. UPDATE real_table JOIN csv_table ON ... SET ..., ..., ...;
  4. DROP TABLE csv_table;

El paso 3 será mucho más rápido que fila por fila, pero seguirá bloqueando todas las filas de la tabla durante un período de tiempo no trivial. Si este tiempo de bloqueo es más importante que el tiempo que lleva todo el proceso, entonces ...

Si nada más está escribiendo en la mesa, entonces ...

  1. CREATE TABLEque coincide con el CSV; sin índices excepto lo que se necesita JOINen el UPDATE. Si es único, hazloPRIMARY KEY .
  2. LOAD DATA en esa mesa
  3. copia el real_tablea 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;

El paso 3 es más rápido que la actualización, especialmente si se dejan índices innecesarios.
El paso 5 es "instantáneo".


Digamos en segundos, por ejemplo, después del paso 3, estamos haciendo el paso 4, luego los nuevos datos se insertan en la tabla real, por lo que perderemos esos datos en la tabla nueva. ¿Cuál es la solución para esto? gracias
AMB

Mira qué pt-online-schema-digest; se encarga de tales problemas a través de a TRIGGER.
Rick James

Probablemente no necesite ningún índice en la tabla LOAD DATA. Agregar índices innecesarios es costoso (en el tiempo).
Rick James

Según la información más reciente, me estoy inclinando hacia el archivo CSV que se está cargando en una tabla MyISAM con solo un AUTO_INCREMENT, y luego agrupando 1K filas a la vez en función de la PK. Pero necesito ver todos los requisitos y el esquema de la tabla antes de intentar detallar los detalles.
Rick James

configuré hash como PRIMARY index, pero aunque fragmentar en 50k usando la consulta de pedido lleva más tiempo, ¿sería mejor si creo un incremento automático? y configurarlo como PRIMARY index?
AMB

3

Usted ha dicho:

  • Las actualizaciones afectan al 6-25% de su tabla
  • Desea hacer esto lo más rápido posible (<1 hora)
  • sin bloqueo
  • no tiene que estar en una sola transacción
  • aún (en un comentario sobre la respuesta de Rick James), expresas preocupación por las condiciones de carrera

Muchas de estas declaraciones pueden ser contradictorias. Por ejemplo, actualizaciones grandes sin bloquear la tabla. O evitar condiciones de carrera sin usar una transacción gigante.

Además, dado que su tabla está muy indexada, tanto las inserciones como las actualizaciones pueden ser lentas.


Evitar condiciones de carrera

Si puede agregar una marca de tiempo actualizada a su tabla, puede resolver las condiciones de la carrera y evitar el registro de medio millón de actualizaciones en una sola transacción.

Esto le permite realizar actualizaciones línea por línea (como lo hace actualmente), pero con confirmación automática o lotes de transacciones más razonables.

Evita las condiciones de carrera (al actualizar línea por línea) realizando una comprobación de que no se haya producido una actualización posterior (UPDATE ... WHERE pk = [pk] AND updated < [batchfile date] )

Y, lo que es más importante, esto le permite ejecutar actualizaciones paralelas .


Correr lo más rápido posible: paralelización

Con esta marca de tiempo, compruebe ahora en su lugar:

  1. Divida su archivo por lotes en trozos de tamaño razonable (digamos 50,000 filas / archivo)
  2. Paralelamente, haga que se lea un script en cada archivo y genere un archivo con 50,000 declaraciones ACTUALIZADAS.
  3. Paralelamente, una vez que (2) finalice, mysqlejecute cada archivo sql.

(p. ej., para bashver splity encontrar xargs -Pformas de ejecutar fácilmente un comando en paralelo de muchas maneras. El grado de paralelismo depende de cuántos hilos esté dispuesto a dedicar a la actualización )


Tenga en cuenta que "línea por línea" es probable que sea 10 veces más lento que hacer cosas en lotes de al menos 100.
Rick James

Tendría que compararlo en este caso para estar seguro. Actualizando 6-25% de una tabla (con 8 índices involucrados con las columnas actualizadas), consideraría la posibilidad de que el mantenimiento del índice se convierta en el cuello de botella.
Peter Dixon-Moses

Quiero decir, en algunos casos podría ser más rápido eliminar índices, actualizar en masa y volver a crearlos después ... pero OP no quiere tiempo de inactividad.
Peter Dixon-Moses

1

Grandes actualizaciones están vinculadas a E / S. Yo sugeriría:

  1. Cree una tabla distinta que almacenará sus 3 campos actualizados con frecuencia. Llamemos a una tabla assets_static donde guarda, bueno, datos estáticos, y a la otra assets_dynamic que almacenará cargadores, descargadores y verificados.
  2. Si puede, use el motor MEMORY para la tabla assets_dynamic . (copia de seguridad en disco después de cada actualización).
  3. Actualice sus activos ligeros y ágiles_dynamic según su actualización 4 (es decir, LOAD INFILE ... INTO temp; UPDATE assets_dynamic a JOIN temp b en a.id = b.id SET [lo que debe actualizarse]. Esto debería tomar menos de un minuto. (En nuestro sistema, assets_dynamic tiene filas y las actualizaciones impactan ~ 6 millones de filas, en poco más de 40 años).
  4. Cuando ejecuta el indexador de Sphinx, JOIN assets_static y assets_dynamic (suponiendo que desea utilizar uno de estos campos como un atributo).

0

Para el UPDATE que corra rápido, necesita

INDEX(uploaders, downloaders, verified)

Puede estar en cualquier mesa. Los tres campos pueden estar en cualquier orden.

Eso facilitará la UPDATEposibilidad de unir rápidamente las filas entre las dos tablas.

Y haga que los tipos de datos sean los mismos en las dos tablas (ambas INT SIGNEDo ambas INT UNSIGNED).


Esto en realidad ralentizó la actualización.
AMB

Hmmm ... por favor proporcione EXPLAIN UPDATE ...;.
Rick James el
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.