I have the following table (a few fields omitted): CREATE TABLE [Device]( [DeviceId] [int] IDENTITY(1,1) NOT NULL, [AuthorizedDate] [datetime2](0) NULL, [DeauthorizedDate] [datetime2](0) NULL, [MemberId] [int] NOT NULL, ... CONSTRAINT [PK_Device] PRIMARY KEY CLUSTERED ( [DeviceId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY] ) ON [PRIMARY] The table is used to track the devices of each member - they're now only allowed to have 5 devices active at the same time and can only make 3 changes in a 90 day period. This table is not sufficient to enforce the 3 changes rule (I won't go into the specifics). To that end I have decided to create a log table: CREATE TABLE [SwapLog]( [SwapLogId] [int] IDENTITY(1,1) NOT NULL, [MemberId] [int] NOT NULL, [DeauthorizationDate] [datetime] NOT NULL, [DeauthorizedDeviceId] [int] NOT NULL, [AuthorizationDate] [datetime] NULL, [AuthorizedDeviceId] [int] NULL, CONSTRAINT [PK_SwapLog] PRIMARY KEY CLUSTERED ( [wapLogId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] The plan is to populate this table when a device is de-authorized, and then to fill in the AuthorizationDate and Id when a new device is subsequently authorized I need to populate the SwapLog table so that I can properly enforce the rules. So, if the Device table has the following records: DeviceId AuthorizedDate DeauthorizedDate MemberId 60 2010-11-09 08:06:38 2010-11-10 00:00:00 2389 1795 2012-01-04 08:57:12 2013-09-02 15:32:59 2389 4467 2011-02-21 14:25:24 2013-09-02 15:33:52 2389 4733 2011-03-02 20:11:54 NULL 2389 11835 2011-11-02 11:38:33 NULL 2389 12707 2011-11-23 16:35:45 NULL 2389 46663 2012-04-13 13:14:56 NULL 2389 56651 2012-07-05 20:10:05 NULL 2389 86247 2012-10-11 12:50:20 NULL 2389 114493 2012-12-05 19:46:30 NULL 2389 123867 2013-03-15 19:39:38 NULL 2389 127595 2013-08-01 18:17:16 NULL 2389 522826 2013-10-09 16:20:31 NULL 2389 554246 2013-10-28 11:23:29 NULL 2389 574637 2013-11-07 16:48:36 NULL 2389 609731 2013-11-25 12:22:47 NULL 2389 651808 2013-12-19 11:25:35 NULL 2389 775957 2014-02-10 13:05:51 NULL 2389 778288 2014-02-11 10:52:19 NULL 2389 824090 2014-03-12 14:34:28 NULL 2389 180842 2013-02-14 07:00:37 2013-08-23 09:49:58 225897 180857 2013-02-14 08:56:42 2013-08-23 09:50:01 225897 181974 2013-02-15 09:44:42 2013-08-23 09:50:04 225897 182026 2013-02-15 12:03:02 2013-08-23 09:50:07 225897 183139 2013-02-16 04:14:20 2014-04-11 09:44:47 225897 186993 2013-02-18 01:42:30 2014-04-11 09:44:55 225897 188424 2013-02-19 11:17:27 2014-04-11 09:45:02 225897 190263 2013-02-21 01:00:11 2014-04-11 09:45:10 225897 359562 2013-07-08 10:07:29 2014-04-11 09:45:17 225897 411095 2013-07-31 09:48:24 2013-08-16 15:08:39 225897 452576 2013-08-20 01:35:52 2013-08-23 09:50:09 225897 462050 2013-08-26 04:21:57 2013-09-06 16:16:14 225897 503700 2013-09-25 08:05:14 2013-10-15 15:02:43 225897 532174 2013-10-16 06:35:07 2013-10-16 12:33:32 225897 532177 2013-10-16 06:45:48 2013-10-16 12:33:35 225897 532301 2013-10-16 11:32:48 2013-10-16 12:33:38 225897 532340 2013-10-16 12:49:47 2013-10-23 13:13:29 225897 533323 2013-10-17 07:12:42 2013-10-23 13:13:37 225897 588192 2013-11-14 04:17:38 2013-12-17 09:16:34 225897 444955 2013-11-20 10:50:22 2013-12-11 09:09:05 225897 600932 2013-11-21 02:06:15 2013-12-17 12:58:32 225897 613745 2013-11-28 09:29:26 2014-02-13 17:21:25 225897 626805 2013-12-05 23:50:03 2014-02-13 17:21:35 225897 637629 2013-12-11 09:09:06 2014-02-13 17:21:42 225897 649121 2013-12-17 09:16:35 2014-02-13 17:21:38 225897 649238 2013-12-17 12:58:32 2014-02-13 17:21:31 225897 445006 2014-02-13 17:25:16 2014-02-14 13:41:05 225897 781025 2014-02-13 17:59:50 2014-02-14 13:41:11 225897 780992 2014-02-18 19:53:23 2014-03-20 08:30:12 225897 793156 2014-02-19 12:19:09 2014-03-20 09:12:42 225897 804491 2014-02-26 11:16:06 2014-03-20 10:40:43 225897 833582 2014-03-19 05:15:26 2014-04-11 09:45:57 225897 834404 2014-03-20 07:58:53 2014-04-11 09:45:52 225897 532426 2014-03-20 08:30:12 2014-04-11 09:45:30 225897 834414 2014-03-20 09:12:43 2014-04-11 09:45:46 225897 834446 2014-03-20 10:40:43 2014-04-11 09:45:36 225897 870418 2014-04-14 12:58:32 NULL 225897 871297 2014-04-15 07:16:26 NULL 225897 898213 2014-05-08 06:11:23 NULL 225897 I would expect the SwapLog table to have the following: SwapLogId MemberId DeauthorizationDate DeauthorizedDeviceId AuthorizationDate AuthorizedDeviceId 1 2389 2010-11-10 00:00:00 60 NULL NULL 2 2389 2013-09-02 15:32:59 1795 2013-10-09 16:20:31 522826 3 2389 2013-09-02 15:33:52 4467 2013-10-28 11:23:29 554246 This way I can tell when device 1795 was deauthorized and exactly which device was authorized in its place. The problem is that the query I'm using to migrate the data is going wrong. I'm getting duplicated data in the authorizationDate field: MemberId DeauthorizedDate DeauthorizedDeviceId AuthorizedDate AuthorizedDeviceId 2389 2013-09-02 15:32:59 1795 2013-10-09 16:20:31 522826 2389 2013-09-02 15:32:59 1795 2013-10-28 11:23:29 554246 2389 2013-09-02 15:32:59 1795 2013-11-07 16:48:36 574637 2389 2013-09-02 15:32:59 1795 2013-11-25 12:22:47 609731 2389 2013-09-02 15:33:52 4467 2013-10-09 16:20:31 522826 2389 2013-09-02 15:33:52 4467 2013-10-28 11:23:29 554246 2389 2013-09-02 15:33:52 4467 2013-11-07 16:48:36 574637 2389 2013-09-02 15:33:52 4467 2013-11-25 12:22:47 609731 This is the query: SELECT d.[MemberId] ,d.[DeauthorizedDate] ,d.[DeviceId] AS [DeauthorizedDeviceId] ,d2.[AuthorizedDate] ,d2.[DeviceId] AS [AuthorizedDeviceId] FROM [Personalization].[Device] d LEFT JOIN [Personalization].[Device] d2 ON d.MemberId = d2.MemberId WHERE d.DeauthorizedDate < d2.AuthorizedDate AND d2.AuthorizedDate <= DATEADD(day, 90, d.DeauthorizedDate) AND d.MemberId = 2389 ORDER BY MemberId, d.DeauthorizedDate I could write something with c# to do this, but I suspect this will take ages as I have a large number of device records. Any help appreciated.
There are a couple of ways to approach this, the following is a recursive solution. To perform optimally, it requires a couple of indexes on the Devices table, which if not generally useful may be removed after building the SwapLog rows: CREATE UNIQUE NONCLUSTERED INDEX uq1 ON Personalization.Device (MemberId, AuthorizedDate) INCLUDE (DeviceId); CREATE UNIQUE NONCLUSTERED INDEX uq2 ON Personalization.Device (MemberId, DeauthorizedDate) INCLUDE (DeviceId) WHERE DeauthorizedDate IS NOT NULL; The recursive query itself is: WITH R AS ( -- Anchor: Earliest deauthorization per member -- and its associated authorization, if any SELECT FirstDeauthPerMember.MemberId, FirstDeauthPerMember.DeauthorizedDate, DeauthorizedDeviceId = FirstDeauthPerMember.DeviceId, Auth.AuthorizedDate, AuthorizedDeviceId = Auth.DeviceId, RunningAuthDate = ISNULL(Auth.AuthorizedDate, FirstDeauthPerMember.DeauthorizedDate) FROM ( SELECT DeauthPerMember.MemberId, DeauthPerMember.DeauthorizedDate, DeauthPerMember.DeviceId FROM ( SELECT D.MemberId, D.DeauthorizedDate, D.DeviceId, RN = ROW_NUMBER() OVER ( PARTITION BY D.MemberId ORDER BY D.DeauthorizedDate) FROM Personalization.Device AS D WHERE D.DeauthorizedDate IS NOT NULL ) AS DeauthPerMember WHERE DeauthPerMember.RN = 1 ) AS FirstDeauthPerMember OUTER APPLY ( SELECT TOP (1) D2.AuthorizedDate, D2.DeviceId FROM Personalization.Device AS D2 WHERE D2.MemberId = FirstDeauthPerMember.MemberId AND D2.AuthorizedDate > FirstDeauthPerMember.DeauthorizedDate AND D2.AuthorizedDate <= DATEADD(DAY, 90, FirstDeauthPerMember.DeauthorizedDate) ORDER BY D2.AuthorizedDate ASC ) AS Auth UNION ALL -- Recursive part: next deauthorization in order -- and its associated authorization, if any SELECT D6.MemberId, D6.DeauthorizedDate, D6.DeauthorizedDeviceId, D6.AuthorizedDate, D6.AuthorizedDeviceId, D6.RunningAuthDate FROM ( SELECT D5.MemberId, D5.DeauthorizedDate, D5.DeauthorizedDeviceId, D5.AuthorizedDate, D5.AuthorizedDeviceId, D5.RunningAuthDate FROM ( SELECT D.MemberId, D.DeauthorizedDate, DeauthorizedDeviceId = D.DeviceId, D4.AuthorizedDate, AuthorizedDeviceId = D4.DeviceId, RN = ROW_NUMBER() OVER ( ORDER BY D.DeauthorizedDate), RunningAuthDate = ISNULL(D4.AuthorizedDate, R.RunningAuthDate) FROM Personalization.Device AS D JOIN R ON R.MemberId = D.MemberId OUTER APPLY ( SELECT D3.AuthorizedDate, D3.DeviceId FROM ( SELECT D2.AuthorizedDate, D2.DeviceId, RN = ROW_NUMBER() OVER ( ORDER BY D2.AuthorizedDate) FROM Personalization.Device AS D2 WHERE D2.MemberId = D.MemberId AND D2.AuthorizedDate > ISNULL(R.AuthorizedDate, R.RunningAuthDate) AND D2.AuthorizedDate > D.DeauthorizedDate AND D2.AuthorizedDate <= DATEADD(DAY, 90, D.DeauthorizedDate) ) AS D3 WHERE D3.RN = 1 ) AS D4 WHERE D.DeauthorizedDate IS NOT NULL AND D.DeauthorizedDate > R.DeauthorizedDate ) AS D5 WHERE D5.RN = 1 ) AS D6 ) SELECT R.MemberId, R.DeauthorizedDate, R.DeauthorizedDeviceId, R.AuthorizedDate, R.AuthorizedDeviceId FROM R ORDER BY R.MemberId, R.DeauthorizedDate OPTION (MAXRECURSION 0); Output: ![Output][1] [**SQLFiddle**][2] (using dbo schema due to SQLFiddle permissions restrictions) Execution plan: ![Execution plan][3] [1]: https://i.stack.imgur.com/s5xCn.png [2]: http://sqlfiddle.com/#!6/9d81c/1 [3]: https://i.stack.imgur.com/kCKFz.png