SAS Hash 对象的系统化用法:定义、键值查找、数据装载与输出
一次把整张表读进内存,再让主表的每一行各自去“钉”上去——这是临床 SAS 编程里最值钱的一条肌肉记忆。 本文给出 hash 对象的完整语法骨架(declare → definekey → definedata → definedone → find / output),四类真实项目形态的 可搬运代码(受试者筛选、横向合并、纵向检索、SDTM/ADaM 衍生),以及八条按官方文档与会议论文归集的易错点。
这是一份语言用法参考,不是某个研究项目的交付物:研究编号与方案号一律写作 NNNN,
数据集与变量名使用 CDISC 标准名(USUBJID、PARAMCD、ADT、AVAL、
AESEQ、CMSEQ 等),公司环境、库名与路径均已剥离。凡涉及 SAS 语言行为的断言,均在正文中以
[doc](官方文档)或 [lit](文献作者陈述)标记出处;凡属工程判断者标 [实践]。
本文不包含任何受试者级数据,也没有可复核的衍生结果。
1需求与数据形态
先说要解决的问题,再说数据结构;这两件事清楚了,语法就只剩下翻译工作。
临床编程里的合并与取数需求,最终都能落进四种形态。先认出自己属于哪一种,方法选择就随之确定:
一对一用 find(),只判有无用 check(),一键多条才有 find_next() 的用武之地。
| 形态 | 典型场景 | 键的性质 |
|---|---|---|
| 横向关联 一对一 / 多对一 |
把 ADSL 的 AGE、SEX、ARMCD 贴到 ADLB 的每一行 |
USUBJID 唯一 |
| 纵向检索 | 同一受试者内部跨访视取值:基线旗标 ABLFL、末次观测 LSTFL |
USUBJID + 时间 / 访视 |
| 多对多展开 | AE × CM 交叉:某项不良事件的持续区间是否被某类合并用药覆盖 | 一个键对应多条数据项 |
| 去重与重整 | 由长表压成一行一受试者;筛选失败人群列表;内存层面的 NODUPKEY 替代 |
去重键 + 需要留下来的那一条 |
1.1 术语
| 名称 | 在本篇中的角色 |
|---|---|
definekey 键变量 | 检索入口。键值相同的两条记录,在默认设置下只有第一条被收下。 |
definedata 数据变量 | 命中后被写回 PDV 的那几个变量;从未调用它时,键自身充当数据项。 |
| PDV | 程序数据向量。hash 对象正是通过改写 PDV 中的变量来“返回”结果,这一点是所有串值问题的根源。 |
rc 返回码 | 0 表示成功,非零表示失败(与 SAS 布尔约定相反)。每个方法调用都必须接。 |
hiter 迭代器 | 配合 ordered: 顺序遍历整张表;多命中场景下来自这一层。 |
下面三条同时成立时,hash 才真正比别的方式省心:① 主表行数远大于被查表,且被查表能整体放进内存;
② 合并不需要排序,主表的行顺序必须原样保留(BDS 结构对此最为敏感);
③ 查到的中间结果需要在同一个 DATA 步内继续被加工,而不是另起一步。
只满足 ① 的时候,PROC SQL 往往同样够用;三条都不成立,hash 只是把事情绕远了。
2核心语法骨架
生命周期只有五步,顺序不能变;每一步背后都有一个默认值,而这些默认值大多不是你想要的。
definedone(),而不在 declare [doc];
declare 与三个 define 调用必须位于 if _n_ = 1 块内 —— 对象实例及其所有条目跨 DATA 步迭代保留,
写在块外就是每行重建一次全表。2.1 骨架:一次装载,逐行查找
data adlb_enr(drop = rc hit);
/* (1) 借壳定型:把将被 hash 读入的变量属性先写死在本 DATA 步里 */
if 0 then set adsl(keep = usubjid age sex armcd trtsdt);
/* (2) 只实例化一次;DEFINEDONE 时才真正读取 adsl */
if _n_ = 1 then do;
declare hash h(dataset: 'adsl', hashexp: 8, duplicate: 'e');
h.definekey('usubjid');
h.definedata('age', 'sex', 'armcd', 'trtsdt');
h.definedone();
call missing(usubjid, age, sex, armcd, trtsdt); /* 避开 uninitialized 提示 */
end;
/* (3) 主表逐行驱动 */
set adlb_raw;
call missing(age, sex, armcd, trtsdt); /* 清掉上一行的残留 */
rc = h.find();
hit = (rc = 0); /* 0 表示成功,把它翻成布尔 */
if not hit then do;
call missing(age, sex, armcd, trtsdt); /* 未命中也必须显式处置 */
put 'ERROR: no ADSL row for ' usubjid=;
end;
run;
三个编号各自对应一类失败。(1) 不做,键与数据变量的长度和类型就由被装载数据集隐式决定,
一旦来源表结构变化,USUBJID 的长度跟着漂。(2) 不做,每轮迭代重建整张表,最贵的那部分被反复支付。
(3) 的 call missing 是全文性价比最高的一行代码 —— 第 5 节会解释它挡住的是哪一类缺陷。
2.2 方法速查
| 成员 | 作用与返回值 | 场合 |
|---|---|---|
definekey('v1','v2') | 定义键。带 dataset: 装载时可用 all:'yes' 把所有变量定为键。 | 装载前 |
definedata('v1','v2') | 定义命中后回写 PDV 的项。all:'yes' 表示整张表的变量都作为数据项。 | 装载前 |
definedone() | 收束定义;若带 dataset:,数据集正是在此刻被装载 [doc]。 | 装载前 |
find() | 查键;命中则把数据变量写回 PDV 并返回 0。find(key: v) 可直接指定键值。 | 要取回数据 |
check() | 只判存在性,不更新数据变量 [doc]。凡是不想弄脏 PDV 的场合都该用它。 | 只问在不在 |
find_next() / find_prev() | 多命中游标。须先 find() 再循环 [doc],取第二条、第三条…… | multidata |
add() / replace() | 运行期写入 / 覆写当前 PDV 里那一条。add() 在键已存在时会失败并返回非零码 [doc]。 | 运行期建表 |
ref() | 查得即调整、查不到即写入的合体;同时是 suminc: 计数的推进入口。 | 累计 / 计数 |
remove() / clear() / delete() | 删单条 / 清空全部条目但保留对象 / 销毁对象本身。 | 表维护 |
output(dataset:'out') | 把整张内存表写成数据集。键不会自动出现,除非它同时也是数据项,或压根没定义过数据项 [doc]。 | 落盘 |
num_items属性,非方法 | 条目数。装载之后、查找之前的一种廉价自检:h.num_items 是否等于预期行数。 | 自检 |
first() / last() / next() / prev() | 按序遍历整表,须配合 ordered: 使用。 | 有序遍历 |
sum() / sumdup() | 读出 suminc: 维护的键汇总值(一键一条 / 一键多条)。 | 内存聚合 |
2.3 DECLARE 参数标签
六个标签全部写在 declare(或 _new_)的括号里。它们决定装载阶段的行为,是“一次装载”里唯一还能拧的旋钮。
| 标签 | 取值与含义 | 默认 |
|---|---|---|
dataset: | 要装载的数据集名(单双引号皆可;含宏变量时必须双引号)。支持数据集选项:
dcl hash h(dataset: 'adsl(where=(fasfl="Y"))') —— keep=、rename=、where= 顺手一并办了 [doc]。 |
— |
duplicate: | 重复键如何处置。'replace' | 'r' 保留最后一条;'error' | 'e' 向日志报错。 |
保留首条 |
multidata: | 'yes' | 'y' 允许一键多条;'no' | 'n' 一键一条。 |
'no' |
hashexp: | 内部桶数的幂次:表大小 = 2n,最大值为 20。桶数不等于可存条目数(每桶可放任意多条),只影响查找效率 [doc]。 | 见文档 |
ordered: | 'ascending' | 'a' | 'yes' | 'y' 升序,'descending' | 'd' 降序,'no' | 'n' 不定义顺序。
影响 output() 的写出顺序与迭代器的遍历顺序。 |
'no' |
suminc: | 指定一个 DATA 步变量作为累加增量。add() / replace() 时该键的汇总值初始化为此变量值;
find() / check() / ref() 时把此变量值累加进去 [doc]。 |
— |
suminc: 的计数是由访问驱动的:find()、check()、ref() 都会把它推进一步 [doc]。
这意味着一次“我只是看看在不在”的 check(),同样会让键的汇总值 +1。做暴露次数、合并用药计数这类派生时,
选错方法就等于选错算式。
2.4 返回码约定
官方文档的统一约定是:返回码为零表示成功,非零表示失败;若不提供接收返回码的变量而调用失败,
日志会被写入相应的错误信息 [doc]。方向恰好与 SAS 布尔量相反,于是
rc = h.find(); hit = (rc = 0); 这一句“翻译”几乎值得无条件写:它同时完成了三件事 ——
接住返回码、把语义翻回来、给复核人员留下一行可读依据。
3临床项目应用示例
四段可直接改写的骨架。写法参考了上行文献里的技法,数据一律按 CDISC 通用结构虚构。
3.1 受试者筛选与分析集归属
场景:由 DM / DS 判定每位受试者是否进入 FAS,产出一行一受试者的旗标表。
这一步的本质是只问“在不在”,没有任何变量需要取回,因此用 check() 而非 find() ——
不动 PDV,也就没有串值的可能。
data pop_flag;
if 0 then set elig(keep = usubjid);
if _n_ = 1 then do;
/* duplicate:'e' —— 让重复键变成日志里的 ERROR,而不是静默丢人 */
declare hash h_fas(dataset: 'elig (where = (fasenrl = "Y"))',
duplicate: 'e');
h_fas.definekey('usubjid');
h_fas.definedone();
call missing(usubjid);
end;
set dm(keep = usubjid siteid age sex);
fasfl = ifc(h_fas.check() = 0, 'Y', 'N'); /* check 不回写任何数据变量 */
keep usubjid siteid age sex fasfl;
run;
这里 duplicate: 'e' 是防御的核心。资格判定记录天然是多行的(每个入组/排除原因一行),
一旦把原始表直接喂进 hash,默认行为会保留首条、静默丢掉其余 —— 人群人数与实际情况对不上,而日志一个字都不提。
'e' 把假设推到台面上:要么先在 SQL / SORT 步内把资格表压成一行一受试者,要么明确改用 'r'。
3.2 横向合并:人口学变量并入每条观测记录
场景:ADLB 每位受试者 × 参数 × 访视多行,需要贴上 AGE、SEX、ARMCD、TRTSDT。
传统写法要先把 ADLB 排序、合并、再排回来;hash 让主表行序完全不动。
data adlb(drop = rc hit);
if 0 then set adsl(keep = usubjid age sex armcd trtsdt);
if _n_ = 1 then do;
declare hash a(dataset: 'adsl', duplicate: 'e', hashexp: 8);
a.definekey('usubjid');
a.definedata('age', 'sex', 'armcd', 'trtsdt');
a.definedone();
call missing(usubjid, age, sex, armcd, trtsdt);
end;
set adlb_raw;
/* 每次 find 之前先把自己清零:这一行的性价比高于全篇任何一处注释 */
call missing(age, sex, armcd, trtsdt);
rc = a.find();
hit = (rc = 0);
if not hit then do;
call missing(age, sex, armcd, trtsdt);
put 'ERROR: ADSL lookup failed for ' usubjid=;
end;
run;
3.3 纵向检索:受试者内跨访视取值
场景:为每位受试者在治疗期开始后的首次可用观测打 ABLFL = 'Y'。做法是让键只含受试者,
把该受试者的全部记录装进同一个键之下,再用 find_next() 走完这条链 —— 全程不排序。
data base_flag(drop = rc quit);
if 0 then set adlb(keep = usubjid paramcd adt aval);
if _n_ = 1 then do;
/* 同一 USUBJID 下多条 —— 必须 multidata:'y' */
declare hash v(dataset: 'adlb', multidata: 'y', ordered: 'a');
v.definekey('usubjid');
v.definedata('paramcd', 'adt', 'aval');
v.definedone();
call missing(usubjid, paramcd, adt, aval);
end;
set subjects(keep = usubjid trtsdt); /* 一行一受试者的驱动表 */
best_adt = .; best_aval = .;
/* 首次 find 取第一条,find_next 沿游标走完本受试者全部记录 */
if v.find() = 0 then do;
do until(quit);
if paramcd = 'LABPARM' and not missing(aval)
and trtsdt le adt then do;
if missing(best_adt) or adt < best_adt then do;
best_adt = adt; best_aval = aval;
end;
end;
quit = (v.find_next() ne 0); /* 用游标自己的返回码退出 */
end;
end;
ablfl = ifc(not missing(best_adt), 'Y', '');
keep usubjid trtsdt best_adt best_aval ablfl;
run;
关键点有三:multidata 必须开,否则同一个 USUBJID 只有第一条进得了表;
find_next() 必须接在成功的 find() 之后(官方文档把它定义为与 FIND 配合使用的游标方法);
退出条件写在循环体内,由游标自身的返回码决定,而不是指望某种隐含终止。
3.4 SDTM/ADaM 衍生:多对多展开与 output() 落盘
场景:判断每条不良事件的持续区间是否被某类合并用药覆盖 —— 一个 AE 可能对应多条 CM,一条 CM 也可能覆盖多个 AE。
这正是 PhUSE 2012 论文 CS05 用 hash 重写的原始案例之一(作者称其实现不依赖任何宏变量,单个 DATA 步完成)。
这类代码的实用价值之外,还集中体现了 output() 最容易踩到的那个陷阱。
data ae_cm_pairs;
if 0 then set cm(keep = usubjid cmstdtc cmendtc cmdecod);
if _n_ = 1 then do;
/* 同一受试者多条 CM —— multidata:'y' */
declare hash cm(dataset: 'cm', multidata: 'y');
cm.definekey('usubjid');
cm.definedata('cmstdtc', 'cmendtc', 'cmdecod');
cm.definedone();
call missing(usubjid, cmstdtc, cmendtc, cmdecod);
end;
set ae(keep = usubjid aeseq aestdtc aeendtc aedecod);
call missing(cmstdtc, cmendtc, cmdecod); /* 先清零,避免上一行的 CM 残留被写出 */
rc = cm.find();
do while(rc = 0);
if cmstdtc le aestdtc and aeendtc le cmendtc then output;
rc = cm.find_next();
end;
drop rc;
run;
官方文档的表述是明确的:hash 对象的键不会被自动写入输出数据集。想把键留在结果里,办法只有两条 ——
把键也写进 definedata(),或者干脆不定义任何数据项(此时键被写入输出数据集)。
上面的例子走的是第一条:cmdecod 属于数据项所以必须显式列出;漏掉任何一个参与结果呈现的变量,
output() 出来的表看起来一切正常,只是少了那几列。这一条也是 WUSS 2022 那篇入门论文里
“不带 multidata 的 hash + output() 等价于 PROC SORT NODUPKEY”这则对照的前提。
4为什么是 hash,而不是 MERGE / SQL / FORMAT
把每种写法的边界一次性摊开;这里的结论由能力边界驱动,而不是由速度驱动。
| 写法 | 能力边界 | 代价 / 限制 | 判定 |
|---|---|---|---|
| DATA step MERGE | 受限 要求两侧按 BY 变量排序;多对多会静默错位 | 两次排序的 I/O;BY 值组合缺失时容易产出意料之外的行对齐 | 够用但笨重 |
| PROC SQL 连接 | 能做 多对多可以笛卡尔积形式表达,不需要排序 | 结果不保证行序;连接与逐行派生无法在同一个动程内交替进行;中间态难以调试 | 部分场景 |
| 自定义 FORMAT CNTLIN /CNTLOUT |
能做 静态字典映射的利器:编码 → 首选术语、ATC 分类 | 装载后不可变更;多返回值需要多个变量或多个格式;不适合内容需要随时改写的任务 | 静态字典首选 |
| SET with POINT= / KEY= | 受限 随机访问单条记录,无需整表装载 | 需要行号或已建索引;代价按访问次数累计,一万次查找就是一万次随机读 | 超大表慎用 |
| Hash 对象 | 能做 无需排序、保留行序、支持多对多、运行期可维护表内容 | 整张被查表的内存占用;DATA 步一结束内容就消失,必须显式写出 | 本文推荐 |
真正让 hash 在这四类场景胜出的,并不是“更快”这一条,而是它可以边读边改那张内存表。
add()、replace()、ref()、suminc: 让累积计数、分组极值、动态去重
全部收缩进一个 DATA 步,中间结果既不必落盘,也不必在多个 PROC 之间维持状态。这一点 MERGE 与 SQL 都做不到;
FORMAT 则完全没有考虑。
5非理想输入下的行为:易错点矩阵
八条症状、各自的机制,以及那一句最小改动就能挡住它的防御代码。
本环境没有 SAS 会话,下列行为均未在本文中执行验证,而是逐条归因于官方文档或公开会议文献: [doc] 为官方文档既定行为,[lit] 为论文作者的陈述,[实践] 为工程判断而非语言规定。 在有 SAS 会话的验证环境中,建议先用三行数据实测确认每条行为在本机 SAS 版本上的实际表现,再据此固化写法。
5.1 场景矩阵
| Case | 情形 | 行为 | 判定 |
|---|---|---|---|
| P1 | 被装载的数据集里存在重复键 | 默认保留第一条,其后同键全部被忽略;'r' 保留最后一条;'e' 向日志报错 [doc] |
静默丢数据 |
| P2 | 某一次 find() 未命中 |
目标数据项不会被自动置为缺失,而是停留在最近一次成功调用的值上 [lit] | 跨行串值 |
| P3 | 键的类型或长度不一致(字符 vs 数值、长度不足) | key: 后的值其类型必须与 definekey 定义的键变量一致 [doc];实务中表现为匹配不上,而不是报错 |
全表失配 |
| P4 | 同一个 DATA 步内多次查找写入同一批变量 | 与 P2 同源:先前的命中把值落下,之后的未命中就拿它当答案 [lit] | 串值 |
| P5 | multidata:'no' 时对同一个键第二次 add() |
ADD 失败并返回非零码 [doc] —— 不接 rc 就把失败咽了下去 |
被吞掉的失败 |
| P6 | declare 没包进 if _n_ = 1 |
对象与条目跨迭代保留,而每执行一次 declare 就重建一次实体集 [doc] [lit] | 逻辑与内存双重浪费 |
| P7 | hashexp: 设得偏小 |
不影响正确性;桶数不等于容量,只影响查找效率,文档注明上限为 20 [doc] | 仅性能 |
| P8 | DATA 步结束前没有 output() |
整张内存表的内容随之消失 [lit] | 一无所获 |
5.2 Findings
症状:分析集人数比实际少几人,日志干净,无人报错。机制:被装载表里同一个
USUBJID 出现多行(同一访视多次采样、重复录入、资格原因表未压缩),默认行为是保留首条、忽略其余。
防御:装载时一律写 duplicate: 'e',把“我以为它唯一”这个假设交给 SAS 去证伪;
需要取最新一条时才改用 'r',并在规格里写明为什么最新那条才是答案。
依据:Component Objects: Reference,DECLARE 语句条目。
症状:少数几行的 ARMCD、TRTSDT 显示为前面某行的取值,QC 平台上表现为“个别人数据错”。
机制:find() 失败时,目标数据变量不会被自动置空,而是保持最近一次成功调用的值 ——
原文的说法是 “it is the value of your last successful call”。防御:两条路任选其一,第 3.2 节的代码里都写着:
查找前先 call missing(...),或在 rc ne 0 分支里显式赋值。绝不能指望 SAS 替你清零。
依据:Axelrod(2019,SGF Paper 3125-2019)第 6 步的原文陈述;本文未执行 SAS,此项未按本 note 实测。
症状:打旗标的派生里,顺带覆盖了主表本应保留的字段值。机制:FIND 与 CHECK 的分工是明确的 ——
CHECK 只返回是否存在,不更新数据变量 [doc];用错名字就把一次查询变成了一次赋值。
防御:凡不需要取回变量的场合(人群旗标、派生内部存在性判断),一律 check()。
附带提醒:在带 suminc: 的表上,check() 同样会推进计数。
症状:一行都没合上,或者只有前几位受试者合上了。机制:键的类型由
definekey 调用那一刻 PDV 中该变量的类型决定,而 key: 后的键值类型必须与之匹配 [doc]。
临床数据里最常见的两例:USUBJID 在源数据里是数值 101001、在 SDTM 里是字符 'NNNN-101-001';
以及没有写出足够的 length,导致长 ID 被截断后再去匹配一个未被截断的键。
防御:用 if 0 then set 在 DATA 步里把键的长度与类型统一固定下来;跨类型匹配时先用
put(..., , best.) / input(..., , best.) 显式转换,不要依赖隐式转换。[实践]
症状:两个方向都会中招。① declare 写在 if _n_ = 1 之外 ——
对象被反复实例化,装载代价每行重复支付一次,运行期 add() 出来的条目也随之错乱。
② 反过来:想按 BY 组分组处理(每位受试者各自的临时子表),却漏了 clear(),
于是后一位受试者的表里混进了前一位的条目。防御:声明一律放进 if _n_ = 1;
需要按组重建时,在 first.usubjid 处显式 clear(),并在 last.usubjid 之前把结果取出。
[实践]
6复用检查清单
把任何一段代码复制进另一个程序之前,逐条核对。
- 先证伪唯一性,再谈正确性。装载时写
duplicate: 'e';只有明确知道想要最新那条时,才在规格里写明理由并改用'r'。 - 每次
find()之前call missing(),或者在未命中分支里显式赋值。两者选一,但必须有一个。 - 不需要取回变量时就用
check()。同时记住:在suminc:表上它同样会计数。 - 每个方法调用都接
rc并翻译成布尔。rc = h.find(); hit = (rc = 0);一行同时提供了返回码、语义和可读依据。 declare与三个 define 一起放进if _n_ = 1;按 BY 组重建时另行在first.处clear()。- 用
if 0 then set固定键与数据变量的长度和类型。别让被装载表的结构变化悄悄改掉USUBJID的长度。 - 输出前确认
definedata()已含全部需要的键。output()不会替你把键带上(除非从未定义数据项)。 multidata:'y'时先find()再循环find_next(),退出条件写在循环体内部。- 按条目数估算
hashexp:(表大小 = 2n,上限 20);它只影响效率、不影响对错。先把内存预算算清楚,超大表不要硬塞。 - 清理 PDV:
rc、hit等临时变量用drop=剔掉,别让它们出现在交付数据集里。 - 日志标准:零 ERROR、零 WARNING,且 uninitialized 提示已用
call missing消除。在受监管环境中,解释不了的 note 等于潜在的未知项。 - 独立双重编程仍然不可省。P1 与 P2 两类缺陷都不报错,只能靠独立实现比对发现。
7参考来源
正文中的 [doc] 与 [lit] 指向下列条目。
| 来源 | 用于支撑本文的哪些断言 |
|---|---|
| SAS Institute,SAS 9.4 / SAS Viya Component Objects: Reference(DECLARE 语句、DEFINEDONE、DEFINEDATA、OUTPUT 方法等条目) | 六项 declare 参数标签的语义与默认值;重复键“保留首条”的默认行为与 'r' / 'e' 的取舍;
数据集在 definedone() 时被装载;hashexp: 表大小 = 2n、上限 20、桶数不等于条目数;
output() 不会自动写入键;未显式赋值时的 uninitialized 提示与 call missing 处置;suminc: 计数由访问驱动。
https://support.sas.com/documentation/cdl/en/lecompobjref/69740/PDF/default/lecompobjref.pdf |
| SAS Help Center,FIND Method(Base SAS 参考) | FIND 与 CHECK 的差异(“数据变量不被更新”);key: 值的类型须与 definekey 定义的键变量一致;
存在多个数据项时应配合 FIND_NEXT / FIND_PREV 使用。
https://support.sas.com/documentation/cdl/en/lrdict/61724/HTML/default/a002588683.htm |
| Axelrod, E.(2019),HASH Beyond Lookups – Take Another Look,SAS Global Forum 2019,Paper 3125-2019,Abt Associates Inc. | Finding 2 的核心依据 —— 原文指出 find() 不成功时目标数据项不会被自动置为缺失,而是保持最近一次成功调用的值,
并给出 call missing() 与未命中分支赋值两条防御写法;另支援两条生命周期陈述:“hash 表中的条目不会在每次 DATA 步迭代时被重新初始化”
与“DATA 步结束时表内容即消失”。 |
| Secosky, J. & Bloom, J.(2007),Getting Started with the DATA Step Hash Object,SAS Global Forum 2007,Paper 271-2007 | DATA 步 hash 对象的基础用法与入门惯例。 |
| Burlew, M. M.(2012),SAS Hash Object Programming Made Easy,SAS Institute Inc. | §2.3 参数标签表的交叉核对来源(dataset:、duplicate:、hashexp:、multidata:、ordered:、suminc:)。 |
| Garbutt, D. J.(2012),Re-Programming a Many-To-Many Merge with Hash Objects,PhUSE 2012(Budapest),Paper CS05 | §3.4 多对多合并示例的行业原型(CM 回并进 AE 的一对多场景),以及作者所述“不依赖任何宏变量、单个 DATA 步完成”的实现主张。 |
| Lafler, K. P.(2011),An Introduction to SAS® Hash Programming Techniques,PharmaSUG 2011,Paper TT15 | PharmaSUG 论坛层面 hash 编程技术的综述性入口。另见同届 King, J. H.(2011), Using a HASH Table to Reference Variables in an Array by Name,PharmaSUG 2011,Paper TT04。 |
| Western Users of SAS Software(2022),Getting Started with DATA Step Hash Objects,Paper WUSS-2022-108 | §3.4 中“不带 multidata 的 hash 加 output() 其结果等价于 PROC SORT NODUPKEY”这则对照。 |