【测试】SAS 编程技巧 - PROC SQL(二)

发布时间 2023-08-03 13:47:01作者: Snoopy1866

上一节中,我们介绍了如何使用 SQL 创建、删除数据集、修改数据集结构,以及如何新增、删除和更新数据集的观测,所涉及到的内容都是对数据集的增、删、改的操作,从本节开始,我们将对 SQL 中最常见,也最灵活的查询操作进行详细的介绍。

查询语句

SQL 的查询操作是通过 SELECT 语句实现的。SELECT 语句包含很多子句,本节介绍的是最简单的查询语句。

查询一个或多个变量

proc sql;
    select USUBJID, SITEID, SEX, AGE
        from DM;
quit;

上述代码使用 SELECT 语句从数据集 DM 中查询了 4 个变量,FROM 子句指定查询的源,即从哪个数据集或视图中进行查询。

查询所有变量

proc sql;
    select * from DM;
quit;

上述代码使用 SELECT 语句从数据集 DM 中查询了所有变量,其中 * 代表 FROM 子句指定的数据集 DM 中的所有变量,这在数据集变量名未知的情况下非常有用。

? 不可滥用 * ,当查询的数据源包含大量变量和观测时,使用 * 会造成性能的严重下降。通常来说,建议使用最小化原则,需要使用什么变量就查询什么变量,无需使用的变量不应当出现在 SELECT 语句中。

查询不重复的观测

默认情况下,SELECT 语句会查询并返回所有匹配的观测,某些变量在不同的观测上可能存在相同的值,如果我们只需要变量的值不重复的观测,可以使用 DISTINCTUNIQUE 关键字去除重复的观测,DISTINCT 与 UNIQUE 效果相同。

proc sql;
    select distinct USUBJID, SITEID, SITENAME, ARM from AE;
quit;

上述代码使用 SELECT 语句从数据集 AE 中查询所有发生了不良事件的受试者信息,由于同一受试者可能发生多次不良事件,因此使用 DISTINCT 关键字去除重复的观测,每位受试者仅保留一条观测。

? 一条 SELECT 语句中只能有一个 DISTINCT 关键字,DISTINCT 关键字必须出现在所有变量名的前面。虽然 DISTINCT 在 SELECT 语句中看起来只对紧随其后的第一个变量生效,但事实上,它作用于 SELECT 语句中出现的所有变量(包括未命名的变量)。这意味着,只有当 SELECT 语句中的变量组合存在重复的观测时,查询结果才会去除这条观测,而并非简单去除第一个变量存在重复的观测。

? 由于数值精度的问题,SELECT DISTINCT 查询的结果可能会出现变量的显示值完全一致的观测。

使用查询结果创建新的数据集

在第一节中,我们提到创建数据集的两种方法:

  1. 通过直接定义变量属性的方式创建数据集
  2. 使用 LIKE 关键字基于其他数据集的结构创建数据集

其实,也可以根据 SELECT 语句查询结果来创建数据集。当需要对 SELECT 语句的查询结果作进一步处理时,就需要将查询结果存储为新的数据集。

proc sql;
    create table AE_UID as
        select distinct USUBJID, SITEID, SITENAME, ARM from AE;
quit;

上述代码使用 SELECT 语句从数据集 AE 中查询所有发生了不良事件的受试者信息,并存储在新的数据集 AE_UID 中。

限定数据集名称

前面几个例子都是仅指定查询的变量名,而没有指定变量所在的数据集。在未指定数据集名称的情况下,FROM 子句的返回结果将作为 SELECT 语句查询的数据源。在某些情况下,不指定数据集名称可能会造成歧义,此时必须在 SELECT 语句中限定变量名所在的数据集名称。

proc sql;
    select
        DM.USUBJID,
        DM.SITEID,
        DM.SEX,
        DM.AGE
    from DM;
quit;

? 上述代码只是对限定数据集名称的一种展示,至于什么情况下会造成变量名的歧义,我们将在以后的章节中进行介绍。

上一节,我们介绍了使用 SELECT 语句对变量进行查询,这一节我们继续介绍 SELECT 的简单查询操作。

常量

常量包括数值常量和字符串常量,有时候也被称为字面量(literal)。

proc sql noprint;
    create table ADSL as
        select
            "TEST-CLINICAL-TRIAL-2023-0012" as STUDYID,
            USUBJID,
            SITEID,
            SEX,
            AGE
        from DM;
quit;

上述代码中,在查询语句中使用了常量 "TEST-CLINICAL-TRIAL-2023-0012",查询结果存储在新创建的数据集 ADSL 中。ADSL 包括数据集 DM 中的 4 个变量,以及使用常量定义的一个变量 STUDYID。

? 使用关键字 AS 可以为未命名的变量指定一个变量名(又称别名),便于后续引用。若查询结果仅仅是为了展示,可以不定义别名;若查询结果将存储在数据集中,则应当指定一个别名,否则 SAS 将自动指定一个变量名(根据查询类型,可以是 _TEMAxxx 或 _TEMGxxx)。基于代码可读性的考虑,依赖 SAS 的自动命名机制是不可取的,在创建数据集的任何时候都应当显式指定变量名。

CASE 表达式

CASE 表达式用于按照条件分支进行查询。

proc sql;
    select
        USUBJID,
        SITEID,
        SEX,
        (case SEX
            when "F" then "男"
            when "M" then "女"
        end) as SEXC
    from DM;
quit;

上述代码使用 CASE 表达式根据变量 SEX 的值衍生新的变量 SEXC,对于某一条观测,其变量 SEX 的值都会与指定的 WHEN 条件进行比较,直到符合某一条 WHEN 条件,此时将 THEN 后面的结果作为查询结果赋值给变量 SEXC。

CASE 表达式还有另一种写法,可以不在 CASE 后面指定变量名,但是在 WHEN 后面指定具体的条件,上述代码可以改写为:

proc sql;
    select
        USUBJID,
        SITEID,
        SEX
        (case
            when SEX = "F" then "男"
            when SEX = "M" then "女"
        end) as SEXC
    from DM;
quit;

? 这两种写法的区别是:第一种适用于要执行的比较仅涉及单个变量的情况,第二种适用于要执行的比较涉及多个变量的情况。

例如,下面的例子就只能使用 CASE 表达式的第二种写法:

proc sql;
    select
        USUBJID,
        SITEID,
        SEX
        (case
            when CMSTDT <  TRTDT then "既往用药"
            when CMSTDT >= TRTDT then "合并用药"
        end) as CMSCAT
    from DM;
quit;

通常,CASE 表达式还可以使用 ELSE 关键字指定一条 “兜底” 的查询结果,当所有 WHEN 条件都不匹配时,将返回 ELSE 表达式结果。

proc sql;
    select
        USUBJID,
        SITEID,
        (case when HEIGHTU = "m" then HEIGHT/WEIGHT**2
              when HEIGHTU = "cm" then HEIGHT/100/WEIGHT**2
              else -1
        end) as BMI
    from DM;
quit;

此外,CASE 表达式也可以嵌套使用,从而实现复杂条件的判断,这类似于 DATA 步中 IF ELSE 语句的嵌套,例如:

proc sql;
    select
        USUBJID,
        SITEID,
        (case when HEIGHTU = "m" then
            (case when WEIGHTU = "kg" then HEIGHT/WEIGHT**2
                  when WEIGHTU = "pound" then HEIGHT/(WEIGHT*0.4536)**2
                  else -1
            end)
              when HEIGHTU = "cm" then
            (case when WEIGHTU = "kg" then HEIGHT/100/WEIGHT**2
                  when WEIGHTU = "pound" then HEIGHT/100/(WEIGHT*0.4536)**2
                  else -1
            end)
              else -1
        end) as BMI
    from DM;
quit;

函数

SELECT 语句中可以使用任何支持的 SAS 函数,PROC SQL 支持大部分 SAS 内置函数以及任何非数组参数的 PROC FCMP 自定义函数。例如:下面的代码在 SELECT 语句中使用了 INTCK 函数计算用药的持续天数。

proc sql;
    select
        USUBJID,
        SITEID,
        CMSTDT,
        CMENDT,
        intck("day", CMSTDT, CMENDT) as CMDURDT
    from CM;
quit;

SELECT 语句支持使用自定义函数。例如:下面的代码在 SELECT 语句中使用了自定义函数 IMPUTE_CMSTDT 用于填补变量 CMSTDT 的缺失日期,受限于篇幅,这里没有给出函数 IMPUTE_CMSTDT 的定义。

proc sql;
    select
        USUBJID,
        SITEID,
        impute_cmstdt(CMSTDTC, CMENDTC, TRTSDTC) as CMSTDT format = yymmdd10.,
        CMENDT
    from CM;
quit;

这里再额外介绍一个 SAS 9.4 尚未公开于文档中的函数:MONOTONIC()。这个函数的作用是自增计数,类似于 DATA 步中的自动变量 _n_,这在附带观测序号输出的情况下是非常有用的。

proc sql;
    create table class as
        select
            MONOTONIC() as seq label = "序号",
            name,
            sex,
            age,
            height,
            weight
        from sashelp.class;
quit;

输出结果:

img

CALCULATED 关键字

CALCULATED 关键字用于引用当前 SELECT 子句中已经计算好的变量,使用 CALCULATED 可以减少不必要的计算过程,降低代码的冗余。

proc sql;
    select
        MONOTONIC() as seq label = "序号",
        name,
        height * 0.0254 as height label = "身高(m)" format = 8.2,
        weight * 0.4536 as weight label = "体重(kg)" format = 8.1,
        (CALCULATED weight / CALCULATED height**2) as BMI1 label = "BMI1(kg/m^2)" format = 8.2,
        (weight / height**2) as BMI2 label = "BMI2(kg/m^2)" format = 8.2
    from sashelp.class;
quit;

上述例子使用 CALCULATED 关键字引用了当前 SELECT 语句中经过单位转换的变量 heightweight,直接使用转换后的结果计算 BMI。若不使用 CALCULATED 关键字,则引用的仍然是未经过单位转换的变量。如下图所示,BMI1 是引用了经过单位转换的变量后的正确计算结果,BMI2 是引用了未经过单位转换的原始变量后的错误计算结果。

img