SQL Server 不可預知的選擇結果(dbms 錯誤?)
下面是一個簡單的例子,它返回奇怪的結果,這是不可預測的,我們無法在我們的團隊中解釋它。我們做錯了什麼還是 SQL Server 錯誤?
經過一番調查,我們將搜尋區域縮小為子查詢中的 union 子句,它從“men”表中選擇一條記錄
它在 SQL Server 2000 中按預期工作(返回 12 行),但在 2008 年和 2012 年它只返回一行。
create table dual (dummy int) insert into dual values (0) create table men ( man_id int, wife_id int ) -- there are 12 men, 6 married insert into men values (1, 1) insert into men values (2, 2) insert into men values (3, null) insert into men values (4, null) insert into men values (5, null) insert into men values (6, 3) insert into men values (7, 5) insert into men values (8, 7) insert into men values (9, null) insert into men values (10, null) insert into men values (11, null) insert into men values (12, 9)
這僅返回一行:1 1 2
select man_id, wife_id, (select count( * ) from (select dummy from dual union select men.wife_id ) family_members ) as family_size from men --where wife_id = 2 -- uncomment me and try again
取消註釋最後一行,它給出:2 2 2
有很多奇怪的行為:
- 在“men”表上進行一系列刪除、創建、截斷和插入後,它有時會起作用(返回 12 行)
- 當您將“union select men.wife_id”更改為“union all select men.wife_id”或“union select isnull(men.wife_id, null)”時(!!!)它返回 12行(如預期的那樣)。
- 奇怪的行為似乎與“wife_id”列的數據類型無關。我們在具有更大數據集的開發系統上觀察到它。
- “where wife_id > 0” 返回 6 行
- 我們還通過這種陳述觀察到視圖的奇怪行為。SELECT * 返回行的子集,SELECT TOP 1000 返回所有
我們做錯了什麼還是 SQL Server 錯誤?
這是一個錯誤結果錯誤,您應該通過通常的支持渠道報告。如果您沒有支持協議,那麼了解付費事件通常會在 Microsoft 確認該行為為錯誤時退款可能會有所幫助。
該錯誤需要三個成分:
- 具有外部引用的嵌套循環(應用)
- 尋找外部引用的內部惰性索引假離線
- 內側串聯運算符
例如,問題中的查詢生成如下計劃:
有很多方法可以刪除這些元素之一,因此該錯誤不再重現。
例如,可以創建恰好意味著優化器選擇不使用惰性索引假離線的索引或統計資訊。或者,可以使用提示來強制雜湊或合併聯合,而不是使用串聯。還可以重寫查詢以表達相同的語義,但這會導致不同的計劃形狀,其中缺少一個或多個所需元素。
更多細節
惰性索引假離線將內部結果行惰性記憶體在由外部引用(相關參數)值索引的工作表中。如果要求惰性索引假離線提供它以前見過的外部引用,它會從其工作表中獲取記憶體的結果行(“倒帶”)。如果向 spool 詢問它以前未見過的外部參考值,它會使用目前外部參考值執行其子樹並記憶體結果(“重新綁定”)。惰性索引假離線上的查找謂詞指示其工作表的鍵。
當閥芯檢查新的外部參考是否與之前看到的相同時,此特定平面形狀會出現問題。
PrepRecompute
Nested Loops Join 正確更新其外部引用,並通過其介面方法通知操作員其內部輸入。在此檢查開始時,內部操作員讀取CParamBounds:FNeedToReload
屬性以查看外部引用是否與上次相比發生了變化。範例堆棧跟踪如下所示:當上面顯示的子樹存在時,特別是在使用 Concatenation 的地方,綁定會出現問題(可能是 ByVal/ByRef/Copy 問題)
CParamBounds:FNeedToReload
,無論外部引用是否實際更改,總是返回 false。當存在相同的子樹,但使用了 Merge Union 或 Hash Union 時,此基本屬性會在每次迭代中正確設置,並且 Lazy Index Spool 每次都會適當地倒帶或重新綁定。順便說一下,Distinct Sort 和 Stream Aggregate 是無可指責的。我懷疑 Merge 和 Hash Union 會複製先前的值,而 Concatenation 使用引用。不幸的是,如果不訪問 SQL Server 原始碼,幾乎不可能驗證這一點。
最終結果是,有問題的計劃形狀中的惰性索引假離線始終認為它已經看到了目前的外部引用,通過尋找其工作表來回退,通常什麼也沒找到,因此沒有為該外部引用返回任何行。在調試器中單步執行時,假離線只執行其
RewindHelper
方法,而從不執行其ReloadHelper
方法(在此上下文中重新載入 = 重新綁定)。這在執行計劃中很明顯,因為假離線下的操作員都具有“執行次數 = 1”。當然,例外情況是對於第一個外部引用,給出了惰性索引假離線。這總是執行子樹並在工作表中記憶體結果行。所有後續迭代都會導致倒帶,僅噹噹前迭代具有與第一次相同的外部引用值時才會產生一行(單個記憶體行)。
因此,對於嵌套循環連接外側的任何給定輸入集,查詢將返回與處理的第一行的重複行一樣多的行(當然,第一行本身加上一個)。
展示
表格和样本數據:
CREATE TABLE #T1 ( pk integer IDENTITY NOT NULL, c1 integer NOT NULL, CONSTRAINT PK_T1 PRIMARY KEY CLUSTERED (pk) ); GO INSERT #T1 (c1) VALUES (1), (2), (3), (4), (5), (6), (1), (2), (3), (4), (5), (6), (1), (2), (3), (4), (5), (6);
以下(普通)查詢使用合併聯合為每行(總共 18 個)生成正確的兩個計數:
SELECT T1.c1, C.c1 FROM #T1 AS T1 CROSS APPLY ( SELECT COUNT_BIG(*) AS c1 FROM ( SELECT T1.c1 UNION SELECT NULL ) AS U ) AS C;
如果我們現在添加一個查詢提示來強制連接:
SELECT T1.c1, C.c1 FROM #T1 AS T1 CROSS APPLY ( SELECT COUNT_BIG(*) AS c1 FROM ( SELECT T1.c1 UNION SELECT NULL ) AS U ) AS C OPTION (CONCAT UNION);
執行計劃有問題的形狀:
結果現在不正確,只有三行:
儘管不能保證這種行為,但聚集索引掃描的第一行的
c1
值為 1。還有另外兩行具有此值,因此總共生成了三行。現在截斷數據表並使用“第一”行的更多重複項載入它:
TRUNCATE TABLE #T1; INSERT #T1 (c1) VALUES (1), (2), (3), (4), (5), (6), (1), (2), (3), (4), (5), (6), (1), (1), (1), (1), (1), (1);
現在串聯計劃是:
並且,如前所述,生成了 8 行,
c1 = 1
當然,所有行都包含:我注意到您已經為此錯誤打開了一個 Connect 項目,但實際上這不是報告對生產有影響的問題的地方。如果是這種情況,您真的應該聯繫 Microsoft 支持。
這個錯誤結果錯誤已在某個階段得到修復。從 2012 年起,它不再為我在任何版本的 SQL Server 上重現。它確實在 SQL Server 2008 R2 SP3-GDR 版本 10.50.6560.0 (X64) 上重現。
為什麼要使用沒有 from 語句的子查詢?我認為這可能會導致 2005 年和 2008 年伺服器的差異。也許您可以使用顯式加入?
select m1.man_id, m1.wife_id, (select count( * ) from (select dummy from dual union select m2.wife_id from men m2 where m2.man_id = m1.man_id) family_members ) as family_size from men m1