Metodo più veloce per wp_insert_post e add_post_meta in blocco
Ho un file CSV che voglio importare, composto da circa 1.500 righe e 97 colonne. Attualmente ci vogliono circa 2-3 ore per completare un'importazione completa e vorrei ottimizzare questo processo se possibile. Al momento, per ogni riga eseguo un $post_id = wp_insert_post
e poi un add_post_meta
per ognuna delle 97 colonne associate a ciascuna riga. Questo metodo è piuttosto inefficiente...
Esiste un modo migliore per gestire questa operazione, mantenendo la relazione tra post e i suoi valori di post_meta?
Al momento sto testando questa operazione in locale con WAMP, ma dovrà essere eseguita su un VPS.

Anche io ho avuto problemi simili tempo fa con un import personalizzato da CSV, ma alla fine ho risolto utilizzando del codice SQL personalizzato per l'inserimento in blocco. All'epoca però non avevo visto questa risposta:
Ottimizzare inserimento ed eliminazione di post per operazioni massive?
che suggerisce di usare wp_defer_term_counting()
per abilitare/disabilitare il conteggio dei termini.
Se dai un'occhiata al codice sorgente del plugin WordPress Importer, noterai queste funzioni appena prima dell'importazione massiva:
wp_defer_term_counting( true );
wp_defer_comment_counting( true );
e poi dopo l'inserimento in blocco:
wp_defer_term_counting( false );
wp_defer_comment_counting( false );
Quindi potrebbe valere la pena provare ;-)
Importare i post come bozza invece che pubblicati velocizzerà le operazioni, poiché si salta il lento processo di ricerca di uno slug univoco per ciascuno. Si potrebbero poi pubblicare in un secondo momento in step più piccoli, ma attenzione: questo approccio richiederebbe di marcare in qualche modo i post importati, per evitare di pubblicare per sbaglio altre bozze! Servirebbe una pianificazione accurata e probabilmente del codice personalizzato.
Se ci sono molti titoli di post simili (stesso post_name
) da importare, wp_unique_post_slug()
può diventare lento a causa del loop di query per trovare uno slug disponibile. Questo può generare un enorme numero di query al database.
Da WordPress 5.1 è disponibile il filtro pre_wp_unique_post_slug
per evitare l'iterazione del loop per lo slug. Vedi ticket core #21112. Ecco un esempio:
add_filter( 'pre_wp_unique_post_slug',
function( $override_slug, $slug, $post_id, $post_status, $post_type, $post_parent ) {
// Imposta un valore di slug univoco per evitare il loop di iterazione
// $override_slug = ...
return $override_slug;
}, 10, 6
);
Se provi ad esempio $override_slug = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"
con $suffix
come $post_id
, noterai che $post_id
è sempre 0
per i nuovi post, come previsto. Ci sono comunque vari modi per generare numeri univoci in PHP, come uniqid( '', true )
. Usa questo filtro con attenzione per assicurarti di avere slug univoci. Potremmo ad esempio eseguire una query di conteggio sui post_name
per verificare.
Un'altra opzione è usare WP-CLI per evitare timeout. Vedi la mia risposta su Creare 20.000 Post o Pagine usando un file .csv?
Possiamo quindi eseguire il nostro script PHP personalizzato import.php
con il comando WP-CLI:
wp eval-file import.php
Evita anche di importare un gran numero di tipi di post gerarchici, poiché l'interfaccia wp-admin attuale non li gestisce bene. Vedi Custom post type - elenco post - schermata bianca della morte
Ecco il grande suggerimento di @otto:
Prima degli inserimenti massivi, disabilita esplicitamente la modalità autocommit
:
$wpdb->query( 'SET autocommit = 0;' );
Dopo gli inserimenti massivi, esegui:
$wpdb->query( 'COMMIT;' );
Penso sia anche buona norma fare un po' di pulizia con:
$wpdb->query( 'SET autocommit = 1;' );
Non ho testato su MyISAM ma dovrebbe funzionare su InnoDB.
Come menzionato da @kovshenin, questo suggerimento non funzionerebbe per MyISAM.

Inoltre, puoi anche utilizzare la funzione query per disattivare l'autocommit prima, e poi effettuare manualmente il commit dopo che le inserzioni sono state completate. Questo accelera notevolmente le operazioni a livello di database quando si effettuano inserimenti massivi. Basta inviare un SET autocommit=0;
prima degli inserimenti, seguito da un COMMIT;
dopo.

Interessante, grazie per il suggerimento! Dovrò testarlo quando torno a casa.

@Otto, grazie per l'ottimo suggerimento. Quindi potremmo fare $wpdb->query('SET autocommit = 0;');
prima degli inserimenti, ma possiamo saltare $wpdb->query('START TRANSACTION;');
in quel caso? Controllerò il manuale di MySQL per saperne di più ;-) saluti.

Ecco un riferimento utile (MySQL 5.1) http://dev.mysql.com/doc/refman/5.1/en/commit.html su questo argomento - almeno per me ;-)

L'avvio di una transazione disabilita implicitamente l'autocommit fino al completamento della transazione con un commit. Tuttavia, per script di importazione monouso, trovo molto più chiaro disattivare semplicemente l'autocommit da solo e poi eseguire COMMIT quando lo desidero. La logica transazionale è ottima per fare più operazioni, ma per un'importazione una tantum singola, è più semplice forzarla manualmente.

Se dovessi fare molte più importazioni rispetto a 1500 post (come i 400k che ho fatto in passato), allora disabiliterei l'autocommit e lo imposterei per fare un COMMIT ogni, diciamo, 500 post.. In questo modo posso ottenere una logica riavviabile da un punto di errore pur mantenendo un'alta velocità.

grazie, lo terrò a mente. Ho cercato "autocommit" su svn.wp-plugins.org ma non ho trovato molto (solo un risultato per un caso di unit test) quindi potrebbe essere una buona idea come opzione per i plugin di importazione ;-)

Non sapevo nemmeno che svn.wp-plugins.org funzionasse ancora. Il nome ufficiale ora è plugins.svn.wordpress.org. :)

quindi metto $wpdb->query('SET autocommit = 0;');
prima dei miei insert e dopo gli insert eseguo $wpdb->query('COMMIT');
è tutto qui?

sì, penso che dovrebbe funzionare e forse aggiungere di nuovo $wpdb->query('SET autocommit = 1;');
dopo.

Quando hai una cache oggetti, la logica delle transazioni potrebbe portare a risultati strani, specialmente se il codice fallisce prima del commit, poiché la cache conterrà dati che non sono nel database, il che potrebbe portare a bug molto difficili da debuggare.

Ottimo punto Mark. Se si tratta solo di inserimenti e non di aggiornamenti, allora wp_suspend_cache_addition( true )
dovrebbe aiutare a NON inserire dati nella cache oggetti. Inoltre, @birgire ha menzionato che non l'hanno testato con MyISAM -- non preoccupartene, il motore di archiviazione non supporta le transazioni, quindi impostare autocommit o avviare una transazione non avrà alcun effetto.

ottimo consiglio @Otto. La mia query prima impiegava 38 secondi, ora impiega 1 secondo.

MyIsam e InnoDB hanno approcci diversi. https://stackoverflow.com/a/32913817/2377343

Ho dovuto aggiungere questo:
remove_action('do_pings', 'do_all_pings', 10, 1);
Tieni presente che questo salterà do_all_pings
, che elabora i pingback, gli enclosure, i trackback e altri ping (link: https://developer.wordpress.org/reference/functions/do_all_pings/). Dalla mia comprensione osservando il codice, i pingback/trackback/enclosure in sospeso verranno comunque elaborati dopo aver rimosso questa linea remove_action
, ma non ne sono completamente sicuro.
Aggiornamento: ho anche aggiunto
define( 'WP_IMPORTING', true );
Oltre a questo sto 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;' );
/* Inserimento di 100.000 post alla volta
includendo l'assegnazione di un termine di tassonomia e l'aggiunta di meta key
(cioè un ciclo `foreach` con ogni iterazione contenente:
`wp_insert_post`, `wp_set_object_terms`, `add_post_meta`.)
*/
$wpdb->query( 'COMMIT;' );
wp_defer_term_counting( false );
wp_defer_comment_counting( false );

Dovrai inserire il post per ottenere il tuo ID, ma la tabella $wpdb->postmeta
è molto semplice nella struttura. Probabilmente potresti usare direttamente un'istruzione INSERT INTO
, come questa dalla documentazione di MySQL: INSERT INTO tbl_name (a,b,c) VALUES(1,2,3),(4,5,6),(7,8,9);
Nel tuo caso...
$ID = 1; // dal tuo wp_insert_post
$values = '($ID,2,3),($ID,5,6),($ID,8,9)'; // costruisci dai tuoi 97 colonne; userei un qualche tipo di ciclo
$wpdb->query("INSERT INTO {$wpdb->postmeta} (post_id,meta_key,meta_value) VALUES {$values}");
Questo non gestirà alcuna codifica, serializzazione, escape, controllo degli errori, duplicazioni o altro, ma mi aspetto che sia più veloce (anche se non l'ho provato).
Non lo farei su un sito in produzione senza test approfonditi, e se dovessi farlo solo una o due volte, userei le funzioni principali e mi prenderei una lunga pausa pranzo durante l'importazione.

Penso che farò una lunga pausa pranzo, preferisco non inserire dati grezzi nelle mie tabelle e non ha senso riscrivere ciò che Wordpress fa già.

è così che avviene l'injection in MySQL, quindi per favore non usare questo metodo.

Tutto è hard-coded, @OneOfOne. L'injection non può - per definizione - avvenire senza input fornito dall'utente. Questa è la natura dell'"injection". L'OP sta importando dati da un file .csv che è sotto il suo controllo usando codice sotto il suo controllo. Non c'è alcuna opportunità per terze parti di iniettare qualcosa. Per favore presta attenzione al contesto.

+1 da parte mia, avevo bisogno di aggiungere 20 valori di campi personalizzati e questo metodo è stato molto più veloce rispetto a "add_post_meta"

Non puoi aspettarti che l'OP controlli accuratamente il file CSV prima di importarlo, quindi dovresti trattarlo come input utente e almeno usare ->prepare()
per le tue istruzioni SQL. Nel tuo scenario, cosa accadrebbe se la colonna ID nel CSV contenesse qualcosa come 1, 'foo', 'bar'); DROP TABLE wp_users; --
? Probabilmente qualcosa di brutto.

Nota importante su 'SET autocommit = 0;'
Dopo aver impostato autocommit = 0
, se lo script interrompe l'esecuzione (per qualche motivo, come exit
, un errore fatale o altro...), le tue modifiche NON VERRANNO SALVATE NEL DATABASE!
$wpdb->query( 'SET autocommit = 0;' );
update_option("qualcosa", "valore");
exit; //supponiamo che qui avvenga un errore o altro...
$wpdb->query( 'COMMIT;' );
In questo caso update_option
non verrà salvato nel database!
Quindi, il miglior consiglio è registrare COMMIT
in una funzione shutdown
come precauzione (nel caso accada un'interruzione imprevista).
register_shutdown_function( function(){ $GLOBALS['wpdb']->query( 'COMMIT;' ); } );
