中繼資料相容與失敗降級
本頁包含查得到卻不能當資料表用的物件、只問目錄檢視真的有的欄位,以及查詢失敗時的降級。 硬規則見中繼資料護欄。
資料行查得到,不代表它是一張資料表
第二層對資料表值函式(IF、TF)也查 sys.columns:它回傳的那組資料行與資料表的 欄位放在同一張目錄檢視裡,鍵就是函式自己的 object_id。不查的症狀是 FROM dbo.fn_LoansByReader(0) f 之後 f. 一個欄位都列不出來,SELECT * 也展不開 ——下游看到的與「這個物件真的沒有欄位」一模一樣。
但「查得到資料行」與「這是一張資料表」是兩件事。SqlObjectKind 上因此有三條述詞, 問錯一條的症狀各不相同:
| 問誰 | 回答什麼 | 誰在問 |
|---|---|---|
HasCatalogColumns | sys.columns 查得到資料行嗎 | 第二層要不要查欄位 |
IsTableShaped | 這個物件本身就是一組資料行嗎 | 滑鼠停留提示列欄位還是參數、第四層要不要查索引 |
IsInsertTarget | INSERT/MERGE 插得進去嗎 | 提交後要不要展開欄位骨架 |
三條原本是同一條 HasColumns,而放寬它到資料表值函式,另外兩件事會一起被放寬: INSERT INTO dbo.fn_LoansByReader 展開成一份剖析不過的欄位骨架,滑鼠停留提示則改列 回傳的資料行,蓋掉使用者正要填的引數——他停在一個函式上,問的是該怎麼呼叫它。 預覽文字同理以定義本文優先:那份文字同時說得出它吃什麼引數、回傳什麼, 而一串資料行說不出該怎麼呼叫它。
只 SELECT 目錄檢視真的有的欄位
第二層要判斷「這個欄位插不插得進去」,其中 GENERATED ALWAYS(時態資料表的期間 欄位、帳本資料表的異動欄位)走 COLUMNPROPERTY(…, 'GeneratedAlwaysType'), 不是 sys.columns.generated_always_type。
那一欄要 SQL Server 2016 才有,直接 SELECT 它會讓整份欄位查詢在更舊的執行個體上 變成語法錯誤——而語法錯誤是 DbException,會被下面那一節降級成「這一輪沒有資料」。 症狀因此不是「少判斷一種欄位」,而是欄位建議、SELECT * 展開與結構預覽在那些 伺服器上一起安靜地消失。COLUMNPROPERTY 對認不得的屬性名稱回傳 NULL, NULL > 0 不成立,舊版自然得到 0,不必為此再開一條依版本組字串的路。
整個目錄檢視或函式「這一版沒有」時另有一條:那不是失敗,不該降級、不該進「詳細記錄」,也不該因為 失敗不進快取而每一次重撞。先問它在不在(sys.all_views),在才用動態 SQL 讀;問不出在不在的內建函式 (CURRENT_TIMEZONE_ID(),版本號又分不出 Azure)包進動態 SQL,以 TRY…CATCH 接住下一層的編譯錯誤、 回一列 NULL。例子見執行個體名單。
「這一版沒有」與「從來就沒有」是同一種病。sys.tables 沒有 uses_quoted_identifier (那是 sys.sql_modules 的欄位),第四層曾經直接 SELECT 它,於是每一張資料表的 索引與條件約束整條查不到,而預覽顯示的是「沒有可用的連線」——連線好好的。 QUOTED_IDENTIFIER 只問得到 OBJECTPROPERTY(…, 'IsQuotedIdentOn'), 且要 CONVERT(bit, …):它回傳 int,讀取端的 GetBoolean 收到會丟 InvalidCastException,而那不是 DbException,降級接不住。
這一族函式加不了限定字,跨連結伺服器時會在對方登入的預設資料庫裡解析 (與 OBJECT_DEFINITION 同一個坑)。因此測試反射掃過每一條查詢,只有說得出 理由並列進 LocalFunctionsAllowed 的才准用。
資料庫說不行的時候
連不上、逾時、權限不足、物件剛被砍掉——這一類 DbException 不會冒出 SqlMetadataCatalog。它們在 TryLoad 那一層降級成「這一輪沒有資料」: 第一層回傳空快照,第二、四層回傳 null。
理由是紀錄檔的訊噪比。冒出去的話會落在 Ssms22 的 SqlAssistPlatformGuard 上, 而它會把每一次都記成一份完整堆疊;連線斷掉時使用者每開一次建議清單就失敗一次, 真正的程式錯誤就埋在裡面找不到了。
降級之後,各個表面本來就備好的處置終於用得上:SELECT * 展開拿到 null 欄位 名稱就整個放棄(不做部分展開),結構預覽顯示「沒有可用的連線;請先在查詢視窗 連上資料庫。」——那句話是為這個情形寫的,在降級之前它永遠不會出現。
降級到一個字都不留是另一回事:查詢寫錯與連線中斷在畫面上長得一模一樣, 唯一分得出來的資訊正是被吃掉的那句 Invalid column name '…'。TryLoad 因此把 「哪一條查詢」加上伺服器說的那句話送進 SqlMetadataFailure.Reporter,Ssms22 接到 「詳細記錄」那一段——平常一個位元組都不寫,訊噪比不變,出問題時打開就看得到。 只送訊息不送堆疊。
第四層(索引、條件約束、觸發程序、擴充屬性)另外有一條:查詢失敗時不回 null, 而是回傳只有第二層、且 IsStructureUnavailable 為真的結構。回 null 的話結構預覽 只有一句「沒有可用的連線」,會把第二層已經畫出來的欄位整片蓋掉,並且把使用者送去 查一個好好的連線。這一份不進快取,也過不了下一節的 CanBuildExecutableScript。
三件事刻意不這樣做:
- 只接
DbException。 參數契約違反與其餘任何例外都是程式錯誤, 該一路浮到平台邊界去留下完整堆疊。 - 失敗不進快取。 否則連線恢復之後仍然拿到空的。
- 空快照不算新鮮。 所以下一次按鍵會自然再試一次,不需要另外一套重試邏輯。
查得到物件、卻查不到它的結構
上一節講的是查詢失敗,這一節講的是查詢成功卻一列都沒有回來——兩件事的處置不同。
物件清單是快取的,而中繼資料的可見度是照權限過濾的。所以第一層看得到的名稱, 第二層不保證還查得到欄位:物件可能在那之後被卸除,或這個登入對它的權限被收回, 而 sys.columns 只是少幾列,不會報錯。OBJECT_DEFINITION 同理, WITH ENCRYPTION 與沒有 VIEW DEFINITION 權限都是安靜地傳回 NULL。
要拿去執行的輸出因此是全有或全無,兩道判斷各有一份出處:
| 問誰 | 回答什麼 |
|---|---|
SqlObjectKinds.HasExecutableScript | 這一類物件寫得出可以執行的 T-SQL 嗎 |
SqlObjectStructure.CanBuildExecutableScript | 這一次查到的資料夠不夠 |
空的索引清單有兩種來源,答案相反:查詢成功而那張表真的沒有索引,以及第四層 失敗所以還沒問到。IsStructureUnavailable 分開這兩件事;混成一件的症狀是一張有 五個索引與一個觸發程序的資料表被重建成什麼都沒有的資料表,而它照樣貼得上去。
兩道都過才組指令碼;任何一道不過就整段換成註解,寫明缺了什麼、原因, 以及查得到的部分——格式只有 SqlObjectStructure.BuildUnavailableScript 一份, 缺定義與缺欄位共用它。半份指令碼是最糟的結果:少了欄位的 CREATE TABLE 只剩一對空括號,卻仍然貼得上去,執行下去建出一張沒有欄位的資料表。 這與 SELECT * 不做部分展開是同一條理由。
缺定義的原因分三種說法,因為三種物件的本文根本不在同一個地方:T-SQL 模組說的是 加密與 VIEW DEFINITION 權限;CLR 物件(PC、FS、FT、TA)與擴充預存程序 (X,sp_executesql 那一族,連參數都不在目錄檢視裡)本來就沒有 T-SQL 本文; 同義字與序列說的是 sys.synonyms/sys.sequences 查不到那一列。說錯的話使用者 會去查一個不存在的原因。
前兩種由定義查詢連同 sys.objects.type 一起帶回來判別(SqlObjectImplementation), 不看物件描述從哪條路建出來:建議清單、SQL Search 與父物件各自記得帶型別代碼的話, 漏掉的那一條會安靜地退回「加密或沒有權限」。
-- 取不到 [dbo].[Lib_Tag] 的欄位。
-- sys.columns 一列都沒有回來,而查詢本身沒有失敗——原因只有兩個:物件在
-- 建議清單被快取之後卸除,或是這個登入對它的權限在那之後被收回。2
3
原因一定要寫進輸出裡。只說「這個物件沒有指令碼」的話,使用者查不出該去看權限、 看物件還在不在,還是看連線,而這三件事的下一步完全不同。
新的表面照同一條走:先問這兩個判斷,寫不出來就換成註解與原因,不要自己再判一次 「有沒有欄位」——判斷分岔的症狀是同一個物件在兩個表面得到不一樣的答案, 而那沒有任何徵兆。給人看的摘要(滑鼠停留提示、預覽的欄位分頁)不在這條規則裡: 那些文字沒有人會拿去執行,缺資料時少一格就是少一格。