“O custo invisível dos campos NULL no MySQL”
A maioria dos desenvolvedores sabe que um campo NULL “economiza” espaço por não armazenar um valor efetivo, mas poucos sabem que existe um custo associado ao controle desses valores através do NULL Bitmap.
Muitos profissionais acreditam que declarar todos os campos como NULL não possui impacto de armazenamento. Na prática, o MySQL precisa manter uma estrutura interna para informar quais colunas possuem valores nulos em cada registro.
Essa estrutura é conhecida como NULL Bitmap.
Para cada coluna que permite NULL, o InnoDB reserva 1 bit dentro desse bitmap.
A fórmula é simples:
Bytes do NULL Bitmap = (Quantidade de colunas NULL + 7) / 8
Arredondando sempre para cima.
Exemplos:
|
Colunas NULL |
Bitmap |
|
1 a 8 |
1 byte |
|
9 a 16 |
2 bytes |
|
17 a 24 |
3 bytes |
|
25 a 32 |
4 bytes |
Essa fórmula parece estranha à primeira vista, mas é bem simples.
O MySQL utiliza 1 bit para cada coluna que permite NULL.
Lembre-se:
1 byte = 8 bits
Então o cálculo basicamente responde:
“Quantos bytes preciso para armazenar N bits?”
Exemplo 1:
1 coluna NULL
nome VARCHAR(100) NULL
Precisamos de:
1 bit
Mas não existe armazenamento de 1 bit isolado.
O menor espaço reservado será:
1 byte
Resultado:
(1 + 7) / 8 = 1
CRIANDO O CENÁRIO PARA TESTES:
Tabela tb_clientes com colunas permitindo NULL.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
DROP DATABASE IF EXISTS `db_bitmap_null`; CREATE DATABASE IF NOT EXISTS `db_bitmap_null` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bg_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; USE `db_bitmap_null`; DROP TABLE IF EXISTS `tb_clientes`; CREATE TABLE IF NOT EXISTS `tb_clientes` ( `id` int NOT NULL AUTO_INCREMENT, `nome` varchar(100) COLLATE utf8mb4_bg_0900_ai_ci DEFAULT NULL, `telefone` varchar(20) COLLATE utf8mb4_bg_0900_ai_ci DEFAULT NULL, `email` varchar(100) COLLATE utf8mb4_bg_0900_ai_ci DEFAULT NULL, `instagram` varchar(100) COLLATE utf8mb4_bg_0900_ai_ci DEFAULT NULL, `facebook` varchar(100) COLLATE utf8mb4_bg_0900_ai_ci DEFAULT NULL, `linkedin` varchar(100) COLLATE utf8mb4_bg_0900_ai_ci DEFAULT NULL, `site` varchar(100) COLLATE utf8mb4_bg_0900_ai_ci DEFAULT NULL, `complemento` varchar(100) COLLATE utf8mb4_bg_0900_ai_ci DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bg_0900_ai_ci; |
Tabela tb_clientes_not_null com colunas permitindo NULL.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
CREATE TABLE `tb_clientes_not_null` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `nome` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_bg_0900_ai_ci', `telefone` VARCHAR(20) NOT NULL COLLATE 'utf8mb4_bg_0900_ai_ci', `email` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_bg_0900_ai_ci', `instagram` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_bg_0900_ai_ci', `facebook` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_bg_0900_ai_ci', `linkedin` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_bg_0900_ai_ci', `site` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_bg_0900_ai_ci', `complemento` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_bg_0900_ai_ci', PRIMARY KEY (`id`) USING BTREE ) COLLATE='utf8mb4_bg_0900_ai_ci' ENGINE=InnoDB; |
Inserindo 500 (milhões de linhas na tabela tb_clientes com colunas NULL)
|
1 2 3 4 5 6 7 8 |
USE db_bitmap_null; INSERT INTO tb_clientes(nome) SELECT 'Thiago Rafael' FROM information_schema.columns c1 CROSS JOIN information_schema.columns c2 CROSS JOIN information_schema.columns c3 LIMIT 500000000; |
A estimativa de tempo decorrido para a máquina a qual foi feito a inserção de dados na tabela tb_clientes foi de 03:11:10. Um tempo razoável.
Inserindo 500 (milhões de linhas na tabela tb_clientes_not_null com colunas NOT NULL)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
INSERT INTO tb_clientes_not_null ( nome, telefone, email, instagram, facebook, linkedin, site, complemento ) SELECT 'Thiago Rafael', '0', '0', '0', '0', '0', '0', '0' FROM information_schema.columns c1 CROSS JOIN information_schema.columns c2 CROSS JOIN information_schema.columns c3 LIMIT 500000000; |
A estimativa de tempo decorrido para a máquina a qual foi feito a inserção de dados na tabela tb_clientes_not_null foi de 02:15:49.
Inserindo500 (milhões de linhas na tabela tb_clientes_vazio com colunas permitindo NULL)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
INSERT INTO tb_clientes_not_null ( nome, telefone, email, instagram, facebook, linkedin, site, complemento ) SELECT 'Thiago Rafael', '', '', '', '', '', '', '' FROM information_schema.columns c1 CROSS JOIN information_schema.columns c2 CROSS JOIN information_schema.columns c3 LIMIT 500000000; |
A estimativa de tempo decorrido para a máquina a qual foi feito a inserção de dados na tabela tb_clientes_not_null foi de 02:31:17.
Query para verificação dos tamanhos das respectivas tabelas:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
SELECT table_name AS tabela, table_rows AS registros, ROUND(data_length / 1024 / 1024 / 1024, 2) AS dados_gb, ROUND(index_length / 1024 / 1024 / 1024, 2) AS indices_gb, ROUND((data_length + index_length) / 1024 / 1024 / 1024, 2) AS total_gb FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name IN ( 'tb_clientes', 'tb_clientes_not_null', 'tb_clientes_vazio' ) ORDER BY total_gb; |
Observem que o primeiro script de inserção na tabela tb_clientes tem em sua composição 9 colunas permitindo NULL.
Mesmo que nenhuma delas esteja nula, o InnoDB precisará reservar:
8 bits = 1 byte
por registro apenas para controlar o estado dessas colunas.
Agora imagine:
- 500 milhões de registros
- 1 byte por registro
Teríamos aproximadamente:
500.000.000 bytes ≈ 476MB apenas para o bitmap de NULL.
Os números ficaram muito interessantes e sustentam uma conclusão técnica bastante forte.
Comparativo estatístico
| Cénario | Tamanho (GB) | Diferença | Crescimento (%) |
| Casmpos NULL (tb_clientes) | 19,34 GB | – | – |
| Campos NOT NULL + string vazia (tb_clientes_vazio) | 23,87 GB | + 4,53 GB | + 23,42 % |
| Campos NOT NULL + ‘0’ (tb_clientes_not_null) | 27,38 GB | + 8,04 GB | + 41,57 % |
Além disso:
- String vazia (
'') → Valor'0'- Crescimento de 3,51 GB (Campos NOT NULL + ‘0’ ) – (Campos NOT NULL + string vazia ”)
- Equivale a aproximadamente 14,71% de aumento.
Outro ponto muito interessante
Muita gente pensa:
“Vou deixar tudo NULL porque um dia posso precisar.”
Mas isso gera:
- NULL Bitmap maior
- Mais lógica nas consultas
- Mais uso de
IS NULL - Mais complexidade em índices
- Maior ocupação em tabelas gigantes
Em muitos cenários, um valor padrão faz mais sentido:
Mas você pode estar se perguntando: qual a relação entre performance e o espaço ocupado em disco pelos campos que permitem valores NULL? Afinal, até aqui estamos falando apenas de bits e bytes adicionais de armazenamento.
A resposta é simples: tudo está diretamente relacionado!
O mecanismo de armazenamento InnoDB organiza os dados em páginas de 16 KB. À medida que cada registro ocupa mais espaço, mesmo que seja apenas alguns bytes adicionais provenientes do NULL Bitmap, a quantidade de registros armazenados por página diminui. Como consequência, o banco de dados precisa criar um número maior de páginas para armazenar a mesma quantidade de informações.
Esse crescimento impacta diretamente as operações de leitura. Quanto mais páginas existirem, maior será a quantidade de páginas que o MySQL precisará acessar para localizar e retornar os dados solicitados por uma consulta. Isso significa mais operações de I/O, maior consumo de memória no Buffer Pool e, potencialmente, um aumento no tempo de resposta das consultas.
Em tabelas pequenas, esse impacto costuma ser imperceptível. Entretanto, em ambientes que armazenam centenas de milhões ou até bilhões de registros, alguns bytes adicionais por linha podem representar gigabytes extras de armazenamento e milhões de páginas adicionais sendo gerenciadas pelo mecanismo de banco de dados.
Em outras palavras, quando falamos sobre otimização de armazenamento, estamos também falando sobre otimização de performance. No InnoDB, espaço e desempenho caminham lado a lado.
Conclusão
Os resultados obtidos demonstram que decisões aparentemente simples de modelagem podem produzir impactos expressivos quando a volumetria cresce. Em uma base com aproximadamente 500 milhões de registros, a tabela cujos campos permaneceram como NULL ocupou 19,34 GB, enquanto a mesma estrutura utilizando strings vazias consumiu 23,87 GB, um aumento de 4,53 GB, equivalente a 23,42%. Quando os campos passaram a armazenar o valor ‘0’, o espaço ocupado chegou a 27,38 GB, representando um crescimento de 8,04 GB ou 41,57% em relação ao cenário com valores NULL.
Embora o InnoDB utilize o NULL Bitmap, que adiciona um pequeno custo para controlar quais colunas possuem valores NULL, esse overhead é significativamente menor do que o espaço necessário para armazenar valores efetivos em cada registro. Em cenários de alta volumetria, poucos bytes adicionais por linha se transformam em gigabytes de armazenamento, aumentando a quantidade de páginas de 16 KB gerenciadas pelo InnoDB e, consequentemente, o volume de dados que precisam ser lidos durante as consultas.
A principal lição é que otimização de banco de dados não se resume à escrita de consultas eficientes. Ela começa na modelagem dos dados. Em ambientes com centenas de milhões ou bilhões de registros, cada byte economizado por registro representa gigabytes poupados em disco, menos páginas para o mecanismo de armazenamento gerenciar e maior eficiência nas operações de leitura e escrita.
Em bancos de dados de grande porte, a diferença entre alguns bytes por registro e uma boa modelagem pode significar dezenas de gigabytes, milhões de páginas a menos para o InnoDB gerenciar e um ganho real de eficiência. Na engenharia de dados, cada byte conta.






