JSON para SQL: comandos INSERT, projeto de tabelas e armadilhas de mapeamento de tipos
Resumo: Transformar JSON em INSERT SQL esconde divisão de tabelas aninhadas, NULL versus ausente, escaping e lacunas entre dialetos. Comandos executáveis e tabelas comparativas para cada armadilha.

Resumo da ópera: decida qual entidade de negócio uma linha representa antes de projetar as tabelas e depois escreva VALUES múltiplos explícitos dentro de uma única transação; resolva nomes de campos e mapeamento de tipos antes da importação em vez de remendá-los em SQL.
O JSON descreve forma; o SQL também descreve restrições. Quando você transporta o primeiro para o segundo, toda restrição que você não escreveu volta depois como dado sujo.
1. Decida primeiro: uma tabela ou um registro por linha JSON
Olhe a camada mais externa dos dados antes de projetar qualquer tabela. Erre nessa decisão e todo INSERT depois dela precisará ser reescrito.
| Estrutura JSON | Projeto de tabela sugerido | Risco principal |
|---|---|---|
Array plano de objetos [{...},{...}] |
Uma tabela, muitas linhas | Chaves inconsistentes entre registros deixam a maioria das colunas de união como NULL |
Objeto envolvendo um array {"data":[...],"total":100} |
Uma tabela, pegue apenas data |
Escrever total em cada linha desloca o significado do campo |
Objeto aninhado {"user":{"name":"x"}} |
Tabela pai mais colunas achatadas, ou pai mais tabela relacionada | Expansão descontrolada, nomes de coluna duplicados |
Campo array {"tags":["a","b"]} |
Tabela filha, ou coluna JSON/TEXT | Uma relação um-para-muitos comprimida em uma célula e não mais consultável por tag |
| Chaves inconsistentes | Colunas de união, ou dividir tabelas por tipo | "Chave ausente" e "valor é null" colapsam no mesmo NULL |
Há apenas um critério: qual entidade de negócio uma linha representa? Se uma linha é um pedido, os itens de linha precisam viver em sua própria tabela. Se uma linha é um usuário, as tags pertencem a uma tabela filha ou a uma coluna JSON, não a uma string unida por vírgulas.
2. De um array para INSERT: o menor modelo executável
A forma mais segura sempre lista os nomes das colunas explicitamente e nunca depende da ordem das colunas:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
INSERT INTO users (id, name, email) VALUES
(1, 'Ada', 'ada@example.com'),
(2, 'Linus', NULL);
Três detalhes importam:
- Escreva todos os nomes de coluna. Quando o schema ganhar uma coluna depois, nada se desloca para o campo errado.
VALUEScom várias linhas funciona no SQLite, PostgreSQL e MySQL, e economiza um grande número de idas e voltas em comparação com um laço de inserts de uma linha.NULLé uma palavra-chave; não a coloque entre aspas. Escrever'NULL'armazena uma string de quatro caracteres.
Se as chaves JSON não corresponderem aos nomes das colunas (userId versus user_id), adicione uma camada de mapeamento. Não adivinhe com COALESCE dentro do SQL. Corrigir nomes de campo na camada de dados é bem mais barato do que remendá-los nas consultas.
3. Objetos aninhados: uma tabela pai mais uma tabela filha
Pedidos são o caso clássico de um-para-muitos:
INSERT INTO orders (id, customer_name, total_cents) VALUES
(1001, 'Ada', 12900);
INSERT INTO order_items (order_id, sku, quantity) VALUES
(1001, 'A-1', 2),
(1001, 'B-7', 1);
Isso corresponde ao JSON {"id":1001,"customer":"Ada","total":129.00,"items":[{"sku":"A-1","quantity":2},{"sku":"B-7","quantity":1}]}.
Pontos-chave:
- O comprimento do array
itemsdetermina o número de linhas filhas; a tabela pai recebe exatamente uma linha. - Armazene dinheiro como centavos inteiros (
total_cents), nunca comoFLOAT. - Dê à tabela filha uma chave estrangeira
order_ide confirme os dois inserts em uma transação, caso contrário o pai pode existir enquanto seus itens estão ausentes. - Se os dados forem somente leitura e ninguém filtrar por item de linha, uma coluna JSON é aceitável (
jsonbno PostgreSQL,JSONno MySQL, ouTEXTno SQLite com as funções json1), mas aceite o custo: sem indexação e consultas desconfortáveis.
Ache objetos aninhados fixos como user.address em colunas (address_city, address_zip). Divida em uma tabela apenas quando o aninhamento puder continuar crescendo; dividir uma forma fixa só acrescenta JOINs.
4. NULL versus campos ausentes: o SQL não tem "chave ausente"
O JSON permite que uma chave esteja totalmente ausente. As colunas SQL são fixas. Após uma importação, os dois casos parecem idênticos — NULL — mesmo que possam significar coisas diferentes:
| Situação no JSON | O que o banco mostra | Tratamento sugerido |
|---|---|---|
| A chave está ausente | NULL |
Adicione um padrão na definição da tabela, ou uma coluna marcadora _present |
A chave existe com null |
NULL |
Significa que o usuário limpou explicitamente o valor |
| A chave existe com string vazia | '' |
Distinguível de NULL, mas fácil de confundir nas consultas |
| Campo numérico ausente | NULL |
Nunca deixe 0 fingir que é "ausente" |
Dois erros comuns:
- Usar string vazia em vez de NULL. Depois,
WHERE email IS NULLnão pega essas linhas, e sua métrica de "usuários sem e-mail" subconta silenciosamente. - Testar com
col = NULL. Na lógica de três valores do SQL,NULL = NULLavalia paraNULL, nãoTRUE. Você precisa escreverIS NULL:
SELECT COUNT(*) FROM users WHERE email IS NULL;
Se você precisa preservar se uma chave estava presente, adicione uma coluna marcadora booleana em vez de inferir a partir de NULL. O contrato da API também deve declarar isso; veja Projeto de API JSON e nomes de campos.
5. Aspas e escaping: nunca concatene strings SQL
Strings JSON podem conter aspas simples, aspas duplas, barras invertidas, quebras de linha e até \u0000. Com strings montadas à mão, um escape esquecido desloca o resto do comando e, no pior caso, vira injeção de SQL.
No SQL padrão, uma aspa simples é escapada duplicando-a:
INSERT INTO notes (id, content) VALUES (1, 'It''s a ''quoted'' value');
Código de produção deve usar binding de parâmetros e deixar o driver cuidar do escaping e dos tipos:
// PostgreSQL usa $1/$2, MySQL usa ?; este exemplo usa node-postgres
await client.query('INSERT INTO users (id, name) VALUES ($1, $2)', [row.id, row.name]);
Escape à mão apenas quando precisar exportar um arquivo .sql, e então no mínimo:
- substitua
'por''; - se quebras de linha forem preservadas, confirme que o banco de destino aceita literais de string multilinha;
- remova
\u0000, já que a maioria dos bancos rejeita NUL dentro de strings; - o MySQL trata a barra invertida como caractere de escape por padrão, então
\precisa virar\\, ou mude para o modo padrão.
Aspas duplas do JSON não precisam de escape dentro de uma string SQL entre aspas simples. Adicionar uma camada extra de escaping é assim que barras invertidas indesejadas acabam nos seus dados.
6. Mapeamento de tipos: onde os seis valores JSON caem no SQL
O JSON tem apenas string, número, booleano, null, objeto e array. O SQL tem um sistema de tipos bem mais fino. Os erros geralmente acontecem em precisão e semântica, não em sintaxe:
| Valor JSON | Tipo SQL sugerido | Onde quebra |
|---|---|---|
| Inteiro | INTEGER / BIGINT |
Acima de 2^53 o lado JavaScript já perdeu precisão, então o valor armazenado também está errado |
| Decimal, dinheiro | NUMERIC / DECIMAL |
FLOAT é ponto flutuante binário; 0.1 + 0.2 não é exato |
| String | TEXT / VARCHAR(n) |
Limites de comprimento truncam; a collation altera comparações |
| Booleano | BOOLEAN / TINYINT(1) |
O SQLite não tem tipo booleano e armazena 0/1 |
| null | NULL |
Indistinguível de um campo ausente |
| String de data | DATE / TIMESTAMP / TIMESTAMPTZ |
Fuso horário perdido, milissegundos truncados, formatos inválidos |
| String de enum | TEXT com CHECK, ou uma tabela de enum |
Diferenças de maiúsculas e minúsculas e valores sujos não têm restrição |
| Objeto / array | Uma tabela filha, ou JSON / JSONB |
Armazenado como string não pode ser indexado nem consultado |
Booleanos escondem o bug mais sutil: false em JSON vira 0 no TINYINT(1) do MySQL, as idas e voltas parecem bem e ninguém percebe. Mas no momento em que algum código faz if (value) na string "false", ela é avaliada como verdadeira. Scripts de conversão devem chamar Boolean(value) explicitamente.
O JSONB do PostgreSQL analisa e deduplica chaves, mantendo a última ocorrência, o que convém para consultas; JSON preserva o texto original e a ordem das chaves. O JSON do SQLite é apenas TEXT mais um conjunto de funções, sem restrição real de tipo. Antes de escolher, pergunte se esta tabela precisa filtrar por campos dentro do JSON.
7. Inserções em massa e transações: lotes, commit, torne reexecutável
Conforme o volume cresce, três coisas decidem se uma importação permanece controlável:
- Lote. Um laço de N comandos
INSERTde uma linha custa N idas e voltas de rede. Combine 500 a 1000 linhas em umVALUESde várias linhas, ou use o carregador em massa do próprio banco (COPYdo PostgreSQL,LOAD DATAdo MySQL, inserts de várias linhas dentro de uma transação SQLite). - Use uma transação. Tudo ou nada, para que uma tabela pela metade nunca sobreviva a um erro:
BEGIN;
INSERT INTO users (id, name) VALUES (1, 'Ada'), (2, 'Linus');
INSERT INTO order_items (order_id, sku, quantity) VALUES (1, 'A-1', 2);
COMMIT;
- Torne idempotente. Scripts de importação são reexecutados constantemente. Use um upsert em vez de excluir e depois inserir:
| Banco de dados | Forma idempotente |
|---|---|
| PostgreSQL / SQLite | INSERT ... ON CONFLICT (id) DO UPDATE SET ... |
| MySQL | INSERT ... ON DUPLICATE KEY UPDATE ... |
ON CONFLICT exige uma restrição de unicidade ou chave primária, caso contrário nunca dispara. Quando não houver chave natural, crie um índice único em uma chave de negócio como email. Quando não houver chave alguma, adicione um id estável no lado JSON — nunca conte com um id autoincremental para idempotência, porque uma reexecução inserirá duplicatas.
8. Diferenças entre dialetos em resumo
| Capacidade | SQLite | PostgreSQL | MySQL |
|---|---|---|---|
VALUES de várias linhas |
Sim | Sim | Sim |
| Upsert | ON CONFLICT |
ON CONFLICT |
ON DUPLICATE KEY UPDATE |
| Chave autoincremental | INTEGER PRIMARY KEY |
GENERATED ... AS IDENTITY |
AUTO_INCREMENT |
| Tipo booleano | Nenhum, armazena 0/1 | boolean |
TINYINT(1) |
| Importação em massa | Inserts de várias linhas em transação | COPY |
LOAD DATA |
| Suporte a JSON | TEXT mais funções json1 |
json / jsonb |
JSON |
| Aspas de identificador | Aspas duplas | Aspas duplas | Crases (padrão) |
Para manter um arquivo SQL funcionando nos três, evite pelo menos ON CONFLICT, crases e SERIAL. Para projetos entre bancos diferentes, gere schema e comandos de upsert separadamente; não espere que um único script sirva para tudo.
9. Três verificações após a importação
Antes de excluir o arquivo de origem, verifique três coisas:
- Contagem de linhas.
SELECT COUNT(*) FROM users;deve ser igual ao comprimento do array JSON. Uma linha a menos geralmente significa que o registro final foi truncado. - A coluna com maior taxa de NULL. Isso aponta para nomes de chave inconsistentes ou aninhamento não expandido. Volte e verifique se a origem usa tanto
userIdquantouser_id. - Dinheiro e identificadores longos. Escolha os valores máximo e mínimo e confirme que nem a conversão para ponto flutuante nem a para inteiro os truncaram.
Se você precisa primeiro entender a forma tabular, use a abordagem de achatamento em Conversão entre JSON, CSV e dados tabulares. Se o público são colegas que só querem ler os dados, as quatro formas de converter JSON para Excel dão menos trabalho do que levantar um banco de dados.
Por que os números perdem precisão depois de importar JSON para SQL?
Inteiros acima de 2^53 já perderam precisão durante o JSON.parse do JavaScript, e escrevê-los em BIGINT não a restaura. Esses ids devem ser strings no JSON e convertidos para BIGINT ou TEXT durante a importação.
O que devo usar como chave primária quando o JSON não tem id?
Deixe o banco gerar uma, ou crie um UUID estável antes de importar. Nunca use o índice do array como id: uma reexecução reordena os dados e aponta as linhas erradas para os registros errados. Um índice único em uma chave de negócio (email, número de pedido) também viabiliza upserts.
O que fazer quando um INSERT em massa encontra um conflito de chave única?
Substitua o INSERT simples pelo upsert correspondente ao seu dialeto e confirme que a coluna em conflito tem uma restrição de unicidade. Excluir primeiro e inserir depois também funciona, mas precisa acontecer dentro de uma transação e você aceita uma breve janela sem linhas.
Arrays aninhados sempre precisam de uma tabela filha?
Não. Para exibição somente leitura sem consultas por subitem, uma coluna JSON é mais simples. No momento em que você precisar filtrar ou agregar por tag ou SKU, uma tabela filha com índices é a escolha sustentável. O fator decisivo é como você consulta, não como os dados são moldados.