Compactação do SQL Server 2014 e tamanho máximo de linha

8

Preciso criar uma ampla tabela desnormalizada com muitas colunas decimais (26,8) (limite inferior a 1024, a maioria das colunas seria nula ou zero). Eu sei sobre 8060 bytes por limitação de linha, então tentei criar tabela com compactação de página. O código abaixo cria a tabela, insere uma linha e consulta o tamanho da linha. O tamanho da linha está muito abaixo do limite, mas se eu tentar adicionar mais uma coluna decimal (26,8) à tabela, a operação falhará com o erro "A criação ou alteração da tabela 't1' falhou porque o tamanho mínimo da linha seria 8074, incluindo 1256 bytes de sobrecarga interna. " Existe alguma maneira de criar tabela única com tantas colunas?

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
fonte
11
FWIW, posso obter 613 DECIMAL(26, 8) NULLcampos em uma tabela, sem compactação de página ou compactação decimal. Ativando a compactação vardecimal, mas não a página, a sobrecarga salta para mais de 1 K. Há uma chance externa de que você possa armazenar mais campos por página sem vardecimal, dependendo dos seus valores.
Jon of All Trades

Respostas:

4

O limite que você está enfrentando não tem nada a ver com os dados armazenados na página. O cálculo é feito com base nos tipos de dados das colunas. É por isso que você encontra o erro sem nenhum dado na tabela. A compactação torna esse limite pior. Você pode ler sobre os detalhes técnicos por trás da sobrecarga aqui .

Você pode solucionar esse problema usando colunas SPARSE . Isso significa que será possível que as inserções falhem, dependendo do que você inserir, mas você pode ignorar o limite de 8060 bytes. O código a seguir mostra que você pode criar 1023 colunas corretamente:

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

No entanto, todas as restrições ao seu redor (leia o artigo vinculado) podem tornar isso inadequado para o seu caso de uso. Especificamente, apenas NULLvalores (não 0) são otimizados para ocupar muito pouco espaço. Se você tentar inserir muitos 0s em uma única linha, receberá um erro. Aqui está o que vejo quando tento inserir 0valores 1023 :

Msg 511, Nível 16, Estado 1, Linha 1 Não é possível criar uma linha do tamanho 17402 que seja maior que o tamanho máximo permitido da linha de 8060.

Suponho que, se você ficou realmente desesperado, pode criar as colunas como VARCHAR(27)alternativa. As colunas de comprimento variável podem ser movidas para fora da página para que você possa exceder o limite de 8060 bytes na definição da tabela, mas a inserção de certas combinações de valores falhará. O SQL Server avisa sobre isso ao criar a tabela:

Aviso: A tabela "t1" foi criada, mas seu tamanho máximo de linha excede o máximo permitido de 8060 bytes. INSERT ou UPDATE para esta tabela falhará se a linha resultante exceder o limite de tamanho.

A compactação de página ou linha pode ser útil se você seguir a VARCHAR(27)abordagem. Isso minimizará o espaço usado por ambos 0e NULL. Com VARCHAR(27)eu sou capaz de inserir 1023 0valores muito bem.

Joe Obbish
fonte
2

Fora dos aspectos técnicos e da solução alternativa proposta (usando VARCHAR(27)colunas) discutidos na resposta de @ Joe , questiono a " necessidade de criar [uma] ampla tabela desnormalizada" conforme expressa pelo OP A menos que haja algum requisito técnico estranho de que todas essas colunas deve estar em uma única tabela, eu sugeriria / recomendaria espalhá-las por quantas tabelas de "irmãos" forem necessárias. Tabelas de irmãos sendo tabelas que:

  • ter um relacionamento de 1 para 1,
  • todos têm exatamente a mesma chave primária,
  • apenas um tem a IDENTITYcoluna (e nenhum FK para os outros)
  • o restante possui uma chave estrangeira (na coluna PK) apontando para a PK da tabela que possui o IDENTITY

Aqui você está dividindo a linha lógica em duas ou mais tabelas físicas. Mas isso é essencialmente o que é normalização, e quais bancos de dados relacionais são projetados para lidar.

Nesse cenário, você incorre em espaço extra usado pela duplicação da PK e em alguma complexidade adicional de consulta devido à necessidade de INNER JOINjuntar as tabelas (frequentemente, mas nem sempre, a menos que todas as SELECTconsultas usem todas as colunas, mas isso geralmente não acontece) ou criar uma transação explícita a INSERTou UPDATE-los juntos ( DELETEpode ser tratada através ON DELETE CASCADEset no FK).

NO ENTANTO, você obtém os benefícios de ter um modelo de dados adequado com tipos de dados nativos adequados e nenhum truque que possa ter consequências imprevisíveis posteriormente. Mesmo que o uso VARCHAR(27)permita que isso funcione em um nível técnico, pragmaticamente, não acho que armazenar decimais como seqüências de caracteres seja do seu interesse / do projeto.

Portanto, se você estiver apenas "precisando" de uma única tabela devido a não perceber que uma única entidade lógica não precisa ser representada fisicamente em um único contêiner, não tente forçar tudo isso em uma única tabela quando ela funcionar. graciosamente em várias tabelas.

O exemplo abaixo ilustra o conceito básico:

CONFIGURAÇÃO

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

TESTE

EXEC #TestInsert 'aa', 454567678989;

EXEC #TestInsert 'bb', 12312312312234;

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

Devoluções:

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
fonte