Dicas T-SQL – CPF: Qual tipo de dados devo utilizar? BIGINT ou CHAR?

Olá humanos,

Nesse post, quero mostrar um caso real de como um problema técnico (TIPO DE DADOS utilizado no CPF) pode afetar milhares de pessoas. Nós vamos discutir a seguinte pergunta:

CPF: Qual tipo de dados devo utilizar? BIGINT ou CHAR?

A) BIGINT

B) CHAR

C) Uai, eu acho que o INT é melhor ainda!

D) Nenhum dos anteriores, é o NVARCHAR seu mané!

E) Depende! Resposta padrão na computação kkkk

F) Tanto faz! Escolhe qualquer um que não vai fazer diferença =)

G) Não sei, vou perguntar pro Fabiano Amorim e pro Fabrício Lima xD

Escolha a sua opção e confira depois o resultado! #gogogo


Introdução – Problema CPF:

Após ver o print da notícia abaixo do dia 27/04/2020 e ter uma discussão muito TOP com o #teampowertuning (thanks Fabiano Amorim e Fabrício Lima), resolvi escrever esse post e compartilhar um pouco desse conhecimento, pois essa é uma dúvida bem comum.

Referência: https://economia.uol.com.br/noticias/redacao/2020/04/27/auxilio-r-600-zero-cpf-some-confirmacao-do-pedido.htm

De forma resumida, o Governo Brasileiro liberou um Auxílio Emergencial para amenizar a crise gerada pela Pandemia do Coronavírus. Contudo, devido à um problema técnico no sistema da Caixa Econômica, milhares de pessoas não conseguiram finalizar o pedido. O problema era que o sistema apagava o zero do início do CPF.

“Como assim Luiz???”

Por exemplo, se alguém informar o CPF 061.497.760-69, desconsiderando a pontuação nós temos o valor 06149776069. Agora vamos comparar os valores que seriam armazenados dependendo do tipo de dados utilizado:

CPF – BIGINT -> 6149776069

CPF – CHAR -> “06149776069”

Dessa forma, conseguimos mostrar o problema, onde os CPFs que iniciam com zero ficam com 10 dígitos ao invés de 11 (os zeros a esquerda são desconsiderados nos tipos numéricos) e retornam um problema no final da análise!

OBS: Gerei o CPF utilizando esse site:

Gerador de CPF: https://www.4devs.com.br/gerador_de_cpf

Agora vamos aprofundar um pouco mais nessa discussão.


SQL Server – Tipos de Dados:

Em primeiro lugar, precisamos entender alguns tipos de dados no SQL Server. Nós precisamos representar um número com 11 dígitos. Aqui a primeira dica é você ignorar a pontuação “.” e “-”. Ou seja, devemos armazenar somente os números “06149776069” ao invés disso “061.497.760-69”. Deixe para colocar a pontuação depois que o dado for retornado, pois você pode economizar mais espaço no armazenamento também.

“Visto isso, quais os tipos de dados que nós poderíamos utilizar?”

Referência: https://docs.microsoft.com/pt-br/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver15

Aqui vou resumir alguns deles (não vou entrar em muitos detalhes):

INT:

Esse tipo de dados utiliza 4 Bytes. Com isso, é possível representar os valores -2^31 (-2,147,483,648) até 2^31-1 (2,147,483,647). Bora testar então!

Como podemos ver, o INT NÃO irá atender ao nosso requisito, pois ele não consegue armazenar um valor tão grande! Inclusive ele gera um erro na query com um ARITHMETIC OVERFLOW!

Msg 8115, Level 16, State 2, Line 7

Arithmetic overflow error converting expression to data type int.

BIGINT:

Esse tipo de dados utiliza 8 Bytes (o dobro do tipo INT). Com isso, é possível representar os valores -2^63 (-9,223,372,036,854,775,808) até 2^63-1 (9,223,372,036,854,775,807). Bora testar também!

Opa! Dessa vez a query foi executada com sucesso, então nós podemos utilizar o BIGINT! Repare que ele pode representar valores muito maiores do que o tipo INT.

CHAR, VARCHAR e NVARCHAR:

Um caractere é representado por um byte com o CHAR(n) que utiliza um tamanho fixo, enquanto o VARCHAR(n) tem um tamanho variável. Representa os caracteres da tabela ASCII.

O NCHAR(n) (fixo) ou NVARCHAR(n) (variável) utilizam 2 bytes por caractere, pois eles representam os caracteres UNICODE. Portanto, utilize esse tipo de dados APENAS se for realmente necessário! Infelizmente, alguns Frameworks criam os objetos no banco de dados utilizando o NVARCHAR por padrão e na maioria das vezes isso poderia ser evitado! Vamos para mais um teste!

Repare que os tipos CHAR E VARCHAR utilizaram 11 bytes (função DATALENGTH), ou seja, ficaram com o mesmo tamanho. Já o NVARCHAR utilizou 22 bytes, como era esperado, pois ele ocupa o dobro do espaço. Como sabemos que o CPF possui 11 dígitos (tamanho fixo), vamos considerar aqui o tipo CHAR.

No post abaixo, o Macoratti faz algumas comparações bem legais entre os tipos “Varchar, Char, NVarchar, NChar e suas variantes MAX”. Recomendo a leitura!

Referência: http://www.macoratti.net/11/03/sql_nv1.htm

Portanto, no nosso caso, vamos focar nos tipos BIGINT e CHAR(11). Agora vamos para a discussão principal do post e a parte mais legal! #gogogo


BIGINT ou CHAR(11)???

Quero destacar aqui dois links muito interessantes que o Fabiano Amorim indicou sobre essa discussão. Recomendo fortemente que leiam eles também!

https://pt.stackoverflow.com/questions/47871/tipo-do-campo-cpf-ou-cnpj-no-banco-de-dados-varchar-ou-int

https://social.msdn.microsoft.com/Forums/pt-BR/7299a954-8520-482a-a345-512b31c410dd/qual-o-melhor-tipo-dados-para-guardar-compos-especficos?forum=520

Como vimos anteriormente, o tipo INT não suporta a quantidade de dígitos do CPF. Portanto, esse tipo já está descartado!

O grande problema de utilizar o tipo BIGINT é que podemos ter uma falha grave, pois como podemos ver, os CPFs que iniciam com 0 ficam com um dígito a menos.

Acredito que só esse argumento já pode ser um grande empecilho para a utilização desse tipo de dados.

“Ah Luiz, mas aí é só completar o 0 que está faltando depois!!!”

OK, isso é verdade. Entretanto, imagine você ter que fazer esse tratamento em todas as aplicações e nas queries que utilizam esse campo. Vai ser um trabalho do cãoo!!! Será que realmente vale a pena esse esforço?

Por exemplo, primeiro nós teríamos que converter o tipo BIGINT para CHAR e depois fazer o tratamento para incluir os zeros no início. Com isso, podemos perder todo o ganho que tínhamos antes, além de aumentar o trabalho de quem está desenvolvendo! Já pensou se o desenvolvedor esquece desse detalhe? Isso poderia gerar um problema grave no seu sistema!

“Pô Luiz, mas o tipo BIGINT utiliza menos espaço que o CHAR(11)!!!”

Verdade também. Vamos fazer mais um teste para mostrar essa diferença.

O CHAR(11) utilizou 11 bytes e o BIGINT apenas 8, ou seja, serão 3 bytes de diferença por cada registro.

Fiz um outro teste inserindo 1 milhão de CPFs em duas tabelas e a diferença no espaço utilizado foi de aproximadamente 15%. É claro que com milhões de registros essa diferença pode ser ainda maior, mas repare que o problema dos CPFs que iniciam com 0 ainda permanece no caso do BIGINT.

Será que essa diferença de espaço irá trazer ganhos consideráveis? Será que a performance irá melhorar?

BIGINT -> 17192 KB

CHAR(11) -> 20264 KB

DIFERENÇA -> 3072 KB

“Ah não Luiz, se o BIGINT é menor, o desempenho dele será MUITO melhor, certo?”

Então, não é bem por aí. Vamos fazer mais testes utilizando a tabela com um milhão de registros.


Teste Performance – Clustered Index Scan

Aqui vou fazer um JOIN da tabela “TESTE_CPF_BIGINT” com ela mesma pelo CPF que é do tipo BIGINT. Nesse caso, a tabela inteira será retornada e por esse motivo o SQL Server faz um “Clustered Index Scan”.

OBS: Vou inserir o resultado em uma tabela apenas para não contabilizar o tempo do SSMS exibir os registros.

Agora vou fazer a mesma coisa com a tabela “TESTE_CPF_CHAR” cujo CPF é do tipo CHAR(11). Aqui o comportamento será o mesmo.

Comparando o Resultado:

Como podemos ver, realmente o tipo BIGINT é mais eficiente e também faz menos leituras na memória (logical reads). Contudo, repare que o tempo total (elapsed time) teve uma diferença de apenas 66 milissegundos. Ou seja, o BIGINT NÃO teve um desempenho muito melhor.

É claro que aqui estamos executando apenas uma query. Pode ser que essa diferença seja maior se milhares de queries fossem executadas simultaneamente ou se a tabela tivesse milhões de registros.


Teste Performance – Clustered Index Seek

Por fim, vamos para os últimos testes fazendo uma busca por apenas um CPF e comparar os resultados.

Comparando o Resultado:

Repare que nesse caso a performance foi a mesma, pois estamos retornando apenas uma linha e isso será bem rápido. Isso aconteceu porque o SQL Server identificou que apenas uma linha seria retornada e fez um “Clustered Index Seek” que é uma operação muito mais rápida! Ou seja, quando precisamos retornar poucos registros, a diferença no tipo de dados não irá influenciar muito no resultado.

O link abaixo faz algumas comparações muito interessantes com JOINs com diferentes tipos de dados. #ficaadica

https://sqlinthewild.co.za/index.php/2011/02/15/are-int-joins-faster-than-string-joins-2/


RESPOSTAS – CPF: Qual tipo de dados devo utilizar? BIGINT ou CHAR?

Finalmente podemos responder cada uma das opções do início do post:

A) BIGINT

  • Se você quer priorizar a performance a qualquer custo, essa seria a melhor opção. Contudo, tenha em mente que você terá que lidar com a questão dos zeros à esquerda e fazendo a formatação do CPF você pode perder o ganho que tinha antes.

B) CHAR

  • Se você quer abrir mão de um pouco de performance mas ter um resultado íntegro sem se preocupar com a formatação do CPF e com zeros à esquerda, essa seria a melhor opção.

C) Uai, eu acho que o INT é melhor ainda!

  • Errado! Como podemos ver nos testes, o tipo INT não suporta a quantidade de números do CPF e gera um erro de ARITHMETIC OVERFLOW!

Msg 8115, Level 16, State 2, Line 7

Arithmetic overflow error converting expression to data type int.

D) Nenhum dos anteriores, é o NVARCHAR seu mané!

  • Errado! O tipo NVARCHAR ocupa o dobro do espaço e não é a melhor opção nesse caso!

E) Depende! Resposta padrão na computação kkkk

  • Errado! Depende de que? Resposta incompleta kkkk

F) Tanto faz! Escolhe qualquer um que não vai fazer diferença =)

  • Errado! Existem algumas diferenças entre cada tipo que podem ser cruciais para o seu ambiente. Se não conseguiu entender, sugiro que leia novamente o post hehe =)

G) Não sei, vou perguntar pro Fabiano Amorim e pro Fabrício Lima xD

  • Errado! Não vale apelar!!! kkkk

Conclusão:

Minhas considerações finais são:

  • Ignore a pontuação (“.” e “-”) ao armazenar o CPF, isso irá economizar espaço;
  • O tipo INT não comporta o CPF que possui 11 dígitos;
  • Os tipos NCHAR ou NVARCHAR não são uma boa opção para serem utilizados no CPF, pois ocupam o dobro de espaço do que o CHAR ou VARCHAR;
  • O tipo BIGINT possui uma performance melhor e ocupa menos espaço, contudo ele possui alguns efeitos colaterais que podem ser tratados posteriormente (zeros à esquerda), mas afetam a codificação ao utilizar o CPF. Esse tratamento pode acabar com o ganho da performance. Então pense bem antes de utilizar esse tipo;
  • Os tipos CHAR ou VARCHAR têm uma performance um pouco pior se comparado ao BIGINT, contudo eles não causam efeitos colaterais e com isso temos menos dor de cabeça na codificação ao utilizar o CPF.

Como podemos ver, não existe apenas uma resposta correta! Cabe a você escolher o que irá atender melhor o seu problema, desde que esteja ciente das consequências de cada escolha!


Download:

Segue abaixo os scripts utilizados nesse post:

https://github.com/luizvitorf/SQLServer/blob/master/Scripts/T-SQL/Dicas%20T-SQL%20-%20CPF%20-%20Qual%20tipo%20de%20dados%20devo%20utilizar%20-%20BIGINT%20OU%20CHAR.sql


Referências:

Segue mais alguns links de referência:

https://docs.microsoft.com/pt-br/sql/t-sql/data-types/int-bigint-smallint-and-tinyint-transact-sql?view=sql-server-ver15

https://docs.microsoft.com/pt-br/sql/t-sql/data-types/char-and-varchar-transact-sql?view=sql-server-ver15

https://docs.microsoft.com/pt-br/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql?view=sql-server-ver15

https://blogfabiano.com/2008/01/07/data-types/


Espero que tenha gostado e que isso também possa ser útil no seu dia a dia. Até o próximo post!

Me siga no LinkedIn e YouTube para ficar por dentro das novidades.

Abraço,

Luiz Vitor França Lima

Consultor SQL Server

9 comentários em “Dicas T-SQL – CPF: Qual tipo de dados devo utilizar? BIGINT ou CHAR?

  1. José D i z Responder

    Luiz, certa vez no grupo SQL Norte houve uma discussão a respeito de armazenamento de CPF e, considerando-se menor ocupação de espaço físico, a solução foi utilizar INTEGER para armazenar o número do CPF (os 9 primeiros algarismos) e TINYINT para armazenar os 2 algarismos finais, que são dígitos verificadores. Ou seja, somente 5 bytes por CPF!

    Para quem opta por armazenar como texto (CHAR ou VARCHAR), a sugestão é o CHAR(n). Afinal, ou serão armazenados n caracteres ou nada. É que caso opte pelo VARCHAR são utilizados 2 bytes adicionais para controle do tamanho da coluna.

    Armazenar campos sem a formatação (CPF, CNPJ etc) é uma preocupação em reduzir o espaço físico em disco. Aliás, a opção por retirar formatação vem da época dos mainframes e da programação em Cobol, quando qualquer byte economizado no armazenamento físico (discos e fitas) era um grande ganho. Temos aqui um ganha-perde: diminui o espaço físico ocupado mas aumenta o processamento ao retirar e reinserir a formatação.

    Então, se o objetivo é reduzir espaço físico eu optaria pelo par INTEGER+TINYINT, que ocupa somente 5 bytes por CPF. Afinal, se para exibir o CPF na tela é necessário acrescentar a formatação pode-se também nesse momento acrescentar zeros à esquerda.

    Já para quem quer reduzir o processamento adicional no tratamento da informação me parece que a melhor solução é armazenar o CPF formatado em coluna CHAR(14). Espaço em disco está cada vez mais barato.

    • Luiz Lima Autor do postResponder

      Muito obrigado pelo complemento José!

      Eu tinha visto algo parecido com essa solução. Realmente ela vai economizar ainda mais espaço, como você explicou.

      Contudo, novamente vamos entrar na questão da performance x espaço x codificação, onde poderíamos ganhar ainda mais performance e espaço, mas ao mesmo tempo iríamos aumentar ainda mais a dificuldade da codificação, pois todas as queries que utilizam CPF passariam a utilizar duas colunas, teriamos que ajustar JOINs, etc.

      Por fim, seria mais uma opção para o arquiteto da solução e ficaria a critério dele escolher a melhor opção =)

      Abraço,
      Luiz Vitor

  2. José D i z Responder

    Luiz, eu somente havia analisado a solução INTEGER + TINYINT do ponto de vista de redução de espaço físico. Não a havia analisado considerando a performance.

    Como sabemos o CPF é composto de 2 campos: o campo que contém o número em si (9 primeiros algarismos do CPF) e o campo que contém os dígitos verificadores (2 últimos algarismos do CPF). Para facilitar, vou denominar o primeiro campo de numeroCPF e o segundo campo de dvCPF. Este segundo campo, dvCPF, somente tem utilidade no caso de entrada de dados do mundo exterior para o banco de dados, de modo a garantir que não houve erro de digitação no campo numeroCPF. Depois disso ele é um inútil nos processos internos do banco de dados pois o campo que contém a informação, numeroCPF, foi validado na porta de entrada.

    Desta forma, nos processos internos do banco de dados basta utilizar a coluna que contém numeroCPF. Ela é suficiente.
    Por exemplo, em uma pesquisa por CPF basta algo como
    SELECT …
    from CLIENTE as C
    where C.numeroCPF = nnn;

    O mesmo processo no caso de junções:
    SELECT …
    from CLIENTE as C
    inner join TABELA as T on T.numeroCPF = C.numeroCPF
    where …;

    Eu somente via a solução INTEGER + TINYINT como uma solução para reduzir espaço físico mas agora percebo que provavelmente também seria mais rápida nos processos internos, se considerarmos os testes do artigo “Are int joins faster than string joins?”.

    A solução INTEGER + TINYINT requer esforço adicional de programação na aplicação (no processo de exibição) pois é necessário primeiro juntar as partes, com atenção aos zeros à esquerda, para depois aplicar a máscara de formatação. É claro que isso pode ser feito em T-SQL, mas provavelmente seja mais eficiente na aplicação.

    • Luiz Lima Autor do postResponder

      Obrigado mais uma vez pelas informações José. Realmente a solução com INTEGER + TINYINT pode ter um desempenho ainda melhor e ser muito útil, desde que o ambiente seja bem controlado e tenha os devidos cuidados. Caso a escolha seja ter a melhor performance a qualquer custo, essa pode ser uma boa opção, mesmo que isso gere um impacto depois na codificação para juntar as duas informações.

      Abraço,
      Luiz Vitor

  3. Alexandre Paiva Responder

    Antigamente quando eu dava aulas no início da carreira simplesmente dizia: vai usar para cálculos? Usa números. Se não vai, então é texto. É simplista, mas não cairia no erro do zero inicial.

    • Luiz Lima Autor do postResponder

      Fala Alexandre, é uma boa observação mesmo. Não faz sentido somar o CPF dos seus familiares por exemplo hehe. Realmente é uma forma simples e fácil de entender o conceito. Obrigado =)

      Abraço,
      Luiz Vitor

Deixe uma resposta