這篇整理一段 SQL 查詢從業務的一句話走到上線之間要做的決定——追問需求、設定判斷標準、選寫法、補前提、量代價、對邊界、交付——每一個決定要決定什麼、決錯之後看到什麼症狀,以及它背後的機制在哪一篇展開。範圍從拿到需求的那一刻開始,到查詢上線、交給下一個環境為止。

各個決定的內容、決錯之後的症狀與機制所在的篇章

決定要決定的決錯之後的症狀機制在
追問需求這句話真正問的是哪一個資料問題查詢寫對了,而它答的是另一題答案要去問提出需求的人
設定判斷標準什麼樣的輸出算對改完查詢不知道有沒有改好SQL.13 查詢的合法性與答案的正確性:引擎檢查的範圍、答案錯掉的成因與查證方法
選寫法除了判斷有沒有,還要不要用到被比對那張表的欄位列數不是預期的,而後面的加總跟著錯SQL.8 IN、EXISTS 與 JOIN:列數、可取用的欄位與 NULL 的處理、SQL.7 表的出現與別名:表名的指稱、別名的作用範圍與自連接、SQL.10 分組與視窗函數:各自的產出、選用的依據與 LAG、LEAD 的相鄰列
補前提這段寫法默默假設了什麼資料長成某個樣子之後答案就錯,而不報錯SQL.6 連接之後的列數與空缺:列數膨脹、外連接補的 NULL 與三值邏輯、SQL.12 分頁的排序鍵與接續標記:同分的列、全序與 OFFSET 的代價
量代價這段要跑多久、跑幾次開發時很快,正式資料上拖垮請求SQL.17 查詢的代價:資料分布、索引與寫法各自的影響、Sargable 條件的形狀決定索引用不用得上
對邊界需求沒說而資料會有的那些平常都對,特定幾筆資料上靜默算錯SQL.11 查詢結果的列序:沒有 ORDER BY 時的來源與 ORDER BY 的規定範圍、SQL.13 查詢的合法性與答案的正確性:引擎檢查的範圍、答案錯掉的成因與查證方法
交付換引擎、換資料量、換讀的人之後還成不成立會報錯的差異搬家當天就浮現,不報錯的要等某一筆資料才現形或始終不出聲SQL.19 引擎寬鬆度與可攜性:各家對同一組寫法的差異、分級與處理時機 管換引擎、SQL.20 關鍵字的宣告與引擎的行為:宣告落空的形態與查證 管換讀的人

決錯之後的症狀有一個共同點:查詢照跑而答案不對。 只有兩個決定決錯時會當場出聲:量代價決錯時查詢慢得看得出來;交付時換了引擎,有一類差異第一次執行就報錯。SQL.19 引擎寬鬆度與可攜性:各家對同一組寫法的差異、分級與處理時機 把跨引擎的差異按發聲的位置分級,第一次執行就報錯的叫當場報錯,其餘的差異要麼等某一筆資料才報錯、要麼始終靜默。其餘的決定決錯時欄位正常、列數合理,沒有任何東西提示要去檢查。

最常被跳過的決定:追問需求、設定判斷標準與對邊界

追問需求與設定判斷標準。 這兩個決定發生在打開編輯器之前,所以不留下任何痕跡——跳過了的查詢跟沒跳過的長得一模一樣。而跳過追問需求的代價是整段查詢答錯題,跳過設定判斷標準的代價是後面每一次修改都不知道有沒有改好。

對邊界。 一句需求描述的是資料正常時的樣子,而正式資料裡會出現提出需求的人沒想到、所以沒說的情形。以「哪幾天氣溫比前一天高」為例:同一個測站同一天有兩筆紀錄(感測器重送、補登、換設備之後兩套系統各寫一筆)、觀測日存的是哪一個時區的日期、三個月後才補進來的舊資料讓昨天的答案今天變了、同一個測站代號在某一天之後指向另一個地點。對邊界這個決定,就是在查詢寫好之後把這些情形逐一列出來,並決定每一種出現時查詢該回什麼。

這份清單沒有地方可以查。需求裡沒有它,schema 也只記下有哪些欄位、各是什麼型別,不記下資料在現實中會怎麼偏離預期。列得出幾種,取決於看過多少這個領域的資料:看過感測器重送,才會想到同一天可能有兩筆;看過補登,才會想到歷史答案會變。所以這一步的能力靠經驗累積,每在正式資料上遇到一種新的情形,就把它加進這個領域的查詢的檢查清單。

跳過它的症狀最難察覺:查詢在大部分資料上都對,只在碰到那幾筆特殊資料時算錯,而且不報錯。那幾筆往往要到有人拿結果去對帳,才被發現。

決定的前提:建表時定下的表的形狀

從追問需求到交付,每一個決定都做在一組已經建好的表上,而表的形狀是建表時的決定造出來的:欄位允不允許為空、鍵唯不唯一、長欄位放哪一張表、常一起取的資料切在幾張表、字串的比較規則寫在哪一層、外鍵生不生效。選寫法、補前提的時候,鍵唯不唯一、欄位允不允許為空已經定了;量代價的時候,長欄位放哪一張表、常一起取的資料切在幾張表也已經定了。這些建表時的決定替往後每一段查詢定了價——手上要交的是一份 schema 設計而不是一段查詢時,問題落在追問需求之前。以鍵唯不唯一為例:「哪幾天氣溫比前一天高」用自連接、視窗函數與相關子查詢三種寫法,共同押的前提,是同一個測站同一天只有一筆;表上有 UNIQUE (測站, 觀測日) 時這個前提由資料庫保證,沒有的話每一段查詢都得自己處理重複,否則自連接會把一列配成多列。每一個決定各自替查詢定了什麼代價,實測在 backend 1.16 Schema 設計決定的查詢代價。

在「哪幾天氣溫比前一天高」這句需求上做完全部決定的示範

SQL.22 查詢設計的完整示範:把「哪幾天氣溫比前一天高」從需求做到上線 拿「哪幾天氣溫比前一天高」這句需求從頭做完這些決定:把「前一天」追問成日曆上的前一天還是資料上的前一筆;用自連接、視窗函數與相關子查詢各寫一次,找出三者共同押的前提(同一個測站同一天只有一筆);看漏掉相隔天數與漏掉分區的兩個錯誤版本:漏掉相隔天數的多報,漏掉分區的錯在哪個方向要看同一天兩個測站誰排在前面;把自連接再分成日期差與等值兩種寫法,在同一組資料上量這兩種自連接、視窗函數與相關子查詢的時間,差到三個量級,其中一種加了索引反而變慢;最後列出重複紀錄、時區、補登與測站搬家這幾種需求沒說的情形。