4. SQL复杂查询基础回顾
临床数据查询的复杂度,往往不来自 SQL 语法本身,而来自数据被拆散在多个业务表中,且同一患者在不同时间点会产生多条记录。要回答“某患者最近一次糖化血红蛋白是多少”“哪些受试者在筛选期使用了禁用药物”“每个中心入组人数与筛选失败率”这类问题,单表查询几乎无能为力。本节把四类复杂查询能力放回临床数据语境中重新梳理:多表连接解决“横向拼装”,子查询解决“分步筛选与聚合”,集合运算解决“纵向合并与差异比较”,窗口函数解决“组内排序与跨行计算”。
一、多表连接:把患者、访视、检验、用药拼成一条完整证据链
临床数据库通常遵循 CDISC SDTM 或类似结构:DM 存人口学与入组信息,SV 存访视,LB 存实验室检验,EX 存暴露用药,AE 存不良事件。它们通过 USUBJID(唯一受试者标识)和 VISITNUM(访视序号)关联。连接的核心不是“把表连起来”,而是先明确连接键的粒度:DM 一行一个受试者,LB 一行一个受试者一次访视一个检验项目,EX 一行一个受试者一段用药。粒度不一致时直接连接会产生笛卡尔式膨胀。
例如要列出每位受试者基线访视的糖化血红蛋白(HbA1c)结果,并带上年龄与性别:
SELECT dm.USUBJID, dm.AGE, dm.SEX, lb.LBTESTCD, lb.LBSTRESN, lb.LBDTC
FROM DM dm
LEFT JOIN LB lb
ON dm.USUBJID = lb.USUBJID
AND lb.LBTESTCD = 'HBA1C'
AND lb.VISITNUM = 1
WHERE dm.ARMCD IS NOT NULL;
这里用 LEFT JOIN 而非 INNER JOIN,是因为部分受试者可能缺失基线检验,若用内连接会静默丢人,导致后续统计分母错误。临床查询中“保留主表全部受试者”通常是默认要求。若还要带出该访视的用药情况,应先把 EX 按受试者和访视聚合,再与主查询连接,避免一对多放大。
二、子查询:先算清楚“谁符合条件”,再取明细
子查询适合表达“先筛选、后取数”的两步逻辑。临床场景中常见三类:标量子查询用于取单个值,如每位受试者的基线值;IN/EXISTS 子查询用于筛选集合,如“发生过 3 级以上不良事件的受试者”;派生表(FROM 中的子查询)用于先聚合再连接。
例如找出筛选期使用过禁用药物(CMCAT='禁用药物')的受试者,并列出其入组信息:
SELECT USUBJID, ARM, RFSTDTC
FROM DM
WHERE USUBJID IN (
SELECT DISTINCT USUBJID
FROM CM
WHERE CMCAT = '禁用药物'
AND CMSTDTC < RFSTDTC
);
EXISTS 与 IN 在语义上接近,但当子查询表很大且只需判断“是否存在”时,EXISTS 往往更高效,且能自然处理 NULL。需要提醒的是:子查询返回多列或多行时,标量位置会报错;NOT IN 遇到 NULL 会整体失效,临床数据中缺失值普遍,应优先用 NOT EXISTS。
三、集合运算:纵向合并与差异比较
UNION ALL 用于把结构相同的查询结果纵向堆叠,例如把“筛选期不良事件”和“治疗期不良事件”合并成一张事件总表,并保留来源标记。UNION 会去重,代价是排序开销,临床明细查询中通常不需要去重,应优先 UNION ALL。
EXCEPT(或 MINUS)用于差异比较,典型用途是“应做未做”核查:先查出所有计划访视,再减去实际发生的访视,得到缺失访视清单。INTERSECT 则用于找交集,如“既完成基线检验又完成基线心电的受试者”。集合运算要求两侧列数、顺序和数据类型一致,临床中常因日期与字符类型不匹配而报错,需要显式转换。
四、窗口函数:组内排序与跨行计算的主力
窗口函数不折叠行,而是在保留明细的同时增加计算列,非常适合临床数据的“每患者第 N 次”“距基线变化”“组内排名”等需求。常用形式为 函数() OVER (PARTITION BY ... ORDER BY ...)。
例如取每位受试者最近一次 HbA1c:
SELECT USUBJID, LBDTC, LBSTRESN
FROM (
SELECT USUBJID, LBDTC, LBSTRESN,
ROW_NUMBER() OVER (
PARTITION BY USUBJID
ORDER BY LBDTC DESC
) AS rn
FROM LB
WHERE LBTESTCD = 'HBA1C'
) t
WHERE rn = 1;
ROW_NUMBER 给组内每行唯一序号,RANK 遇并列会跳号,DENSE_RANK 不跳号。临床中若同一访视有重复检验,需先明确保留规则(如取最新、取均值),否则 rn=1 的结果不稳定。LAG/LEAD 可取上一次/下一次访视值,用于计算相对基线的变化量;SUM() OVER 可做累计入组人数;AVG() OVER (PARTITION BY SITEID) 可算各中心均值而不丢失受试者明细。
五、四类能力如何协同
真实临床查询往往是组合拳:用 CTE 先算基线值,用窗口函数标记访视序号,用连接拼上人口学与用药,用 EXISTS 排除禁用药物,最后用 UNION ALL 合并不同来源事件。理解每类能力的适用边界,比记住语法更重要:连接管“横向关联”,子查询管“分步条件”,集合运算管“纵向合并与差异”,窗口函数管“组内跨行”。
把上述四类能力放进一个可复用的查询骨架中,通常先用 CTE 固定基线人群与基线值,再用窗口函数标记访视顺序,接着用连接补齐人口学与用药,最后用 EXISTS 做安全性排除、用 UNION ALL 合并事件来源。这样写出的 SQL 既保留了受试者粒度,又能逐层解释每个数字的来源,便于与统计师、数据管理员和医学审核人员对齐口径。