SAS 語言
撰寫符合慣用風格、生產品質的 SAS 程式碼。此技能涵蓋 SAS 語言本身,與任何框架無關。
範圍
- DATA 步驟程式設計(SET/MERGE/BY,RETAIN,陣列,DO 迴圈,Hash 物件,first./last. 處理)
- PROC SQL(聯結,子查詢,檢視,使用 CONNECT TO 的 Pass-Through)
- 巨集語言(%macro/%mend,巨集變數,%local/%global,條件式 %IF 邏輯,引用函數 %STR/%NRSTR/%BQUOTE,CALL SYMPUTX)
- 常用 PROC:SORT,MEANS/SUMMARY,FREQ,TRANSPOSE,REPORT,PRINT,CONTENTS,DATASETS,FORMAT,IMPORT/EXPORT
- 格式與輸入格式(透過 PROC FORMAT 的使用者自訂格式,Picture 格式)
- ODS 輸出(HTML,PDF,RTF,Excel,OUTPUT 目的地)
- 檔案 I/O:LIBNAME,FILENAME,INFILE/INPUT,FILE/PUT
工作流程
對每個產生 SAS 程式碼的請求套用此循環——無論是新程式、編輯或除錯:
- 分析請求。 識別輸入:資料集、欄位、巨集變數、平台(SAS 9 或 Viya)。記下期望的輸出:一個資料集、一份報表(ODS)、一個巨集變數或一個檔案。如果請求不明確,請陳述您的假設並繼續進行。
- 起草程式碼。 根據以下樣式規則,撰寫最簡短的正確程式。使用二階層資料集名稱,為字元變數明確指定
length,在巨集內部使用%local,並為proc sql加上quit;。傾向使用proc sort nodupkey而非手動撰寫的去重複邏輯。 - 執行(或描述執行過程)。 如果有可用的 SAS 工作階段,請提交並擷取日誌。如果沒有,請根據常見陷阱清單在腦中追蹤程式碼——指出任何您無法驗證的步驟。
- 檢查日誌。 將任何
WARNING:或ERROR:視為需要修復的缺陷,而不是可忽略的註記。常見罪魁禍首:未初始化的變數、隱含的數值→字元轉換、因隱含 length=8 導致的截斷、「沒有觀測值」、未解析的巨集參考、唯讀的自動巨集變數。在合理的情況下也應去除多餘的NOTE。 - 根據陷阱清單審查。 在返回前,根據常見陷阱清單重新閱讀最終程式碼。修正或註記任何被標記的問題。
輸入: 一個請求(自然語言或 .sas 檔案),可選的範例資料或日誌片段。輸出: 符合慣用風格的 SAS 程式碼(2 空格縮排,以 run;/quit; 結尾),以及在必要時,針對所處理的陷阱或所做的假設加上簡短說明。驗證: 返回的程式碼在心理日誌掃描中必須沒有任何 WARNING/ERROR,並且在實際執行(如果可行)時產生一個乾淨的日誌。
不可協商:無 WARNING
生成的 SAS 程式碼必須乾淨執行——日誌中零 WARNING(和零 ERROR)。將每個 WARNING: 視為缺陷:未初始化的變數、隱含的型別轉換、應被視為警告的截斷註記、「沒有觀測值」、未解析的巨集參考等。如果某個警告確實無法避免,請刻意將其抑制(例如使用明確的選項)並說明原因。同樣地,在合理的情況下也應避免多餘的 NOTE。
樣式規則
這些規則遵循 @sasjs/core 編碼標準——將它們應用於所有 SAS 程式碼:
- 每行一個陳述式;縮排 = 2 個空格,無跳格鍵,無尾隨空白
- 每行不超過 80 個字元;Unix(LF)換行;UTF-8
- 完全避免非 ASCII / 特殊字元——以達到跨 SAS 安裝和編碼的最大相容性
- 步驟總是以
run;結尾;對於proc sql(以及連接到 CAS 的程序),必須使用quit;以避免在 Viya 上出現WARNING: You cannot disconnect or terminate session ... - 所有資料集參考必須是二階層(
work.blah,而非blah)——這可以防止DATASTMTCHK=ALLKEYWORDS和作用中的USER函式庫 - 為字元變數使用明確的
length/attrib,而不是依賴預設值(避免隱含的 length=8 截斷) - 為求清晰,使用字面後綴(
'01JAN2020'd,'12:30't) - 傾向使用
proc sort搭配nodupkey,而非手動的去重複邏輯 - 巨集:
- 即使沒有參數,定義時也要加上括號:
%macro x();而非%macro x; - 結尾的
%mend;必須重複巨集名稱 - 巨集呼叫不以分號結尾:
%my_macro()而非%my_macro(); - 必要參數使用位置標記法;選用參數使用關鍵字(
var=)樣式 - 巨集名稱使用小寫,採用動詞-名詞慣例
- 巨集變數不使用尾隨點號(
&var而非&var.),除非需要防止不正確的解析 - 巨集變數名稱不區分大小寫:
&Foo、&FOO和&foo都解析為同一個符號(這同樣適用於%symexist/%superq/symget參數,這些參數接受的是名稱而非值)。不要將大小寫不匹配當成錯誤來追查——這絕不會是原因。 - 所有巨集變數必須是
%local,除非故意設為全域(全域變數應使用應用程式前綴以避免衝突);在 DATA 步驟中使用call symputx(而非symput) - 在巨集內部使用
/* */進行註解(星號註解會被編譯到巨集中) - 使用
%length(&var)=0檢查來保護巨集邏輯,而非&var=(空的比較是不安全的)
- 即使沒有參數,定義時也要加上括號:
- 避免命名衝突:使用基於
%sysfunc-/&syslast的工作表格(例如data &output; set &syslast; run;),而不是硬編碼的名稱 - WORK 函式庫在工作階段終止時會自動清除:SAS 在工作階段結束時會刪除 WORK 中的每個表格,因此對於一般的暫存表格,不需要明確的清理(
proc datasets lib=work nolist; delete ...; quit;)——請不要添加它,並且在審查時不要將其缺失標記為缺陷。僅在暫存檔案過大或數量過多,有導致執行期間磁碟空間不足的風險時,才進行明確清理;在這種情況下,請在不再需要時立即刪除它們,而不是留待工作階段結束時處理。 - 無開放式(非巨集)條件程式碼:將平台分支或有條件執行的區塊(例如
%if %mf_getplatform()=VIYA %then %do; ... %end;)包裝在%macro ... %mend中,並呼叫該巨集。在程式層級的開放式%if在某些執行環境(作業/排程器/測試框架)中會失敗,並且會隱藏作用域洩漏問題。
可攜性意識
- 注意 SAS 9.4 和 Viya 之間的程式碼差異(例如 CAS 動作 vs. 程序,用於載入 sashdat 的
proc casutil,在鎖定的伺服器上沒有 X 指令) - 除非要求,否則避免硬編碼的實體路徑和特定於引擎的選項
- 使用 if/else 邏輯時,不要有開放的巨集程式碼:將分支區塊包裝在
%macro ... %mend中並呼叫它們——開放的%if/%else在所有的 SAS 環境中並不會如預期般運作。
Before / After 範例
每組配對展示一個常見的 WARNING 或缺陷模式及其慣用的修正方式,並說明它所解決的日誌問題。
DATA 步驟:未初始化的變數 + 隱含長度截斷
Before——region 被參考但從未被指派或透過 set 引入,因此日誌顯示 WARNING: Variable region is uninitialized;並且 name 從來源繼承了 first_name 的長度(通常為 $8),靜默地截斷了串接後的值:
data work.customers;
set work.source;
region = upcase(region); /* region 不在 work.source 中 */
name = catx(' ', first_name, last_name); /* 被截斷為來源中 name 的長度 */
run;
After——region 透過 set(或明確指派)引入,並且在 set 之前使用明確的 length 來放寬 name,使 catx 不會被截斷:
data work.customers;
length name $ 50;
set work.source; /* region 必須存在於 work.source 中 */
region = upcase(region);
name = catx(' ', first_name, last_name);
run;
PROC SQL:隱含轉換 + 笛卡兒乘積
Before——使用字元 id 與數值 cust_id 進行聯結,觸發了 NOTE: Character values have been converted to numeric(或在某些選項下產生 WARNING),而缺少聯結鍵則有導致笛卡兒爆炸的風險:
proc sql;
create table work.joined as
select a.*, b.balance
from work.orders a, work.accounts b
where a.id = b.cust_id;
quit;
After——使用明確的 length/cast 使型別匹配,明確的 inner join,以及一個防止因缺少鍵而產生笛卡兒乘積的防護:
proc sql;
create table work.joined as
select a.id, a.amount, b.balance
from work.orders as a
inner join work.accounts as b
on a.id = input(b.cust_id, 8.) /* 明確的 cast;或修正上游長度 */
;
quit;
如果一個鍵值欄位在兩側都確實有重複值,請先進行彙總或去重複——絕不要依賴偶然的 1:1 聯結。
巨集:缺少 %local + 不安全的空值檢查
Before——巨集內部的 %let 在全域作用域建立了 dsid(沒有 %local),而 %if &filter= 是不安全的——當 &filter 為空時,它解析為 %if = %then,這會造成語法錯誤而不是一個乾淨的分支:
%macro apply_filter(filter=);
%if &filter= %then %do;
data work.out; set work.in; run;
%end;
%else %do;
data work.out; set work.in; where &filter; run;
%end;
%let dsid = &syslast;
%mend apply_filter;
After——%local dsid 將暫存變數保持在巨集作用域內,%length(&filter)=0 安全地防護了空值檢查,巨集定義使用了括號,並且 %mend 重複了名稱:
%macro apply_filter(filter=);
%local dsid;
%if %length(&filter)=0 %then %do;
data work.out; set work.in; run;
%end;
%else %do;
data work.out; set work.in; where &filter; run;
%end;
%let dsid = &syslast;
%mend apply_filter;
審查時需標記的常見陷阱
- 非預期的多對多 MERGE
- 自動變數
_ERROR_/ 隱含的 RETAIN 意外 - 巨集時序問題:在
¯ovar存在之前就參考它,%if評估資料步驟變數(應改用if/symget) - 自動巨集變數(
&syscc,&syswarningtext,&syserrortext,&sysdate,&sysuserid等)是唯讀的——嘗試覆寫它們(例如%let syswarningtext=;或call symput('syscc',...))會引發ERROR: Unable to assign value to a macro variable that is read only(或類似訊息)。永遠不要試圖「清除」它們。 - 因第一次指派時的隱含 length=8 導致的截斷
proc sql笛卡兒乘積警告
限制
此技能是一個靜態的語言參考——它為撰寫和審查 SAS 程式碼提供指導。它不執行 SAS 程式碼、不執行 shell 命令、不存取檔案系統、不連接資料庫,也不發送網路請求。所有程式碼範例僅供說明,必須由使用者提交到 SAS 工作階段。像「檔案 I/O」、「PROC SQL」、「指令」和「執行」等術語指的是 SAS 語言概念(檔案處理陳述式、SQL 程序、巨集執行環境),而不是由此技能執行的作業系統層級操作。