7

考虑到以下情况:

CREATE TABLE Members (MemberID INT)
INSERT Members VALUES (1001)

CREATE TABLE PCPs (PCPID INT)
INSERT PCPs VALUES (231)
INSERT PCPs VALUES (327)
INSERT PCPs VALUES (390)

CREATE TABLE Plans (PlanID INT)
INSERT Plans VALUES (555)
INSERT Plans VALUES (762)

CREATE TABLE MemberPCP (
    MemberID INT
    , PCP INT
    , StartDate DATETIME
    , EndDate DATETIME)
INSERT MemberPCP VALUES (1001, 231, '2002-01-01', '2002-06-30')
INSERT MemberPCP VALUES (1001, 327, '2002-07-01', '2003-05-31')
INSERT MemberPCP VALUES (1001, 390, '2003-06-01', '2003-12-31')

CREATE TABLE MemberPlans (
    MemberID INT
    , PlanID INT
    , StartDate DATETIME
    , EndDate DATETIME)
INSERT MemberPlans VALUES (1001, 555, '2002-01-01', '2003-03-31')
INSERT MemberPlans VALUES (1001, 762, '2003-04-01', '2003-12-31')

我正在寻找一种简洁的方法来构建成员/PCP/计划关系的时间表,其中 PCP 或成员计划的更改将导致结果中出现单独的开始/结束行。例如,如果在几年内,一名成员更改了他们的 PCP 两次和一次计划,但每次都在不同的日期,我会看到如下内容:

MemberID  PCP  PlanID  StartDate    EndDate
1001      231  555     2002-01-01   2002-06-30
1001      327  555     2002-07-01   2003-03-31
1001      327  762     2003-04-01   2003-05-31
1001      390  762     2003-06-01   2003-12-31

如您所见,对于涉及成员/PCP/计划关联差异的每个日期期间,我需要一个单独的结果行。我有一个解决方案,但它与 WHERE 子句中的许多 CASE 语句和条件逻辑非常复杂。我只是在想有一种更简单的方法可以做到这一点。

谢谢。

4

4 回答 4

2

与 T-SQL 兼容。我同意格伦的一般方法。

另一个建议:如果您允许在您的业务期间之间进行跳跃,则此代码将需要进一步调整。否则,我认为从下一条记录的 StartDate 推迟 EndDate 值会更好地从您的代码中获得更多受控行为。在这种情况下,您希望在数据到达此查询之前确保规则。

编辑:刚刚从 Andriy M 的帖子中了解了 With 语句和 SQL Fiddle。你也可以在 SQL Fiddle 看到我的答案

编辑:修复了 Andriy 指出的错误。

WITH StartDates AS (
SELECT MemberId, StartDate FROM MemberPCP UNION
SELECT MemberId, StartDate FROM MemberPlans UNION
SELECT MemberId, EndDate + 1 FROM MemberPCP UNION
SELECT MemberId, EndDate + 1 FROM MemberPlans
),
EndDates AS (
SELECT MemberId, EndDate = StartDate - 1 FROM MemberPCP UNION
SELECT MemberId, StartDate - 1 FROM MemberPlans UNION
SELECT MemberId, EndDate FROM MemberPCP UNION
SELECT MemberId, EndDate FROM MemberPlans
),
Periods AS (
SELECT s.MemberId, s.StartDate, EndDate = min(e.EndDate)
  FROM StartDates s
       INNER JOIN EndDates e
           ON s.StartDate <= e.EndDate
          AND s.MemberId = e.MemberId
 GROUP BY s.MemberId, s.StartDate
)
SELECT MemberId = p.MemberId,
       pcp.PCP, pl.PlanId,
       p.StartDate, p.EndDate
  FROM Periods p
       LEFT JOIN MemberPCP pcp
           -- because of the way we divided period,
           -- there will be one and only one record that fits this join clause
           ON p.StartDate >= pcp.StartDate
          AND p.EndDate <= pcp.EndDate
          AND p.MemberId = pcp.MemberId
       LEFT JOIN MemberPlans pl
           ON p.StartDate >= pl.StartDate
          AND p.EndDate <= pl.EndDate
          AND p.MemberId = pl.MemberId
 ORDER BY p.MemberId, p.StartDate
于 2012-07-02T22:22:15.743 回答
1

我的方法是将每个成员的唯一开始日期组合作为起点,然后从那里构建查询的其他部分:

--
-- Traverse down a list of 
-- unique Member ID and StartDates
-- 
-- For each row find the most 
-- recent PCP for that member 
-- which started on or before
-- the start date of the current
-- row in the traversal
--
-- For each row find the most 
-- recent PlanID for that member
-- which started on or before
-- the start date of the current
-- row in the traversal
-- 
-- For each row find the earliest
-- end date for that member
-- (from a collection of unique
-- member end dates) that happened
-- after the start date of the
-- current row in the traversal
-- 
SELECT MemberID,
  (SELECT TOP 1 PCP 
   FROM MemberPCP 
   WHERE MemberID = s.MemberID 
   AND StartDate <= s.StartDate 
   ORDER BY StartDate DESC
  ) AS PCP,
  (SELECT TOP 1 PlanID 
   FROM MemberPlans 
   WHERE MemberID = s.MemberID 
   AND StartDate <= s.StartDate 
   ORDER BY StartDate DESC
  ) AS PlanID,
  StartDate,  
  (SELECT TOP 1 EndDate 
   FROM (
    SELECT MemberID, EndDate 
    FROM MemberPlans 
    UNION 
    SELECT MemberID, EndDate 
    FROM MemberPCP) e
   WHERE EndDate >= s.StartDate 
   ORDER BY EndDate
  ) AS EndDate
FROM ( 
  SELECT
    MemberID,
    StartDate
  FROM MemberPlans
  UNION 
  SELECT
    MemberID,
    Startdate
  FROM MemberPCP
) s
ORDER BY StartDate
于 2012-06-14T21:58:40.460 回答
1

作为可能不是最有效但至少简单直接的解决方案,我将执行以下操作:

  • 1)扩大范围;

  • 2)加入扩展范围;

  • 3) 对结果进行分组。

当然,这假设只使用日期(即时间部分00:00适用于每个表StartDateEndDate两个表)。

为了扩大日期范围,我更喜欢使用数字表,如下所示:

SELECT
  m.MemberID,
  m.PCP,
  Date = DATEADD(DAY, n.Number, m.StartDate)
FROM MemberPCP m
  INNER JOIN Numbers n
    ON n.Number BETWEEN 0 AND DATEDIFF(DAY, m.StartDate, m.EndDate)

同样对于MemberPlans.

要生成一个组合行集,我会使用FULL JOIN,但如果您事先知道两个表涵盖完全相同的时间段,INNER JOIN我也会这样做:

SELECT *
FROM MemberPCPExpanded pcp
  FULL JOIN MemberPlansExpanded plans
    ON pcp.MemberID = plans.MemberID AND pcp.Date = plans.Date

现在您只需要对结果行进行分组,并为每个组合找到最小和最大日期(MemberID, PCP, PlanID)

SELECT
  MemberID  = ISNULL(pcp.MemberID, plans.MemberID),,
  pcp.PCP,
  plans.PlanID,
  StartDate = MIN(ISNULL(pcp.Date, plans.Date)),
  EndDate   = MAX(ISNULL(pcp.Date, plans.Date))
FROM MemberPCPExpanded pcp
  FULL JOIN MemberPlansExpanded plans
    ON pcp.MemberID = plans.MemberID AND pcp.Date = plans.Date
GROUP BY
  ISNULL(pcp.MemberID, plans.MemberID),
  pcp.PCP,
  plans.PlanID

请注意,如果您使用INNER JOIN而不是FULL JOIN,则不需要所有这些ISNULL()表达式,只需选择任一表的列即可,例如,pcp.MemberID代替ISNULL(pcp.MemberID, plans.MemberID)pcp.Date代替ISNULL(pcp.Date, plans.Date)

完整的查询可能如下所示:

WITH MemberPCPExpanded AS (
  SELECT
    m.MemberID,
    m.PCP,
    Date = DATEADD(DAY, n.Number, m.StartDate)
  FROM MemberPCP m
    INNER JOIN Numbers n
      ON n.Number BETWEEN 0 AND DATEDIFF(DAY, m.StartDate, m.EndDate)
),
MemberPlansExpanded AS (
  SELECT
    m.MemberID,
    m.PlanID,
    Date = DATEADD(DAY, n.Number, m.StartDate)
  FROM MemberPlans m
    INNER JOIN Numbers n
      ON n.Number BETWEEN 0 AND DATEDIFF(DAY, m.StartDate, m.EndDate)
)
SELECT
  MemberID  = ISNULL(pcp.MemberID, plans.MemberID),
  pcp.PCP,
  plans.PlanID,
  StartDate = MIN(ISNULL(pcp.Date, plans.Date)),
  EndDate   = MAX(ISNULL(pcp.Date, plans.Date))
FROM MemberPCPExpanded pcp
  FULL JOIN MemberPlansExpanded plans
    ON pcp.MemberID = plans.MemberID AND pcp.Date = plans.Date
GROUP BY
  ISNULL(pcp.MemberID, plans.MemberID),
  pcp.PCP,
  plans.PlanID
ORDER BY
  MemberID,
  StartDate

您可以在 SQL Fiddle尝试此查询。

于 2012-07-03T10:13:33.763 回答
0

也许这会给一个开始的一些想法:

SELECT y.memberid, y.pcp, z.planid, x.startdate, x.enddate
  FROM (
        WITH startdates AS (

            SELECT startdate FROM memberpcp
            UNION
            SELECT startdate FROM memberplans
            UNION
            SELECT enddate + 1 FROM memberpcp
            UNION
            SELECT enddate + 1 FROM memberplans

            ), enddates AS (
            SELECT enddate FROM memberpcp
            UNION
            SELECT enddate FROM memberplans

          )

        SELECT s.startdate, e.enddate
          FROM startdates s 
              ,enddates e
          WHERE e.enddate = (SELECT MIN(enddate)
                               FROM enddates
                               WHERE enddate > s.startdate)
       ) x
       ,memberpcp y
       ,memberplans z

  WHERE (y.startdate, y.enddate) = (SELECT startdate, enddate FROM memberpcp WHERE startdate <= x.startdate AND enddate >= x.enddate)
    AND (z.startdate, z.enddate) = (SELECT startdate, enddate FROM memberplans WHERE startdate <= x.startdate AND enddate >= x.enddate)

我在 Oracle 上运行,结果如下:

1001    231 555 01-JAN-02   30-JUN-02
1001    327 555 01-JUL-02   31-MAR-03
1001    327 762 01-APR-03   31-MAY-03
1001    390 762 01-JUN-03   31-DEC-03

这个想法是首先定义不同的日期范围。那是在“WITH”子句中。然后查找其他表中的每个范围。这里有很多关于重叠范围等的假设。但也许是一个开始。我试着在没有分析函数的情况下查看这个,因为可能没有对 tsql 分析函数的良好支持?我不知道。在构建真实的日期范围时,范围也需要由 memberid 构建。

于 2012-06-15T18:06:45.417 回答