展開 SELECT *
游標停在選取清單的 * 後面時,旁邊會出現「按 Tab 展開所有欄位」,按下去就換成 完整的欄位清單:
SELECT *| FROM dbo.PUBLISHER
↓ Tab
SELECT PublisherId, PublisherName, CreatedAt, ModifiedAt FROM dbo.PUBLISHER提示不是裝飾:使用者不會憑空去試按 Tab,而按下去之前也看不出這一次到底展不展得開 ——衍生資料表少寫一個別名、CTE 裡有個沒有名稱的運算式,都會讓同一個星號變成 展不開的。提示出現與否就是那份判斷的結果,看得到就一定按得動。
提示走的是編輯器自己的提示視窗(與滑鼠停留提示同一套),不會搶走鍵盤焦點; 建議清單開著時它會讓位,兩個小視窗貼在同一行旁邊只會互相擋住。出現與收起由共用的 Editor/CaretHint 在游標、文字、清單、設定或焦點變動後重新判斷——游標跟著編輯位移 時平台不發游標事件,只聽那一個的話,按 Tab 展開之後提示會一直留著。
哪些星號算數
* 在 T-SQL 裡絕大多數時候是乘號。唯一分得開的依據是它前面接什麼:
| 寫法 | 展開 | 為什麼 |
|---|---|---|
SELECT * | 是 | 選取清單的開頭 |
SELECT TOP 10 PERCENT WITH TIES * | 是 | 中間夾的都是 SELECT 的前置詞 |
SELECT GETDATE(), * | 是 | 同一份選取清單裡的逗號之後 |
SELECT a.* | 是 | 只展開 a 這一個來源 |
SELECT COUNT(*) | 否 | 前面是左括號 |
SELECT 5 * 3、SELECT a * b | 否 | 前面是數字或識別字 |
INSERT INTO t (a, *) | 否 | 那串逗號是資料行清單,不是選取清單 |
ORDER BY a, * | 否 | 往回走先遇到 BY |
數字刻意只在 TOP 後面才放行。少了這個條件,SELECT 5 * 3 的乘號就會被當成 萬用字元——而那正是第一版的行為。
欄位從哪裡來
判斷「這個星號是不是萬用字元」在 Core/Wildcards/SqlWildcardAnalyzer, 「這個來源給得出哪些欄位」則在 Core/Parsing/SqlColumnSourceResolver—— 後者與建議清單的欄位建議共用。分開的理由是它們回答的是不同的問題: 乘號與萬用字元怎麼分只有展開需要知道,而別名指向哪些欄位是兩個功能都要問的。
| 來源 | 欄位怎麼取得 |
|---|---|
| 資料表、檢視 | 中繼資料的第二層 |
衍生資料表 (SELECT …) d | 讀它自己的選取清單 |
CTE WITH c AS (…) | 讀主體的選取清單;有寫資料行清單就以它為準 |
帶資料行清單 … AS T (a, b) | 就是那份清單;(VALUES …) 只有這條路 |
暫存資料表 #Loan、資料表變數 @rows | 讀 CREATE TABLE/DECLARE … TABLE 的資料行清單 |
巢狀的 * | 往內遞迴,把最外層的別名一路帶著走 |
資料行清單只有衍生資料表與 OPENROWSET/OPENQUERY/OPENDATASOURCE 接得住, 文法就是這樣切的:dbo.fn(x) f (NOLOCK) 是舊式資料表提示,形狀一樣,而 NOLOCK 不是保留字,猜括號內容分不出來。
SELECT … INTO #Loan 投影出來的暫存資料表往它讀的那張表追下去,追到中繼資料 也算數——展開等得起一次查詢。QuickInfo 與預覽不等,所以同一張 #Loan 在那兩處 可能什麼都不顯示;那不是分岔,是同一份遞迴在兩種預算下停在不同的地方。
(SELECT * FROM dbo.PUBLISHER c) d 的欄位在外層要寫成 d.欄位——內層的 c 在外面根本不存在。三種欄位別名寫法都讀得出來:Id AS Code、Total = Qty * Price、 ISNULL(Memo, '') Note。
遞迴 CTE 取 UNION ALL 之前那一段的欄位名稱,後面那一段不必看;直接參照自己的 CTE(WITH c AS (SELECT * FROM c))則整個放棄。主體還沒關上時算到文字結尾:游標常在 遞迴的那一段裡,JOIN c ON … = c. 要的正是它自己。
任何一個來源解析不出來就完全不展開,不做部分展開:SELECT Qty * Price 這種 沒有名稱的運算式在外層無從稱呼。少了幾個欄位的 SELECT 仍然執行得動, 卻執行出錯的結果——那比什麼都不做糟糕得多。
同一條規則管住「讀來源」那一步:來源自己帶的括號與尾巴——引數清單、資料表提示、 TABLESAMPLE、時態表的 FOR SYSTEM_TIME …——一律整段跳完。不讀內容與不跳過是兩件事:停在括號前面的話, 後面那個逗號就不再是來源清單的逗號,FROM dbo.Loan l (NOLOCK), dbo.Copy c 的 dbo.Copy 整個消失——而這一次連「解析不出來」都不會發生,展開的是一份看起來 完整的錯誤結果。
加不加別名
敘述裡只有一個資料來源時不加:欄位名稱不可能模稜兩可,補上去只是雜訊。 有兩個以上就一律加上別名,否則 SELECT Name FROM A a JOIN B b 會因為欄位名稱 模稜兩可而執行失敗。使用者自己寫了限定字(a.*、dbo.PUBLISHER.*)時照原文 帶回去:把 dbo.PUBLISHER 改寫成 PUBLISHER 雖然也合法,卻是他沒有要求的改動。
方括號跟著「插入物件時加上方括號」走,與建議清單提交時是同一條規則。
欄位怎麼排
「SELECT * 展開後的欄位排版」有三種選擇,差別只在整份放不下一行的時候:
| 設定 | 放得下一行 | 放不下 |
|---|---|---|
| 放得下就排成一行(預設) | 一行 | 每欄一行 |
| 永遠每欄一行 | 每欄一行 | 每欄一行 |
| 依行寬排滿 | 一行 | 一行放多個欄位 |
預設值是前者:絕大多數的 SELECT * 攤開來還是一行讀得完,而一張一百多欄的 資料表攤成一行等於逼使用者橫向捲動去讀自己剛剛產生的東西。
每欄一行是唯一一個輸出形狀只由欄位數量決定的模式——換行位置不受欄位名稱 長度與 * 的縮排位置影響,所以新增或移除一個欄位只會動到一行,比對版本差異 最清楚。另外兩種都由字元數決定換行點:改一個欄位名,後面的排法會整段跟著變。
換行一律對齊原本 * 的位置。第一個欄位永遠留在原地,把它推到下一行只會讓 SELECT 孤零零地留在上一行。前導空白裡的定位字元原樣保留、其餘換成空白: 定位字元換成空白會讓對齊在定位寬度不是 4 的機器上跑掉。
行寬本身(120 個字元)不做成設定。排法是使用者感覺得到的,分界點落在 118 還是 124 他不會有意見。
Tab 的四種意思
Tab 在編輯器裡可能是提交建議清單、前進到下一個 Snippet 欄位、展開萬用字元, 或一般縮排。全部由 SqlTabCommandHandler 依這個順序判斷;Shift+Tab 只在 Snippet session 開著時回到上一欄,其餘交回編輯器。
**清單開著時一定要讓開。**不先問過 broker 的話,使用者按 Tab 想提交清單選取項時, 可能會變成 Snippet 跳欄或展開萬用字元。Snippet 與萬用字元不能各自匯出一個 Before=default 的 Tab handler;兩者的先後順序沒有保證。
欄位已經在快取裡(建議清單開過一次就預熱好了)就在按鍵的同一個回合裡改完, 看起來就像按鍵直接改了文字;沒命中才去查資料庫,而那一次 Tab 仍然算被處理掉 ——不吞掉的話編輯器會先插入一個定位字元,等查詢回來時要展開的位置早就被它推走了。 要換掉的範圍以 ITrackingSpan 記住,查詢期間使用者繼續打字也不會換錯地方。