病人手术后 6 月内咨询标记

问题描述

表 tb_healthy 记录病人的就诊历史,每个病人有多条记录,Visit_code 为 Surgery 表示手术,为 Office 表示咨询;有的病人只有手术,有的只有咨询。现在要判断两件事:这个病人有没有咨询过;这个病人有没有在手术之后 6 个月内咨询过。前者写入 Office_Visit,后者写入 SX_past_6mo,有记 1,没有记 0。

源数据

Id

Visit_Date

Visit_code

A30

5/15/2004

Surgery

A30

2/5/2005

Office

B01

12/7/2002

Office

B01

11/21/2002

Surgery

C01

12/1/2001

Office

C01

11/1/2001

Office

C01

5/15/2001

Surgery

C01

4/15/2001

Surgery

C02

12/1/2001

Surgery

C03

12/1/2001

Office

期望结果

Office_Visit 表示有咨询记录,SX_past_6mo 表示手术后 6 个月内有咨询记录:

Id

Office_Visit

SX_past_6mo

A30

1

0

B01

1

1

C01

1

1

C02

0

0

C03

1

0

A30 手术在 2004-05-15,咨询在 2005-02-05,相隔 8 个多月,超出了 6 个月,所以 Office_Visit 是 1 而 SX_past_6mo 是 0,可见 "有咨询" 和 "手术后 6 个月内有咨询" 是两件事。

C01 有两条手术记录和两条咨询记录,只有 2001-05-15 的手术与 2001-11-01 的咨询配成了 6 个月内的组合,所以 SX_past_6mo 记 1;C02 只有手术没有咨询、C03 只有咨询没有手术,两个标记都记 0。

SQLazy 分步实现

这道题要同时回答两个问题,适合把两件事分开算、最后再合起来。先把字符串日期转成真正的日期类型,再把手术记录和咨询记录各筛一份出来。"有没有咨询过" 用汇总功能按病人分组取最大值就能得到。"手术后 6 个月内有没有咨询过" 要靠拼接功能把咨询记录挂到手术记录上,挂的条件就是 "咨询日期不早于手术日期,且不晚于手术日期加 6 个月",这是一个跨表的区间条件,正好用得上拼接功能的非等值条件参数。配成对的记录汇总一下就有结果,没配上的病人在最后合并时补 0。

【点击在线运行本例】

下面逐一解释这些步骤。

Name

Anchor

Statement

t1

tb_healthy

compute date(Visit_Date; format "M/d/yyyy") as dt

t_sx

t1

filter Visit_code="Surgery"

t_off

t1

filter Visit_code="Office"

t_results

t1

summarize max if(Visit_code="Office" then 1 else 0) as Office_Visit; group Id

t_match

t_sx

join Id; with t_off; Id; take dt as off_dt; inner; condition ((dt>=dt@) and (dt<=(dt@ elapse 6 month)))

t_sx_yes

t_match

summarize max 1 as SX_past_6mo; group Id

t_merged

t_results

join Id; with t_sx_yes; Id; take SX_past_6mo

t_final

t_merged

compute nvl(SX_past_6mo,0) as SX_past


t_final

derive Id, Office_Visit, SX_past

第 1 步:把字符串日期转成日期类型

compute date(Visit_Date; format "M/d/yyyy") as dt

Visit_Date 是 "5/15/2004" 这样的字符串,不能直接比大小。用 date 函数按格式 "M/d/yyyy" 解析成日期类型,写入新列 dt。这一步是后面所有日期比较的前提。

Picture2png

第 2 步:把手术记录和咨询记录各筛一份

filter Visit_code="Surgery"

filter Visit_code="Office"

两份记录后面要分别用于不同的计算:手术记录当拼接的基准表,咨询记录当被挂上去的关联表。比较用的是符号 "=",不是 SQL 里的 "=="。

Picture3png
第 3 步:先回答 "有没有咨询过"
summarize max if(Visit_code=“Office” then 1 else 0) as Office_Visit; group Id
汇总功能按 Id 分组,组内对条件式 if(Visit_code=“Office” then 1 else 0) 取最大值:组里只要有一条咨询记录就会算出 1,一条都没有则全是 0。聚合算法 max 写在被聚合项之前,结果命名为 Office_Visit。

Picture4png
第 4 步:把 6 个月内的咨询挂到手术上
join Id; with t_off; Id; take dt as off_dt; inner; condition ((dt>=dt@) and (dt<=(dt@ elapse 6 month)))
这是本题的核心一步。拼接功能以手术表 t_sx 为基准表,按 Id 关联咨询表 t_off,把咨询日期 dt 挂上来并改名为 off_dt。
条件是跨表的区间判断,写在条件参数里:带 @ 的 dt@ 是基准表(手术)的日期,不带 @ 的 dt 是关联表(咨询)的日期,整个条件读作 "咨询日期不早于手术日期,且不晚于手术日期往后 6 个月"。往后推 6 个月用偏移函数写成 (dt@ elapse 6 month)。
注意:条件式里引用关联表字段要用它在本表里的原名 dt,不能用改名后的 off_dt;内连接会把配不上窗口的手术记录丢掉,剩下的就是配成对的记录。

Picture5png
第 5 步:把配成对的记录汇总成标记
summarize max 1 as SX_past_6mo; group Id
按 Id 分组,组内对常数 1 取最大值。配成过对的病人至少有一行记录,算出 1;一个对都没配成的病人不会出现在这张表里,这正是后面要补 0 的原因。这里聚合的是常数 1 而不是某个字段,因为需要的信息只有 "有没有配上",不需要具体数值。

Picture6png
第 6 步:把两个结果合并起来
join Id; with t_sx_yes; Id; take SX_past_6mo
以第 3 步的结果为主表,用左连接把第 5 步的标记挂上去。左连接保证没有配成对的病人也留在结果里,只是 SX_past_6mo 为空。这里不写连接方式,缺省就是左连接。

Picture7png
第 7 步:空值补 0
compute nvl(SX_past_6mo,0) as SX_past
用 nvl 把空值换成 0,命名成 SX_past,这样输出里的标记都是 0 或 1。最后一步用导出表挑出需要的三列:病人 Id、有没有咨询 Office_Visit、手术后 6 个月内有没有咨询 SX_past。

Picture8png

最后选取需要输出的字段。

编译生成 SQL

确认上述 7 步逻辑后,SQLazy 编译器自动生成原生 SQL(这里是 Oracle 语法):

WITH t1 AS (
    SELECT Id, Visit_Date, Visit_code
      , TO_DATE(Visit_Date, 'M/d/yyyy') AS dt
    FROM tb_healthy
  ),
  t_results AS (
    SELECT Id
      , MAX(CASE
        WHEN Visit_code = 'Office' THEN 1
        ELSE 0
      END) AS Office_Visit
    FROM t1
    GROUP BY Id
  ),
  t_off AS (
    SELECT Id, Visit_Date, Visit_code, dt
    FROM t1
    WHERE Visit_code = 'Office'
  ),
  t_sx_yes AS (
    SELECT Id, MAX(1) AS SX_past_6mo
    FROM (
      SELECT t_sx.Id, t_sx.Visit_Date, t_sx.Visit_code, t_sx.dt, t_off.dt AS off_dt
      FROM t1
        INNER JOIN t_off
        ON t_sx.Id = t_off.Id
          AND ((t_off.dt >= t_sx.dt)
            AND (t_off.dt <= ADDMONTHS(t_sx.dt, 6)))
      WHERE t_sx.Visit_code = 'Surgery'
    ) t_2
    GROUP BY Id
  ),
  t_merged AS (
    SELECT t_results.Id, t_results.Office_Visit, t_sx_yes.SX_past_6mo
    FROM t_results
      LEFT JOIN t_sx_yes ON t_results.Id = t_sx_yes.Id
  ),
  t_final AS (
    SELECT Id, Office_Visit, SX_past_6mo
      , COALESCE(SX_past_6mo, 0) AS SX_past
    FROM t_merged
  )
SELECT Id, Office_Visit, SX_past
FROM t_final
ORDER BY Id

SQLazy 让你用业务语言描述逻辑,而不是用 SQL 语法写嵌套查询。这道题要同时回答 "有没有咨询过" 和 "手术后 6 个月内有没有咨询过",SQLazy 把它们拆成两组各自独立的步骤,先分别算清楚,最后合并。拼接功能的非等值条件参数是关键:"咨询日期落在手术日期之后的 6 个月内" 本来是跨表的区间判断,SQLazy 用 @ 后缀区分两张表的字段,区间条件直接写在拼接语句里,不必先做笛卡尔积再过滤。汇总功能让 "有没有" 这类问题变得很短,组内取最大值就能得到 0 或 1,不必先筛记录再判断是否存在。每一步都是一张可以单独查看的中间结果表,出错时能立刻看出是哪一步的问题。

官方链接

SQLazy 在线体验:sqlazy.com(免费,无需注册)

SQLazy 项目仓库:github.com/SPLWare/SQLazy