返回

SQL查询:处理重叠数据和非重叠数据,优化enrollment时间段

mysql

SQL 查询:处理重叠和非重叠数据

在数据分析和处理过程中,常常遇到需要筛选出特定时间段内的数据,并考虑时间段重叠情况的场景。这其中一个挑战在于,如何有效识别和处理重叠的记录,并筛选出符合要求的非重叠或最优记录。针对enrollment记录的重叠情况,本文将探讨几种解决办法,并提供相应的SQL代码。

理解问题

一个典型的问题是:给定一系列包含开始和结束时间的数据记录(比如示例中的enrollments表),我们希望从中选取那些非重叠,或是部分非重叠,并满足特定条件的数据。这里关键在于如何有效地比较记录的时间段,并依据重叠情况筛选或排序记录。

问题可以进一步细化为几个需要处理的关键点:

  • 非重叠记录的选择: 仅保留那些开放时间(open_date)不与之前有效记录的开放时间-结束时间(open_date - closed_date)时间段重叠的记录。
  • 相同开始时间的选择: 如果两条记录有相同的开放时间,应该保留结束时间(closed_date)较晚的那条。
  • 开放记录的处理: 若开放的记录其开放时间与之前有效记录不重叠,则应保留这些记录。
  • 依赖于先前有效记录: 判断当前记录是否有效依赖于之前已被视为有效的记录。一个被判断为无效的记录不应影响之后记录的有效性判定。

解决方案一:使用窗口函数和自连接

此方法通过利用窗口函数和自连接来实现记录间的比较。核心思想是为每一条记录计算一个表示是否和先前有效记录重叠的标志位,再基于这个标志位筛选结果。

步骤:

  1. 使用自连接,获取每条记录之前的记录。
  2. 利用窗口函数 LAG() 获取前一条记录的closed_date
  3. 计算一个 overlap 标记,如果当前记录的 open_date 在前一记录的 open_dateclosed_date 之间,则 overlap = 1, 反之 overlap=0 。注意 closed_date 可能为NULL ,这部分需要额外处理。
  4. 对相同 open_date 的记录,依据 closed_date 选择保留较大的一条。
  5. 筛选所有 overlap = 0 的记录。

SQL 代码:

WITH RankedEnrollments AS (
    SELECT
        e.*,
        LAG(e.closed_date, 1, '1900-01-01'::date) OVER (PARTITION BY e.case_id ORDER BY e.open_date,e.closed_date ) as prev_closed_date,
         LAG(e.open_date, 1, '1900-01-01'::date) OVER (PARTITION BY e.case_id ORDER BY e.open_date,e.closed_date) as prev_open_date
    FROM
        enrollments e
),
OverlapCalculated as(
    SELECT
      *,
        CASE
          WHEN  open_date < prev_closed_date  THEN 1
            ELSE 0
          END AS is_overlap
     FROM RankedEnrollments
),
GroupedAndFiltered as(
SELECT
  *,
    ROW_NUMBER() OVER (PARTITION BY case_id,open_date ORDER BY closed_date DESC ) AS rn
    from  OverlapCalculated
)
, RecursiveEnrollments as(
SELECT
*, 0 as valid FROM GroupedAndFiltered where rn=1
UNION ALL
SELECT  
       e.* , 
       CASE WHEN  e.is_overlap= 1 and re.valid =1 THEN 1 ELSE 0  END   as valid    
       FROM GroupedAndFiltered e JOIN RecursiveEnrollments re ON  re.case_id= e.case_id AND re.enrollment_id != e.enrollment_id  AND e.open_date >= re.open_date   AND  (e.open_date>re.open_date and re.valid=0 OR re.valid=1)   AND ( re.open_date <=e.open_date ) ORDER BY  e.case_id , e.open_date
)

SELECT 
     case_id,enrollment_id,open_date,closed_date,status 
FROM RecursiveEnrollments 
where valid=0 or status='open'
ORDER BY case_id , open_date

解释:

  1. RankedEnrollments: 此CTE通过 LAG 获取每条记录的前一条 closed_date。使用'1900-01-01'作为初始值,以处理第一条记录,防止前一条 closed_date 为NULL导致逻辑异常。
  2. OverlapCalculated: 此 CTE 基于 RankedEnrollments 中获得的数据判断是否有重叠的情况。
  3. GroupedAndFiltered CTE 中相同 open_date 只会选取最近 closed_date 的记录。
  4. RecursiveEnrollments CTE 以递归方式处理数据,初始条件选取没有相同 open_date的记录;递归逻辑基于OverlapCalculated表里的is_overlap标记位以及上一个状态valid标记判断当前记录是否valid. 如果当前记录 is_overlap 和 前置有效记录的valid同时为1, 则此记录的有效值也为 1
  5. 最后,从 RecursiveEnrollments 表中筛选出有效的记录,即valid为 0 或者 status为open的记录。

解决方案二: 循环遍历的方式(PL/pgSQL function )

如果上述方法逻辑过于复杂难以理解,可以考虑使用循环遍历的方法实现:

  1. 创建一个临时的表存储最终结果。
  2. 排序所有记录根据 case_idopen_date
  3. 遍历每一个记录,若和已存的临时表里的记录不重叠,则插入此记录。 循环中使用 CASE WHEN closed_date IS NULL OR existing.closed_date IS NULL 进行判断;
  4. 如果open_date 相等则使用 closed_date 更晚的记录更新已存的记录。

SQL 代码 (PL/pgSQL function):

CREATE OR REPLACE FUNCTION process_enrollments()
RETURNS TABLE (
    case_id INT,
    enrollment_id VARCHAR(10),
    open_date DATE,
    closed_date DATE,
    status VARCHAR(10)
)
AS $
DECLARE
    rec RECORD;
    prev_rec RECORD;
    tmp_rec RECORD;
    last_open_date DATE;
    overlap_flag BOOLEAN := FALSE;
BEGIN
    -- 临时表
    CREATE TEMP TABLE temp_enrollments(
      case_id INT,
      enrollment_id VARCHAR(10),
      open_date DATE,
      closed_date DATE,
      status VARCHAR(10)
    );

    FOR rec IN
        SELECT * FROM enrollments ORDER BY case_id, open_date
    LOOP
    overlap_flag := FALSE;
    -- 获取临时表
       FOR tmp_rec IN
            SELECT * from temp_enrollments where case_id = rec.case_id
        LOOP

        IF rec.open_date < tmp_rec.closed_date  or tmp_rec.closed_date IS NULL THEN
                overlap_flag:= TRUE;
              END IF;
              -- 处理开始时间相同的情况
        END LOOP;
     
         IF NOT overlap_flag THEN
             --  不重叠记录处理
            INSERT INTO temp_enrollments(case_id, enrollment_id, open_date, closed_date, status)
            VALUES(rec.case_id,rec.enrollment_id,rec.open_date,rec.closed_date,rec.status);
        ELSE 
        FOR prev_rec IN
                SELECT * from temp_enrollments WHERE open_date = rec.open_date  and  case_id = rec.case_id
            LOOP
            -- 处理open_date相同closed_date不同情况,选取更大的 closed_date
                IF prev_rec.closed_date IS NULL  or (rec.closed_date IS NOT NULL AND rec.closed_date > prev_rec.closed_date)  THEN
                    UPDATE temp_enrollments SET closed_date = rec.closed_date , enrollment_id = rec.enrollment_id  , status= rec.status  WHERE enrollment_id = prev_rec.enrollment_id and case_id = rec.case_id;
                END IF;
            END LOOP;
        END IF;

        last_open_date:= rec.open_date;
        END LOOP;
    RETURN QUERY SELECT * FROM temp_enrollments;
    DROP TABLE temp_enrollments;
END;
$ LANGUAGE plpgsql;


-- 调用
SELECT * FROM process_enrollments();

解释:

此函数使用了 plpgsql 进行循环处理:

  1. 创建临时表存储结果
  2. 对数据按照 case_id 和 open_date 进行排序,遍历每一个数据
  3. 在内层循环中和临时表的数据对比判断是否重叠。如果 open_date 早于已存储记录的 closed_date,则标识为重叠记录。 如果已经存储,需要判断新的 closed_date 是否更大,取更大的记录。
  4. 处理结束,返回临时表记录

安全建议

在处理时间范围的SQL查询时,应该:

  • 正确处理NULL值 : 使用 COALESCECASE WHEN x IS NULL 处理时间字段中的空值。
  • 使用标准时间格式 : 统一数据的时间格式,避免因格式不一致而导致比较错误。
  • 仔细测试边界情况 : 特别是检查时间范围恰好相邻,或是起始和结束时间相同的情况。

这些解决方案提供了解决重叠和非重叠 SQL 查询的方法,选择适合的方法需要基于具体的数据量和性能需求。正确地处理这类查询是确保数据分析准确性的重要步骤。