Artigo Build·Desenvolvimento·13 min de leitura de leitura

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.

Vitor Morais

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

Valide com 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

Em Postgres, COPY é 10-50x mais rápido que INSERT em loop. 1 milhão de linhas = ~30-60s vs ~30-60 minutos com INSERT. Para migração inicial, sempre use COPY.

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

LOCAL INFILE precisa de permissão server-side: 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 clientes

SQLite 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

Em SQLite com better-sqlite3, insert dentro de transação é ~100x mais rápido que sem. Para 100k linhas: com transação ~200ms, sem ~20s.

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.csv

Node.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

Staging table permite validar sem poluir a tabela de produção. Em pipeline crítico (cobrança, dados regulatórios), é prática obrigatória. Algumas empresas mantêm staging persistente com auditoria de cada import.

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

Performance aproximada de importação (1M linhas)
CritérioTempo médio
Postgres COPY30-60s
MySQL LOAD DATA30-90s
SQLite .import45s-2min
SQLite INSERT em transação10-30s (surpreendentemente rápido)
MongoDB mongoimport1-3min
MongoDB insertMany via driver2-5min
Postgres INSERT em loop sem batch1-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: gzip reduz 70-90% do tamanho. Postgres aceita COPY 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.

#csv#importar#banco de dados#postgresql#mysql#sqlite#mongodb#copy#load data#etl

Artigos relacionados