ORDER BY RAND() 很慢?EXPLAIN SELECT * FROM money_logs ORDER BY RAND() LIMIT 1;
-- type=ALL, Using temporary; Using filesort
-- 15萬筆資料需 0.3 秒,資料量增長呈 N log N 惡化
無法使用索引,需建立臨時表並檔案排序,大表災難性。
-- 1. 取得最大 ID (可快取)
SELECT MAX(id) FROM tbl;
-- 2. 應用層產生隨機數 $rand = mt_rand(1, $max_id);
-- 3. 直接點查
SELECT * FROM tbl WHERE id = $rand;
兩條極簡查詢,極快。但 ID 有空洞 (刪除導致) 會查無資料。
SELECT t.* FROM tbl t
JOIN (SELECT CEIL(RAND() * (SELECT MAX(id) FROM tbl)) AS id) r
USING (id);
SELECT MAX(id) 被優化器常數化r 只產生一行,JOIN 走 const 類型
SELECT t.* FROM tbl t
JOIN (SELECT (RAND() * (SELECT MAX(id) FROM tbl)) AS id) r
WHERE t.id >= r.id
ORDER BY t.id ASC
LIMIT 1;
>= 隨機值 的第一筆,自動跳過空洞CEIL(),直接用浮點數比較range 掃描極少行數當 ID 分布極不均勻 (如 1,2,4,8,16),上述方法會讓大間距 ID 被選中機率飆升。
map_tbl(row_id PK, real_id),row_id 連續無空洞row_id,再 JOIN 回主表AFTER INSERT/DELETE/UPDATE 觸發器同步維護映射表
-- 觸發器範例 (刪除時壓縮 row_id)
CREATE TRIGGER tad_tbl AFTER DELETE ON tbl
FOR EACH ROW BEGIN
DELETE FROM map_tbl WHERE real_id = OLD.id;
UPDATE map_tbl SET row_id = row_id - 1 WHERE real_id > OLD.id;
END;
用儲存程序迴圈寫入暫存表,再一次性讀取:
CREATE PROCEDURE get_rands(IN cnt INT)
BEGIN
DROP TEMPORARY TABLE IF EXISTS rands;
CREATE TEMPORARY TABLE rands (rand_id INT);
loop_me: LOOP
IF cnt < 1 THEN LEAVE loop_me; END IF;
INSERT INTO rands
SELECT t.id FROM tbl t
JOIN (SELECT (RAND() * (SELECT MAX(id) FROM tbl)) AS id) r
WHERE t.id >= r.id ORDER BY t.id ASC LIMIT 1;
SET cnt = cnt - 1;
END LOOP;
END;
| 資料量 | ORDER BY RAND() | RAND()*MAX(ID) (JOIN) | 空洞修正版 |
|---|---|---|---|
| 1,000 | 0.72s | 0.52s | 0.57s |
| 10,000 | 2.09s | 0.61s | 0.61s |
| 100,000 | 18.68s | 0.61s | 0.61s |
| 1,000,000 | 179s | 0.63s | 0.63s |
優化版趨近常數時間,ORDER BY RAND() 百萬級近 3 分鐘,生產環境不可用。