SAS 语言
编写地道、高质量的生产级 SAS 代码。此技能涵盖 SAS 语言本身,与任何框架无关。
范围
- DATA 步编程(SET/MERGE/BY、RETAIN、数组、DO 循环、哈希对象、first./last. 处理)
- PROC SQL(连接、子查询、视图、使用 CONNECT TO 进行直通)
- 宏语言(%macro/%mend、宏变量、%local/%global、条件 %IF 逻辑、引用函数 %STR/%NRSTR/%BQUOTE、CALL SYMPUTX)
- 常用过程:SORT、MEANS/SUMMARY、FREQ、TRANSPOSE、REPORT、PRINT、CONTENTS、DATASETS、FORMAT、IMPORT/EXPORT
- 格式和输入格式(通过 PROC FORMAT 自定义,图片格式)
- ODS 输出(HTML、PDF、RTF、Excel、OUTPUT 目标)
- 文件 I/O:LIBNAME、FILENAME、INFILE/INPUT、FILE/PUT
工作流程
对每个生成 SAS 代码的请求(新程序、编辑或调试)应用此循环:
- 分析请求。 识别输入:数据集、列、宏变量、平台(SAS 9 vs 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) - 优先使用带
nodupkey的proc sort,而非手动去重逻辑 - 宏:
- 即使没有参数,也要用括号定义:
%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 环境中的行为都不如预期。
前后对比示例
每对示例都展示了一个常见的 WARNING 或易出错模式及其地道的修复方法,并指出了它解决的日志问题。
DATA 步:未初始化变量 + 隐式长度截断
之前 —— 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;
之后 —— 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:隐式转换 + 笛卡尔积
之前 —— 在字符型 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;
之后 —— 显式声明 length/进行类型转换以使类型匹配,使用显式内连接,并添加守卫以防止在键缺失时产生笛卡尔积:
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.) /* 显式类型转换;或修复上游长度 */
;
quit;
如果某个键列在两边都合法地存在重复值,请先聚合或去重——绝不依赖偶然的 1:1 连接。
宏:缺少 %local + 不安全的空值检查
之前 —— 宏内部的 %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;
之后 —— %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评估 DATA 步变量(应使用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 过程、宏执行上下文),而不是此技能执行的操作系统级别的操作。