展開的欄位、參數與預留值
本頁處理 INSERT 欄位、EXEC 參數、預留值與對齊;安全寫回與 MERGE 見 整句展開, 函式的括號與引數見函式呼叫。
哪些欄位插不進去
四種,漏掉任何一種的症狀相同——展開出來的 INSERT 一執行就錯:
| 排除 | 判斷依據 |
|---|---|
IDENTITY | sys.columns.is_identity |
| 計算欄位 | sys.columns.is_computed |
rowversion(舊名 timestamp) | 型別名稱;sys.columns 沒有這個旗標 |
時態與帳本資料表的 GENERATED ALWAYS 欄位 | COLUMNPROPERTY(…, 'GeneratedAlwaysType') |
最後一種刻意不讀 sys.columns.generated_always_type:那一欄要 SQL Server 2016 才有, 直接 SELECT 它會讓整份欄位查詢在更舊的執行個體上變成語法錯誤,而 TryLoad 會把它降級成「這一輪沒有資料」——於是欄位建議、萬用字元展開與結構預覽 會在那些伺服器上一起安靜地消失。COLUMNPROPERTY 對認不得的屬性名稱回傳 NULL, 舊版因此自然得到 0。
一個欄位都撈不到時整個放棄,維持只插入名稱:同義字在 sys.columns 裡沒有列, 組出 INSERT INTO syn () VALUES () 比什麼都不做糟糕得多。這與 SELECT * 不做 部分展開是同一條理由。
值先填什麼
三條,順序不能對調:
- 有
DEFAULT條件約束 →DEFAULT - 可為 NULL →
NULL - 其餘 → 依型別的預留值(
''、N''、0、0x、NEWID())
VALUES (DEFAULT) 對「沒有預設值而且 NOT NULL」的欄位是執行期錯誤,所以第一條 只能給真的有 DEFAULT 條件約束的欄位。
日期時間型別給 NULL 而不是 '':空字串轉成日期是 1900-01-01,那是一個執行得動的 錯值,而預留值要的正是「看得出來還沒填」。NULL 在 NOT NULL 的欄位上會失敗, 而失敗看得見。
型別讀不出來(SELECT … INTO #Loan 投影出來的欄位, 見指令碼宣告的資料表)時走第二條:那些欄位一律當成 可為 NULL,值填 NULL,註解也不寫型別那一段。讀不出型別不是漏了什麼——它要追到最 內層的資料表才算得出來,而 NULL 是唯一不會替使用者猜錯內容的預留值。
參數的選擇性從定義讀,不從中繼資料讀
sys.parameters.has_default_value 對 T-SQL 模組永遠是 0——那一欄只對 CLR 模組 有效。所以「哪些參數可以省略」只能剖析 OBJECT_DEFINITION 的參數清單, 在 Core/Statements/SqlModuleParameterDefaults。
定義是第三層資料,本來不在按鍵路徑上;但提交也不在按鍵路徑上,而且 GetDetailAsync 的同一次呼叫本來就會把欄位、參數與定義一起帶回來,因此不多付 任何一次往返。讀不出來就少標幾個「選擇性」,不猜——猜錯會讓使用者刪掉一個其實 必填的參數。
OUTPUT 參數傳的必須是變數,光給字面值是語法錯誤,所以整段前面會補上 DECLARE。 使用者已經宣告過同名變數時會撞名,但那是一個當場看得見的編譯錯誤;少了 DECLARE 則是連編譯都過不了,那比什麼都不做糟糕。
沒有參數的模組不展開(展開起來與只插入名稱一模一樣)。擴充預存程序在 sys.parameters 裡也沒有列,同樣落在這一條。
「EXEC 展開時包含選擇性參數」關掉之後,這份選擇性清單改當篩子用:標成選擇性的 整列不展開。省略它們本來就是合法的呼叫方式,而參數二三十個的程序展開出來有一半 是使用者接著要一行一行刪掉的。讀不出定義時清單是空的,於是每個參數都算必填—— 方向與上一段一致:寧可展開得多,也不要因為讀不到定義而把該填的參數吞掉。 篩完一個不剩時退回只插入名稱,走的是「沒有參數的模組不展開」同一條路。
續行的對齊
EXEC 的續行對齊到第一個參數所在的欄,每一列的 @ 因此落在同一個位置; 代價是名稱長的模組會把整段推向右邊。INSERT 的欄位與 VALUES 一律每列一個, 而且不跟「SELECT * 展開後的欄位排版」那個設定走——那個設定的三種排法都在權衡 「一行讀不讀得完」,而這裡的兩份清單是成對的:第三個欄位對第三個值, 攤成一行就對不起來,而對不起來的代價是把值填錯格。
縮排取語句所在行的前導空白,整段重複到每一行,定位字元原樣保留:每一行前面放的 都是同一串字元,在定位寬度不是 4 的機器上也對得齊。
EXEC 與 EXECUTE 照使用者原本寫的帶回去。統一改寫成 EXEC 也合法,但那是他 沒有要求的改動——與展開萬用字元時保留他自己寫的限定字是同一條。
INSERT INTO 與單獨的 INTO 必須分開
SELECT * INTO #tmp 的 INTO 後面是一個還不存在的新名稱,在那裡展開骨架會蓋掉 使用者正在取的名字。所以認的是 INSERT INTO 這兩個字,而不是 INTO 一個字。
替換的範圍從 INSERT 開始,不是從 INTO——只從 INTO 開始換會在編輯器裡留下一個 孤零零的 INSERT。