AndreyR
AndreyR

Reputation: 331

Arithmetic overflow on large table

I have a table with 5 billions of rows in SQL Server 2014 (Developer Edition, x64, Windows 10 Pro x64):

CREATE TABLE TestTable
(
  ID BIGINT IDENTITY(1,1),
  PARENT_ID BIGINT NOT NULL,
  CONSTRAINT PK_TestTable PRIMARY KEY CLUSTERED (ID)
);

CREATE NONCLUSTERED INDEX IX_TestTable_ParentId
ON TestTable (PARENT_ID);

I'm trying to apply the following patch:

-- Create non-nullable column with default (should be online operation in Enterprise/Developer edition)
ALTER TABLE TestTable
ADD ORDINAL TINYINT NOT NULL CONSTRAINT DF_TestTable_Ordinal DEFAULT 0;
GO

-- Populate column value for existing data
BEGIN

  SET NOCOUNT ON;

  DECLARE @BATCH_SIZE BIGINT = 1000000;
  DECLARE @COUNTER BIGINT = 0;

  DECLARE @ROW_ID BIGINT;
  DECLARE @ORDINAL BIGINT;

  DECLARE ROWS_C CURSOR
    LOCAL FORWARD_ONLY FAST_FORWARD READ_ONLY
  FOR 
    SELECT
      ID AS ID,
      ROW_NUMBER() OVER (PARTITION BY PARENT_ID ORDER BY ID ASC) AS ORDINAL
    FROM
      TestTable;

  OPEN ROWS_C;

  FETCH NEXT FROM ROWS_C
  INTO @ROW_ID, @ORDINAL;

  BEGIN TRANSACTION;

  WHILE @@FETCH_STATUS = 0
  BEGIN

    UPDATE TestTable
    SET
      ORDINAL = CAST(@ORDINAL AS TINYINT)
    WHERE
      ID = @ROW_ID;

    FETCH NEXT FROM ROWS_C
    INTO @ROW_ID, @ORDINAL;

    SET @COUNTER = @COUNTER + 1;

    IF @COUNTER = @BATCH_SIZE
    BEGIN
      COMMIT TRANSACTION;
      SET @COUNTER = 0;
      BEGIN TRANSACTION;
    END;

  END;

  COMMIT TRANSACTION;

  CLOSE ROWS_C;
  DEALLOCATE ROWS_C;

  SET NOCOUNT OFF;

END;
GO

-- Drop default constraint from the column
ALTER TABLE TestTable
DROP CONSTRAINT DF_TestTable_Ordinal;
GO

-- Drop IX_TestTable_ParentId index
DROP INDEX IX_TestTable_ParentId
ON TestTable;
GO

-- Create IX_TestTable_ParentId_Ordinal index
CREATE UNIQUE INDEX IX_TestTable_ParentId_Ordinal
ON TestTable (PARENT_ID, ORDINAL);
GO

The aim of patch is to add a column, called ORDINAL, which is an ordinal number of the record within the same parent (defined by PARENT_ID). The patch is run using SQLCMD.

The patch is done is this way for a set of reasons:

The patch works perfect on a small database with a few millions of rows, but, when applied to the one with billions of rows, I get:

Msg 3606, Level 16, State 2, Server XXX, Line 22
Arithmetic overflow occurred.

My first guess was ORDINAL value is too big to fit into TINYINT column, but this is not the case. I created a test database with similar structure and populated with data (more than 255 rows per parent). The error message I get is still arithmetic exception, but with different message code and different wording (explicitly saying it can't fit data into TINYINT).

Currently I have a couple of suspicions, but I haven't managed to find anything that could help me:

Do you have any ideas on what could the problem be?

Upvotes: 0

Views: 334

Answers (1)

Steve Ford
Steve Ford

Reputation: 7763

How about using a While loop but making sure that you keep the same parent_ids together:

DECLARE @SegmentSize BIGINT = 1000000
DECLARE @CurrentSegment BigInt = 0

WHILE 1 = 1
BEGIN

    ;With UpdateData  As
    (
        SELECT  ID AS ID,
                ROW_NUMBER() OVER (PARTITION BY PARENT_ID ORDER BY ID ASC) AS ORDINAL
        FROM TestData
        WHERE ID > @CurrentSegment AND ID <= (@CurrentSegment + @SegmentSize)
    )
    UPDATE TestData 
        SET Ordinal = UpdateDate.Ordinal
    FROM TestData
    INNER JOIN UpdateData ON TestData.Id = UpdateData.Id    

    IF @@ROWCOUNT = 0
    BEGIN
        BREAK
    END

    SET @CurrentSegment = @CuurentSegment + @SegmentSize
END 

EDIT - Amended to segment on Parent_Id as per request. This should be reasonably quick as Parent_id is indexed (added Option(Recompile) to ensure that actual value is used for the lookup. Because you are not updating the whole table this will limit the Transaction Log growth!

DECLARE @SegmentSize BIGINT = 1000000
DECLARE @CurrentSegment BigInt = 0

WHILE 1 = 1
BEGIN

    ;With UpdateData  As
    (
        SELECT  ID AS ID,
                ROW_NUMBER() OVER (PARTITION BY PARENT_ID ORDER BY ID ASC) AS ORDINAL
        FROM TestData
        WHERE Parent_ID > @CurrentSegment AND
              Parent_ID <= (@CurrentSegment + @SegmentSize)
    )
    UPDATE TestData 
        SET Ordinal = UpdateDate.Ordinal
    FROM TestData
    INNER JOIN UpdateData ON TestData.Id = UpdateData.Id
    OPTION (RECOMPILE)  

    IF @@ROWCOUNT = 0
    BEGIN
        BREAK
    END

    SET @CurrentSegment = @CuurentSegment + @SegmentSize
END 

Upvotes: 1

Related Questions