MySQL翻頁同分資料順序變了:ORDER BY要加唯一鍵

網站列表依分數排序,翻到下一頁卻看到重複項目。先確認查詢有沒有把「同分時誰先顯示」寫清楚。ORDER BY score DESC只決定分數不同的先後;四筆資料都是一百分時,它們在排序條件中仍是平手。LIMIT與OFFSET只是切出位置,不會替你補齊平手規則。

先看排序是否能區分每一筆結果

常見查詢是SELECT … ORDER BY score DESC LIMIT 2。它不等於「同分依id由小到大」,也不等於「依新增時間顯示」。若你需要其中一種規則,就必須把它放進ORDER BY;資料目前看起來剛好按照id,不能當成永久契約。

MySQL手冊明確指出,ORDER BY欄位值相同的資料,其餘欄位的先後可能不同,且LIMIT可能影響執行方式。因此不能根據這次兩次查詢結果一樣,就認定排序已完整。這篇案例也不刻意把資料庫弄成每次亂序:它要驗證的是明確排序後應取得哪一組id,不是假裝每個執行計畫都會重排。

用五筆資料重現同分邊界

下面只在自己的測試資料庫建立小表。案例使用MySQL 8.4.9,四筆一百分的id依4、2、3、1順序插入,最後再加入一筆九十分。插入順序特意不同於id順序,讓兩者容易區分;不要在正式列表資料表上刪除或重建資料。

CREATE TABLE scores (
  id INT PRIMARY KEY,
  score INT NOT NULL
);
INSERT INTO scores VALUES
  (4,100), (2,100), (3,100),
  (1,100), (5,90);
SELECT id, score
FROM scores
ORDER BY score DESC
LIMIT 2;

本次第一個查詢得到id 1與2,但這只是實際觀察值。SQL仍只要求分數由高到低,沒有要求同分的id順序。不要把這份輸出包裝成某種索引下的固定保證,也不要靠多跑幾次直到出現不同結果才承認問題。

加唯一鍵作最後一層排序

先問一個具體問題:四筆一百分的項目,產品希望哪兩筆在第一頁?若沒有答案,查詢也不可能自行猜中。可以依建立時間、人工優先序或唯一編號,但必須選定規則;只加一個非唯一欄位,遇到它也相同時仍然沒有解決平手。

SELECT id, score
FROM scores
ORDER BY score DESC, id ASC
LIMIT 2 OFFSET 0;

SELECT id, score
FROM scores
ORDER BY score DESC, id ASC
LIMIT 2 OFFSET 2;

現在同分時依id由小到大,第一頁固定是1、2,第二頁是3、4。主鍵id唯一,能打破這張表中的所有平手。這次結果與測試輸出一致;排序規則也能直接從查詢讀出來,不必知道儲存引擎如何放置資料。

若你的業務要顯示最新項目,可以用created_at DESC,再接id DESC處理同時建立的資料。重要的不是一定要用升冪id,而是最後的排序組合能把查詢結果中的每一列區分開。排序方向要符合產品規則,所有頁面也必須沿用同一套方向。

唯一鍵必須對目前的結果列真正唯一。例如列表先把文章與多個標籤JOIN起來,一篇文章可能出現多列,只用文章id仍可能平手。這時先確認是否應去重或彙整;若每列都要展示,就需要包含能區分JOIN結果的鍵。不要把基礎表主鍵直接當成所有查詢的唯一列識別。

前端收到結果後若再自行排序,也要使用一致的規則。資料庫已依score、id切好頁,瀏覽器卻只按標題重新排當頁項目,使用者看到的次序就不再對應整份資料的排序。先確認排序到底由哪一層負責,讓切頁與顯示沿用同一個定義。

完整排序仍不等於跨請求快照

加上唯一id解決的是同一組資料的先後未定。使用者看完第一頁後,另一個人新增高分項目,原本的資料位置仍會往後移。第二頁用OFFSET 2跳過的是當時的新位置,因此可能再次遇到先前看過的項目;這與同分沒有完整排序是兩個不同問題。

InnoDB的一致性讀取有自己的交易與隔離規則。兩次獨立網頁請求通常不能直接視為同一個讀取快照。這裡沒有讓多個交易同時修改資料,也沒有量測隔離設定,所以不把案例延伸成「加id即可避免所有漏列、重複」。若需要固定一批匯出結果,應另外設計快照或可重現的選取範圍。

對連續瀏覽的大型列表,可以評估用上一頁最後一筆的排序值作游標,而不是只保存頁碼。但游標查詢同樣要沿用score與id的完整排序,並處理分數異動、刪除與篩選條件。它不會自動凍結資料;導入前先寫清楚使用者期待的是即時列表,還是一份固定結果。

驗收時比較整段結果與分頁切片

保留查詢參數也很重要。頁碼通常從一開始顯示,但OFFSET從零起算;每頁兩筆時,第二頁跳過兩筆,而不是跳過一筆。把頁碼轉換與資料庫排序分別檢查,避免在平手規則修好後,仍因頁碼計算錯誤而出現重複。這個案例直接寫OFFSET 0與2,就是為了讓切片位置清楚可見。

先不加LIMIT跑完整排序,得到預期id序列,再對照第一頁與第二頁能否拼回相同順序。這個小表應是1、2、3、4、5;第一頁1、2,第二頁3、4。把實際業務篩選也放入相同查詢,避免第一頁與第二頁使用不同WHERE條件,卻誤以為是排序出錯。

測試資料至少包含超過一頁容量的同值群組,否則平手剛好落在同一頁,不容易暴露頁面邊界。再加入分數不同的資料,確認主要排序方向沒有被第二個欄位取代。主排序與平手規則一起驗,才能知道修改沒有把產品原本的排名意義改掉。

最後再看效能。正確排序可以先寫清楚,但能否利用索引、是否額外排序,取決於完整查詢與索引結構。用EXPLAIN檢查實際執行方式,再決定要不要調整索引;不要單憑這張五筆測試表推論正式網站的耗時,也不要在這次診斷中順手新增大型索引。

列表若允許修改分數,測試時先把資料固定,驗證排序與分頁本身,再另開一組有變動的情境。固定資料的兩頁結果不能拿來證明並行更新安全;有更新時的重複也不能反過來否定完整排序。分兩步觀察,才知道哪個限制需要在產品說明或資料讀取策略中處理。

排查列表重複時,把原查詢、排序欄位、每頁筆數與兩頁id保存下來。先補齊同值的最後一層規則,再核對兩次請求之間是否有新增或排名變動。能把這兩類原因分開,修改才會對準問題,而不是只把OFFSET改成另一個數字暫時躲過它。

參考資料

原文鏈接:https://wntheme.com/mysql-pagination-stable-tie-order/,轉載請註明出處。
0

評論0

顯示驗證碼
沒有帳號?註冊  忘記密碼?