Иногда в процессе проектирования базы данных разработчики могут недооценивать масштаб проекта. Если бы этот проект стал успешным, это привело бы к значительным нагрузкам и чрезмерной нагрузке на систему и ее разработчиков в будущем. В конце концов люди понимают, что используют ключ идентификации INT с максимальным значением 2 147 483 647 в качестве первичного ключа огромной таблицы.

С одной стороны, 2 миллиардов значений должно быть вполне достаточно. Но что, если к вам ежедневно добавляется более 10 миллионов записей, а вы уже использовали более миллиарда из них? Это дает вам только 114 дней, чтобы внести необходимые исправления или пострадать от последствий. Это не так уж много, особенно если ключ идентификации «жестко подключен» как к веб-интерфейсу, так и к клиентскому приложению.

Если вы достигли этого печального момента, не бойтесь, эта статья здесь, чтобы помочь, даже если уже слишком поздно что-то менять, а ваше приложение работает 24/7. В худшем случае вы должны были уже увидеть следующее сообщение об ошибке: Ошибка арифметического переполнения при преобразовании IDENTITY в тип данных INT. Мы в Дистилери можем предложить вам несколько полезных решений, когда эта ситуация оказывается чрезвычайной в вашем MS SQL Server.

  1. Переключитесь на БОЛЬШОЙ. В идеальном мире лучшим вариантом было бы изобрести машину для путешествий во времени, вернуться в прошлое и изменить INT на BIGINT. Однако изменение INT на BIGINT на более продвинутом этапе процесса разработки вызовет изменения не только в базе данных, но и в серверных, и в клиентских приложениях. Если у вас есть возможность изменить INT на BIGINT без серьезных последствий, сделайте это и сделайте это быстро.
  2. Используйте отрицательные значения. Шаг приращения по умолчанию равен единице — IDENTITY(1,1). В тот момент, когда вы собираетесь достичь 2 миллиардов, вы можете использовать
DBCC CHECKIDENT (TableWithPKViolation, −2147483647, reseed)

тем самым добавляя дополнительное время перед переключением на BIGINT. Однако это может создать проблему, если логика вашего домена не позволяет использовать отрицательные значения. Прежде чем переключаться, убедитесь, что логика домена может приспособиться.

3. Создайте таблицу с неиспользуемыми значениями. Вам нужно найти те значения ключа идентификации, которые были пропущены, создав таким образом пул свободных и неиспользуемых значений. Такой подход позволяет создать дополнительное время перед переходом к BIGINT. В любой ситуации, когда вы используете ключи инкремента, вы увидите пропущенные значения, потому что такие значения резервируются в начале транзакции и никогда не возвращаются в основной пул в случае отката. Таким образом, новая транзакция получит новое значение. Вы можете использовать больше свободных значений, если удалите данные из таблицы. Ваша основная задача — сформировать таблицу, содержащую все доступные значения и логическое поле, которое работает как атрибут резервирования значений. Вы можете удалить зарезервированные значения из такой таблицы в соответствии с расписанием, например. один раз в день. После этого вам нужно создать функцию, которая предоставит необходимое количество идентификационных ключей, и использовать полученные результаты с помощью команды SET IDENTITY INSERT ON. Если вы регулярно удаляете информацию из основной таблицы, вы можете добавить такие удаленные значения в свою таблицу как свободные значения. Этот метод можно использовать только в том случае, если вам не нужно сохранять порядок записей в таблице, другими словами, если вы не используете ORDER BY Id. В противном случае вам придется выбрать другой вариант сортировки.

Ниже вы найдете примеры третьего метода. Если вы уже удалили некоторую информацию из таблицы, имея менее одного миллиарда записей, будет более эффективно создать полную таблицу со значениями, удалив используемые идентификационные ключи.

  1. Создайте таблицу для создания последовательности и заполните ее значениями от 1 до 2 147 483 647.
CREATE TABLE [dbo].[IntRange](     
    [Id] [int] NOT NULL 
) ON [PRIMARY]
SET ANSI_NULLS ON 
GO  
SET QUOTED_IDENTIFIER ON 
GO  
CREATE PROCEDURE [dbo].[spNewIDPopulateInsert]     
    @batchsize INT = 10000,     
    @startFrom INT = NULL 
AS 
BEGIN     
    SET NOCOUNT ON;     
    SET XACT_ABORT ON;      
    IF @startFrom IS NULL     
    BEGIN         
        SELECT @startFrom = MAX(id)         
        FROM dbo.IntRange;      
    END;      
   
    DECLARE @startId INT = ISNULL(@startFrom,0);     
    DECLARE @rowscount INT = @batchsize;     
    DECLARE @maxId INT = 2147483647;      
    WHILE @rowscount = @batchsize     
    BEGIN         
        INSERT INTO dbo.IntRange         
        (id)         
        SELECT id         
        FROM (             
            SELECT TOP (@batchsize)                 
                @startId + ROW_NUMBER() OVER(ORDER BY          
            TableWithPKViolationId) AS id 
          FROM dbo.TableWithPKViolation --any table where you have @batchsize rows             
            ) AS genids         
        WHERE id < @maxId;           
        SET @rowscount = @@ROWCOUNT;                          
        
        SET @startId = @startId + @rowscount;          
        PRINT CONVERT(VARCHAR(50),GETDATE(),121)+' '+ CAST(@startId AS VARCHAR(50));     
    END 
END  
GO
exec dbo.spNewIDPopulateInsert     
     @batchsize = 10000000
  1. Этот скрипт использует таблицу TableWithPKViolation для создания последовательности. Однако вы можете использовать любой желаемый или любимый метод, включая команду SEQUENCE, встроенную в MS SQL Server. Мы выбрали этот метод как наиболее продуктивный.
ALTER TABLE [dbo].[IntRange] ADD PRIMARY KEY CLUSTERED  
(     
    [Id] ASC 
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = OFF) ON [PRIMARY] 
GO

2. Создайте таблицу свободных значений и заполните ее.

SET ANSI_NULLS ON 
GO  
SET QUOTED_IDENTIFIER ON 
GO  
CREATE TABLE [dbo].[NewIds](     
    [NewId] [int] NOT NULL,     
    [DateUsedUtc] [datetime] NULL 
) ON [PRIMARY]  
GO
SET ANSI_NULLS ON 
GO  
SET QUOTED_IDENTIFIER ON 
GO  
CREATE PROCEDURE [dbo].[spNewIDPopulateInsertFiltered]       
    @batchsize INT = 10000,     
    @startFrom INT = NULL,     
    @endTill INT = NULL 
AS 
BEGIN     
    SET NOCOUNT ON;     
    SET XACT_ABORT ON;      
    IF @startFrom IS NULL     
    BEGIN         
        SELECT @startFrom = MAX([NewId])         
        FROM dbo.NewIds;      
    END;      
    DECLARE @startId INT = ISNULL(@startFrom,0);     
    DECLARE @rowscount INT = @batchsize;     
    DECLARE @maxId INT = ISNULL(@endTill,2147483647);     
    DECLARE @endId INT = @startId + @batchsize;      
    WHILE @startId < @maxId BEGIN INSERT INTO [NewIds] ([NewId])          
        SELECT IR.id FROM [dbo].[IntRange] AS IR WHERE IR.id >= @startId         
        AND IR.id < @endId          
        AND NOT EXISTS ( SELECT 1 FROM [dbo].[TableWithPKViolation] as Tb WITH (NOLOCK)              
            WHERE Tb.TableWithPKViolationId = IR.id );              
            SET @rowscount = @@ROWCOUNT;              
            SET @startId = @endId;              
            SET @endId = @endId + @batchsize;              
            IF @endId > @maxId SET @endId = @maxId;          
        
        PRINT CONVERT(VARCHAR(50),GETDATE(),121)+' '+ CAST(@startId AS VARCHAR(50));     
     END 
END  
GO
----- 
-----Run each part in separate window in parallel 
-----  
--part 1 
exec dbo.spNewIDPopulateInsertFiltered @batchsize = 10000000, @startFrom = 1,      
    @endTill= 500000000 
--end of part 1  
--part 2 
exec dbo.spNewIDPopulateInsertFiltered @batchsize = 10000000, @startFrom = 500000000,      
    @endTill= 1000000000 
--end of part 2  
--part 3 
exec dbo.spNewIDPopulateInsertFiltered @batchsize = 10000000, @startFrom = 1000000000,      
    @endTill= 1500000000 
--end of part 3  
--part 4 
DECLARE @maxId INT  
SELECT @maxId = MAX(TableWithPKViolationId) 
FROM dbo.TableWithPKViolation  
exec dbo.spNewIDPopulateInsertFiltered @batchsize = 10000000, @startFrom = 1500000000,      
    @endTill= @maxId 
--end of part 4
ALTER TABLE [dbo].[NewIds] ADD  CONSTRAINT [PK_NewIds] PRIMARY KEY CLUSTERED  
(     
    [NewId] ASC 
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 
GO  
CREATE NONCLUSTERED INDEX [IX_NewIds_DateUsedUtc] ON [dbo].[NewIds] (     
    [DateUsedUtc] ASC 
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF)  
GO  
ALTER TABLE [dbo].[NewIds] SET ( LOCK_ESCALATION = DISABLE ) 
GO

Убедитесь, что таблица сформирована правильно. Таблица NewId не должна содержать идентификационных ключей, включенных в основную таблицу TableWithPKViolation.

3. Создайте процесс, который будет помечать и возвращать все доступные значения.

create PROCEDURE [dbo].[spGetTableWithPKViolationIds]     
    @batchsize INT = 1 
AS 
BEGIN     
    SET NOCOUNT ON;     
    SET XACT_ABORT ON;      
 
    DECLARE @rowcount INT,         
        @now DATETIME = GETUTCDATE();      
    BEGIN TRAN      
    UPDATE TOP (@batchsize) dbo.NewIds     
    SET DateUsedUtc = @now     
    OUTPUT inserted.[NewId]     
    WHERE DateUsedUtc IS NULL;      
    SET @rowcount = @@ROWCOUNT; 
      
    IF @rowcount != @batchsize     
    BEGIN          
       DECLARE @msg NVARCHAR(2048);          
       SET @msg = 'TableWithPKViolationId out of ids.'             
           + 'sp spGetTableWithPKViolationIds, table NewIds. '             
           + 'Ids requested '             
           + CAST(@batchsize AS NVARCHAR(255))              
           + ', IDs available '             
           + CAST(@rowcount AS NVARCHAR(255));          
          RAISERROR(@msg, 16,1);         
          ROLLBACK;                                         
    END     
    ELSE     
    BEGIN         
        COMMIT TRAN     
    END; 
END 
GO

4. Возьмите все процедуры, которые добавляют информацию в основную таблицу и возвращают SCOPE_IDENTITY(), и добавьте вызов процедуры. Если ваше время ограничено или ваше оборудование относительно мощное, вы можете создать триггер INSTEAD OF INSERT, избежав таким образом изменений в огромном количестве процедур. Я покажу вам пример изменения процедуры.

CREATE TABLE #tmp_Id (Id INT);  
INSERT INTO #tmp_Id 
EXEC spGetTableWithPKViolationIds @batchsize=@IDNumber;  
SELECT @newVersionId = Id 
FROM #tmp_Id;  
SET IDENTITY_INSERT [dbo].[TableWithPKViolation] ON;

Обратите внимание, что для параметра SET IDENTITY_INSERT требуется пользователь с достаточными правами для использования команды ALTER для таблицы TableWithPKViolation.

5. Далее нам нужно установить очистку таблицы идентификационных ключей в соответствии с расписанием.

create PROCEDURE dbo.spCleanupNewIds @batchSize INT = 4999 
AS 
BEGIN     
    SET NOCOUNT ON      
    DECLARE @minId INT     
    DECLARE @maxId INT      
    SELECT @minId = Min([NewId]), @maxId = MAX([NewId])     
    FROM dbo.NewIds WITH (NOLOCK)     
    WHERE DateUsedUtc IS NOT NULL;      
    DECLARE @totRowCount INT = 0     
    DECLARE @rowCount INT = @batchSize      
    WHILE @rowcount = @batchsize     
    BEGIN         
        DELETE TOP (@batchsize)         
        FROM dbo.NewIds         
        WHERE DateUsedUtc IS NOT NULL 
        AND [NewId] >= @minId AND [NewId] <= @maxId          
        SET @rowcount = @@ROWCOUNT          
        SET @totRowCount = @totRowCount + @rowcount     
    END      
    
    PRINT 'Total records cleaned up - ' + CAST(@totRowCount AS VARCHAR(100)) 
END 
GO

В случае, когда последовательность идентификационных ключей играет важную роль, этот метод будет очень полезен, поскольку все новые значения задаются постепенно. Однако нужно решить проблему переходного периода. Лучшее решение — переключить существующие процедуры, требующие надлежащей последовательности, на другое поле в той же таблице, например. в поле даты.

Ну вот! Это интеллектуальное решение позволяет выиграть дополнительное время перед переходом на BIGINT. Тем не менее, мы настоятельно рекомендуем вам избегать подобных сложных ситуаций любой ценой, планируя свой переход заранее. Если вам нужна помощь в администрировании вашей базы данных, наша команда DBA будет рада помочь!