Como Importar CSV para Banco de Dados (2026): PostgreSQL, MySQL, SQLite e MongoDB
Importar CSV em banco de dados parece simples — abrir um script, rodar INSERT em loop — até você topar com 10 milhões de linhas, encoding quebrado e validação de schema. Este guia cobre os comandos nativos dos principais bancos (COPY, LOAD DATA, mongoimport), performance e estratégias de staging.
Por Vitor Morais
Fundador do MochaLabz ·
Converta JSON em CSV
Se seu dado vem de API em JSON, converta para CSV primeiro — download direto.
Usar conversor →Importar CSV em banco de dados é uma das operações mais comuns em ETL, migrações e ingestão de dados terceiros. Em volumes pequenos (até 10 mil linhas), qualquer abordagem funciona. A partir de 100 mil linhas, escolher o comando certo é diferença entre segundos e horas. E a partir de 10 milhões, estratégias de staging e particionamento viram obrigatórias.
Este guia cobre os comandos nativos dos quatro bancos mais usados em 2026 — PostgreSQL, MySQL, SQLite e MongoDB — com sintaxe, performance esperada, tratamento de encoding e erros, e estratégias para arquivos grandes.
Antes de importar: validação e preparo
- Schema consistente: todas as linhas têm as mesmas colunas na mesma ordem? Linhas desalinhadas geram erro silencioso em muitos bancos.
- Encoding UTF-8 with BOM: abrir o arquivo em editor e confirmar. Mojibake aparece quando o encoding está errado.
- Delimitador: vírgula, ponto-e-vírgula ou tab? Depende do idioma (Excel PT-BR usa ; por padrão).
- Quote character: aspas duplas é padrão. Se o CSV usa simples, configure no comando.
- Tabela de destino já criada: com os tipos certos.
- Índices removidos (para volumes grandes): recria depois.
Dica
head e wc antes de importar: head -5 arquivo.csv mostra estrutura; wc -l arquivo.csv conta linhas. Isso evita surpresa na hora do import.PostgreSQL: COPY (mais rápido)
Sintaxe básica
-- Criar a tabela
CREATE TABLE clientes (
id SERIAL PRIMARY KEY,
nome VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
cpf VARCHAR(14) NOT NULL,
criado_em TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Importar via COPY (do servidor)
COPY clientes (nome, email, cpf)
FROM '/caminho/arquivo.csv'
WITH (
FORMAT csv,
HEADER true,
DELIMITER ',',
ENCODING 'UTF8'
);\\copy via psql (arquivo no cliente)
COPY FROM exige que o arquivo esteja no servidor. Para importar arquivo local usando psql, use \\copy (cliente-side):
# No terminal
psql -U usuario -d banco
# Dentro do psql
\copy clientes (nome, email, cpf) FROM './arquivo.csv' WITH CSV HEADER;Performance: otimizações
-- 1. Remover índices antes do COPY
ALTER TABLE clientes DISABLE TRIGGER ALL;
-- Drop indexes
DROP INDEX IF EXISTS idx_email;
DROP INDEX IF EXISTS idx_cpf;
-- 2. COPY
COPY clientes FROM '/arquivo.csv' WITH CSV HEADER;
-- 3. Recriar índices
CREATE INDEX idx_email ON clientes(email);
CREATE INDEX idx_cpf ON clientes(cpf);
-- 4. Re-ativar triggers
ALTER TABLE clientes ENABLE TRIGGER ALL;
-- 5. Atualizar estatísticas
VACUUM ANALYZE clientes;Contexto
Postgres via driver Node.js
import { Client } from "pg";
import { from } from "pg-copy-streams";
import { createReadStream } from "node:fs";
async function importar() {
const client = new Client({ connectionString: DATABASE_URL });
await client.connect();
const stream = client.query(
from("COPY clientes (nome, email, cpf) FROM STDIN WITH (FORMAT csv, HEADER true)"),
);
const fileStream = createReadStream("./arquivo.csv");
fileStream.pipe(stream);
return new Promise((resolve, reject) => {
stream.on("finish", async () => {
await client.end();
resolve(undefined);
});
stream.on("error", reject);
});
}MySQL: LOAD DATA INFILE
-- Criar tabela
CREATE TABLE clientes (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
cpf VARCHAR(14) NOT NULL,
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Importar CSV (arquivo no servidor)
LOAD DATA INFILE '/caminho/arquivo.csv'
INTO TABLE clientes
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES
(nome, email, cpf);
-- Ou LOCAL INFILE (arquivo no cliente)
LOAD DATA LOCAL INFILE './arquivo.csv'
INTO TABLE clientes
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES
(nome, email, cpf);Atenção
SET GLOBAL local_infile = 1. Em ambientes hospedados (AWS RDS, Google Cloud SQL), pode estar desabilitado por padrão. Em produção, prefira subir arquivo para storage (S3) e importar pelo comando nativo.MySQL performance: transações grandes
-- Desabilitar checks durante import
SET autocommit = 0;
SET unique_checks = 0;
SET foreign_key_checks = 0;
-- Importar
LOAD DATA INFILE '/arquivo.csv' INTO TABLE clientes ...;
-- Reativar
COMMIT;
SET autocommit = 1;
SET unique_checks = 1;
SET foreign_key_checks = 1;
-- Atualizar estatísticas
ANALYZE TABLE clientes;SQLite: .import do CLI
# No terminal
sqlite3 banco.db
# Dentro do sqlite3
.mode csv
.import arquivo.csv clientes
# Com header: pula primeira linha
.mode csv
.import --skip 1 arquivo.csv clientesSQLite via Node.js (better-sqlite3)
import Database from "better-sqlite3";
import Papa from "papaparse";
import { readFileSync } from "node:fs";
const db = new Database("./banco.db");
db.exec(`CREATE TABLE IF NOT EXISTS clientes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
cpf TEXT NOT NULL
)`);
// Lê e parseia CSV
const csv = readFileSync("./arquivo.csv", "utf8");
const { data } = Papa.parse<{ nome: string; email: string; cpf: string }>(csv, {
header: true,
skipEmptyLines: true,
});
// Insert em transação (100x mais rápido)
const insert = db.prepare(
"INSERT INTO clientes (nome, email, cpf) VALUES (?, ?, ?)",
);
const insertMany = db.transaction((rows: typeof data) => {
for (const row of rows) {
insert.run(row.nome, row.email, row.cpf);
}
});
insertMany(data);
console.log(`${data.length} linhas importadas.`);Dica
MongoDB: mongoimport
# Instalar MongoDB Database Tools (separado do MongoDB Server)
# Importar CSV
mongoimport \
--db meubanco \
--collection clientes \
--type csv \
--headerline \
--file ./arquivo.csv
# Com autenticação
mongoimport \
--uri "mongodb+srv://user:pass@cluster.mongodb.net/meubanco" \
--collection clientes \
--type csv \
--headerline \
--file ./arquivo.csv
# Com validação de tipos
mongoimport \
--db meubanco \
--collection clientes \
--type csv \
--headerline \
--columnsHaveTypes \
--file ./arquivo.csvNode.js com driver oficial
import { MongoClient } from "mongodb";
import Papa from "papaparse";
import { readFileSync } from "node:fs";
const client = new MongoClient(process.env.MONGO_URI!);
await client.connect();
const db = client.db("meubanco");
const collection = db.collection("clientes");
const csv = readFileSync("./arquivo.csv", "utf8");
const { data } = Papa.parse<Record<string, string>>(csv, {
header: true,
skipEmptyLines: true,
});
// Insert em batch (mais rápido que um a um)
const BATCH_SIZE = 10_000;
for (let i = 0; i < data.length; i += BATCH_SIZE) {
const batch = data.slice(i, i + BATCH_SIZE);
await collection.insertMany(batch, { ordered: false });
console.log(`Inseridos: ${i + batch.length} / ${data.length}`);
}
await client.close();Strategy staging table: importação validada
Para imports em produção onde integridade importa, use tabela staging:
-- 1. Criar tabela staging sem constraints
CREATE TABLE clientes_staging (
nome TEXT,
email TEXT,
cpf TEXT
);
-- 2. COPY sem validação
COPY clientes_staging FROM '/arquivo.csv' WITH CSV HEADER;
-- 3. Validar
SELECT * FROM clientes_staging WHERE email NOT LIKE '%@%';
SELECT COUNT(*) FROM clientes_staging WHERE LENGTH(cpf) != 14;
SELECT email, COUNT(*) FROM clientes_staging GROUP BY email HAVING COUNT(*) > 1;
-- 4. Inserir na tabela final (apenas linhas válidas)
INSERT INTO clientes (nome, email, cpf)
SELECT nome, email, cpf
FROM clientes_staging
WHERE email LIKE '%@%.%'
AND LENGTH(cpf) = 14
ON CONFLICT (email) DO NOTHING; -- ignora duplicatas
-- 5. Drop staging
DROP TABLE clientes_staging;Contexto
Lidando com encoding
Cenário clássico: CSV gerado em Windows com encoding Windows-1252 importado em Postgres UTF-8. Acentos viram caracteres corrompidos.
# Detectar encoding no Linux
file -i arquivo.csv
# Pode retornar: arquivo.csv: text/plain; charset=iso-8859-1
# Converter para UTF-8
iconv -f ISO-8859-1 -t UTF-8 arquivo.csv -o arquivo-utf8.csv
# Ou em Python
python3 -c "
import codecs
with codecs.open('arquivo.csv', 'r', 'iso-8859-1') as f:
data = f.read()
with codecs.open('arquivo-utf8.csv', 'w', 'utf-8') as f:
f.write(data)
"Performance comparada
| Critério | Tempo médio |
|---|---|
| Postgres COPY | 30-60s |
| MySQL LOAD DATA | 30-90s |
| SQLite .import | 45s-2min |
| SQLite INSERT em transação | 10-30s (surpreendentemente rápido) |
| MongoDB mongoimport | 1-3min |
| MongoDB insertMany via driver | 2-5min |
| Postgres INSERT em loop sem batch | 1-3 horas (evite) |
Arquivos muito grandes (10M+ linhas)
Estratégias para escalar além de 10 milhões:
- Particione o CSV:
split -l 1000000 arquivo.csv parte_gera arquivos de 1M cada. Importe em paralelo. - Desabilite WAL temporariamente (Postgres): insira em tabela UNLOGGED, mova depois.
- Comprima o CSV:
gzipreduz 70-90% do tamanho. Postgres aceitaCOPY FROM PROGRAM ‘gunzip -c arquivo.csv.gz’. - Use pg_bulkload (Postgres): 2-5x mais rápido que COPY nativo para terabytes.
- Streaming direto da fonte: evite salvar CSV se possível — conecte ferramenta ETL (Airbyte, Fivetran) que puxa dados direto.
Pipeline ETL completo com Node.js
import { Client } from "pg";
import { from } from "pg-copy-streams";
import { createReadStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Transform } from "node:stream";
async function importarCsv(caminho: string) {
const client = new Client({ connectionString: DATABASE_URL });
await client.connect();
try {
// 1. Criar tabela staging
await client.query(`
CREATE UNLOGGED TABLE IF NOT EXISTS clientes_staging (
nome TEXT, email TEXT, cpf TEXT
)
`);
// 2. Truncar staging para import limpo
await client.query("TRUNCATE clientes_staging");
// 3. COPY via stream
const stream = client.query(
from("COPY clientes_staging FROM STDIN WITH (FORMAT csv, HEADER true)"),
);
await pipeline(createReadStream(caminho), stream);
console.log("CSV importado para staging");
// 4. Contar linhas
const countResult = await client.query("SELECT COUNT(*) FROM clientes_staging");
console.log(`Total em staging: ${countResult.rows[0].count}`);
// 5. Inserir apenas válidos na tabela final
const inserted = await client.query(`
INSERT INTO clientes (nome, email, cpf)
SELECT DISTINCT nome, email, cpf
FROM clientes_staging
WHERE email LIKE '%@%.%' AND LENGTH(cpf) = 14
ON CONFLICT (email) DO NOTHING
RETURNING id
`);
console.log(`Inseridos na tabela final: ${inserted.rowCount}`);
// 6. Limpar staging
await client.query("DROP TABLE clientes_staging");
} finally {
await client.end();
}
}
importarCsv("./arquivo.csv");Idempotência: rodar import 2x sem duplicar
Use ON CONFLICT (Postgres) ou ON DUPLICATE KEY UPDATE (MySQL):
-- Postgres: se já existe, não faz nada
INSERT INTO clientes (nome, email, cpf)
SELECT nome, email, cpf FROM clientes_staging
ON CONFLICT (email) DO NOTHING;
-- Postgres: atualiza os campos se já existe
INSERT INTO clientes (nome, email, cpf)
SELECT nome, email, cpf FROM clientes_staging
ON CONFLICT (email) DO UPDATE
SET nome = EXCLUDED.nome, cpf = EXCLUDED.cpf;
-- MySQL: atualizar em caso de duplicata
LOAD DATA INFILE '/arquivo.csv' INTO TABLE clientes
REPLACE -- substitui linha em caso de PK/UNIQUE conflict
FIELDS TERMINATED BY ','
IGNORE 1 LINES;Validação pré-insert com Pandas (Python)
import pandas as pd
df = pd.read_csv("arquivo.csv", encoding="utf-8")
# Schema
print("Colunas:", df.columns.tolist())
print("Tipos:", df.dtypes)
print("Linhas:", len(df))
# Validações
print("Nulos:", df.isnull().sum())
print("Duplicados:", df.duplicated().sum())
# CPF com tamanho inválido
df["cpf_invalido"] = df["cpf"].str.len() != 14
print("CPFs inválidos:", df["cpf_invalido"].sum())
# E-mails inválidos
df["email_invalido"] = ~df["email"].str.contains("@")
print("E-mails inválidos:", df["email_invalido"].sum())
# Apenas linhas válidas
df_valido = df[~df["cpf_invalido"] & ~df["email_invalido"]]
# Exporta para CSV limpo
df_valido.drop(columns=["cpf_invalido", "email_invalido"]).to_csv(
"arquivo-limpo.csv", index=False
)Erros clássicos
- INSERT em loop sem transação: 1M de linhas leva horas. Use batch.
- Mojibake por encoding: sempre confirme encoding UTF-8 antes.
- Delimitador errado: CSV brasileiro com “,” gera erro; ajuste para “;” no comando.
- Índices ativos durante import massivo: diminui 5-10x a velocidade. Remove, importa, recria.
- Constraints violadas sem fallback: abort parcial pode deixar banco em estado intermediário. Use transação.
- Linhas com quebra interna não escapada: CSV malformado quebra o parser. Use ferramentas que lidam com multiline corretamente.
- Valores vazios interpretados como zero ou NULL diferentes: string vazia “” é texto vazio, não NULL. Configure o comando.
Ferramentas GUI para ingestão visual
- DBeaver: import CSV com mapping visual de colunas.
- DataGrip: similar, com preview.
- pgAdmin: específico para Postgres.
- MongoDB Compass: para MongoDB.
- Airbyte / Fivetran: ETL completo se você faz ingestão contínua.
Importar CSV em uma frase
Escolher o comando nativo do banco (COPY, LOAD DATA, mongoimport) é a diferença entre minutos e horas em milhões de linhas. Combinado com staging table para validação e índices removidos durante import, o fluxo é robusto para qualquer volume. Falhar em escolher a ferramenta certa é um dos erros mais frequentes e custosos em ETL de CSV.
Perguntas frequentes
Qual o jeito mais rápido de importar CSV no PostgreSQL?+
Use COPY (comando nativo do Postgres). É 10-50x mais rápido que INSERT em loop. Sintaxe: COPY tabela FROM '/caminho/arquivo.csv' WITH (FORMAT csv, HEADER true). Para CSVs até 1 GB, COPY roda em minutos. Acima disso, considere COPY via stdin de psql (pg_bulkload para terabytes). Importante: o arquivo precisa estar no filesystem do servidor Postgres, não no cliente, para COPY direto — alternativamente, use \copy do psql (client-side).
MySQL equivalente do COPY do Postgres?+
LOAD DATA INFILE. Sintaxe: LOAD DATA INFILE '/caminho/arquivo.csv' INTO TABLE tabela FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 ROWS. Ou LOAD DATA LOCAL INFILE para arquivo no cliente (precisa local_infile=1 ativo no servidor). Performance: similar ao COPY do Postgres. Para volumes muito grandes, particione o CSV em arquivos menores e importe em paralelo.
Como lidar com encoding quando o CSV tem acentos quebrados?+
CSV sem BOM aberto por software que assume encoding errado vira mojibake (João vira João). Soluções: (1) salve o CSV em UTF-8 with BOM antes de importar; (2) no comando de import, especifique encoding: COPY ... WITH (FORMAT csv, ENCODING 'UTF8') em Postgres; SET NAMES utf8mb4 antes de LOAD DATA em MySQL; (3) se já está quebrado, execute script em Python com chardet para detectar encoding original, reconverta para UTF-8.
CSV com milhões de linhas trava o banco?+
Com o comando certo (COPY, LOAD DATA), não. Postgres COPY importa 1 milhão de linhas em 30-60 segundos em hardware moderno. 10 milhões em 5-10 minutos. Para volumes maiores, estratégias: (1) remova índices antes do COPY, recrie depois (índices tornam insert mais lento); (2) use tabela UNLOGGED temporária para importar, depois INSERT INTO tabela_final; (3) disable autovacuum durante import; (4) aumente maintenance_work_mem.
Como importar CSV ignorando linhas com erro?+
Em Postgres, use COPY FROM com LOG ERRORS (extensão file_fdw ou ferramenta externa) para pular linhas problemáticas. Alternativa simples: importe para tabela staging sem constraints, identifique linhas problemáticas com queries, corrija ou descarte, insira na tabela final com INSERT. Em MySQL: IGNORE no LOAD DATA ignora erros de constraint mas registra warnings. Para workflow robusto, prefira staging table.
Qual diferença entre mongoimport e insertMany?+
mongoimport é comando CLI especializado em ingestão massiva. Aceita CSV, JSON e TSV, processa em streaming, tolera falhas por linha e é 5-10x mais rápido que insertMany em grandes volumes. insertMany via driver JavaScript/Python tem vantagens em workflow programático (pode transformar dados antes de inserir, controle de batch size, retry logic). Use mongoimport para ingestão pontual; insertMany para ETL contínuo.
É necessário validar CSV antes de importar?+
Sim, sempre. Validação mínima: (1) schema (todas as linhas têm as mesmas colunas), (2) tipos (número em coluna numérica), (3) constraints (CPF válido, email válido, data em range razoável), (4) duplicatas (únicas em coluna UNIQUE). Importação sem validação gera tabela com dados inconsistentes que quebram queries downstream. Ferramentas: csvlint (Node), pandas (Python), csvkit. Em pipeline crítico, valide em staging table antes de mover.
SQLite tem comando nativo de importação?+
Sim. Dentro do sqlite3 CLI: .mode csv seguido de .import arquivo.csv tabela. Suporta BOM, encoding UTF-8 e é rápido para volumes moderados (até 1 GB). Para ingestão programática, use better-sqlite3 (Node) ou sqlite3 (Python) com transação explícita envolvendo insertMany — batelada de 10k linhas por transação é 100x mais rápido que insert um a um.
Artigos relacionados
SQL para Iniciantes (2026): Guia Prático com Formatação, Exemplos e Boas Práticas
Guia de SQL do zero: SELECT, WHERE, JOIN, GROUP BY, tipos de dados, formatação consistente e erros que iniciantes cometem em produção.
Como Converter JSON para CSV em JavaScript (2026): Guia Completo com Código
Guia definitivo de conversão JSON → CSV em JavaScript: abordagens manuais, PapaParse, edge cases com arrays aninhados, encoding UTF-8 com BOM e exemplos prontos em Node.js e browser.
Como Ler CSV em JavaScript (2026): FileReader, Node.js, Streams e PapaParse
Guia completo de leitura de CSV em JavaScript: FileReader no browser, csv-parse e streams no Node.js, PapaParse para grandes volumes, parser RFC 4180 do zero, BOM, encoding e benchmarks.