Metodo più veloce per wp_insert_post e add_post_meta in blocco

8 giu 2013, 19:37:53
Visualizzazioni: 16.2K
Voti: 21

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.

1
Commenti

Oltre ai suggerimenti per WP qui sotto, considera anche l'utilizzo di InnoDB in MySQL e l'esecuzione di commit delle transazioni in batch, come indicato in questa risposta.

webaware webaware
9 giu 2013 01:20:00
Tutte le risposte alla domanda 4
14
32

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.

8 giu 2013 22:36:03
Commenti

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.

Otto Otto
8 giu 2013 23:27:00

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

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

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

birgire birgire
9 giu 2013 00:05:30

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

birgire birgire
9 giu 2013 00:15:43

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.

Otto Otto
9 giu 2013 00:46:58

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

Otto Otto
9 giu 2013 00:48:13

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

birgire birgire
9 giu 2013 12:23:28

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

Otto Otto
10 giu 2013 19:26:09

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

yeahman yeahman
6 mag 2014 20:27:25

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

birgire birgire
6 mag 2014 20:36:19

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.

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

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.

kovshenin kovshenin
2 ago 2016 03:01:42

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

Annapurna Annapurna
22 ago 2017 13:47:20

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

T.Todua T.Todua
14 giu 2021 16:35:24
Mostra i restanti 9 commenti
0

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 );
2 ago 2016 00:41:38
6

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.

8 giu 2013 20:13:34
Commenti

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

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

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

OneOfOne OneOfOne
9 mar 2015 19:04:46

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.

s_ha_dum s_ha_dum
10 apr 2015 03:00:16

+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"

Zorox Zorox
5 ott 2015 15:48:10

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.

kovshenin kovshenin
2 ago 2016 02:58:08

questa funzione salterà tutti gli hook esterni (se ci sono azioni collegate a update_post_meta)

T.Todua T.Todua
26 apr 2018 00:13:57
Mostra i restanti 1 commenti
0

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;' );    } );
14 set 2018 20:04:11