PlusMagi's Blog By Pitt Phunsanit Database,MariaDB,MySql,Programming,WordPress คู่มือการแก้ปัญหา Lock Wait Timeout และปรับปรุงข้อมูล WordPress ขนาดใหญ่ด้วย PHP Script แบบปลอดภัย

คู่มือการแก้ปัญหา Lock Wait Timeout และปรับปรุงข้อมูล WordPress ขนาดใหญ่ด้วย PHP Script แบบปลอดภัย

การจัดการข้อมูลจำนวนมากในฐานข้อมูล WordPress (ระดับหลายหมื่นถึงแสนเรคคอร์ด) เช่น การรันคำสั่ง SQL เพื่อสลับค่าคอลัมน์ผ่าน Self-Join มักจะพบปัญหาคลาสสิกอย่าง #1205 – Lock wait timeout exceeded หรือปัญหาเว็บล่มจาก Error 524 Gateway Timeout บนหน้าเว็บเบราว์เซอร์ เนื่องจากระบบต้องใช้ทรัพยากรสูงในการสแกนตารางขนาดใหญ่


ปัญหาคลาสสิกจากการใช้ LIMIT + OFFSET แบบดั้งเดิม

ในกระบวนการประมวลผลข้อมูลแบบแบ่งรอบ (Batch Processing) นักพัฒนาส่วนใหญ่มักคุ้นเคยกับการใช้คำสั่งแบ่งหน้าในรูปแบบ LIMIT 2000 OFFSET 2000 แต่เมื่อนำมาใช้กับตารางขนาดใหญ่ที่มีเรคคอร์ดหลักหมื่นขึ้นไป วิธีนี้จะส่งผลเสียร้ายแรง 2 ประการ

  1. ประสิทธิภาพลดลงแบบทวีคูณ (Performance Degradation)
    เมื่อค่า OFFSET สูงขึ้น (เช่น OFFSET 20000) MySQL จะต้องอ่านและนับข้อมูลตั้งแต่แถวแรกสุดจนถึงเรคคอร์ดที่ 20,000 ก่อนจะตัดทิ้งตามจำนวน Offset ทำให้การทำงานในแต่ละรอบใช้เวลาช้าลงเรื่อย ๆ จนเกิด Timeout
  2. ข้อมูลตกหล่นหรือซ้ำซ้อน (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

  1. เข้า Server ผ่าน SSH และย้ายไปที่โฟลเดอร์ของเว็บไซต์
    cd /srv/www/your-domain.com/public_html/
    
  2. สั่งรันสคริปต์
    php run-update.php
    
  3. สังเกตหน้าจอระบบจะพิมพ์รายงานสถานะความคืบหน้าออกมาเป็นระยะจนกระทั่งเสร็จสิ้น
  4. ข้อควรระวัง: เมื่อใช้งานเสร็จแล้ว ให้ ลบไฟล์ run-update.php ทิ้งทันที เพื่อความปลอดภัยของระบบ