I have a SQL Server 2008 R2 database that manages lists of recipients. Each list of recipients needs to be assigned a block of serial numbers that are guaranteed to be unique for the last 90 days. A serial number is simply an integer between 1 and 999999.
I don't have to track which serial number gets associated with which recipient. There could be 500K+ recipients per list.
I have the following tables:
ListJob
ListJobId int PK
ListJobName varchar(64)
Recipient
RecipientId int PK
ListJobId int FK
Name varchar(64)
ListJobSerialRange
ListJobSerialRangeId int PK
ListJobId int FK
DateClaimed datetime
SerialNumberStart int
SerialNumberEnd int
The ListJobSerialRange
table stores the claimed serial number range(s) that will be assigned to the recipients in the application layer. The sum of all the ranges that are assigned to a list job must equal the number of recipients, as each recipient will ultimately be assigned one serial number.
Recipients can be added and removed from a ListJob
frequently. If we add recipients, we will need to claim additional serial numbers for them. If we remove recipients we want to free up the claimed serial numbers so we can re-use them so we don't waste serial numbers within the 90 day period.
Each list job will ultimately have one set of ranges. It won't have some that are claimed before the 90 days and some that are after the 90 days.
Here's a quick example:
Now ListJob C has 1000 recipients. It needs to claim TWO ranges to fulfill all recipients:
10,001 to 10,500
20,501 to 21,000
Here's what I have so far for my stored procedure:
CREATE PROCEDURE [dbo].[ClaimSerialNumbers]
@ListJobId int,
@NumDaysUnique int,
@MaxSerialNumber int
as begin
set nocount on
declare @RecipientCount int
declare @QuantityClaimed int
declare @QuantityNeeded int
declare @DateThreshold smalldatetime
set @DateThreshold = dateadd(day, 0-@NumDaysUnique, getdate())
select @RecipientCount = count(*)
from dbo.Recipient
where ListJobId = @ListJobId
select @QuantityClaimed = sum(SerialNumberEnd - SerialNumberStart + 1)
from dbo.ListJobSerialRange
where ListJobId = @ListJobId
set @QuantityNeeded = @QuantityClaimed - @RecipientCount
if (@QuantityNeeded < 0) begin
delete dbo.ListJobSerialRange
where ListJobId = @ListJobId
set @QuantityNeeded = @RecipientCount
end
if (@QuantityNeeded = 0) begin
-- if we run the sproc twice and nothing has changed, then nothing to do
return 0
end
-- now the hard part:
-- i need to claim some serial numbers
-- ???
end
I finally got something I think will work.. First, I grab all of the claimed serial number ranges within the date threshold and sort them by the start number ascending and dump them in a temp table. Then the next part is the key concept.. I can peek at the previous row and the next row. That is a slight performance issue that i have to select from the temp table 3 times. but there really shouldn't be that many in there since i have already narrowed it down to the ones that are greater than the date threshold. then it's just a matter of looking at the cases where there can be gaps.
CREATE PROCEDURE [dbo].[ClaimSerialNumbers]
@ListJobId int,
@NumDaysUnique int,
@MaxSerialNumber int
as
SET NOCOUNT ON;
BEGIN TRANSACTION
declare @RecipientCount int = 0
declare @QuantityClaimed int = 0
declare @QuantityNeeded int = 0
declare @Now smalldatetime = getdate()
declare @DateThreshold smalldatetime = dateadd(day, 0-@NumDaysUnique, @Now)
declare @RunningTotal int = 0
declare @UnusedRangeCount int = 0
--***************************************
-- Get number of recipients for list job
--***************************************
select @RecipientCount = coalesce(count(*), 0)
from dbo.Recipient with(nolock)
where ListJobId = @ListJobId
--***************************************
-- get how many serial numbers are already claimed
--***************************************
select @QuantityClaimed = coalesce(sum(SR.SerialNumberEnd - SR.SerialNumberStart + 1), 0)
from dbo.ListJobSerialRange SR
where ListJobId = @ListJobId
--***************************************
-- Determine how many we still need
--***************************************
set @QuantityNeeded = @QuantityClaimed - @RecipientCount
--***************************************
-- if we have more claimed that number of recipients
-- we have deleted recipients and need to free up
-- some serial numbers
--***************************************
if (@QuantityNeeded < 0) begin
delete dbo.ListJobSerialRange
where ListJobId = @ListJobId
set @QuantityNeeded = @RecipientCount
end
--***************************************
-- if we need to claim some serial numbers
--***************************************
if (@QuantityNeeded > 0) begin
if object_id('tempdb..#SortedRanges') is not null drop table #SortedRanges
if object_id('tempdb..#UnusedRanges') is not null drop table #UnusedRanges
create table #SortedRanges(RowNumber int, SerialNumberStart int, SerialNumberEnd int)
create table #UnusedRanges(SerialNumberStart int, SerialNumberEnd int, RunningTotal int)
--***************************************
-- put all the existing ranges within the last N days into a temp table
--***************************************
insert #SortedRanges
(RowNumber, SerialNumberStart, SerialNumberEnd)
select RowNumber = row_number() over (order by SerialNumberStart)
, SerialNumberStart
, SerialNumberEnd
from dbo.ListJobSerialRange
where DateClaimed >= @DateThreshold
order by SerialNumberStart
--***************************************
-- if no ranges exist in the last N days, then the whole set is available
--***************************************
if (@@rowcount = 0)
begin
insert dbo.ListJobSerialRange
(ListJobId, DateClaimed, SerialNumberStart, SerialNumberEnd)
values (@ListJobId, @Now, 1, @QuantityNeeded)
end
--***************************************
-- if we have serial numbers that were used in the last n days
-- then look for gaps in the rangesand add those gaps to the unused range temp table
--***************************************
else
begin
;with UnusedRangesFirstPass as (
select distinct
case
-- start of range (gap at beginning)
when P.RowNumber is null and C.SerialNumberStart > 1 then 1
-- start of range (gap at end)
when N.RowNumber is null and C.SerialNumberEnd < @MaxSerialNumber then C.SerialNumberEnd + 1
-- start of range (middle row gaps)
else
case
when P.SerialNumberEnd is not null and P.SerialNumberEnd < C.SerialNumberStart - 1 then P.SerialNumberEnd + 1
when N.SerialNumberStart is not null and N.SerialNumberStart > C.SerialNumberEnd + 1 then C.SerialNumberEnd + 1
end
end as UnusedStart,
case
-- end of range (gap at beginning)
when P.RowNumber is null and C.SerialNumberStart > 1 then (C.SerialNumberStart - 1)
-- end of range (gap at end)
when N.RowNumber is null and C.SerialNumberEnd < @MaxSerialNumber then @MaxSerialNumber
-- end of range (middle row gaps)
else
case
when P.SerialNumberEnd is not null and P.SerialNumberEnd < C.SerialNumberStart - 1 then C.SerialNumberStart - 1
when N.SerialNumberStart is not null and N.SerialNumberStart > C.SerialNumberEnd + 1 then N.SerialNumberStart - 1
end
end as UnusedEnd
from #SortedRanges C -- current row
left join #SortedRanges P ON P.RowNumber = C.RowNumber - 1 -- peek at previous row
left join #SortedRanges N ON N.RowNumber = C.RowNumber + 1 -- peek at next row
)
insert #UnusedRanges
(SerialNumberStart, SerialNumberEnd, RunningTotal)
select UnusedStart, UnusedEnd, 0
from UnusedRangesFirstPass
where UnusedStart is not null
and UnusedEnd is not null
--***************************************
-- update all unused ranges and calculate a running total
-- for each row
--***************************************
update #UnusedRanges
set @RunningTotal = RunningTotal = @RunningTotal + (SerialNumberEnd - SerialNumberStart + 1)
--***************************************
-- claim the unused ranges.
-- only claim the exact number we need
--***************************************
insert dbo.ListJobSerialRange
(ListJobId, DateClaimed, SerialNumberStart, SerialNumberEnd)
select @ListJobId, @Now, UR1.SerialNumberStart,
case
when UR1.RunningTotal > @QuantityNeeded then UR1.SerialNumberEnd - (UR1.RunningTotal - @QuantityNeeded)
else UR1.SerialNumberEnd
end
from #UnusedRanges UR1
left join #UnusedRanges UR2 on UR1.RunningTotal > UR2.RunningTotal
group by UR1.SerialNumberStart, UR1.SerialNumberEnd, UR1.RunningTotal
having coalesce(sum(UR2.RunningTotal),0) < @QuantityNeeded
end
end
--***************************************
-- return results and commit
--***************************************
select ListJobSerialRangeId, ListJobId, DateClaimed, SerialNumberStart, SerialNumberEnd
from dbo.ListJobSerialRange
where ListJobId = @ListJobId
COMMIT TRANSACTION
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments