{"id":541,"date":"2014-10-09T10:22:12","date_gmt":"2014-10-09T08:22:12","guid":{"rendered":"http:\/\/dbahire.com\/?p=541"},"modified":"2022-03-10T12:49:51","modified_gmt":"2022-03-10T11:49:51","slug":"probando-la-manera-mas-rapida-de-importar-una-tabla-en-mysql-y-unos-resultados-interesantes-del-rendimiento-de-5-7","status":"publish","type":"post","link":"https:\/\/jynus.com\/dbahire\/probando-la-manera-mas-rapida-de-importar-una-tabla-en-mysql-y-unos-resultados-interesantes-del-rendimiento-de-5-7\/","title":{"rendered":"Probando la manera m\u00e1s r\u00e1pida de importar una tabla en MySQL (y unos resultados interesantes del rendimiento de 5.7)"},"content":{"rendered":"<p>Como mencion\u00e9 en mi anterior entrada, <a href=\"\/dbahire\/cambios-en-variables-globales-de-configuracion-entre-mysql-5-6-20-y-mysql-5-7-4-milestone-14\/\" title=\"Cambios en variables globales de configuraci\u00f3n entre MySQL 5.6.20 y MySQL 5.7.4 \u201cMilestone 14\u2033\">donde comparaba las opciones de configuraci\u00f3n por defecto de 5.6 y 5.7<\/a>, he estado haciendo algunos tests para un tipo particular de carga en varias versiones de MySQL. Lo que he estado comprobando es las diferentes maneras de cargar un archivo CSV (el mismo fichero que <a href=\"\/dbahire\/que-herramienta-de-compresion-deberia-usar-para-las-copias-de-seguridad-de-mi-base-de-datos\/\" title=\"\u00bfQu\u00e9 herramienta de compresi\u00f3n deber\u00eda usar para las copias de seguridad de mi base de datos? (Parte I: compresi\u00f3n)\">us\u00e9 para comprobar las herramientas de compresi\u00f3n<\/a>) en MySQL. Aquellos administradores de bases de datos y programadores MySQL con experiencia probablemente ya conozc\u00e1is la respuesta, as\u00ed que pod\u00e9is saltar a los resultados de 5.7 respecto de 5.6. Sin embargo, la primera parte de este post est\u00e1 dedicado a los desarrolladores\/aquellos que dan sus primeros pasos con MySQL que desean saber la respesta a la pregunta del t\u00edtulo, paso a paso. Debo decir que yo tambi\u00e9n he aprendido algo, ya que sobrestim\u00e9 e infravalor\u00e9 algunos efectos de ciertas opciones de configuraci\u00f3n para este tipo de carga.<\/p>\n<p><strong>Aviso: no pretendo realizar benchmarks formales, la mayor\u00eda de los resultados obtenidos aqu\u00ed se han creado ejecut\u00e1ndose un par de veces, y muchos de ellos con la configuraci\u00f3n por defecto. Esto ha sido por dise\u00f1o, ya que intento mostrar alguna de las &#8220;malas pr\u00e1cticas&#8221;<\/strong> para la gente que acaba de empezar a trabajar con MySQL, y lo que deber\u00edan evitar hacer. Tan s\u00f3lo la comparativa entre 5.6 y 5.7 me ha llamado la antenci\u00f3n. <strong>Aviso adicional<\/strong>: No me llamo a m\u00ed mismo programador, y mucho menos un programador de Python, as\u00ed que me disculpo por adelantado por mi c\u00f3digo- despu\u00e9s de todo, <em>esto va de MySQL<\/em>. El enlace de descarga de los scripts est\u00e1 al final del post.<\/p>\n<h3>Las reglas<\/h3>\n<p>Empiezo con un <a href=\"\/dbahire\/nodes.osm.7z\">archivo CSV<\/a> (recordad que realmente es un archivo de valores separado por comas, <em>TSV<\/em>) que tiene 3.700.635.579 bytes de tama\u00f1o, y 46.741.126 filas y tiene la siguiente pinta:<\/p>\n<pre>171773  38.6048402      -0.0489871      4       2012-08-25 00:37:46     12850816        472193  rubensd\n171774  38.6061981      -0.0496867      2       2008-01-19 10:23:21     666916  9250    j3m\n171775  38.6067166      -0.0498342      2       2008-01-19 10:23:21     666916  9250    j3m\n171776  38.6028122      -0.0497136      5       2012-08-25 00:26:40     12850816        472193  rubensd\n171933  40.4200658      -3.7016652      6       2011-11-29 05:37:42     9984625 33103   sergionaranja \n<\/pre>\n<p>Quiero cargarlo en la siguiente tabla, con la estructura que muestro a continuaci\u00f3n:<\/p>\n<pre lang=\"sql\">CREATE TABLE `nodes` (\n  `id` bigint PRIMARY KEY,\n  `lat` decimal(9,7),\n  `lon` decimal(10,7),\n  `version` int,\n  `timestamp` timestamp,\n  `changeset` bigint,\n  `uid` bigint,\n  `user` varchar(255)\n);\n<\/pre>\n<p>El tiempo de importaci\u00f3n termina en el momento que la tabla es <em>crash safe<\/em> (resistente a fallos) (aunque quede E\/S pendiente). Eso quiere decir que para InnoDB, el \u00faltimo <code>COMMIT<\/code> realizado tiene que haberse hecho de manera satisfactoria y <code>flush_log_at_trx_commit<\/code> debe valer 1, de manera que aunque queden operaciones de escritura por hacerse, es completamente consistente con disco. Para MyISAM, eso quiere decir que fuerzo la ejecuci\u00f3n de un <code>FLUSH TABLES<\/code> antes de terminar el test. Estos, por supuesto, no son equivalentes, pero al menos es una manera de asegurarse de que todo est\u00e1 m\u00e1s o menos sincronizado con disco. \u00c9sta es la parte final de todos mis scripts:<\/p>\n<pre lang=\"python\"># finishing\nif (options.engine == 'MyISAM'):\n    cursor.execute('FLUSH TABLES')\nelse:\n    db.commit()\n\ncursor.close()\ndb.close()\n<\/pre>\n<p>Para el hardware y sistema operativo, comprobad los detalles de m\u00e1quina de <a href=\"http:\/\/dbahire.com\/que-herramienta-de-compresion-deberia-usar-para-las-copias-de-seguridad-de-mi-base-de-datos\/\" title=\"\u00bfQu\u00e9 herramienta de compresi\u00f3n deber\u00eda usar para las copias de seguridad de mi base de datos? (Parte I: compresi\u00f3n)\">este post antiguo<\/a>&#8211; us\u00e9 el mismo entorno que el mencionado aqu\u00ed, con la excepci\u00f3n de usar CentOS7 en vez de 6.5.<\/p>\n<h3>Empezando con un m\u00e9todo muy ingenuo<\/h3>\n<p>Digamos que soy un desarrollador al que le han asignado cargar de manera regular un archivo en MySQL, \u00bfc\u00f3mo lo har\u00eda? Probablemente estar\u00eda tentado de usar una biblioteca de parseo de CSV, el conector de MySQL, y los unir\u00eda a ambos juntos en un bucle. \u00bfEso funcionar\u00eda, no? La parte principal del c\u00f3digo ser\u00eda algo como esto (<code>load_data_01.py<\/code>):<\/p>\n<pre lang=\"python\"># import\ninsert_node = \"INSERT INTO nodes (id, lat, lon, version, timestamp, changeset, uid, user) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\"\n\nwith open(CSV_FILE, 'rb') as csv_file:\n    csv_reader = csv.reader(csv_file, delimiter='\\t')\n    for node in csv_reader:\n        cursor.execute(insert_node, node)\n<\/pre>\n<p>Como estoy interpretando el papel de un desarrollador sin experiencia, tambi\u00e9n usar\u00eda la configuraci\u00f3n por defecto. Veamos qu\u00e9 resultados obtenemos (de nuevo, esta es la raz\u00f3n por la cual llamo a esto &#8220;pruebas&#8221;, y no benchmarks). Menos es mejor:<br \/>\n<a href=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_011.png\"><img loading=\"lazy\" decoding=\"async\" src=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_011.png\" alt=\"load_data_01\" class=\"aligncenter size-full wp-image-487\" width=\"955\" height=\"769\"><\/a><\/p>\n<table>\n<thead style=\"text-align:center; font-weight: bold;\">\n<tr>\n<td><em>MySQL Version<\/em><\/td>\n<td>5.1.72<\/td>\n<td>5.5.39<\/td>\n<td>5.6.20<\/td>\n<td>5.7.4<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>Load time (seconds)<\/em><\/td>\n<td>4708.594<\/td>\n<td>6274.304<\/td>\n<td>6499.033<\/td>\n<td>6939.722<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Guau, \u00bf5.1 es un 50% m\u00e1s r\u00e1pido que el resto de versiones? <strong>Incorrecto<\/strong>, recordad que 5.5 fue la primera versi\u00f3n en introducir InnoDB como el motor por defecto, e InnoDB tiene una sobrecarga debido a las transacciones, y normalmente no tiene una muy buena configuraci\u00f3n por defecto (al contrario que MyISAM, que estan simple que las opciones por defecto pueden funcionar en muchos casos). Normalicemos los resultados por motor de almacenamiento:<br \/>\n<a href=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_01_by_engine.png\"><img loading=\"lazy\" decoding=\"async\" src=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_01_by_engine.png\" alt=\"load_data_01_by_engine\" class=\"aligncenter size-full wp-image-489\" width=\"850\" height=\"716\"><\/a><\/p>\n<table>\n<thead style=\"text-align:center; font-weight: bold;\">\n<tr>\n<td><em>MySQL Version<\/em><\/td>\n<td>5.1.72<\/td>\n<td>5.5.39<\/td>\n<td>5.6.20<\/td>\n<td>5.7.4<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>MyISAM<\/em><\/td>\n<td>4708.594<\/td>\n<td>5010.655<\/td>\n<td>5149.791<\/td>\n<td>5365.005<\/td>\n<\/tr>\n<tr>\n<td><em>InnoDB<\/em><\/td>\n<td>6238.503<\/td>\n<td>6274.304<\/td>\n<td>6499.033<\/td>\n<td>6939.722<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Esto ya parece m\u00e1s razonable, \u00bfno? Sin embargo, en este caso, parece haber una ligero empeoramiento en el rendimiento de un s\u00f3lo hilo a la vez que las versiones van progresando, especialemnte en MySQL 5.7. Por supuesto, es pronto para sacar conclusiones, porque <strong>este m\u00e9todo de carga de un archivo CSV, fila a fila, es uno de los m\u00e1s lentos, y adem\u00e1s estamos usando unas opciones de configuraci\u00f3n muy pobres (las que vienen por defecto) -las cuales var\u00edan de versi\u00f3n a versi\u00f3n- y que no deber\u00edan usarse para llegar a ninguna conclusi\u00f3n<\/strong>.<\/p>\n<p>Lo que podemos decir es que MyISAM parece funcionar mejor para este escenario muy espec\u00edfico por las razones que comentaba antes, pero a\u00fan as\u00ed tardar\u00edamos de 1 a 2 horas en cargar un archivo tan sencillo.<\/p>\n<h3>Otro m\u00e9todo incluso m\u00e1s ingenuo<\/h3>\n<p>La siguiente pregunta no es: \u00bfpodemos hacerlo mejor? sino \u00bfpodemos hacerlo incluso m\u00e1s lento? Un texto en particular me llam\u00f3 la atenci\u00f3n mientras miraba la documentaci\u00f3n del conector de MySQL:<\/p>\n<blockquote><p>Since by default Connector\/Python does not autocommit, it is important to call this method after every transaction that modifies data for tables that use transactional storage engines.<\/p><\/blockquote>\n<p>que es algo as\u00ed como:<\/p>\n<blockquote><p>Debido a que por defecto Connector\/Python no realiza autocommit, es importante llamar a este m\u00e9todo despu\u00e9s de cualquier transacci\u00f3n que modifique datos en tablas de motores transaccionales.<\/p><\/blockquote>\n<p>-de la <a href=\"http:\/\/dev.mysql.com\/doc\/connector-python\/en\/connector-python-api-mysqlconnection-commit.html\">documentaci\u00f3n de connector\/python<\/a><\/p>\n<p>Pens\u00e9- \u00a1ah!, entonces podemos reducir el tiempo de importaci\u00f3n realizando <em>commit<\/em> para cada fila de la base de datos, una a una, \u00bfno? Despu\u00e9s de todo, estamos insertando en la tabla en una \u00fanica transacci\u00f3n enorme. \u00a1Seguro que crear un enorme n\u00famero de transacciones ser\u00e1 mejor! \ud83d\ude42 Este es el c\u00f3digo ligeramente modificado (<code>load_data_02.py<\/code>):<\/p>\n<pre lang=\"python\">insert_node = \"INSERT INTO nodes (id, lat, lon, version, timestamp, changeset, uid, user) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\"\n\nwith open('\/tmp\/nodes.csv', 'rb') as csv_file:\n    csv_reader = csv.reader(csv_file, delimiter='\\t')\n    for node in csv_reader:\n        cursor.execute(insert_node, node)\n        db.commit()\n<\/pre>\n<p>Y en este caso no tengo ni siquiera un bonito gr\u00e1fico que ense\u00f1aros, porque <strong>despu\u00e9s de 2 horas, 19 minutos y 39.405 segundos, cancel\u00e9 la importaci\u00f3n porque s\u00f3lo se hab\u00edan insertado 111533 nodos<\/strong> en MySQL 5.1.72 con InnoDB con la configuraci\u00f3n por defecto (<code>innodb_flush_log_at_trx_commit = 1<\/code>). Obviamente, millones de <code>fsyinc<\/code>s no har\u00e1n que la carga sea m\u00e1s r\u00e1pida, considerad esto como una lecci\u00f3n aprendida.<\/p>\n<h3>Haciendo progresos: multi-inserts<\/h3>\n<p>El siguiente paso que quer\u00eda probar es c\u00f3mo de efectivo es agrupar consultas en inserciones m\u00faltiples. Este m\u00e9todo lo usa <code>mysqldump<\/code>, y supuesta mente minimiza la carga extra de SQL por tener que gestionar cada consulta por separado (parseo, comprobaci\u00f3n de permisos, plan de la consulta, etc.). \u00c9ste es el c\u00f3digo principal (<code>load_data_03.py<\/code>):<\/p>\n<pre lang=\"python\">concurrent_insertions = 100\n[...]\nwith open(CSV_FILE, 'rb') as csv_file:\n    csv_reader = csv.reader(csv_file, delimiter='\\t')\n    i = 0\n    node_list = []\n    for node in csv_reader:\n        i += 1\n\tnode_list += node\n        if (i == concurrent_insertions):\n            cursor.execute(insert_node, node_list)\n            i = 0\n            node_list = []\n    csv_file.close()\n\n# insert the reminder nodes\nif i &gt; 0:\n    insert_node = \"INSERT INTO nodes (id, lat, lon, version, timestamp, changeset, uid, user) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\"\n    for j in xrange(i - 1):\n        insert_node += ', (%s, %s, %s, %s, %s, %s, %s, %s)'\n    cursor.execute(insert_node, node_list)\n<\/pre>\n<p>Lo probamos con, por ejemplo, 100 filas insertadas en cada consulta. \u00bfCu\u00e1les son los resultados? Menos es mejor:<br \/>\n<a href=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_03.png\"><img loading=\"lazy\" decoding=\"async\" src=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_03.png\" alt=\"load_data_03.py results\" class=\"aligncenter size-full wp-image-494\" width=\"835\" height=\"703\"><\/a><\/p>\n<table>\n<thead style=\"text-align:center; font-weight: bold;\">\n<tr>\n<td><em>MySQL Version<\/em><\/td>\n<td>5.1.72<\/td>\n<td>5.5.39<\/td>\n<td>5.6.20<\/td>\n<td>5.7.4<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>MyISAM<\/em><\/td>\n<td>1794.693<\/td>\n<td>1822.081<\/td>\n<td>1861.341<\/td>\n<td>1888.283<\/td>\n<\/tr>\n<tr>\n<td><em>InnoDB<\/em><\/td>\n<td>3645.454<\/td>\n<td>3455.800<\/td>\n<td>2849.299<\/td>\n<td>3032.496<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Con este m\u00e9todo observamos <strong>una mejora en el tiempo de importaci\u00f3n de 262-284% respecto del tiempo original para MyISAM y de 171-229% respecto del tiempo original de InnoDB<\/strong>. Recordad que este m\u00e9todo no escalar\u00e1 indefinidamente, ya que encontraremos un l\u00edmite en el tama\u00f1o de paquete si intentamos insertar demasiadas filas a la vez. Sin embargo, se trata de una clara ventaja sobre el insertado fila a fila.<\/p>\n<p>Los tiempos para MyISAM son esencialmente los mismos entre versiones, mientras que InnoDB muestra una mejora a lo largo del tiempo (que podr\u00edan deberse a los cambios en c\u00f3digo y la optimizaci\u00f3n, pero tambi\u00e9n a la configuraci\u00f3n por defecto del tama\u00f1o del log de transacciones), excepto de nuevo entre 5.6 y 5.7.<\/p>\n<h3>El m\u00e9todo correcto para importar datos: Load Data<\/h3>\n<p>Si se tiene un m\u00ednimo de experiencia con MySQL, conocer\u00e9is la existencia de una palabra clave especializada para importaciones, y \u00e9sta es <code>LOAD DATA<\/code>. Veamos como quedar\u00eda el c\u00f3digo usando esta opci\u00f3n (<code>load_data_04.py<\/code>):<\/p>\n<pre lang=\"python\"># data import\nload_data = \"LOAD DATA INFILE '\" + CSV_FILE + \"' INTO TABLE nodes\"\n\ncursor.execute(load_data)\n<\/pre>\n<p>\u00bfSimple, verdad? Con esto estamos minimizando la sobrecarga de SQL, y ejecutando el bucle en c\u00f3digo C compilado de MySQL. Echemos un vistazo a los resultados (menos es mejor):<br \/>\n<a href=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_041.png\"><img loading=\"lazy\" decoding=\"async\" src=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_041.png\" alt=\"load_data_04.py results\" class=\"aligncenter size-full wp-image-498\" width=\"833\" height=\"696\"><\/a><\/p>\n<table>\n<thead style=\"text-align:center; font-weight: bold;\">\n<tr>\n<td><em>MySQL Version<\/em><\/td>\n<td>5.1.72<\/td>\n<td>5.5.39<\/td>\n<td>5.6.20<\/td>\n<td>5.7.4<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>MyISAM<\/em><\/td>\n<td>141.414<\/td>\n<td>149.612<\/td>\n<td>155.181<\/td>\n<td>166.836<\/td>\n<\/tr>\n<\/tbody>\n<tbody>\n<tr>\n<td><em>InnoDB<\/em><\/td>\n<td>2091.617<\/td>\n<td>1890.972<\/td>\n<td>920.615<\/td>\n<td>1041.702<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>En este caso, <strong>MyISAM tiene una mejora dram\u00e1tica &#8211;<code>LOAD DATA<\/code> convierte la importaci\u00f3n en 12 veces m\u00e1s r\u00e1pida. InnoDB, de nuevo todav\u00eda con los par\u00e1metros por defecto mejora en 3 veces, y m\u00e1s significativamente en las versiones m\u00e1s recientes (5.6, 5.7) que en las anteriores (5.1, 5.5).<\/strong> Mi predicci\u00f3n es que esto tiene mucho m\u00e1s que ver con la diferente configuraci\u00f3n de los archivos de log que con cambios en el c\u00f3digo.<\/p>\n<h3>Intentado mejorar Load Data para MyISAM<\/h3>\n<p>\u00bfPodemos mejorar los tiempos de carga para MyISAM? Hay dos cosas que podemos hacer -aumentar el tama\u00f1o de <code>key_cache_size<\/code> y deshabilitar Performance Schema para 5.6 y 5.7. Establec\u00ed el tama\u00f1o de la cach\u00e9 de \u00edndices (<code>key_cache_size<\/code>) en <code>600M<\/code> (intentado que cupiera la clave primaria en memoria) y cambi\u00e9 <code>performance_schema = 0<\/code>, probando las 3 combinaciones restantes. Menos es mejor:<br \/>\n<a href=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_04_myisam.png\"><img loading=\"lazy\" decoding=\"async\" src=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_04_myisam.png\" alt=\"load_data_04.py results for myisam\" class=\"aligncenter size-full wp-image-500\" width=\"795\" height=\"773\"><\/a><\/p>\n<table>\n<thead style=\"text-align:center; font-weight: bold;\">\n<tr>\n<td><em>MySQL Version<\/em><\/td>\n<td>5.1.72<\/td>\n<td>5.5.39<\/td>\n<td>5.6.20<\/td>\n<td>5.7.4<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>default<\/em><\/td>\n<td>141.414<\/td>\n<td>149.612<\/td>\n<td>155.181<\/td>\n<td>166.836<\/td>\n<\/tr>\n<tr>\n<td><em>key_buffer_size=600M<\/em><\/td>\n<td>136.649<\/td>\n<td>170.622<\/td>\n<td>182.698<\/td>\n<td>191.228<\/td>\n<\/tr>\n<tr>\n<td><em>key_buffer_size=600M, P_S = OFF<\/em><\/td>\n<td>133.967<\/td>\n<td>170.677<\/td>\n<td>177.724<\/td>\n<td>186.171<\/td>\n<\/tr>\n<tr>\n<td><em>P_S = OFF<\/em><\/td>\n<td>142.592<\/td>\n<td>145.679<\/td>\n<td>150.684<\/td>\n<td>159.702<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Hay varias cosas que destacar aqu\u00ed:<\/p>\n<ul>\n<li><code>P_S=ON<\/code> and <code>P_S=OFF<\/code> no deber\u00eda tener un efecto en MySQL 5.1 y 5.5, pero se muestran resultados diferentes debido a errores de medida. De ah\u00ed podemos entender que s\u00f3lo 2 cifras son realmente significativas.<\/li>\n<li><code>key_buffer_cache<\/code>, en general, no mejora el rendimiento, de hecho se podr\u00eda decir que estad\u00edsticamente la empeora. Esto es razonable porque, despu\u00e9s de todo, estoy escribiendo a la cach\u00e9 de archivos, y una cach\u00e9 de claves mayor puede requerir una reserva o copia de memoria m\u00e1s costosa. Esto deber\u00eda investigarse m\u00e1s a fondo para llegar a una conclusi\u00f3n.<\/li>\n<li>Performance_schema podr\u00eda estar empeorando el rendimiento de esta carga, pero no estoy seguro estad\u00edsticamente hablando.<\/li>\n<li>MyISAM (o quiz\u00e1s el servidor MySQL) parece haber empeorado su rendimiento para este tipo de carga (carga por lotes con un s\u00f3lo hilo).<\/li>\n<\/ul>\n<p>Hay m\u00e1s cosas que me gustar\u00eda probar con MyISAM, como ver el impacto de los diferentes formatos de fila (fixed) pero me gustar\u00eda continuar con los otros motores.<\/p>\n<h3>Intentado mejorar Load Data en InnoDB<\/h3>\n<p>InnoDB es un motor mucho m\u00e1s interesante, ya que es ACIDo por defecto, y m\u00e1s complejo. \u00bfPodemos hacerlo tan r\u00e1pido como MyISAM para una importaci\u00f3n?<\/p>\n<p>La primera cosa que quiero cambiar son los valores por defecto de <code>innodb_log_file_size<\/code> y <code>innodb_buffer_pool_size<\/code>. El log es diferente antes y despu\u00e9s de 5.6, y no es adecuado para una gran carga de escritura. Lo cambi\u00e9 para un primer test a <code>2G<\/code>, que es el mayor tama\u00f1o que 5.1 y 5.5 pueden usar (lo puse a 2.147.483.136 ya que tiene que se menor de 2G), lo cual quiere decir que tenemos un tama\u00f1o total de los logs de 4G. Tambi\u00e9n cambi\u00e9 el buffer pool a una tama\u00f1o m\u00e1s conveniente, 8GB, suficiente para albergar el tama\u00f1o completo del conjunto de datos. Recordad que una de las razones por las cuales InnoDB es tan lento a la hora de realizar importaciones es porque escribe las nuevas p\u00e1ginas (al menos) dos veces a disco -en el log, y en el espacio de tablas. Sin embargo, con estos par\u00e1metros, la segunda escritura deber\u00eda realizarse en su mayor\u00eda a trav\u00e9s del buffer de memoria. Estos son los nuevos resultados (menos es mejor):<br \/>\n<a href=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_04_innodb1.png\"><img loading=\"lazy\" decoding=\"async\" src=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_04_innodb1.png\" alt=\"load_data_04.py innodb results\" class=\"aligncenter size-full wp-image-505\" width=\"804\" height=\"697\"><\/a><\/p>\n<table>\n<thead style=\"text-align:center; font-weight: bold;\">\n<tr>\n<td><em>MySQL Version<\/em><\/td>\n<td>5.1.72<\/td>\n<td>5.5.39<\/td>\n<td>5.6.20<\/td>\n<td>5.7.4<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>default<\/em><\/td>\n<td>1923.751<\/td>\n<td>1797.220<\/td>\n<td>850.636<\/td>\n<td>1008.349<\/td>\n<\/tr>\n<tr>\n<td><em>log_file_size=2G, buffer_pool=8G<\/em><\/td>\n<td>1044.923<\/td>\n<td>1012.488<\/td>\n<td>743.818<\/td>\n<td>850.868<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Ahora esto es un test que parece m\u00e1s razonable. Podemos comentar que:<\/p>\n<ul>\n<li>La mayor\u00eda de las mejoras que ten\u00edamos antes en 5.6 y 5.7 respecto a 5.1 y 5.5 se deb\u00edan al tama\u00f1o de log 10 veces m\u00e1s grande.<\/li>\n<li>A\u00fan as\u00ed, 5.6 y 5.7 son m\u00e1s r\u00e1pidos que 5.1 y 5.5 (algo razonable, ya que 5.6 ha visto unos cambios en InnoDB impresionantes, tanto en c\u00f3digo como en configuraci\u00f3n)<\/li>\n<li>InnoDB contin\u00faa siendo al menos 5 veces m\u00e1s lento que MyISAM<\/li>\n<li>\u00a1Y 5.7 contin\u00faa siendo m\u00e1s lento que 5.6! Estamos teniendo una regresi\u00f3n consistente de un 13-18% en 5.7 (y me estoy empezando, ahora s\u00ed, a preocupar)<\/li>\n<\/ul>\n<p>Coment\u00e9 antes que el mayor overhead de InnoDB es escribir los datos 2 veces (al log y a las tablas). Esto es en realidad incorrecto, ya que podr\u00eda tener que escribirlos 3 veces (al \u00e1rea de doble escritura) e incluso 4, en el log binario. El log binario no est\u00e1 activo por defecto, pero el <em>double write<\/em> s\u00ed, ya que permite protegernos de la corrupci\u00f3n. Aunque nunca recomendamos deshabilitar esta estructura en producci\u00f3n, la verdad es que en una importaci\u00f3n nos da igual si los datos terminan corruptos (ya que podr\u00edamos borrarlos y volver a reimportarlos). Tambi\u00e9n hay <a href=\"http:\/\/www.percona.com\/blog\/2014\/05\/23\/improve-innodb-performance-write-bound-loads\/\">opciones en algunos sistemas de archivos para evitar tener que activarlo<\/a>.<\/p>\n<p>Otras caracter\u00edsticas que est\u00e1n en InnoDB por seguridad, no por rendimiento, son los checksums de InnoDB- \u00e9stos incluso se convierten en el cuello de botella en sistemas de almacenamiento utrarr\u00e1pidos como tarjetas flash PCI. En estos casos, \u00a1la CPU es demasiado lenta para calcularlos! Sospecho que esto no ser\u00e1 un problema en este caso porque las versiones m\u00e1s modernas de MySQL (5.6 y 5.7) tienen la opci\u00f3n de cambiarlo por la funci\u00f3n acelerada por hardare CRC32 y, mayormente, porque estoy usando discos magn\u00e9ticos, que son el verdadero cuello de botella aqu\u00ed. Pero no nos fiemos de lo que hemos aprendido y pong\u00e1moslo a prueba.<\/p>\n<p>La otra cosa que podemos comprobar es la sobrecarga de performance_schema. He encontrado casos en los que por el tipo de operaciones produce tiene un efecto negativo bastante notable, mientras que casi ninguno en otros. Probemos tambi\u00e9n a activarlo y deshabilitarlo.<\/p>\n<p>Estos son los resultados (menos es mejor):<br \/>\n<a href=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_04_innodb_21.png\"><img loading=\"lazy\" decoding=\"async\" src=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_04_innodb_22.png\" alt=\"load_data_04.py results for innodb optimized\" class=\"aligncenter size-full wp-image-507\" width=\"821\" height=\"774\"><\/a><\/p>\n<table>\n<thead style=\"text-align:center; font-weight: bold;\">\n<tr>\n<td><em>MySQL Version<\/em><\/td>\n<td>5.1.72<\/td>\n<td>5.5.39<\/td>\n<td>5.6.20<\/td>\n<td>5.7.4<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>default security and monitoring enabled<\/em><\/td>\n<td>1044.923<\/td>\n<td>1012.488<\/td>\n<td>743.818<\/td>\n<td>850.868<\/td>\n<\/tr>\n<tr>\n<td><em>doublewrite=off<\/em><\/td>\n<td>896.423<\/td>\n<td>848.110<\/td>\n<td>483.542<\/td>\n<td>468.943<\/td>\n<\/tr>\n<tr>\n<td><em>doublewrite=off,checksums=none<\/em><\/td>\n<td>889.827<\/td>\n<td>846.552<\/td>\n<td>488.311<\/td>\n<td>476.916<\/td>\n<\/tr>\n<tr>\n<td><em>doublewrite=off,checksums=none,P_S=off<\/em><\/td>\n<td><\/td>\n<td><\/td>\n<td>488.273<\/td>\n<td>467.716<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Hay varias cosas que comentar aqu\u00ed, alguna de las cuales ni siquiera soy capaz de proporcionar una explicaci\u00f3n:<\/p>\n<ul>\n<li>El doublewrite no reduce el rendimiento a la mitad, pero impacta de manera significativa en el rendimiento (entre un 15% y un 30%)<\/li>\n<li>Sin la escritura doble, la mayor\u00eda de la regresi\u00f3n de 5.7 desaparece (\u00bfpor qu\u00e9?)<\/li>\n<li>El doublewrite es m\u00e1s significativo en 5.6 y 5.7 que en versiones previas de MySQL. Me atrever\u00eda a decir que la mayor\u00eda de otros cuellos de botella han sido eliminados (\u00bfo quiz\u00e1 sea algo como las particiones del buffer pool estando activas por defecto?)<\/li>\n<li>Los checksums de innodb no producen absolutamente ninguna diferencia para este tipo de carga y hardware en particular.<\/li>\n<li>De nuevo, no puedo dar significado estad\u00edstico a una p\u00e9rdida de rendimiento del performance schema. Sin embargo, he obtenido resultados muy variables en los tests, con resultados como un 10% de latencia extra en los valores centrales respecto aquellas con \u00e9l deshabilitado, pero no estoy 100% seguro de esto.<\/li>\n<\/ul>\n<p>En resument, con unos pocos cambios aqu\u00ed y all\u00ed, podemos obtener resultados que tan s\u00f3lo son el doble de lentos que MyISAM, en vez de 5 veces o 12 veces.<\/p>\n<h3>Importar en MyISAM, convertir a InnoDB<\/h3>\n<p>He visto a veces a algunas personas en los foros recomendar la importaci\u00f3n de una tabla en formato MyISAM, y despu\u00e9s convertirla a InnoDB. Veamos si podemos pillar o confirmar este mito con el siguiente c\u00f3digo (<code>load_data_06.py<\/code>):<\/p>\n<pre lang=\"python\">load_data = \"LOAD DATA INFILE '\/tmp\/nodes.csv' INTO TABLE nodes\"\nalter_table = \"ALTER TABLE nodes ENGINE=InnoDB\"\n\ncursor.execute(load_data)\ncursor.execute(alter_table)\n<\/pre>\n<p>Esta es la comparaci\u00f3n (menos es mejor):<br \/>\n<a href=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_06.png\"><img loading=\"lazy\" decoding=\"async\" src=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_06.png\" alt=\"load_data_06.py results\" class=\"aligncenter size-full wp-image-509\" width=\"811\" height=\"729\"><\/a><\/p>\n<table>\n<thead style=\"text-align:center; font-weight: bold;\">\n<tr>\n<td><em>MySQL Version<\/em><\/td>\n<td>5.1.72<\/td>\n<td>5.5.39<\/td>\n<td>5.6.20<\/td>\n<td>5.7.4<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>LOAD DATA InnoDB<\/em><\/td>\n<td>1923.751<\/td>\n<td>1797.220<\/td>\n<td>850.636<\/td>\n<td>1008.349<\/td>\n<\/tr>\n<tr>\n<td><em>LOAD DATA MyISAM; ALTER TABLE ENGINE=InnoDB<\/em><\/td>\n<td>2075.445<\/td>\n<td>2041.893<\/td>\n<td>1537.775<\/td>\n<td>1600.467<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Puedo ver c\u00f3mo esto podr\u00eda ser casi cierto en 5.1, pero es claramente falso en todas las versiones soportadas de MySQL. Sin embargo, tengo que remarcar que <em>es m\u00e1s r\u00e1pido<\/em> que importar la tabla dos veces, una para MyISAM y otra para InnoDB.<\/p>\n<p>Dejar\u00e9 como deberes para el lector comprobarlo para otros motores, como MEMORY o CSV [Pista: Quiz\u00e1 podemos importar a \u00e9ste \u00faltimo motor de una <em>manera diferente<\/em>].<\/p>\n<h3>Carga de manera paralela<\/h3>\n<p>MyISAM escribe a las tablas usando un bloqueo a nivel de tabla (aunque en algunos casos es posible realizar inserts concurrentes), pero InnoDB s\u00f3lo requiere un bloqueo a nivel de fila en muchos casos. \u00bfPodr\u00edamos acelerar el proceso realizando una importaci\u00f3n en paralelo? Esto es lo que quise probar con mi \u00faltimo test. No conf\u00edo en mis habilidades de programaci\u00f3n (o m\u00e1s bien no tengo mucho tiempo) para realizar el movimiento de cursor de archivo y el particionamiento de manera eficiente, as\u00ed que comenzar\u00e9 con un .csv ya dividido en 8 trozos. No deber\u00eda llevar mucho tiempo, pero las herramientas limitadas de la librer\u00eda de threads por defecto, as\u00ed como mi falta de tiempo me hicieron optar por este plan. Tan s\u00f3lo debemos ser conscientes de que no comenzamos con el mismo escenario exacto en este caso. Este es el c\u00f3digo (<code>load_data_08.py<\/code>):<\/p>\n<pre lang=\"python\">NUM_THREADS = 4\n\ndef load_data(port, csv_file):\n    db = mysql.connector.connect(host=\"localhost\", port=port, user=\"msandbox\", passwd=\"msandbox\", database=\"test\")\n    cursor = db.cursor()\n    load_data_sql = \"LOAD DATA INFILE '\" + csv_file + \"' INTO TABLE nodes\"\n    cursor.execute(load_data_sql)   \n    db.commit()\n\nthread_list = []\n\nfor i in range(1, NUM_THREADS + 1):\n   t = threading.Thread(target=load_data, args=(port, '\/tmp\/nodes.csv' + `i`))\n   thread_list.append(t)\n\nfor thread in thread_list:\n   thread.start()\n\nfor thread in thread_list:\n   thread.join()\n<\/pre>\n<p>Y estos son los resultados, con diferentes par\u00e1metros (menos es mejor):<br \/>\n<a href=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_08.png\"><img loading=\"lazy\" decoding=\"async\" src=\"\/dbahire\/wp-content\/uploads\/2014\/10\/load_data_08.png\" alt=\"load_data_08.py results\" class=\"aligncenter size-full wp-image-511\" width=\"794\" height=\"772\"><\/a><\/p>\n<table>\n<thead style=\"text-align:center; font-weight: bold;\">\n<tr>\n<td><em>MySQL Version<\/em><\/td>\n<td>5.1.72<\/td>\n<td>5.5.39<\/td>\n<td>5.6.20<\/td>\n<td>5.7.4<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>1 thread, log_file_size=2G, buffer_pool=8G<\/em><\/td>\n<td>894.367<\/td>\n<td>859.965<\/td>\n<td>488.273<\/td>\n<td>467.716<\/td>\n<\/tr>\n<tr>\n<td><em>8 threads, log_file_size=2G, buffer_pool=8G<\/em><\/td>\n<td>752.233<\/td>\n<td>704.444<\/td>\n<td>370.598<\/td>\n<td>290.343<\/td>\n<\/tr>\n<tr>\n<td><em>8 threads, log_file_size=5G, buffer_pool=20G<\/em><\/td>\n<td><\/td>\n<td><\/td>\n<td>301.693<\/td>\n<td>243.544<\/td>\n<\/tr>\n<tr>\n<td><em>4 threads, log_file_size=5G, buffer_pool=20G<\/em><\/td>\n<td><\/td>\n<td><\/td>\n<td>295.884<\/td>\n<td>245.569<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>En este m\u00e9todo podemos ver que:<\/p>\n<ul>\n<li>Hay pocos cambios de rendimiento entre realizar la carga paralela con 4 u 8 threads. Esta es una m\u00e1quina con 4 cores (8 HT)<\/li>\n<li>La paralelizaci\u00f3n ayuda, aunque no escala (4-8 hilos proporcionan alrededor de un 33% de velocidad)<\/li>\n<li>\u00c9ste es el caso en le que 5.6 y especialmente 5.7 destacan<\/li>\n<li>Un log de transacciones y un buffer pool mayor (de m\u00e1s de 4G, s\u00f3lo disponible en 5.6+) siguen siendo \u00fatiles para reducir la carga<\/li>\n<li>La carga paralela con 5.7 es la manera m\u00e1s r\u00e1pida en la que puedo cargar este archivo usando InnoDB: 243 segundos. Es 1.8x veces la manera m\u00e1s r\u00e1pida en la que puedo cargar una tabla en MyISAM (5.1, monohilo): 134 seconds. \u00a1Esto son casi 200K filas\/s!<\/li>\n<\/ul>\n<h3>Resumen y preguntas abiertas<\/h3>\n<ul>\n<li>La manera m\u00e1s r\u00e1pida en la que puedes importar una tabla en MySQL sin usar archivos binarios es usando la sintaxis <code>LOAD DATA<\/code>. Usa la paralelizaci\u00f3n en InnoDB para mejorar los resultados, y recuerda ajustar par\u00e1metros b\u00e1sicos como el tama\u00f1o de archivo de transacciones y el buffer pool. Una programaci\u00f3n y imporaci\u00f3n hecha con el debido cuidado pueden convertir un problema de m\u00e1s de 2 horas en un proceso de apenas 2 minutos. Es posible deshabilitar temporalmente algunas caracter\u00edsticas de seguridad para obtener rendimiento extra.<\/li>\n<li>Parece haber una importante regresi\u00f3n en 5.7 para esta carga en particular de una importaci\u00f3n monohlo tanto para MyISAM como para InnoDB, con unos resultados hasta un 15% peores en rendimiento que 5.6. Todav\u00eda no s\u00e9 porqu\u00e9.<\/li>\n<li>En el aspecto positivo, parece haber un aumento (de hasta el 20%) en relaci\u00f3n con 5.6 en una carga paralela de escrituras.<\/li>\n<li>Es posible que haya un impacto en el rendimiento de Performance schema para este tipo de operaci\u00f3n, pero soy incapaz de medirlo de manera fiable (es m\u00e1s cercando a 0 que mi error de medida). Esto es algo positivo.<\/li>\n<\/ul>\n<p>Estar\u00eda agradecido si me pudierais indicar cualquier error en alguna de mis suposiciones.<\/p>\n<p>Aqu\u00ed pod\u00e9is descargar los diferentes <a href=\"http:\/\/dbahire.com\/wp-content\/uploads\/2014\/10\/load_data_python.zip\">scripts de Python usados para probar la carga de datos en MySQL<\/a>.<\/p>\n<p>Recordad que esto no son benchmarks &#8220;formales&#8221;, y que ya no dispongo de acceso a la m\u00e1quina donde los gener\u00e9. Todav\u00eda tengo pendiente el analizar si existe el mismo problema en 5.7.5. Hay <a href=\"http:\/\/smalldatum.blogspot.com\/2014\/10\/low-concurrency-performance-for-point.html\">otras personas apuntando a regresiones bajo limitada concurrencia, como Mark Callaghan<\/a>, quiz\u00e1 est\u00e9n relacionados? Como de costumbre, pod\u00e9is dejarme un comentario <a href=\"http:\/\/dbahire.com\/probando-la-manera-mas-rapida-de-importar-una-tabla-en-mysql-y-unos-resultados-interesantes-del-rendimiento-de-5-7\">aqu\u00ed<\/a> o mandarme una respuesta mediante <a href=\"http:\/\/twitter.com\/dbahire_es\">Twitter<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Como mencion\u00e9 en mi anterior entrada, donde comparaba las opciones de configuraci\u00f3n por defecto de 5.6 y 5.7, he estado haciendo algunos tests para un tipo particular de carga en varias versiones de MySQL. Lo que he estado comprobando es<\/p>\n","protected":false},"author":1,"featured_media":504,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[85],"tags":[197,198,86,163,158,204,203,205,199,200,457,194,195,90,165,112,201,196,202,206,107],"class_list":["post-541","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-mysql-es","tag-5-1-es","tag-5-5-es","tag-5-6-es","tag-5-7-es","tag-5-7-4","tag-benchmark-es","tag-carga","tag-csv-es","tag-import-es","tag-load-data-es","tag-mysql-es","tag-mysql-5-1","tag-mysql-5-5","tag-mysql-5-6-es","tag-mysql-5-7-es","tag-mysqldump-es","tag-performance-es","tag-primary","tag-rendimiento","tag-tsv-es","tag-velocidad"],"_links":{"self":[{"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/posts\/541","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/comments?post=541"}],"version-history":[{"count":12,"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/posts\/541\/revisions"}],"predecessor-version":[{"id":1050,"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/posts\/541\/revisions\/1050"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/media\/504"}],"wp:attachment":[{"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/media?parent=541"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/categories?post=541"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jynus.com\/dbahire\/wp-json\/wp\/v2\/tags?post=541"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}