MySQL 的 ORDER BY RAND() 會生成臨時表、為每行賦值隨機數、再排序,時間複雜度 O(N log N)。當表超過 1 萬行,查詢時間呈指數級增長,極易導致 CPU 飆升、連接堵塞。本文提供一套「分批讀取 + PHP 打亂 + 批量寫入」的工程化方案,適用於百萬級數據的隨機重排/匿名化/測試數據生成場景。
LIMIT offset, size 讀取固定塊(如 1000 條),避免全表掃描。INSERT ... VALUES (...), (...), ... 多行語法或事務批量提交。is_processed 字段,支持斷點續傳。<?php
// config.php
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', 'password');
define('DB_NAME', 'test_db');
define('BATCH_SIZE', 1000); // 每批讀取條數
define('TOTAL_RECORDS', 85000); // 總記錄數(可用 COUNT(*) 動態獲取)
// rand_reorder.php
require 'config.php';
$mysqli = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($mysqli->connect_errno) die('DB連接失敗: ' . $mysqli->connect_error);
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$offset = ($page - 1) * BATCH_SIZE;
if ($offset >= TOTAL_RECORDS) {
echo "✅ 所有數據處理完成!
";
echo '重新開始';
exit;
}
// 1. 讀取未處理數據
$sql = "SELECT id, content, type_id FROM source_table WHERE is_processed = 0 ORDER BY id LIMIT ?, ?";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param('ii', $offset, BATCH_SIZE);
$stmt->execute();
$result = $stmt->get_result();
$rows = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
if (empty($rows)) {
echo "⚠️ 無更多數據,可能已處理完畢。
";
exit;
}
// 2. Fisher-Yates 洗牌算法(原地打亂)
$count = count($rows);
for ($i = $count - 1; $i > 0; $i--) {
$j = mt_rand(0, $i);
[$rows[$i], $rows[$j]] = [$rows[$j], $rows[$i]];
}
// 3. 批量插入目標表
$insertSql = "INSERT INTO target_table (id, content, type_id, is_processed) VALUES ";
$values = [];
$types = '';
$params = [];
foreach ($rows as $row) {
$values[] = '(?, ?, ?, 1)';
$types .= 'isi';
$params[] = $row['id'];
$params[] = $row['content'];
$params[] = $row['type_id'];
}
$insertSql .= implode(', ', $values);
$stmt = $mysqli->prepare($insertSql);
$stmt->bind_param($types, ...$params);
$stmt->execute();
$stmt->close();
// 4. 更新源表標記(可選:若只讀不改源表則跳過)
$ids = array_column($rows, 'id');
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$updateSql = "UPDATE source_table SET is_processed = 1 WHERE id IN ($placeholders)";
$stmt = $mysqli->prepare($updateSql);
$types = str_repeat('i', count($ids));
$stmt->bind_param($types, ...$ids);
$stmt->execute();
$stmt->close();
// 5. 自動跳轉下一批
$nextPage = $page + 1;
$processed = $page * BATCH_SIZE;
echo "✅ 第 $page 批處理完成(已處理 ~$processed 條)
";
echo '';
?>
| 優化點 | 說明 |
|---|---|
| Prepared Statements | 防注入 + 批量綁定參數,減少解析開銷 |
| Fisher-Yates 洗牌 | O(N) 時間、O(1) 空間,均勻隨機,優於 shuffle() |
| 多行 INSERT | 單條 SQL 寫入整批,減少網絡往返 |
| 斷點續傳 | is_processed + page 參數,意外中斷可恢復 |
| CLI 模式更穩定 | 生產環境建議改寫為 CLI 腳本配合 pcntl_fork 多進程並行 |
若僅需「隨機抽取 N 條」而非重排全表,推薦以下兩種方案:
-- 1. 獲取最大 ID
SELECT MAX(id) INTO @max_id FROM source_table;
-- 2. 生成隨機起始點
SET @start = FLOOR(RAND() * (@max_id - 1000));
-- 3. 向後取 N 條(需配合 WHERE id >= @start ORDER BY id LIMIT N)
SELECT * FROM source_table WHERE id >= @start ORDER BY id LIMIT 1000;
<?php
$total = $mysqli->query("SELECT COUNT(*) FROM source_table")->fetch_row()[0];
$ids = [];
while (count($ids) < 5) {
$id = mt_rand(1, $total);
if (!in_array($id, $ids)) $ids[] = $id;
}
$placeholders = implode(',', array_fill(0, 5, '?'));
$stmt = $mysqli->prepare("SELECT * FROM source_table WHERE id IN ($placeholders)");
$stmt->bind_param(str_repeat('i', 5), ...$ids);
$stmt->execute();
?>
| 方案 | 耗時 | CPU | 適用場景 |
|---|---|---|---|
| ORDER BY RAND() LIMIT 5 | ~2.3 秒 | 高 | 極小表(<1000 行) |
| 主鍵區間 + LIMIT | ~0.008 秒 | 低 | 連續 ID、抽樣 |
| PHP 隨機 ID + IN | ~0.012 秒 | 低 | 稀疏 ID、抽樣 |
| 分批洗牌重排 (本文方案) | ~3.5 秒 (全表) | 中 | 全表重排/匿名化 |
ORDER BY RAND() 於大表任何場景。