วัน: 11 กันยายน 2026

คู่มือการแก้ปัญหา 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 ทิ้งทันที เพื่อความปลอดภัยของระบบ