
一、MySQL分頁為什么會慢常見分頁SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;LIMIT offset, size表示跳過前offset條再返回size條。上面的 SQL 不是直接跳到第 1000001 條而是需要沿著結果順序讀取大量記錄跳過前 100 萬條只返回最后 20 條。頁碼越深需要檢查并丟棄的記錄越多。可以簡單理解為LIMIT 0, 20 檢查約20條 LIMIT 1000, 20 檢查約1020條 LIMIT 1000000, 20 檢查約1000020條因此傳統分頁的成本通常隨著offset增大而增長。二、分頁的主要性能問題1. 深分頁掃描大量無用記錄SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;真正返回20條但前面100萬條都屬于無效工作帶來更多BTree葉子節點遍歷Buffer Pool訪問CPU條件判斷磁盤I/O可能的回表查詢。2. 二級索引排序可能產生大量回表假設有索引CREATE INDEX idx_created_at ON orders(created_at);查詢SELECT * FROM orders ORDER BY created_at LIMIT 1000000, 20;二級索引葉子節點主要保存(created_at, 主鍵id)但SELECT *需要完整記錄所以可能通過主鍵到聚簇索引查詢數據。InnoDB二級索引記錄包含主鍵完整行數據則存放在聚簇索引中。深分頁情況下可能出現掃描大量二級索引記錄 ↓ 通過主鍵回表 ↓ 丟棄前面的記錄 ↓ 只返回20條具體是否以及何時回表由執行計劃決定。3. 排序不能使用索引時出現filesortSELECT * FROM orders WHERE status 1 ORDER BY amount LIMIT 100000, 20;如果沒有合適索引MySQL可能先找出符合條件的記錄再進行filesort。即使最終只返回20條也可能需要讀取和處理大量候選記錄。可以通過EXPLAIN的Extra是否出現Using filesort判斷。4. 分頁結果可能不穩定不寫ORDER BYSELECT * FROM orders LIMIT 20, 20;數據庫不保證每次返回順序一致。即使寫了ORDER BY created_at如果多條記錄的created_at相同它們之間的順序仍不確定應增加唯一字段ORDER BY created_at DESC, id DESCMySQL官方文檔也建議增加額外排序列使順序具有確定性。三、優化一建立匹配條件和排序的聯合索引查詢SELECT id, user_id, status, created_at FROM orders WHERE user_id 100 AND status 1 ORDER BY created_at DESC, id DESC LIMIT 20;建立索引CREATE INDEX idx_user_status_time_id ON orders(user_id, status, created_at DESC, id DESC);索引順序可以理解為等值查詢列 → 排序列 → 唯一排序列 user_id, status, created_at, id這樣MySQL可以先定位用戶和狀態再按照索引順序讀取減少掃描和額外排序。MySQL能夠在索引順序滿足ORDER BY時避免filesort但要注意索引能夠減少排序和過濾成本卻不能從根本上解決巨大offset帶來的跳過成本。四、優化二游標分頁最推薦也叫Keyset PaginationSeek Pagination基于最后一條記錄分頁第一頁SELECT * FROM orders WHERE user_id 100 AND status 1 ORDER BY created_at DESC, id DESC LIMIT 20;記錄最后一條數據created_at 2026-08-01 10:00:00 id 5000下一頁SELECT * FROM orders WHERE user_id 100 AND status 1 AND ( created_at 2026-08-01 10:00:00 OR (created_at 2026-08-01 10:00:00 AND id 5000) ) ORDER BY created_at DESC, id DESC LIMIT 20;配合索引CREATE INDEX idx_user_status_time_id ON orders(user_id, status, created_at DESC, id DESC);執行過程變成從上一頁最后位置附近定位 ↓ 繼續讀取20條優點深度增加時性能比較穩定不需要跳過前面幾十萬條數據新增時不容易出現重復數據特別適合信息流、訂單列表和滾動加載缺點不能方便地直接跳到第10000頁前端需要保存上一頁最后一條記錄的游標排序字段最好穩定且不允許為NULL最好加入唯一字段id避免游標位置不唯一五、優化三延遲關聯如果業務必須使用頁碼和大offset可以先使用覆蓋索引找到20個主鍵再查詢完整數據SELECT o.* FROM orders AS o JOIN ( SELECT id FROM orders WHERE status 1 ORDER BY created_at DESC, id DESC LIMIT 1000000, 20 ) AS p ON p.id o.id ORDER BY o.created_at DESC, o.id DESC;配合索引CREATE INDEX idx_status_time_id ON orders(status, created_at DESC, id DESC);內部查詢只讀取索引中的小字段掃描覆蓋索引 → 獲得20個id → 只回表20次它減少了大量無意義的回表但仍然需要跳過100萬條索引記錄所以是緩解方案不是深分頁的根本解決方案