SQL Server 2014 compresión y tamaño máximo de fila

8

Necesito crear una amplia tabla desnormalizada con muchas columnas decimales (26,8) (menos del límite de 1024 columnas, la mayoría de las columnas serían nulas o cero). Sé acerca de la limitación de 8060 bytes por fila, así que intenté crear una tabla con compresión de página. El siguiente código crea una tabla, inserta una fila y consulta el tamaño de la fila. El tamaño de la fila está muy por debajo del límite, pero si intento agregar una columna decimal más (26,8) a la tabla, la operación falla con el error "Error al crear o modificar la tabla 't1' porque el tamaño mínimo de la fila sería 8074, incluyendo 1256 bytes de sobrecarga interna ". ¿Hay alguna forma de crear una sola tabla con tantas columnas?

drop table t1
GO
create table t1(c1 decimal(26, 8) null)
with (data_compression = page)
GO

declare @i int = 2;
declare @sql varchar(100);
while @i <= 486
begin
    set @sql = 'alter table t1 add c' + convert(varchar, @i) + ' decimal(26, 8) null';
    execute (@sql);
    set @i += 1;
end;
GO


insert into t1(c1) select 0
GO
declare @i int = 2;
declare @sql varchar(100);
while @i <= 486
begin
    set @sql = 'update t1 set c' + convert(varchar, @i) + ' = 0';
    execute (@sql);
    set @i += 1;
end;
GO

select max_record_size_in_bytes from sys.dm_db_index_physical_stats (db_id(), object_id('t1'), NULL, NULL, 'DETAILED')
GO
Alex
fuente
1
FWIW, puedo obtener 613 DECIMAL(26, 8) NULLcampos en una tabla, sin compresión de página o compresión decimal. Al habilitar la compresión vardecimal pero no la página, la sobrecarga salta a más de 1 K. Existe la posibilidad externa de que pueda almacenar más campos por página sin vardecimal, dependiendo de sus valores.
Jon of All Trades

Respuestas:

4

El límite con el que se encuentra no tiene nada que ver con los datos almacenados en la página. El cálculo se realiza en función de los tipos de datos de las columnas. Es por eso que te encuentras con el error sin ningún dato en la tabla. La compresión empeora este límite. Puede leer sobre los detalles técnicos detrás de los gastos generales aquí .

Puede solucionar este problema utilizando columnas SPARSE . Eso significa que será posible que las inserciones fallen, dependiendo de lo que inserte, pero puede omitir el límite de 8060 bytes. El siguiente código muestra que puede crear 1023 columnas muy bien:

drop table t1
GO
create table t1(c1 decimal(26, 8) null)
GO

declare @i int = 2;
declare @sql varchar(100);
while @i <= 1023
begin
    set @sql = 'alter table t1 add c' + convert(varchar, @i) + ' decimal(26, 8) SPARSE null';
    execute (@sql);
    set @i += 1;
end;
GO

Sin embargo, todas las restricciones que lo rodean (lea el artículo vinculado) pueden hacer que esto no sea adecuado para su caso de uso. Específicamente, solo los NULLvalores (no 0) están optimizados para ocupar muy poco espacio. Si intenta insertar demasiados correos 0electrónicos en una sola fila, obtendrá un error. Esto es lo que veo cuando intento insertar 0valores 1023 :

Msg 511, Nivel 16, Estado 1, Línea 1 No se puede crear una fila de tamaño 17402 que sea mayor que el tamaño de fila máximo permitido de 8060.

Supongo que si realmente te desesperas podrías crear las columnas como en su VARCHAR(27)lugar. Las columnas de longitud variable se pueden mover fuera de la página para que pueda exceder el límite de 8060 bytes en la definición de la tabla, pero la inserción de ciertas combinaciones de valores fallará. SQL Server le advierte sobre esto al crear la tabla:

Advertencia: se ha creado la tabla "t1", pero su tamaño máximo de fila excede el máximo permitido de 8060 bytes. INSERTAR o ACTUALIZAR en esta tabla fallará si la fila resultante excede el límite de tamaño.

La compresión de páginas o filas puede ser útil si sigue el VARCHAR(27)enfoque. Eso minimizará el espacio utilizado por ambos 0y NULL. Con VARCHAR(27)soy capaz de insertar 1023 0valores muy bien.

Joe Obbish
fuente
2

Fuera de los aspectos técnicos y la solución propuesta (usando VARCHAR(27)columnas) discutidas en la respuesta de @ Joe , cuestiono la " necesidad de crear [una] tabla desnormalizada amplia" como lo expresa el OP A menos que haya algún requisito técnico extraño de que todas estas columnas debe estar en una sola tabla, sugeriría / recomendaría distribuirlas en tantas tablas de "hermanos" como sea necesario. Las tablas hermanas son tablas que:

  • tener una relación de 1 a 1 entre sí,
  • todos tienen exactamente la misma clave primaria,
  • solo uno tiene la IDENTITYcolumna (y no FK para los demás)
  • el resto tiene una clave externa (en la columna PK) que apunta a la PK de la tabla que tiene IDENTITY

Aquí está dividiendo la fila lógica en dos o más tablas físicas. Pero eso es esencialmente lo que es la normalización de todos modos, y qué bases de datos relacionales están diseñadas para manejar.

En este escenario, se incurre en algo de espacio adicional utilizado al duplicar la PK, y en una complejidad de consulta adicional debido a la necesidad de INNER JOINunir las tablas (con frecuencia pero no siempre, a menos que todas las SELECTconsultas usen todas las columnas, pero eso generalmente no sucede) o cree una Transacción explícita a ellos INSERTo UPDATEjuntos ( DELETEpuede manejarse a través del ON DELETE CASCADEconjunto en el FK).

SIN EMBARGO, obtienes los beneficios de tener un modelo de datos adecuado con tipos de datos nativos y adecuados, y ningún truco que podría tener consecuencias imprevistas más adelante. Incluso si el uso VARCHAR(27)permite que esto funcione a nivel técnico, pragmáticamente no creo que almacenar decimales como cadenas sea lo mejor para su / el proyecto.

Entonces, si solo está "necesitando" una sola tabla debido a que no se da cuenta de que una sola entidad lógica no necesita ser representada físicamente en un solo contenedor, entonces no intente forzar todo esto en una sola tabla cuando funcione con gracia en varias mesas.

El siguiente ejemplo ilustra el concepto básico:

PREPARAR

CREATE TABLE tempdb.dbo.T1
(
  [ID] INT NOT NULL IDENTITY(11, 2) PRIMARY KEY,
  [Col1] VARCHAR(25),
  [Col2] DATETIME NOT NULL DEFAULT (GETDATE())
);

CREATE TABLE tempdb.dbo.T2
(
  [ID] INT NOT NULL PRIMARY KEY
                    FOREIGN KEY REFERENCES tempdb.dbo.T1([ID]) ON DELETE CASCADE,
  [Col3] UNIQUEIDENTIFIER,
  [Col4] BIGINT
);

GO
CREATE PROCEDURE #TestInsert
(
  @Val1 VARCHAR(25),
  @Val4 BIGINT
)
AS
SET NOCOUNT ON;

BEGIN TRY
  BEGIN TRAN;

  DECLARE @InsertedID INT;

  INSERT INTO tempdb.dbo.T1 ([Col1])
  VALUES (@Val1);

  SET @InsertedID = SCOPE_IDENTITY();

  INSERT INTO tempdb.dbo.T2 ([ID], [Col3], [Col4])
  VALUES (@InsertedID, NEWID(), @Val4);

  COMMIT TRAN;
END TRY
BEGIN CATCH
  IF (@@TRANCOUNT > 0)
  BEGIN
    ROLLBACK TRAN;
  END;

  THROW;
END CATCH;

SELECT @InsertedID AS [ID];
GO

PRUEBA

EXEC #TestInsert 'aa', 454567678989;

EXEC #TestInsert 'bb', 12312312312234;

SELECT *
FROM   tempdb.dbo.T1
INNER JOIN tempdb.dbo.T2
        ON T2.[ID] = T1.[ID];

Devoluciones:

ID  Col1  Col2                     ID  Col3                                  Col4
11  aa    2017-07-04 10:39:32.660  11  44465676-E8A1-4F38-B5B8-F50C63A947A4  454567678989
13  bb    2017-07-04 10:41:38.180  13  BFE43379-559F-4DAD-880B-B09D7ECA4914  12312312312234
Solomon Rutzky
fuente