Método más rápido para wp_insert_post y add_post_meta en masa
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

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.

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.

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

@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.

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í ;-)

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.

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.

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 ;-)

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

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

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

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.

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.

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

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

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 );

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.

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.

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.

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

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.

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;' ); } );
