sqlsql-server-2008mintransitivity

SQL Server Query to find min date based on transitive relationship


Running into a bit of a headache with this one query and some hints/suggestions would be greatly appreciated. I couldn't find anything really related to my problem -I found some stuff on transitive closures which isn't quite what I need since my data can possibly create a loop/cycle which, I think, would cause a recursive call to infinitely loop.

Say I have two basic tables with the data displayed below them. Full disclosure: the Memberships table is a CTE which has already done a fair bit of logic to calcualte the CServiceDate column value. The Transfers table is an actual table which doesn't contain a whole lot other than a PK and FromMembershipID and ToMembershipID relationships.

Memberships

 
+==========+==============+================+==============+
| PersonID | MemberShipID | MembershipDate | CServiceDate |
+==========+==============+================+==============+
|        1 |           15 | Aug-01-2016    | Aug-27-2017  |
+----------+--------------+----------------+--------------+
|        1 |           16 | Mar-25-2016    | Sep-01-2000  |
+----------+--------------+----------------+--------------+
|        1 |           17 | Dec-06-2011    | May-15-1995  |
+----------+--------------+----------------+--------------+
|        1 |           18 | Jan-12-2009    | Feb-28-1998  |
+----------+--------------+----------------+--------------+
|        1 |           19 | Apr-08-2016    | Jul-10-1994  |
+----------+--------------+----------------+--------------+
|        1 |           20 | Jun-11-2010    | Nov-12-1997  |
+----------+--------------+----------------+--------------+

Transfer

+=====+=====+==================+================+
| TID | PID | FromMembershipID | ToMembershipID |
+=====+=====+==================+================+
|   1 |   1 |               16 |            15  |
+-----+-----+------------------+----------------+
|   2 |  1  |               18 |             17 |
+-----+-----+------------------+----------------+
|   3 |   1 |               19 |            17  |
+-----+-----+------------------+----------------+
|   4 |   1 |               20 |            18  |
+-----+-----+------------------+----------------+
|   5 |   1 |               20 |            19  |
+-----+-----+------------------+----------------+

Problem What I need from a query is for each row in the Memberships CTE (i.e. for each MembershipID), I want to return the MIN CServiceDate for all related MembershipIDs. I will call this MIN value the ECSD (Expected Credited Service Date). The calculation of the ECSD has only two conditions:

Expected Output of the ECSD column

+==========+==============+================+==============+=============+
| PersonID | MemberShipID | MembershipDate | CServiceDate |    ECSD     |
+==========+==============+================+==============+=============+
|        1 |           15 | Aug-01-2016    | Aug-27-2017  | Sep-01-2000 |
+----------+--------------+----------------+--------------+-------------+
|        1 |           16 | Mar-25-2016    | Sep-01-2000  | Sep-01-2000 |
+----------+--------------+----------------+--------------+-------------+
|        1 |           17 | Dec-06-2011    | May-15-1995  | May-15-1995 |
+----------+--------------+----------------+--------------+-------------+
|        1 |           18 | Jan-12-2009    | Feb-28-1998  | Feb-28-1998 |
+----------+--------------+----------------+--------------+-------------+
|        1 |           19 | Apr-08-2016    | Jul-10-1994  | Jul-10-1994 |
+----------+--------------+----------------+--------------+-------------+
|        1 |           20 | Jun-11-2010    | Nov-12-1997  | Nov-12-1997 |
+----------+--------------+----------------+--------------+-------------+

Please Note:

Example 2

Memberships

+==========+==============+================+==============+
| personid | membershipid | membershipdate | CServiceDate |
+==========+==============+================+==============+
|   499510 |       548426 | 2014-09-29     | 2014-09-29   |
+----------+--------------+----------------+--------------+
|   499510 |       548428 | 2014-01-29     | 2014-01-29   |
+----------+--------------+----------------+--------------+
|   499510 |       548425 | 2012-05-28     | 2012-05-28   |
+----------+--------------+----------------+--------------+
|   499510 |       548429 | 2011-11-23     | 2011-11-23   |
+----------+--------------+----------------+--------------+
|   499510 |       548427 | 2008-07-03     | 2008-07-03   |
+----------+--------------+----------------+--------------+
|   499510 |       548431 | 2001-05-01     | 1976-01-01   |
+----------+--------------+----------------+--------------+
|   499510 |       548430 | 1998-10-08     | 1998-10-08   |
+----------+--------------+----------------+--------------+

Transfers

+=======+========+==================+================+
|  tid  |  pid   | FromMembershipID | ToMembershipID |
+=======+========+==================+================+
| 10664 | 499510 |           548430 |         548431 |
+-------+--------+------------------+----------------+
| 10665 | 499510 |           548431 |         548427 |
+-------+--------+------------------+----------------+
| 10666 | 499510 |           548427 |         548429 |
+-------+--------+------------------+----------------+
| 10667 | 499510 |           548429 |         548425 |
+-------+--------+------------------+----------------+
| 10668 | 499510 |           548425 |         548428 |
+-------+--------+------------------+----------------+
| 10669 | 499510 |           548428 |         548426 |
+-------+--------+------------------+----------------+
| 13085 | 499510 |           548430 |         548427 |
+-------+--------+------------------+----------------+
| 13086 | 499510 |           548427 |         548425 |
+-------+--------+------------------+----------------+
| 13087 | 499510 |           548425 |         548426 |
+-------+--------+------------------+----------------+
| 13088 | 499510 |           548431 |         548429 |
+-------+--------+------------------+----------------+
| 13089 | 499510 |           548429 |         548428 |
+-------+--------+------------------+----------------+

Expected Outcome

+==========+==============+================+==============+============+
| personid | membershipid | membershipdate | CServiceDate |    ECSD    |
+==========+==============+================+==============+============+
|   499510 |       548426 | 2014-09-29     | 2014-09-29   | 1976-01-01 |
+----------+--------------+----------------+--------------+------------+
|   499510 |       548428 | 2014-01-29     | 2014-01-29   | 1976-01-01 |
+----------+--------------+----------------+--------------+------------+
|   499510 |       548425 | 2012-05-28     | 2012-05-28   | 1976-01-01 |
+----------+--------------+----------------+--------------+------------+
|   499510 |       548429 | 2011-11-23     | 2011-11-23   | 1976-01-01 |
+----------+--------------+----------------+--------------+------------+
|   499510 |       548427 | 2008-07-03     | 2008-07-03   | 1976-01-01 |
+----------+--------------+----------------+--------------+------------+
|   499510 |       548431 | 2001-05-01     | 1976-01-01   | 1976-01-01 |
+----------+--------------+----------------+--------------+------------+
|   499510 |       548430 | 1998-10-08     | 1998-10-08   | 1998-10-08 |
+----------+--------------+----------------+--------------+------------+

Thanks!


Solution

  • Here's an approach that uses CTEs to compile a list of transitive relationships. One CTE defines the relationships recursively from FromMembershipID to ToMembershipID. The 2nd CTE does the same thing in the opposite direction - that way, you can combine the results into a 3rd CTE that lists every transitive relationship between memberships.

    For instance, in the first result set, 20 => 19, and 19 => 17, and in reverse, 17 => 19 and 19 => 20, so you get:

    20   19
    20   17
    19   17
    17   19
    17   20
    19   20
    

    Then you can use that list transitive relationships to get the min CServiceDate by membership.

    This gives the correct output for both sets of sample data. You'll probably run into the 100 level recursion limit if you have much more data, but you can configure that. Or find a better solution :) but technically, this works:

    ;with cte_trans_relationships (frommembershipid, tomembershipid)
    as
    (
        select frommembershipid, tomembershipid
        from @transfer
        union all
        select c.frommembershipid, t.tomembershipid
        from cte_trans_relationships c
        inner join @transfer t on c.tomembershipid = t.frommembershipid
    ),
    cte_trans_relationships2 (tomembershipid, frommembershipid)
    as
    (
        select tomembershipid, frommembershipid
        from @transfer
        union all
        select c.tomembershipid, t.frommembershipid
        from cte_trans_relationships c
        inner join @transfer t on c.frommembershipid = t.tomembershipid
    ),
    cte_trans_all (m1, m2)
    as
    (
        select distinct frommembershipid m1, tomembershipid m2
        from cte_trans_relationships c
        union 
        select distinct tomembershipid, frommembershipid
        from cte_trans_relationships2 c2    
    )
    select m.personid, m.membershipid, m.membershipdate, m.cservicedate, 
        case when sq.cservicedate < cservicedate1 and sq.cservicedate < cservicedate2 then sq.cservicedate
        when cservicedate1 < sq.cservicedate and cservicedate1 < cservicedate2 then cservicedate1
        else cservicedate2 end as ECSD
    from @memberships m
    inner join 
    (
        select m.personid, m.membershipid, isnull(m.cservicedate, dateadd(year, 100, getdate())) as cservicedate, 
        isnull(min(m1.cservicedate), dateadd(year, 100, getdate())) as cservicedate1,
        isnull(min(m2.cservicedate), dateadd(year, 100, getdate())) as cservicedate2
        from @memberships m
        left join cte_trans_all sq_trans1 on m.membershipid = sq_trans1.m1
        left join @memberships m1 on sq_trans1.m2 = m1.membershipid and m1.membershipdate < m.membershipdate
        left join cte_trans_all sq_trans2 on m.membershipid = sq_trans2.m1
        left join @memberships m2 on sq_trans2.m2 = m2.membershipid and m2.membershipdate < m.membershipdate
        group by m.personid, m.membershipid, m.cservicedate
    )sq on m.personid = sq.personid and m.membershipid = sq.membershipid
    

    Here are sample DDL/DML statements for testing:

    declare @memberships table (personid int, membershipid int, membershipdate datetime, cservicedate datetime)
    insert into @memberships values (1, 15, '8/1/2016', '8/27/2017')
    insert into @memberships values (1, 16, '3/25/2016', '9/1/2000')
    insert into @memberships values (1, 17, '12/6/2011', '5/15/1995')
    insert into @memberships values (1, 18, '1/12/2009', '2/28/1998')
    insert into @memberships values (1, 19, '4/8/2016', '7/10/1994')
    insert into @memberships values (1, 20, '6/11/2010', '11/12/1997')
    --insert into @memberships values (499510, 548426, '9/29/2014', '9/29/2014')
    --insert into @memberships values (499510, 548428, '1/29/2014', '1/29/2014')
    --insert into @memberships values (499510, 548425, '5/28/2012', '5/28/2012')
    --insert into @memberships values (499510, 548429, '11/23/2011', '11/23/2011')
    --insert into @memberships values (499510, 548427, '7/3/2008', '7/3/2008')
    --insert into @memberships values (499510, 548431, '5/1/2001', '1/1/1976')
    --insert into @memberships values (499510, 548430, '10/8/1998', '10/8/1998')
    
    declare @transfer table (tid int, pid int, frommembershipid int, tomembershipid int)
    insert into @transfer values (1, 1, 16, 15)
    insert into @transfer values (2, 1, 18, 17)
    insert into @transfer values (3, 1, 19, 17)
    insert into @transfer values (4, 1, 20, 18)
    insert into @transfer values (5, 1, 20, 19)
    --insert into @transfer values (10664, 499510, 548430, 548431)
    --insert into @transfer values (10665, 499510, 548431, 548427)
    --insert into @transfer values (10666, 499510, 548427, 548429)
    --insert into @transfer values (10667, 499510, 548429, 548425)
    --insert into @transfer values (10668, 499510, 548425, 548428)
    --insert into @transfer values (10669, 499510, 548428, 548426)
    --insert into @transfer values (13085, 499510, 548430, 548427)
    --insert into @transfer values (13086, 499510, 548427, 548425)
    --insert into @transfer values (13087, 499510, 548425, 548426)
    --insert into @transfer values (13088, 499510, 548431, 548429)
    --insert into @transfer values (13089, 499510, 548429, 548428)
    

    Here are the 2 result sets:

    personid  membershipid  membershipdate    cservicedate  ECSD
    1         15            2016-08-01        2017-08-27    2000-09-01
    1         16            2016-03-25        2000-09-01    2000-09-01
    1         17            2011-12-06        1995-05-15    1995-05-15
    1         18            2009-01-12        1998-02-28    1998-02-28
    1         19            2016-04-08        1994-07-10    1994-07-10
    1         20            2010-06-11        1997-11-12    1997-11-12
    
    personid    membershipid    membershipdate  cservicedate    ECSD
    499510      548425          2012-05-28      2012-05-28      1976-01-01
    499510      548426          2014-09-29      2014-09-29      1976-01-01
    499510      548427          2008-07-03      2008-07-03      1976-01-01
    499510      548428          2014-01-29      2014-01-29      1976-01-01
    499510      548429          2011-11-23      2011-11-23      1976-01-01
    499510      548430          1998-10-08      1998-10-08      1998-10-08
    499510      548431          2001-05-01      1976-01-01      1976-01-01