1. Introdução

Participar dos fóruns de SQL Server é estar às vezes frente a perguntas inesperadas. Recentemente encontrei tópico em que era questionado se seria correto utilizar varchar(99) para declarar todas as colunas para armazenamento de string de tamanho variável, mesmo que a coluna fosse armazenar strings de tamanho máximo bem menores do que 99, citando caso de 10 caracteres.

2. O que diz a documentação?

Na documentação para o tipo de dados varchar(n) consta que “O tamanho do armazenamento é o tamanho real dos dados inseridos + 2 bytes”. Ou seja, se em determinada linha o tamanho do string a ser armazenado na coluna é de 12 bytes, independente da coluna ser declarada como varchar(20) ou varchar(8000) no banco de dados aquela informação ocupará 14 bytes no banco de dados. Considerando-se este aspecto, então a resposta à pergunta seria “pode declarar todas as colunas como varchar(99) que não há problema”.

3. O que faz o otimizador de consultas?

Quando o otimizador de consultas do SQL Server gera o plano de execução de consulta, e no mesmo ocorrem determinadas ações, é necessário estimar o quanto de memória deverá ser alocada previamente para a execução da consulta; para isso um dos parâmetros é o tamanho da linha. No caso de linhas em que há colunas de tamanho variável, o gerenciador não mantém informações sobre qual é o tamanho médio das informações gravadas na coluna e utiliza então valor arbitrário, definido a partir do tamanho máximo que a coluna permite. Por exemplo, no caso de uma coluna declarada como varchar(100), o otimizador de consultas considera a metade mais 2 bytes, 50 + 2, como o valor a ser utilizado no cálculo do tamanho estimado da linha, em memória.

Antes de continuar é necessário revisar o conceito de “concessão de memória” (grant memory, em inglês). Na introdução do artigo Understanding SQL server memory grant (vide item Referências, ao final) consta que “memory grant is a part of server memory used to store temporary row data while sorting and joining rows. (...) This reservation improves query reliability under server load, because a query with reserved memory is less likely to hit out-of-memory while running, and the server prevents one query from dominating entire server memory”.

4. Testando...

Supondo que no banco de dados testeDB exista tabela denominada Correspondencia e que nela constem as colunas Nome, Logradouro, Cidade e Estado com os seguintes tamanhos máximos: 60, 80, 40, 40 (respectivamente) caracteres. Para fins de testes, a mesma tabela será declarada com os valores definidos como máximo mas também com valor fixo de 100.

-- código #1
USE testDB;
 
CREATE TABLE Correspondencia_1 (
  Id int primary key,
  Nome varchar(60),
  Idade int,
  Logradouro varchar(80),
  Cidade varchar(40),
  Estado varchar(40)
);
 
CREATE TABLE Correspondencia_2 (
  Id int primary key,
  Nome varchar(100),
  Idade int,
  Logradouro varchar(100),
  Cidade varchar(100),
  Estado varchar(100)
);

Se formos considerar o tamanho máximo de cada coluna, cada linha da tabela Correspondencia_1 terá 228 bytes (4 + 60 + 4 + 80 + 40 + 40). Já para a tabela Correspondencia_2 cada linha terá 408 bytes (4 + 100 + 4 + 100 + 100 + 100). Observe que nesses cálculos não foram considerados header e outras informações de controle da linha.

As duas tabelas serão carregadas com informação semelhante, usando o seguinte código:

-- código #2
declare @max int, @rc int;
set @max = 10000;
set @rc = 1;
 
set nocount on;
 
-- carrega linha inicial
INSERT into Correspondencia_1 (Id, Nome, Idade, Logradouro, Cidade, Estado)
  values (1, replicate('A', 60), 80, replicate('B', 80),
          replicate('C', 40), replicate('D', 40));
 
-- repete a linha inicial até completar N linhas
while @rc * 2 <= @max
  begin
  INSERT into Correspondencia_1
              (Id, Nome, Idade, Logradouro, Cidade, Estado)
       SELECT Id + @rc, Nome, ((Id % 60)+1), Logradouro, Cidade, Estado
         from Correspondencia_1;
  set @rc = @rc * 2;
  end;
-- último bloco
INSERT into Correspondencia_1
            (Id, Nome, Idade, Logradouro, Cidade, Estado)
     SELECT Id + @rc, Nome, Idade, Logradouro, Cidade, Estado
       from Correspondencia_1
       where (Id + @rc) <= @max;
 
-- carrega tabela Correspondencia_2
INSERT into Correspondencia_2
            (Id, Nome, Idade, Logradouro, Cidade, Estado)
     SELECT Id, Nome, Idade, Logradouro, Cidade, Estado
       from Correspondencia_1;

Após carregadas as duas tabelas, o seguinte código é executado na tabela Correspondencia_1:

-- código #3
SELECT Estado, Cidade, Nome
  from Correspondencia_1
  order by Estado, Cidade;

Ao analisar a imagem abaixo do plano de execução da consulta na tabela Correspondencia_1, observe qual é o valor de “Tamanho Estimado da Linha”.

E na figura abaixo qual é o valor de “Concessão de Memória”.

Após, ao executar o seguinte código, agora na tabela Correspondencia_2:

-- código #4
SELECT Estado, Cidade, Nome
  from Correspondencia_2
  order by Estado, Cidade;

observe no plano de execução abaixo qual é o valor de “Tamanho Estimado da Linha”:

e também qual é o valor de “Concessão de Memória”:

5. Conclusão

Compilando em um quadro as informações obtidas, temos

Tabela

Tamanho Estimado de Linha  Concessão de Memória 
 Correspondencia_1  85 B

 2656
 Correspondencia_2  165 B

 4224

O mesmo conteúdo foi carregado nas duas tabelas mas os valores do quadro diferem, sendo confirmado que ao superestimar o tamanho das colunas há maior reserva de memória para a execução da consulta.

No caso dos códigos #3 e #4 o cálculo de Tamanho Estimado de Linha é simples: obtém-se o tamanho estimado das colunas que estão na lista de colunas:

  • tabela Correspondencia_1: temos os tamanhos máximos de 40 (Estado), 40 (Cidade) e 60 (Nome). Obtendo-se a metade e somando 2 para cada coluna, temos (40 / 2 + 2) + (40 / 2 + 2) + (60 / 2 + 2) = 22 + 22 + 32 = 76 bytes
  • tabela Correspondencia_2: temos os tamanhos máximos de 100 (Estado), 100 (Cidade) e 100 (Nome). Obtendo-se a metade e somando 2 para cada coluna, temos (100 / 2 + 2) + (100 / 2 + 2) + (100 / 2 + 2) = 52 + 52 + 52 = 156 bytes
A diferença entre os valores calculados acima e os exibidos nos planos de execução é de 9 bytes, que referem-se ao header e outros controles. A forma como o cálculo exato é realizado encontra-se no documento “Estimate the Size of a Clustered Index”.

6. Referências

Tópico que originou este artigo

Documentação relacionada ao assunto

Artigo completo

  • clique aqui para obter a versão completa deste artigo, em formato PDF