3. 队列定义要素的SQL表达
队列定义不是一句“把符合条件的患者查出来”,而是把临床问题翻译成一组可执行、可复现、可审计的SQL条件。一个完整的队列定义通常由五个要素构成:入组标准(谁可以进来)、排除标准(谁必须剔除)、索引日期(以哪一天作为时间原点)、基线期(索引日期之前用于刻画患者状态的观察区间)、随访窗口(索引日期之后用于观察结局的时间区间)。这五个要素在SQL中分别对应不同的条件类型和时间逻辑,混在一起写会导致口径不清、结果不可复现。
一、索引日期:整个队列的时间原点
索引日期(index date)是队列中每位患者的“第0天”,所有基线期和随访窗口都相对它计算。索引日期必须由数据中的真实事件确定,常见选择包括首次确诊日期、首次用药日期、首次住院日期或首次满足入组条件的日期。在SQL中,索引日期通常通过聚合函数取得:
-- 以首次确诊日期作为索引日期
SELECT patient_id,
MIN(diagnosis_date) AS index_date
FROM diagnosis
WHERE icd_code LIKE 'E11%' -- 2型糖尿病
GROUP BY patient_id;
这里的关键是MIN():它保证每位患者只取最早一次事件,避免同一患者因多次就诊产生多条索引记录。如果索引日期依赖多个条件的先后关系(例如“确诊后首次使用某药”),则需要先用窗口函数或自连接确定事件顺序,再取满足顺序的那一天。
二、入组标准与排除标准:集合的加与减
入组标准是“必须满足”的条件集合,在SQL中表现为多个WHERE或EXISTS条件的交集;排除标准是“一旦满足即剔除”的条件集合,表现为NOT EXISTS或反向条件。二者在逻辑上是“先取交集、再做差集”的关系。例如:
-- 入组:年龄≥18岁,且有2型糖尿病确诊
-- 排除:既往有1型糖尿病、妊娠糖尿病或终末期肾病
SELECT d.patient_id, MIN(d.diagnosis_date) AS index_date
FROM diagnosis d
JOIN patient p ON p.patient_id = d.patient_id
WHERE d.icd_code LIKE 'E11%'
AND p.birth_date <= DATE_SUB(d.diagnosis_date, INTERVAL 18 YEAR)
AND NOT EXISTS (
SELECT 1 FROM diagnosis x
WHERE x.patient_id = d.patient_id
AND x.icd_code IN ('E10%','O24%','N18%')
)
GROUP BY d.patient_id;
需要特别注意的是排除标准的时间范围。如果排除条件是“既往病史”,则必须在索引日期之前判断;如果排除条件是“随访期间发生的事件”,则属于结局或删失逻辑,不能混入基线排除。把排除条件的时间边界写错,是队列构建中最常见的口径错误之一。
三、基线期:索引日期之前的观察窗口
基线期用于提取患者进入队列前的特征,如合并症、既往用药、实验室指标。它的时间逻辑是“索引日期之前的一段固定长度区间”,通常写作[index_date - N天, index_date),左闭右开,避免把索引当天的信息算入基线。例如取索引前365天内的合并症:
-- 基线期:索引日期前365天内的合并症
SELECT c.patient_id, c.icd_code
FROM comorbidity c
JOIN cohort co ON co.patient_id = c.patient_id
WHERE c.diagnosis_date >= DATE_SUB(co.index_date, INTERVAL 365 DAY)
AND c.diagnosis_date < co.index_date;
基线期长度必须统一,否则不同患者的基线特征不可比。常见选择是180天、365天或“有记录以来的全部历史”,但后者会引入随访时长偏倚,需谨慎使用。
四、随访窗口:索引日期之后的观察区间
随访窗口定义从索引日期开始、到结局事件、死亡、失访或研究截止日期为止的时间区间。在SQL中,随访窗口通常不直接写成固定天数,而是通过“事件日期与索引日期之差”来计算随访时长,并用CASE WHEN判断结局是否发生:
-- 随访窗口:索引后最长730天,观察是否发生心梗
SELECT co.patient_id, co.index_date,
MIN(e.event_date) AS event_date,
CASE WHEN MIN(e.event_date) IS NOT NULL
THEN DATEDIFF(MIN(e.event_date), co.index_date)
ELSE DATEDIFF('2024-12-31', co.index_date)
END AS followup_days,
CASE WHEN MIN(e.event_date) IS NOT NULL THEN 1 ELSE 0 END AS event_flag
FROM cohort co
LEFT JOIN event e
ON e.patient_id = co.patient_id
AND e.event_date >= co.index_date
AND e.event_date < DATE_ADD(co.index_date, INTERVAL 730 DAY)
GROUP BY co.patient_id, co.index_date;
这里用LEFT JOIN保证没有事件的患者也保留在队列中,event_flag区分事件发生与否,followup_days同时承担了随访时长和删失时间的表达。若研究截止日期早于730天,则应以截止日期为准,避免把不存在的观察时间算作随访。
五、时间逻辑的常见陷阱
第一,边界开闭不一致。基线期若写成<= index_date,会把索引当天的诊断算入基线,造成“用未来信息预测过去”。第二,索引日期与入组条件脱节。若索引日期取首次确诊,而入组条件却用任意一次确诊,两者可能不是同一天,导致基线期错位。第三,随访窗口未处理竞争风险。死亡、失访、研究截止都会提前终止随访,若只用固定730天,会把已死亡患者的“空随访”算作无事件。第四,多次事件未去重。同一患者多次住院,若不加MIN()或ROW_NUMBER(),会产生重复队列记录。