在開發數據量較大的應用時,我們經常需要遍歷整個表進行數據處理,比如生成站點地圖(sitemap)、導出數據、批量更新等。最常見的做法是使用 LIMIT offset, size 分頁查詢。然而,當 offset 變得很大時,查詢會越來越慢,甚至導致超時或服務器負載飆升。本文將介紹一種簡單卻極其高效的方法——基於主鍵的遊標查詢(WHERE id > last_id),它能讓你的數據遍歷速度恆定,即使處理百萬級數據也能輕鬆應對。
傳統分頁的痛點
假設我們有一張歌曲表 music_songs,主鍵爲 song_id,我們需要導出所有歌曲的 URL 到 sitemap。傳統分頁寫法:
SELECT song_id FROM music_songs ORDER BY song_id LIMIT 1000000, 1000;這條 SQL 在執行時,數據庫需要先掃描並跳過前 100 萬行,然後返回接下來的 1000 行。即使 song_id 有主鍵索引,MySQL 也會先讀取 100 萬行的索引項,再回表獲取數據,當偏移量巨大時,這部分開銷非常可觀,而且隨着頁碼增加,性能線性下降。
更糟糕的是,使用 OFFSET 會導致重複掃描:每次查詢都要從第一行開始數,越往後越慢。在生成 600 萬條數據的 sitemap 時,最初的幾十萬條還能較快返回,但越到後面,每一批都要等待幾十秒甚至幾分鐘,整個導出過程可能需要幾個小時。
遊標查詢的核心思想
遊標查詢利用主鍵自增的特性,每次只查詢 id 大於上一次最大 id 的記錄,從而避免“跳過”已讀數據。它的基本形式是:
SELECT * FROM table WHERE id > last_id ORDER BY id LIMIT batch_size;每次查詢後,將本批次最大的 id 作爲下一輪的 last_id,循環直至沒有數據返回。
這種方式的查詢性能是恆定的,因爲每次都是利用主鍵索引直接定位到 last_id 之後的位置,然後掃描 batch_size 條記錄。無論數據總量多大,每批查詢的時間幾乎相同。
爲什麼遊標查詢這麼快?
- 索引直接定位:
id > last_id可以迅速定位到索引中的起始位置,不需要掃描之前的記錄。 - 順序讀取:由於數據在索引中是有序存儲的,後續的
ORDER BY id只是順序讀取,非常高效。 - 固定掃描量:每批只掃描
batch_size條數據,不會隨着數據總量增長而增加。
遊標查詢的適用場景
- 全表數據導出:如生成 sitemap、導出 CSV、備份數據等。
- 批量處理任務:對每一條數據執行某種操作(如 AI 生成文章、更新字段),且不需要跳躍式訪問。
- 數據遷移:將數據從一個表複製到另一個表。
- 即時數據流處理:從數據庫持續讀取新數據(類似消息隊列)。
遊標查詢的限制
- 必須基於單調遞增的主鍵(或其它排序字段)才能保證順序。如果主鍵不是單調遞增,但業務中有時間戳字段等,也可以使用類似方法,但需要建立對應索引。
- 不能隨機跳頁:如果需要實現“跳到第 N 頁”的用戶界面,遊標查詢就不適合了,因爲
last_id必須由上一頁決定。此時仍應使用LIMIT offset, size,但可以考慮在業務中優化(如減少頁數、使用緩存)。 - 注意排序字段的穩定性:如果排序字段存在重複值,必須確保排序唯一,否則可能丟失數據。通常主鍵是唯一且遞增的,最爲安全。
性能對比實測
在我處理 600 萬條歌曲數據的 sitemap 生成時,使用傳統 OFFSET 分頁,生成到 100 多個 XML 文件就耗時數小時,且越往後越慢。改用遊標查詢後,整個過程在幾秒內完成,每批查詢穩定在毫秒級。性能差異驚人!
總結
遊標查詢(WHERE id > last_id)是一種簡單而強大的優化技巧,尤其適合需要遍歷整個表的數據處理場景。它摒棄了笨重的 OFFSET,利用主鍵索引的有序性,實現了恆定時間的分頁。在實際開發中,當你需要“逐條處理所有數據”時,不妨試試這個方法,也許能給你帶來意想不到的性能提升。
提示:如果查詢中還需要其他過濾條件(如 status = 1),一定要在 (status, id) 上建立聯合索引,以保證查詢依然能高效定位。例如:
ALTER TABLE music_songs ADD INDEX idx_status_id (status, id);然後查詢改爲:
SELECT * FROM music_songs WHERE status = 1 AND id > last_id ORDER BY id LIMIT batch_size;這樣既能過濾,又能利用索引有序性,性能同樣出色。