One of our measures depends on identifying the first person to contact the client at least 30 days after the last "tagged" contact, where the first contact is always tagged. For example, we first gain a client on July 1st - that staff member gets a contact "point". Then, the customer rings up on July 10th and speaks to a different staff member - they do not get a contact "point". The customer rings on September 1st (62 days after the last tagged contact on July 1st) and speak to a third staff member - they get a contact point. Finally, they speak to a fourth person on September 20th - that fourth person does not gain a point.
We implement this is SQL Server using a loop:
- Tag the first event
- Find all events at least 30 days after the latest tagged event and "score" the earliest
- Repeat step 2 until there are no more events found
I would love to remove this loop because it is very slow, reading the table again and again, and in general I'd prefer set-based operations rather than looping. I was hoping someone has solved this already and could give some advice.
Here's what is looks like now:
DROP TABLE IF EXISTS #CUSTOMER_CONTACT
--Set up our table with the contact dates of each customer
--This is the "live" table in our system
CREATE TABLE #CUSTOMER_CONTACT (
CUSTOMER_CODE INT NOT NULL,
EVENT_DATE DATE NOT NULL,
CONTACT_STAFF_CODE VARCHAR(20) NOT NULL,
FIRST_CONTACT_IN_30 INT NOT NULL,
CONSTRAINT PK_#CUSTOMER_CONTACT PRIMARY KEY CLUSTERED ( CUSTOMER_CODE, EVENT_DATE )
)
--Insert some dummy data for one customer over a few years
INSERT INTO #CUSTOMER_CONTACT( CUSTOMER_CODE, EVENT_DATE, CONTACT_STAFF_CODE, FIRST_CONTACT_IN_30 )
VALUES ( 1, '2014-10-07', 'A', 0 ),
( 1, '2014-12-01', 'A', 0 ),
( 1, '2015-10-06', 'B', 0 ),
( 1, '2015-10-07', 'C', 0 ),
( 1, '2015-10-15', 'C', 0 ),
( 1, '2015-11-10', 'C', 0 ),
( 1, '2016-03-15', 'D', 0 ),
( 1, '2016-03-18', 'A', 0 ),
( 1, '2016-05-02', 'D', 0 ),
( 1, '2016-09-09', 'D', 0 ),
( 1, '2017-10-09', 'C', 0 ),
( 1, '2017-10-10', 'B', 0 ),
( 1, '2017-10-11', 'B', 0 ),
( 1, '2017-10-20', 'D', 0 ),
( 1, '2018-10-10', 'D', 0 ),
( 1, '2019-03-07', 'D', 0 ),
( 1, '2019-06-12', 'A', 0 ),
( 1, '2019-08-02', 'A', 0 )
--Tag the very first contact per client so the staff person "A" gets a "point"
UPDATE C
SET C.FIRST_CONTACT_IN_30 = 1
FROM #CUSTOMER_CONTACT AS C
INNER JOIN (
SELECT CUSTOMER_CODE,
MIN( EVENT_DATE ) AS FIRST_EVENT_DATE
FROM #CUSTOMER_CONTACT
GROUP BY CUSTOMER_CODE
) AS M
ON M.CUSTOMER_CODE = C.CUSTOMER_CODE
AND M.FIRST_EVENT_DATE = C.EVENT_DATE
DROP TABLE IF EXISTS #NEXT_CONTACT_DATE
--This temp table will store the next event to be tagged
CREATE TABLE #NEXT_CONTACT_DATE (
CUSTOMER_CODE INT NOT NULL,
EVENT_DATE DATE NOT NULL,
CONSTRAINT PK_#NEXT_CONTACT_DATE PRIMARY KEY CLUSTERED ( CUSTOMER_CODE, EVENT_DATE )
)
--Just in case there is a flaw in my logic for the WHILE I'll use this to cap the iterations
DECLARE @I INT = 1
--For each customer, as long as I can find any record at least 30 days after the latest tagged records, keep
--on looping
WHILE EXISTS (
SELECT *
FROM #CUSTOMER_CONTACT AS CC
INNER JOIN (
--Find the latest tagged event
SELECT CUSTOMER_CODE,
MAX( EVENT_DATE ) AS LATEST_CONTACT_DATE
FROM #CUSTOMER_CONTACT
WHERE FIRST_CONTACT_IN_30 = 1
GROUP BY CUSTOMER_CODE
) AS L
ON L.CUSTOMER_CODE = CC.CUSTOMER_CODE
--Find any event at least 30 days after that
AND DATEADD( DAY, 30, L.LATEST_CONTACT_DATE ) <= CC.EVENT_DATE
) AND
--And make sure I haven't hit my iteration limit
@I <= 100
BEGIN
TRUNCATE TABLE #NEXT_CONTACT_DATE
--Now store the earliest contact date for each customer that was at least 30 days
--after the latest tagged event date
INSERT INTO #NEXT_CONTACT_DATE( CUSTOMER_CODE, EVENT_DATE )
SELECT CC.CUSTOMER_CODE,
MIN( CC.EVENT_DATE ) AS EVENT_DATE
FROM #CUSTOMER_CONTACT AS CC
INNER JOIN (
SELECT CUSTOMER_CODE,
MAX( EVENT_DATE ) AS LATEST_CONTACT_DATE
FROM #CUSTOMER_CONTACT
WHERE FIRST_CONTACT_IN_30 = 1
GROUP BY CUSTOMER_CODE
) AS L
ON L.CUSTOMER_CODE = CC.CUSTOMER_CODE
AND DATEADD( DAY, 30, L.LATEST_CONTACT_DATE ) <= CC.EVENT_DATE
GROUP BY CC.CUSTOMER_CODE
--Update the main table to tag that contact date to give a "point" to that staff member
UPDATE CC
SET CC.FIRST_CONTACT_IN_30 = 1
FROM #CUSTOMER_CONTACT AS CC
INNER JOIN #NEXT_CONTACT_DATE AS N
ON N.CUSTOMER_CODE = CC.CUSTOMER_CODE
AND N.EVENT_DATE = CC.EVENT_DATE
--Don't forget to increment so my loop isn't endless
SET @I = @I + 1
END
--Check the data at the end.
--It looks good to me
SELECT CC.CUSTOMER_CODE,
CC.EVENT_DATE,
CC.CONTACT_STAFF_CODE,
CC.FIRST_CONTACT_IN_30
FROM #CUSTOMER_CONTACT AS CC
The below table shows the results. You can see that each of the first 3 events is tagged as they are all 30 days apart, but the 4th event is less than 30 days after the 3rd event. Event 5 is still within 30 days of event 3, but event 6 is now more than 30 days after the latest tagged event, so it is also tagged.
CUSTOMER_CODE EVENT_DATE CONTACT_STAFF_CODE FIRST_CONTACT_IN_30
1 07/10/2014 A 1
1 01/12/2014 A 1
1 06/10/2015 B 1
1 07/10/2015 C 0
1 15/10/2015 C 0
1 10/11/2015 C 1
1 15/03/2016 D 1
1 18/03/2016 A 0
1 02/05/2016 D 1
1 09/09/2016 D 1
1 09/10/2017 C 1
1 10/10/2017 B 0
1 11/10/2017 B 0
1 20/10/2017 D 0
1 10/10/2018 D 1
1 07/03/2019 D 1
1 12/06/2019 A 1
1 02/08/2019 A 1
You can assume that there are no duplicate contact dates for one customer so I don't need to worry about two rows being flagged.
I haven't gotten anywhere with taking this out of the loop. I keep thinking there's something clever with ROW_NUMBER, or some cumulative SUM, but I get stuck on the fact that the counter needs to reset each time an event is tagged, which might 31 days after the last tagged one, or 365 days.