MS SQL:用 PK 查詢卻出現多筆資料時,先檢查 JOIN,別急著加 NOLOCK

這次錯誤是在用 PetaPoco 的 SingleOrDefault 取資料時發生,例外訊息是 System.InvalidOperationException: Sequence contains more than one element。一開始很容易把焦點放在「明明 WHERE 用的是主鍵,怎麼還會有兩筆?」但這個訊息真正表示的是:送進 SingleOrDefault 的結果集超過一列,而不是資料表的主鍵約束一定失效。

先分清楚:主表一列,不等於整個查詢只會回一列

假設主表 Orders 的 Id 是主鍵,但查詢再 JOIN 到一對多的 OrderItems。只要同一張訂單有兩個明細,SQL 的結果就會是兩列。這時候主表的主鍵沒有問題,問題是查詢的資料粒度已經從「一張訂單」變成「一張訂單的一個明細」。

SELECT o.Id, COUNT(*) AS RowCount
FROM dbo.Orders AS o
LEFT JOIN dbo.OrderItems AS i ON i.OrderId = o.Id
WHERE o.Id = @Id
GROUP BY o.Id;

先用這種方式確認 JOIN 後的列數,再把原始查詢的 SELECT 欄位縮小到主鍵與各 JOIN 的關聯欄位。通常可以很快看出是哪一段 JOIN 放大了結果集。若查詢經過 View、條件式 JOIN、歷程資料表或多對多關聯,也要一併檢查;看起來像是 PK 條件的查詢,實際上可能沒有只讀主表。

排查順序

  1. 先在資料庫直接跑原 SQL。不要只看 ORM 產生的例外,先確認是 SQL 回傳多筆,還是映射設定把資料展開。
  2. 逐段拿掉 JOIN。從主表開始,一段一段加回關聯,找出結果由一列變成多列的地方。
  3. 確認商業規則是否真的要求唯一。例如「目前有效的設定」可能需要 IsCurrent = 1,而不是只用外鍵連結。若此規則成立,應用唯一索引或約束把它保護起來。
  4. 讓查詢與 API 契約一致。真的只允許零或一筆時才使用 SingleOrDefault;若需求本來就容許多筆,應改成回傳集合,不要用 FirstOrDefault 靜悄悄丟掉資料。

不要用 DISTINCT 或 NOLOCK 把症狀蓋掉

DISTINCT 只有在重複列本來就沒有意義時才合適;它無法說明 JOIN 為何產生多列,也可能掩蓋應該留意的關聯資料。更不應把 WITH (NOLOCK) 當成這個問題的解法。NOLOCK 等同於 READ UNCOMMITTED,可能讀到尚未提交、之後又被回滾的內容,也可能讓結果出現重複或遺漏列。它不能把一對多 JOIN 變回一對一,反而會讓診斷更不穩定。

若真正遇到的是讀寫競爭或鎖定等待,應先量測查詢與交易範圍,再和資料庫管理者討論是否適合啟用 READ_COMMITTED_SNAPSHOT 或採用 SNAPSHOT 隔離層級。這是系統層級的取捨,不應在單一查詢中隨手加入提示詞。

可作為修正方向的查詢設計

若呼叫端只需要訂單本身,就先只查 Orders,再以另一個查詢讀取明細;若必須一次取回,則讓資料存取層明確回傳「訂單+明細集合」,而不是把展開後的每一列交給只能接受單筆資料的方法。這樣當資料結構改變時,例外會更接近真正的原因。

這次問題最後應該被記成「查詢結果的唯一性與程式契約不一致」,而不是「PK 查詢需要加 NOLOCK」。把原因寫清楚,比讓例外暫時消失更能避免下一次在別的 JOIN 上重演。

參考資料

Author image
關於 Richard Zheng
About me 喜歡爬山,瑜伽,溜冰,喜歡新奇的事,最喜歡的還是寫程式帶來的成就感,對於資訊會不斷的出現新事物也能抱持好奇與熱忱。近期開始將學習的心得寫在Blog,發現思路更清晰也加深了記憶。 紙上得來終覺淺,絕知此事要躬行