「隨機推薦」、「隨機展示」是常見業務需求。MySQL 中 ORDER BY RAND() 雖然語法簡單,但在大數據量下性能極差。本文對比四種主流方案,從原理、性能、適用場景維度深度分析,助你選擇最優方案。
| 方案 | 核心思路 | 時間複雜度 | 適用數據量 | 優點 | 缺點 |
|---|---|---|---|---|---|
| ORDER BY RAND() | 全表掃描 + 臨時表排序 | O(N log N) | < 1萬 | 語法簡單、真隨機 | 大表極慢、臨時表磁盤 I/O |
| 主鍵範圍 + RAND() | 估算主鍵範圍隨機取點 | O(1) ~ O(log N) | 1萬 - 100萬 | 無全表掃描、利用索引 | 需連續主鍵、分布均勻 |
| 隨機偏移量 LIMIT | COUNT(*) 後隨機 OFFSET | O(1) 計數 + O(offset) 掃描 | 10萬 - 1000萬 | 實現簡單、真隨機 | 大 OFFSET 性能下降 |
| 預計算隨機值 | 字段存隨機數,索引排序 | O(log N) | 任意量級 | 性能最穩定、支持分頁 | 需維護額外字段、寫入開銷 |
<?php // 只取 1 條 $sql = "SELECT * FROM `articles` WHERE `status` = 1 ORDER BY RAND() LIMIT 1"; // 取 N 條 $sql = "SELECT * FROM `articles` WHERE `status` = 1 ORDER BY RAND() LIMIT 10";
原理:MySQL 為每行生成隨機數,寫入臨時表,排序後取前 N 行。
實測:10 萬行表耗時 ~800ms;100 萬行超 10s,甚至 OOM。嚴禁生產環境大表使用。
<?php
function getRandomByIdRange($pdo, $table, $where = '1', $limit = 1) {
// 1. 獲取主鍵範圍
$stmt = $pdo->query("SELECT MIN(id) AS min_id, MAX(id) AS max_id FROM `$table` WHERE $where");
$range = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$range || $range['min_id'] === null) return [];
$minId = (int)$range['min_id'];
$maxId = (int)$range['max_id'];
$results = [];
// 2. 循環隨機取點,直到湊夠數量
while (count($results) < $limit) {
$randomId = mt_rand($minId, $maxId);
$stmt = $pdo->prepare("SELECT * FROM `$table` WHERE id >= ? AND $where LIMIT ?");
$stmt->execute([$randomId, $limit - count($results)]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) continue;
// 去重合併
foreach ($rows as $row) {
if (!isset($results[$row['id']])) {
$results[$row['id']] = $row;
}
}
// 防止死循環:數據極稀疏時
if ($randomId === $maxId) break;
}
return array_values($results);
}
// 使用
$articles = getRandomByIdRange($pdo, 'articles', 'status = 1', 5);
優化點:
id >= ? 而非 id = ?,應對主鍵不連續(刪除導致空洞)適用條件:主鍵為自增整數、分布相對均勻、刪除率不高。
<?php
function getRandomByOffset($pdo, $table, $where = '1', $limit = 1) {
// 1. 獲取總數(可緩存)
$countSql = "SELECT COUNT(*) FROM `$table` WHERE $where";
$total = (int)$pdo->query($countSql)->fetchColumn();
if ($total === 0) return [];
// 2. 計算隨機偏移量
$offset = mt_rand(0, max(0, $total - $limit));
// 3. 執行查詢
$sql = "SELECT * FROM `$table` WHERE $where LIMIT $offset, $limit";
return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
性能特徵:
COUNT(*) 在 MyISAM 瞬間返回,InnoDB 需掃描(可緩存或用近似值)LIMIT offset, n 需掃描 offset 行,offset 越大越慢優化變體:結合方案二,先隨機 ID 定位,再 LIMIT 0, n:
// 改進版:隨機 ID 定位 + 向後取數
$stmt = $pdo->prepare("SELECT id FROM `$table` WHERE $where ORDER BY id LIMIT ?, 1");
$stmt->execute([$offset]);
$startId = $stmt->fetchColumn();
if ($startId) {
$sql = "SELECT * FROM `$table` WHERE id >= ? AND $where LIMIT ?";
return $pdo->prepare($sql)->execute([$startId, $limit])->fetchAll();
}
表結構調整:
ALTER TABLE `articles` ADD COLUMN `rand_sort` DOUBLE NOT NULL DEFAULT 0, ADD INDEX `idx_rand_sort` (`rand_sort`);
初始化/更新隨機值:
<?php
// 批量更新(定時任務每天/每小時執行)
$pdo->exec("UPDATE `articles` SET `rand_sort` = RAND() WHERE `status` = 1");
// 新增記錄時自動賦值
// INSERT INTO articles (..., rand_sort) VALUES (..., RAND())
查詢極簡高效:
// 真隨機、支持分頁、性能 O(log N) $sql = "SELECT * FROM `articles` WHERE `status` = 1 ORDER BY `rand_sort` LIMIT 10"; // 翻頁 $sql = "SELECT * FROM `articles` WHERE `status` = 1 ORDER BY `rand_sort` LIMIT 20, 10";
優勢:查詢性能與常規索引排序一致,支持任意分頁、組合排序(如 ORDER BY rand_sort, views DESC)。
成本:寫入額外開銷、存儲空間、定期重算任務。
開始 │ ├─ 數據量 < 1萬? → 是 → ORDER BY RAND() 結束 │ ├─ 主鍵自增整數且分布均勻? → 是 → 方案二(主鍵範圍) 結束 │ ├─ 需支持隨機分頁/組合排序? → 是 → 方案四(預計算字段) 結束 │ └─ 通用場景 → 方案三(隨機 OFFSET)或 改進版 OFFSET
Using filesort、Using temporary,優化索引沒有銀彈,只有權衡。小表用 RAND(),大表看主鍵分布選範圍法或 OFFSET 法,極致性能預計算字段。建議在項目中封裝 RandomSelector 服務類,根據表統計信息自動選擇最優策略,業務層零感知調用 $selector->getRandom($table, $where, $limit)。