การจัดการข้อมูลจำนวนมากในฐานข้อมูล WordPress (ระดับหลายหมื่นถึงแสนเรคคอร์ด) เช่น การรันคำสั่ง SQL เพื่อสลับค่าคอลัมน์ผ่าน Self-Join มักจะพบปัญหาคลาสสิกอย่าง #1205 – Lock wait timeout exceeded หรือปัญหาเว็บล่มจาก Error 524 Gateway Timeout บนหน้าเว็บเบราว์เซอร์ เนื่องจากระบบต้องใช้ทรัพยากรสูงในการสแกนตารางขนาดใหญ่
ปัญหาคลาสสิกจากการใช้ LIMIT + OFFSET แบบดั้งเดิม
ในกระบวนการประมวลผลข้อมูลแบบแบ่งรอบ (Batch Processing) นักพัฒนาส่วนใหญ่มักคุ้นเคยกับการใช้คำสั่งแบ่งหน้าในรูปแบบ LIMIT 2000 OFFSET 2000 แต่เมื่อนำมาใช้กับตารางขนาดใหญ่ที่มีเรคคอร์ดหลักหมื่นขึ้นไป วิธีนี้จะส่งผลเสียร้ายแรง 2 ประการ
- ประสิทธิภาพลดลงแบบทวีคูณ (Performance Degradation)
เมื่อค่า OFFSET สูงขึ้น (เช่นOFFSET 20000) MySQL จะต้องอ่านและนับข้อมูลตั้งแต่แถวแรกสุดจนถึงเรคคอร์ดที่ 20,000 ก่อนจะตัดทิ้งตามจำนวน Offset ทำให้การทำงานในแต่ละรอบใช้เวลาช้าลงเรื่อย ๆ จนเกิด Timeout - ข้อมูลตกหล่นหรือซ้ำซ้อน (Data Gaps & Duplicates)
หากตารางมีการอัปเดต ลบ หรือแทรกข้อมูลใหม่ระหว่างที่สคริปต์กำลังรันอยู่ ลำดับของแถว (Row Order) จะเกิดการขยับตัว ทำให้คำสั่ง OFFSET ข้ามเรคคอร์ดบางตัวไป หรือดึงข้อมูลเดิมกลับมาทำซ้ำ ส่งผลให้ความสมบูรณ์ของข้อมูลเสียหาย
ทางออก: การใช้ Cursor-based Pagination (Keyset Pagination)
เพื่อกำจัดปัญหาดังกล่าว สคริปต์นี้จึงเลือกใช้เทคนิค Cursor-based Pagination โดยอาศัยค่า Primary Key ล่าสุด ($last_id) มาเป็นจุดอ้างอิง และใช้เงื่อนไข WHERE ID > $last_id แทนการใช้ OFFSET
- การทำงาน
MySQL จะใช้ประโยชน์จากระบบ Index ของคอลัมน์IDทำให้สามารถกระโดดพุ่งตรงไปยังตำแหน่งข้อมูลที่ต้องการได้ทันทีโดยไม่ต้องสแกนแถวก่อนหน้า - ความเสถียร
ต่อให้ข้อมูลจะมีการขยับหรือแก้ไขระหว่างประมวลผล การอ้างอิงตามค่า ID ที่สูงขึ้นเรื่อย ๆ จะช่วยรับประกันได้ว่าไม่มีเรคคอร์ดใดตกหล่นและไม่มีการวนกลับมาทำซ้ำอย่างแน่นอน
โค้ดสคริปต์ฉบับสมบูรณ์: run-update.php
สร้างไฟล์ชื่อ run-update.php ไว้ที่โฟลเดอร์หลักของ WordPress (ระดับเดียวกับ wp-config.php):
<?php
// Disable output compression and buffering for real-time terminal feedback
if (function_exists('apache_setenv')) {
apache_setenv('no-gzip', '1');
}
@ini_set('zlib.output_compression', 'Off');
@ini_set('output_buffering', 'Off');
@ini_set('implicit_flush', 'true');
for ($i = 0; $i < ob_get_level(); $i++) {
ob_end_flush();
}
ob_implicit_flush(true);
echo "[INFO] Loading WordPress environment...\n";
flush();
// Disable caching plugins to prevent lock wait timeouts and query interception
define( 'W3TC_DISABLE_CACHING', true );
define( 'WP_USE_THEMES', false );
require_once( __DIR__ . '/wp-load.php' );
if ( function_exists( 'w3tc_pgcache_flush' ) ) {
@w3tc_pgcache_flush();
}
global $wpdb;
// Extend session-level lock wait timeout to handle large data sets safely
$wpdb->query( "SET SESSION innodb_lock_wait_timeout = 300" );
$table = $wpdb->posts;
$batch = 2000; // Number of records processed per batch
$last_id = 0; // Tracks the last processed ID for cursor-based pagination
$total_updated = 0;
echo "[INFO] Starting cursor-based batch updates...\n";
flush();
for ($i = 1; $i <= 300; $i++) {
// Fetch records strictly greater than the last processed ID to prevent gaps or overlaps
$rows = $wpdb->get_results($wpdb->prepare("
SELECT p1.ID AS id1, p1.post_name
FROM {$table} p1
WHERE p1.ID > %d
AND p1.post_content = ''
AND p1.post_name REGEXP '^[0-9]+$'
ORDER BY p1.ID ASC
LIMIT %d
", $last_id, $batch));
if (empty($rows)) {
echo "\n[SUCCESS] All records have been successfully updated!\n";
break;
}
$count = 0;
foreach ($rows as $row) {
// Update the pointer to the current row ID
$last_id = $row->id1;
// Find the preceding post with an empty content to swap data with
$id2 = $wpdb->get_var($wpdb->prepare(
"SELECT ID FROM {$table} WHERE ID < %d AND post_content = '' ORDER BY ID DESC LIMIT 1",
$row->id1
));
if (!$id2) continue;
$target = $wpdb->get_row($wpdb->prepare(
"SELECT post_title, post_name FROM {$table} WHERE ID = %d",
$id2
));
if ($target) {
// Direct query update bypassing cache hooks
$wpdb->query($wpdb->prepare(
"UPDATE {$table} SET post_title = %s, post_name = %s WHERE ID = %d",
$target->post_title,
$target->post_name,
$row->id1
));
$count++;
}
}
$total_updated += $count;
echo "-> Batch {$i} | Reached ID: {$last_id} | Updated in this batch: {$count} | Total cumulative: {$total_updated}\n";
flush();
// Terminate loop early if the fetched dataset is smaller than the batch size (end of table reached)
if (count($rows) < $batch) {
echo "\n[SUCCESS] Reached the end of the table records!\n";
break;
}
}
// Automatically sync the auto_increment value to the highest current post ID
$max_id = $wpdb->get_var("SELECT MAX(ID) FROM {$table}");
$next_ai = intval($max_id) + 1;
$wpdb->query("ALTER TABLE {$table} AUTO_INCREMENT = {$next_ai}");
echo "[INFO] Table auto_increment successfully reset to: {$next_ai}\n";
วิธีการใช้งานผ่าน Terminal (CLI)
การรันผ่าน Command Line ช่วยตัดปัญหาเรื่องการตัดการเชื่อมต่อของเบราว์เซอร์ได้ 100% พร้อมแสดงผลลัพธ์แบบ Real-time
- เข้า Server ผ่าน SSH และย้ายไปที่โฟลเดอร์ของเว็บไซต์
cd /srv/www/your-domain.com/public_html/
- สั่งรันสคริปต์
php run-update.php
- สังเกตหน้าจอระบบจะพิมพ์รายงานสถานะความคืบหน้าออกมาเป็นระยะจนกระทั่งเสร็จสิ้น
- ข้อควรระวัง: เมื่อใช้งานเสร็จแล้ว ให้ ลบไฟล์
run-update.phpทิ้งทันที เพื่อความปลอดภัยของระบบ