¡Hola, chicos!
Todo está bien ?

Hoy les voy a hablar de un problema que encontré recientemente, donde una rutina utilizaba la vista de catálogo sys.syscolumns para obtener información sobre las columnas de una tabla de hechos (BI) y a partir de cierto punto, al intentar ejecutar la consulta SELECT * FROM sys.syscolumns, SQL Server devolvió el siguiente mensaje:

Mensaje 220, Nivel 16, Estado 1, Línea 1
Error de desbordamiento aritmético para el tipo de datos smallint, valor = 40003.

Esta fue la primera vez que vi que una consulta simple en una vista de catálogo bancario arrojaba un error. Entonces analicemos nuestro escenario:

    1) El mensaje de error nos dice que hubo un desbordamiento en un campo Smallint, cuyo valor sería 40003 (límite Smallint: 32767)
    2) Como es una vista de catálogo, no hay opción de cambiarla para aumentar el tamaño de la columna.
    3) Las columnas de la vista sys.syscolumns que tienen el tipo smallint son: xusertype, length, colid, xoffset, colstat, number, colorder, offset, usertype, prec.
    4) Analizando las columnas smallint, podemos observar que las únicas columnas donde podemos influir en los valores son las columnas colid y colorder, que son identificadores de columnas dentro de la tabla (objeto padre) e identificador de orden de columnas (generalmente las dos columnas tienen el mismo valor), que pueden variar según la cantidad de columnas de nuestra tabla.

Sospechando de estas columnas y teniendo en cuenta que están llenas de incremento automático, imaginé que, por alguna razón, esta rutina de BI podría estar eliminando y recreando columnas en la tabla de hechos diariamente, provocando que este número solo aumentara y, en cierto punto, alcanzara el límite del tipo de datos pequeño. Consulté con el analista responsable de la rutina que presentaba esta falla y confirmó mi sospecha. La rutina realmente borra y crea columnas a diario.

Después de confirmar el problema de origen, pasamos a la solución por ahora, que era crear otra tabla con la misma estructura que la tabla original, copiar los datos a esta otra tabla, eliminar la tabla original y cambiar el nombre de la nueva tabla al nombre original. De esta manera, las columnas colid y colorder de esta tabla se “restablecerían” a 1, 2, 3… La solución final debe realizarse en la rutina, pero al menos pudimos hacer que la rutina funcione y no permitir que otras rutinas/usuarios no puedan usar esta vista de catálogo bancario debido a una tabla.

Una solución rápida que también podría resolver el problema por ahora sería cambiar la rutina ETL para usar una de las siguientes opciones, que tienen la columna de definición de orden de tipo int, cuyo límite es 2,147,483,647:

  • sys.all_columns (columna_id)
  • sys.columns (columna_id)
  • INFORMACIÓN_ESQUEMA.COLUMNAS (POSICIÓN_ORDINAL)

Si desea simular este caso, siga el siguiente script:

-- Consultando a view
SELECT * FROM sys.syscolumns


-- Criando a estrutura da tabela
IF (OBJECT_ID('Testes.dbo.Estouro_Sys_Columns') IS NOT NULL) DROP TABLE Testes.dbo.Estouro_Sys_Columns
CREATE TABLE Testes.dbo.Estouro_Sys_Columns (
    Id INT IDENTITY(1,1),
    Nome VARCHAR(50),
    Tmp_Coluna1 VARCHAR(20),
    Tmp_Coluna2 VARCHAR(20),
    Tmp_Coluna3 VARCHAR(20),
    Tmp_Coluna4 VARCHAR(20)
)

CREATE CLUSTERED INDEX SK01_Id ON dbo.Estouro_Sys_Columns(Id)



-- Simulando a rotina sendo executada várias vezes

DECLARE @Contador INT = 1, @Total INT = 10000

WHILE(@Contador <= @Total)
BEGIN
    
    ALTER TABLE Testes.dbo.Estouro_Sys_Columns DROP COLUMN
        Tmp_Coluna1,
        Tmp_Coluna2,
        Tmp_Coluna3,
        Tmp_Coluna4
        
        
    ALTER TABLE Testes.dbo.Estouro_Sys_Columns ADD
        Tmp_Coluna1 VARCHAR(20),
        Tmp_Coluna2 VARCHAR(20),
        Tmp_Coluna3 VARCHAR(20),
        Tmp_Coluna4 VARCHAR(20)
        
    
    SET @Contador = @Contador + 1
    
END


-- Tentando consultar a view novamente
SELECT * FROM sys.syscolumns -- ERRO! Arithmetic overflow error for data type smallint, value = 40003.


-- CORRIGINDO O PROBLEMA - Criação da estrutura igual à tabela original
IF (OBJECT_ID('Testes.dbo.Estouro_Sys_Columns2') IS NOT NULL) DROP TABLE Testes.dbo.Estouro_Sys_Columns2
CREATE TABLE Testes.dbo.Estouro_Sys_Columns2 (
    Id INT IDENTITY(1,1),
    Nome VARCHAR(50),
    Tmp_Coluna1 VARCHAR(20),
    Tmp_Coluna2 VARCHAR(20),
    Tmp_Coluna3 VARCHAR(20),
    Tmp_Coluna4 VARCHAR(20)
)

CREATE CLUSTERED INDEX SK01_Id ON dbo.Estouro_Sys_Columns2(Id)


-- CORRIGINDO O PROBLEMA - Inserindo os dados da tabela (caso haja)
SET IDENTITY_INSERT dbo.Estouro_Sys_Columns2 ON

INSERT INTO dbo.Estouro_Sys_Columns2 ( 
    Id,
    Nome,
    Tmp_Coluna1,
    Tmp_Coluna2,
    Tmp_Coluna3,
    Tmp_Coluna4 
)
SELECT * FROM dbo.Estouro_Sys_Columns


-- CORRIGINDO O PROBLEMA - Apagando a tabela antiga e renomeando a nova
DROP TABLE dbo.Estouro_Sys_Columns
EXEC sp_rename 'dbo.Estouro_Sys_Columns2', 'Estouro_Sys_Columns'


-- Consultando a view
SELECT * FROM sys.syscolumns -- WHERE id = OBJECT_ID('dbo.Estouro_Sys_Columns')