Metodă mai rapidă pentru wp_insert_post și add_post_meta în masă
Am un fișier CSV pe care vreau să îl import, care conține aproximativ 1.500 de rânduri și 97 de coloane. În prezent, un import complet durează între 2-3 ore și aș dori să îmbunătățesc acest proces dacă există o posibilitate. Momentan, pentru fiecare rând fac un $post_id = wp_insert_post
și apoi un add_post_meta
pentru cele 97 de coloane asociate fiecărui rând. Acest proces este destul de ineficient...
Există o metodă mai bună de a aborda această situație, astfel încât să pot obține un post_id și să păstrez relația dintre post și valorile sale post_meta?
În prezent testez acest proces pe mașina mea locală cu WAMP, dar ulterior va rula pe un VPS.

Am avut probleme similare acum ceva timp cu un import CSV personalizat, dar am rezolvat folosind niște interogări SQL personalizate pentru inserarea în masă. Dar nu văzusem acest răspuns pe atunci:
Optimizarea inserării și ștergerii postărilor pentru operațiuni în masă?
să folosești wp_defer_term_counting()
pentru a activa sau dezactiva numărarea termenilor.
De asemenea, dacă verifici sursa plugin-ului WordPress importer, vei vedea aceste funcții chiar înainte de importul în masă:
wp_defer_term_counting( true );
wp_defer_comment_counting( true );
și apoi după inserarea în masă:
wp_defer_term_counting( false );
wp_defer_comment_counting( false );
Așadar, acesta ar putea fi ceva de încercat ;-)
Importarea postărilor ca draft în loc de publish va accelera și ea procesul, deoarece este omis procesul lent de găsire a unui slug unic pentru fiecare. Se poate, de exemplu, să le publicăm mai târziu în pași mai mici, dar reține că acest tip de abordare ar necesita marcarea postărilor importate într-un fel, ca să nu publicăm orice draft-uri mai târziu! Acest lucru ar necesita o planificare atentă și cel mai probabil niște codare personalizată.
Dacă există, de exemplu, multe titluri de postări similare (același post_name
) de importat, atunci wp_unique_post_slug()
poate deveni lent, datorită iterației prin interogări pentru a găsi un slug disponibil. Acest lucru poate genera un număr enorm de interogări la baza de date.
Începând cu WordPress 5.1, filtrul pre_wp_unique_post_slug
este disponibil pentru a evita iterația pentru slug. Vezi ticket-ul de core #21112. Iată un exemplu:
add_filter( 'pre_wp_unique_post_slug',
function( $override_slug, $slug, $post_id, $post_status, $post_type, $post_parent ) {
// Setează o valoare unică pentru slug pentru a scurcircuita bucla de iterație.
// $override_slug = ...
return $override_slug;
}, 10, 6
);
Dacă încerci, de exemplu, $override_slug = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"
cu $suffix
ca $post_id
, atunci vom observa că $post_id
este întotdeauna 0
pentru postările noi, așa cum e de așteptat. Există însă diverse modalități de a genera numere unice în PHP, cum ar fi uniqid( '', true )
. Dar folosește acest filtru cu grijă pentru a te asigura că ai slug-uri unice. Am putea, de exemplu, să rulăm o interogare de numărare după grup pe post_name
pentru a fi siguri.
O altă opțiune ar fi să folosești WP-CLI pentru a evita timeout-ul. Vezi, de exemplu, răspunsul meu postat pentru Crearea a 20.000 de Postări sau Pagini folosind un fișier .csv?
Apoi putem rula script-ul PHP personalizat de import import.php
cu comanda WP-CLI:
wp eval-file import.php
De asemenea, evită importul unui număr mare de tipuri de postări ierarhice, deoarece interfața actuală wp-admin nu le gestionează bine. Vezi, de exemplu, Custom post type - listă de postări - ecran alb al morții
Iată sfatul minunat de la @otto:
Înainte de inserări în masă, dezactivează modul autocommit
explicit:
$wpdb->query( 'SET autocommit = 0;' );
După inserările în masă, rulează:
$wpdb->query( 'COMMIT;' );
De asemenea, cred că ar fi o idee bună să faci niște curățenie precum:
$wpdb->query( 'SET autocommit = 1;' );
Nu am testat acest lucru pe MyISAM, dar ar trebui să funcționeze pe InnoDB.
După cum a menționat @kovshenin, acest sfat nu ar funcționa pentru MyISAM.

Pe lângă aceasta, poți folosi și funcția de interogare pentru a dezactiva autocommit înainte, iar apoi să faci commit manual după ce inserțiile au fost efectuate. Acest lucru accelerează semnificativ operațiunile la nivel de bază de date atunci când faci inserții în masă. Pur și simplu trimite un SET autocommit=0;
înaintea inserțiilor, urmat de un COMMIT;
după aceea.

Interesant, mulțumesc pentru asta! Va trebui să testez când ajung acasă.

@Otto, mulțumesc pentru pontul excelent. Deci am putea face $wpdb->query('SET autocommit = 0;');
înaintea inserțiilor, dar putem sărim peste $wpdb->query('START TRANSACTION;');
în acest caz? O să verific manualul MySQL pentru a afla mai multe despre asta ;-) noroc.

Iată o referință utilă (MySQL 5.1) http://dev.mysql.com/doc/refman/5.1/en/commit.html pe această temă - cel puțin pentru mine ;-)

Începerea unei tranzacții dezactivează implicit autocommit-ul până când tranzacția este finalizată cu un commit. Cu toate acestea, pentru scripturile de import de unică folosință, consider mult mai clar să dezactivez eu manual autocommit-ul și apoi să fac COMMIT când doresc. Logica tranzacțională este excelentă pentru operații multiple, dar pentru un import unic, este mai ușor să o forțezi manual.

Dacă aș face mult mai multe importuri decât 1500 de articole (cum am făcut odată cu 400k), atunci aș dezactiva autocommit-ul și l-aș configura să facă COMMIT la fiecare, să zicem, 500 de articole.. În acest fel pot obține o logică restartabilă de la punctul de eșec, menținând totuși o viteză mare.

mersi, voi ține cont de asta. Am căutat pe svn.wp-plugins.org pentru "autocommit" dar nu am găsit prea multe (doar un rezultat pentru un caz de testare) deci ar putea fi o idee bună ca opțiune pentru plugin-urile de import ;-)

nici nu știam că svn.wp-plugins.org încă funcționează. Numele oficial acum este plugins.svn.wordpress.org. :)

deci am pus $wpdb->query('SET autocommit = 0;');
înainte de inserările mele și după inserări execut $wpdb->query('COMMIT');
asta e tot?

da, cred că asta ar trebui să funcționeze și poate adăugați $wpdb->query('SET autocommit = 1;');
din nou după aceea.

Când aveți un cache de obiecte, logica tranzacției poate avea rezultate ciudate, mai ales dacă codul va eșua înainte de commit, deoarece cache-ul va avea date care nu sunt în baza de date, ceea ce poate duce la bug-uri foarte greu de depanat.

Bun punct, Mark. Dacă acestea sunt doar inserări și nu actualizări, atunci wp_suspend_cache_addition( true )
ar trebui să ajute să NU pună date în cache-ul de obiecte. De asemenea, @birgire a menționat că nu au testat acest lucru cu MyISAM - nu vă deranjați, motorul de stocare nu suportă tranzacții, așa că setarea autocommit sau începerea unei tranzacții nu va avea niciun efect.

sfat excelent @Otto. Interogarea mea anterior dura 38 de secunde, acum durează 1 secundă.

MyIsam și InnoDB au abordări diferite. https://stackoverflow.com/a/32913817/2377343

A trebuit să adaug asta:
remove_action('do_pings', 'do_all_pings', 10, 1);
Rețineți că aceasta va omite do_all_pings
, care procesează pingback-uri, enclosures, trackback-uri și alte ping-uri (link: https://developer.wordpress.org/reference/functions/do_all_pings/). După cum înțeleg din analizarea codului, pingback-urile/trackback-urile/enclosures în așteptare vor fi în continuare procesate după ce eliminați această linie remove_action
, dar nu sunt complet sigur.
Actualizare: Am adăugat și
define( 'WP_IMPORTING', true );
În plus, folosesc:
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;' );
/* Inserarea a 100.000 de posturi simultan
inclusiv atribuirea unui termen de taxonomie și adăugarea de meta chei
(adică o buclă `foreach` unde fiecare iterație conține:
`wp_insert_post`, `wp_set_object_terms`, `add_post_meta`.)
*/
$wpdb->query( 'COMMIT;' );
wp_defer_term_counting( false );
wp_defer_comment_counting( false );

Va trebui să inserați postarea pentru a obține ID-ul, dar tabelul $wpdb->postmeta
are o structură foarte simplă. Probabil că puteți folosi o instrucțiune directă INSERT INTO
, ca aceasta din documentația MySQL: INSERT INTO tbl_name (a,b,c) VALUES(1,2,3),(4,5,6),(7,8,9);
În cazul dumneavoastră...
$ID = 1; // de la wp_insert_post
$values = '($ID,2,3),($ID,5,6),($ID,8,9)'; // construiți din cele 97 de coloane; aș folosi un fel de buclă
$wpdb->query("INSERT INTO {$wpdb->postmeta} (post_id,meta_key,meta_value) VALUES {$values}");
Aceasta nu va gestiona codificarea, serializarea, escaparea, verificarea erorilor, duplicările sau orice altceva, dar mă aștept să fie mai rapid (deși nu am încercat).
Nu aș face acest lucru pe un site de producție fără teste amănunțite, iar dacă ar trebui să o fac doar o dată sau de două ori, aș folosi funcțiile de bază și aș lua o pauză lungă de prânz în timp ce datele se importă.

Cred că o să iau o pauză lungă de prânz, mai degrabă decât să introduc date brute în tabelele mele și nu are sens să rescriu ceea ce Wordpress va face deja.

Totul este hard-codat, @OneOfOne. Injecția nu se poate - prin definiție - întâmpla fără input furnizat de utilizator. Aceasta este natura "injecției". OP importă date dintr-un fișier .csv care este sub controlul său folosind cod sub controlul său. Nu există posibilitatea ca o terță parte să injecteze ceva. Te rog să fii atent la context.

+1 de la mine, a trebuit să adaug 20 de valori de câmpuri personalizate și asta a fost mult mai rapid decât "add_post_meta"

Nu poți aștepta ca OP să verifice amănunțit fișierul CSV înainte de import, așadar ar trebui să-l tratezi ca pe o intrare de la utilizator și cel puțin să folosești ->prepare()
pentru instrucțiunile tale SQL. În scenariul tău, ce s-ar întâmpla dacă coloana ID din CSV ar conține ceva de genul 1, 'foo', 'bar'); DROP TABLE wp_users; --
? Ceva rău, probabil.

Notă importantă despre 'SET autocommit = 0;'
după setarea autocommit = 0
, dacă scriptul se oprește din execuție (din orice motiv, cum ar fi exit
, eroare fatală sau altele), atunci modificările NU VOR FI SALVATE ÎN BAZA DE DATE!
$wpdb->query( 'SET autocommit = 0;' );
update_option("ceva", "valoare");
exit; //să presupunem că aici apare o eroare sau orice altceva...
$wpdb->query( 'COMMIT;' );
În acest caz update_option
nu va fi salvat în baza de date!
Deci, cel mai bun sfat este să înregistrăm COMMIT
într-o funcție shutdown
ca măsură de precauție (în cazul în care apare orice încheiere neașteptată).
register_shutdown_function( function(){ $GLOBALS['wpdb']->query( 'COMMIT;' ); } );
