Método más rápido para wp_insert_post y add_post_meta en masa

8 jun 2013, 19:37:53
Vistas: 16.2K
Votos: 21

Tengo un archivo CSV que quiero importar que contiene aproximadamente 1,500 filas y 97 columnas. Actualmente tarda entre 2-3 horas en completar la importación completa y me gustaría mejorar esto si existe alguna forma. En este momento, por cada fila estoy haciendo un $post_id = wp_insert_post y luego un add_post_meta para las 97 columnas asociadas con cada fila. Esto es bastante ineficiente...

¿Existe una mejor manera de hacer esto para obtener un post_id y mantener la relación entre el post y sus valores de post_meta?

Actualmente estoy probando esto en mi máquina local con WAMP pero luego lo ejecutaré en un VPS

1
Comentarios

Además de los consejos de WP a continuación, también considera usar InnoDB en MySQL y confirmar transacciones por lotes, según esta respuesta.

webaware webaware
9 jun 2013 01:20:00
Todas las respuestas a la pregunta 4
14
32

Tuve problemas similares hace algún tiempo con una importación personalizada de CSV, pero terminé usando SQL personalizado para la inserción masiva. Sin embargo, no había visto esta respuesta en ese entonces:

¿Optimizar la inserción y eliminación de publicaciones para operaciones masivas?

para usar wp_defer_term_counting() para habilitar o deshabilitar el conteo de términos.

Además, si revisas el código fuente del plugin de importación de WordPress, verás estas funciones justo antes de la importación masiva:

wp_defer_term_counting( true );
wp_defer_comment_counting( true );

y luego después de la inserción masiva:

wp_defer_term_counting( false );
wp_defer_comment_counting( false );

Así que esto podría ser algo para probar ;-)

Importar publicaciones como borrador en lugar de publicado también acelerará el proceso, ya que se omite el lento proceso de encontrar un slug único para cada una. Por ejemplo, podrías publicarlas más tarde en pasos más pequeños, pero ten en cuenta que este tipo de enfoque necesitaría marcar las publicaciones importadas de alguna manera, para no publicar cualquier borrador más tarde. Esto requeriría una planificación cuidadosa y muy probablemente algo de código personalizado.

Si hay, por ejemplo, muchos títulos de publicaciones similares (mismo post_name) para importar, entonces wp_unique_post_slug() puede volverse lento debido a la iteración de consultas en bucle para encontrar un slug disponible. Esto puede generar una gran cantidad de consultas a la base de datos.

Desde WordPress 5.1, el filtro pre_wp_unique_post_slug está disponible para evitar la iteración en bucle para el slug. Consulta el ticket del núcleo #21112. Aquí hay un ejemplo:

add_filter( 'pre_wp_unique_post_slug', 
    function( $override_slug, $slug, $post_id, $post_status, $post_type, $post_parent ) {
        // Establece un valor de slug único para evitar el bucle de iteración del slug.
        // $override_slug = ...

        return $override_slug;
    }, 10, 6
);

Si se prueba, por ejemplo, $override_slug = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix" con $suffix como $post_id, notaríamos que $post_id siempre es 0 para nuevas publicaciones, como era de esperar. Sin embargo, hay varias formas de generar números únicos en PHP, como uniqid( '', true ). Pero usa este filtro con cuidado para asegurarte de tener slugs únicos. Podríamos, por ejemplo, ejecutar una consulta de conteo grupal después en post_name para estar seguros.

Otra opción sería usar WP-CLI para evitar tiempos de espera. Consulta, por ejemplo, mi respuesta publicada para ¿Crear 20,000 publicaciones o páginas usando un archivo .csv?

Entonces podemos ejecutar nuestro script de importación PHP personalizado import.php con el comando WP-CLI:

wp eval-file import.php

También evita importar una gran cantidad de tipos de publicaciones jerárquicos, ya que la interfaz actual de wp-admin no los maneja bien. Consulta, por ejemplo, Tipo de publicación personalizada - lista de publicaciones - pantalla blanca de la muerte

Aquí está el gran consejo de @otto:

Antes de inserciones masivas, deshabilita el modo autocommit explícitamente:

$wpdb->query( 'SET autocommit = 0;' );

Después de las inserciones masivas, ejecuta:

$wpdb->query( 'COMMIT;' );

También creo que sería una buena idea hacer algo de mantenimiento como:

$wpdb->query( 'SET autocommit = 1;' );

No he probado esto en MyISAM pero debería funcionar en InnoDB.

Como mencionó @kovshenin, este consejo no funcionaría para MyISAM.

8 jun 2013 22:36:03
Comentarios

Además de esto, también puedes usar la función de consulta para desactivar el autocommit antes, y luego confirmar manualmente después de que se hayan realizado las inserciones. Esto acelera significativamente las operaciones a nivel de la base de datos cuando se realizan inserciones masivas. Simplemente envía un SET autocommit=0; antes de las inserciones, seguido de un COMMIT; después.

Otto Otto
8 jun 2013 23:27:00

¡Interesante, gracias por eso! Tendré que probarlo cuando llegue a casa.

Corey Rowell Corey Rowell
8 jun 2013 23:31:19

@Otto, gracias por el excelente consejo. Entonces podríamos hacer $wpdb->query('SET autocommit = 0;'); antes de las inserciones, pero ¿podemos omitir $wpdb->query('START TRANSACTION;'); en ese caso? Revisaré el manual de MySQL para aprender más al respecto ;-) saludos.

birgire birgire
9 jun 2013 00:05:30

Aquí hay una referencia útil (MySQL 5.1) http://dev.mysql.com/doc/refman/5.1/en/commit.html sobre este tema - al menos para mí ;-)

birgire birgire
9 jun 2013 00:15:43

Iniciar una transacción deshabilita implícitamente el autocommit hasta que la transacción finaliza con un commit. Sin embargo, para scripts de importación de un solo uso, me resulta mucho más claro simplemente desactivar el autocommit yo mismo y luego hacer COMMIT cuando lo desee. La lógica transaccional es genial para hacer múltiples cosas, pero para una importación única y puntual, es más fácil hacerlo a la fuerza bruta.

Otto Otto
9 jun 2013 00:46:58

Si estuviera haciendo muchas más importaciones que 1500 posts (como los 400k que he hecho antes), entonces deshabilitaría el autocommit y lo configuraría para hacer un COMMIT cada, digamos, 500 posts... De esta manera puedo tener una lógica reiniciable desde un punto de fallo dado manteniendo aún una alta velocidad.

Otto Otto
9 jun 2013 00:48:13

gracias, lo tendré en cuenta. busqué "autocommit" en svn.wp-plugins.org pero no encontré mucho (solo un resultado para un caso de prueba unitaria) así que podría ser una buena idea como opción para los plugins de importación ;-)

birgire birgire
9 jun 2013 12:23:28

Ni siquiera sabía que svn.wp-plugins.org aún funcionaba. El nombre oficial ahora es plugins.svn.wordpress.org. :)

Otto Otto
10 jun 2013 19:26:09

entonces pongo $wpdb->query('SET autocommit = 0;'); antes de mis inserciones y después de las inserciones ejecuto $wpdb->query('COMMIT'); ¿es así?

yeahman yeahman
6 may 2014 20:27:25

sí, creo que eso debería funcionar y quizás agregar $wpdb->query('SET autocommit = 1;'); nuevamente después.

birgire birgire
6 may 2014 20:36:19

Cuando tienes una caché de objetos, la lógica de transacciones puede terminar con resultados extraños, especialmente si el código falla antes del commit, ya que la caché tendrá datos que no están en la base de datos, lo que podría llevar a errores muy difíciles de depurar.

Mark Kaplun Mark Kaplun
15 dic 2014 07:30:16

Buen punto Mark. Si estos son solo inserts y no updates, entonces wp_suspend_cache_addition( true ) debería ayudar a NO poner cosas en la caché de objetos. También @birgire mencionó que no probaron esto con MyISAM -- no te molestes, este motor de almacenamiento no soporta transacciones, así que establecer autocommit o iniciar una transacción no tendrá ningún efecto.

kovshenin kovshenin
2 ago 2016 03:01:42

gran consejo @Otto. Mi consulta antes tomaba 38 segundos, ahora toma 1 segundo.

Annapurna Annapurna
22 ago 2017 13:47:20

MyIsam e InnoDB tienen enfoques diferentes. https://stackoverflow.com/a/32913817/2377343

T.Todua T.Todua
14 jun 2021 16:35:24
Mostrar los 9 comentarios restantes
0

Tuve que agregar esto:

    remove_action('do_pings', 'do_all_pings', 10, 1);

Ten en cuenta que esto omitirá do_all_pings, que procesa pingbacks, enclosures, trackbacks y otros pings (enlace: https://developer.wordpress.org/reference/functions/do_all_pings/). Según mi entendimiento al revisar el código, los pingbacks/trackbacks/enclosures pendientes aún se procesarán después de eliminar esta línea remove_action, pero no estoy completamente seguro.

Actualización: También agregué

    define( 'WP_IMPORTING', true );

Además de eso, estoy usando:

    ini_set("memory_limit",-1);
    set_time_limit(0);
    ignore_user_abort(true);

    wp_defer_term_counting( true );
    wp_defer_comment_counting( true );
    $wpdb->query( 'SET autocommit = 0;' );

    /* Insertando 100,000 posts a la vez
       incluyendo asignar un término de taxonomía y agregar meta keys
       (es decir, un bucle `foreach` donde cada iteración contiene:
       `wp_insert_post`, `wp_set_object_terms`, `add_post_meta`.)
    */

    $wpdb->query( 'COMMIT;' );
    wp_defer_term_counting( false );
    wp_defer_comment_counting( false );
2 ago 2016 00:41:38
6

Necesitarás insertar el post para obtener tu ID pero la tabla $wpdb->postmeta es muy simple en estructura. Probablemente podrías usar una sentencia directa INSERT INTO, como esta de la documentación de MySQL: INSERT INTO tbl_name (a,b,c) VALUES(1,2,3),(4,5,6),(7,8,9);

En tu caso...

$ID = 1; // desde tu wp_insert_post
$values = '($ID,2,3),($ID,5,6),($ID,8,9)'; // construye desde tus 97 columnas; yo usaría algún tipo de bucle
$wpdb->query("INSERT INTO {$wpdb->postmeta} (post_id,meta_key,meta_value) VALUES {$values}");

Eso no manejará codificación, serialización, escape, comprobación de errores, duplicados ni nada más, pero esperaría que fuera más rápido (aunque no lo he probado).

No haría esto en un sitio en producción sin pruebas exhaustivas, y si solo tuviera que hacerlo una o dos veces, usaría las funciones principales y me tomaría un almuerzo largo mientras se importan las cosas.

8 jun 2013 20:13:34
Comentarios

Creo que me tomaré un almuerzo largo, prefiero no insertar datos crudos en mis tablas y no tiene sentido reescribir lo que WordPress ya hace.

Corey Rowell Corey Rowell
8 jun 2013 21:27:24

así es como ocurre la inyección SQL, así que por favor no uses esto.

OneOfOne OneOfOne
9 mar 2015 19:04:46

Todo está hard-coded, @OneOfOne. La inyección no puede ocurrir—por definición—sin entrada suministrada por el usuario. Esa es la naturaleza de la "inyección". El OP está importando datos de un archivo .csv que está bajo su control usando código bajo su control. No hay oportunidad para que un tercero inyecte nada. Por favor presta atención al contexto.

s_ha_dum s_ha_dum
10 abr 2015 03:00:16

+1 de mi parte, necesitaba agregar 20 valores de campos personalizados y esto fue mucho más rápido que "add_post_meta"

Zorox Zorox
5 oct 2015 15:48:10

No puedes esperar que el OP revise minuciosamente el archivo CSV antes de importarlo, por lo que deberías tratarlo como entrada de usuario y al menos usar ->prepare() en tus sentencias SQL. En tu escenario, ¿qué pasaría si la columna ID en el CSV contuviera algo como 1, 'foo', 'bar'); DROP TABLE wp_users; --? Probablemente algo malo.

kovshenin kovshenin
2 ago 2016 02:58:08

esta función omitirá todos los hooks externos (si hay acciones enganchadas a update_post_meta)

T.Todua T.Todua
26 abr 2018 00:13:57
Mostrar los 1 comentarios restantes
0

Nota importante sobre 'SET autocommit = 0;'

Después de configurar autocommit = 0, si el script detiene su ejecución (por alguna razón, como exit, un error fatal, etc.), ¡tus cambios NO SE GUARDARÁN EN LA BASE DE DATOS!

$wpdb->query( 'SET autocommit = 0;' );

update_option("algo", "valor");     

exit; //supongamos que aquí ocurre un error o algo...

$wpdb->query( 'COMMIT;' );

En este caso, ¡update_option no se guardará en la base de datos!

Por lo tanto, el mejor consejo es registrar COMMIT en una función de shutdown como precaución (en caso de que ocurra alguna terminación inesperada).

register_shutdown_function( function(){     $GLOBALS['wpdb']->query( 'COMMIT;' );    } );
14 sept 2018 20:04:11