A versão curta: aponte o DuckDB para o arquivo.
duckdb -c "SELECT region, SUM(amount) FROM 'sales.csv' GROUP BY region"
Sem esquema, sem importação, sem servidor, sem CREATE TABLE. O CSV fica exatamente onde está e a linha de cabeçalho vira os nomes das colunas. Se você for instalar só uma coisa hoje, instale esta.
O resto desta página explica por que isso funciona, o que fazer quando você não pode usar o DuckDB e como lidar com as duas coisas que pegam todo mundo: as aspas no nome do arquivo e as colunas cujos nomes têm espaços.
Por que você não precisa carregar o arquivo em lugar nenhum
O instinto de «primeiro colocar os dados num banco» é um resquício do tempo em que os motores SQL só sabiam ler o próprio armazenamento. Os motores modernos não têm mais essa limitação. DuckDB, SQLite e várias ferramentas pequenas de linha de comando tratam um CSV como uma tabela durante uma única consulta, inferem os tipos por amostragem, respondem e esquecem tudo.
Isso muda a conta por completo. Uma pergunta pontual sobre um arquivo que chegou por e-mail não vale mais um esquema, um script de carga e uma etapa de limpeza. Vale uma linha.
Carregar num banco de verdade continua sendo a escolha certa em exatamente dois casos: os dados vão ser consultados repetidamente por mais de uma pessoa, ou precisam de restrições e relacionamentos que um único arquivo plano não consegue expressar. Nenhum dos dois descreve «qual é o total por região nesta exportação».
As opções, com honestidade
| Ferramenta | Instalação | Melhor para | Fraca em |
|---|---|---|---|
| DuckDB | Um binário, sem dependências | Tudo até dezenas de GB | Quase nada nesta tarefa |
SQLite + .import | Já vem no macOS e no Linux | Guardar a tabela para depois | Tipos: tudo entra como TEXT |
q | pip install q-text-as-data | One-liners rápidos num pipeline de shell | Arquivos grandes; é mais lento |
csvsql (csvkit) | pip install csvkit | Gerar um CREATE TABLE para um banco de verdade | Velocidade: é o mais lento daqui |
Pandas + pandasql | Você já tem o pandas | Quando a resposta alimenta mais código Python | O dialeto SQL é o do SQLite, não o do Postgres |
| App de celular com SQL | App Store / Play | O arquivo chegou no seu e-mail, no celular | Qualquer coisa que você queira automatizar |
O ranking honesto é curto: DuckDB para quase tudo, SQLite quando você não pode instalar nada e um app de celular quando você não está num computador. Os outros são bons e têm seus momentos, mas nenhum supera o DuckDB nesta tarefa específica.
DuckDB, direito
A instalação é um único binário: brew install duckdb, ou baixe e coloque no seu path. Depois:
duckdb
D SELECT * FROM 'orders.csv' LIMIT 10;
Duas coisas para saber logo de cara:
O nome do arquivo precisa de aspas simples. FROM orders.csv é interpretado como um esquema chamado orders e uma tabela chamada csv, e a mensagem de erro não deixa isso claro. FROM 'orders.csv' é o certo.
Nomes de coluna com espaços precisam de aspas duplas. SELECT "Order Date" FROM …. Com aspas simples você recebe o texto literal, em silêncio, em todas as linhas, o que faz parecer que a consulta funcionou.
Use um glob para ler um diretório inteiro quando a exportação veio em partes mensais:
SELECT * FROM 'exports/*.csv'
Quando a inferência de tipos erra, geralmente numa coluna de IDs com zeros à esquerda ou em datas num formato que ela não reconhece, assuma o controle da leitura em vez de brigar com ela:
SELECT * FROM read_csv('orders.csv',
header = true,
columns = {'id': 'VARCHAR', 'ordered_at': 'DATE', 'amount': 'DOUBLE'},
dateformat = '%d/%m/%Y'
)
O read_csv também aceita delim, quote, escape, ignore_errors = true e sample_size = -1 (varrer o arquivo inteiro antes de decidir os tipos, em vez das primeiras 20.480 linhas). Este último é a solução quando uma coluna parece numérica por dez mil linhas e depois encontra N/A.
Grave a resposta em arquivo sem sair do SQL:
COPY (SELECT * FROM 'orders.csv' WHERE amount > 1000)
TO 'large-orders.csv' (HEADER, DELIMITER ',');
E se o arquivo for consultado mais do que meia dúzia de vezes, converta-o uma vez com COPY (SELECT * FROM 'orders.csv') TO 'orders.parquet' e consulte o Parquet. Ele tem tipos, é comprimido e costuma ser de cinco a vinte vezes mais rápido de varrer.
SQLite, quando você não pode instalar nada
O sqlite3 já vem no macOS e na maioria das distribuições Linux, então esta é a opção que funciona numa máquina bloqueada.
sqlite3 shop.db
sqlite> .mode csv
sqlite> .import orders.csv orders
sqlite> SELECT region, COUNT(*) FROM orders GROUP BY region;
A pegadinha, e ela importa: o .import transforma toda coluna em TEXT. A ordenação passa a ser lexicográfica, então 100 vem antes de 20, e o SUM() dá surpresas. Faça o cast na consulta (CAST(amount AS REAL)) ou crie a tabela com tipos reais antes de importar.
Ao contrário do DuckDB, isso deixa um shop.db para trás, o que é bom se você quiser a tabela de novo amanhã e bagunça se não quiser.
q, para um pipeline de shell
Quando a consulta é um passo numa cadeia de comandos de shell, o q lê a entrada padrão como qualquer outra ferramenta Unix:
q -H -d, "SELECT region, SUM(amount) FROM - GROUP BY region" < orders.csv
-H indica que a primeira linha é cabeçalho, -d, define o delimitador e - é o stdin. Por baixo é SQLite, então o dialeto é o do SQLite. É também a opção razoável mais lenta num arquivo grande: tudo bem para algumas centenas de milhares de linhas, penoso além disso.
Quando o arquivo está no seu celular
Nada disso ajuda quando a exportação é um anexo no seu e-mail e você não está numa mesa. Isso acontece com frequência, e a resposta de sempre, «mande para você mesmo e olhe depois», é ruim.
Executa SQL de verdade no arquivo, no iOS e no Android, com um construtor visual de filtros para quando você prefere não digitar uma consulta no teclado do celular e um assistente em linguagem simples para quando você ainda não sabe os nomes das colunas.
O que ele fazO dialeto é o do SQLite, então as consultas deste artigo se aplicam com as ressalvas de sempre: sem as funções de janela dos exemplos do DuckDB, e valem os mesmos hábitos com CAST.
Consultas para deixar à mão
O que tem neste arquivo:
SELECT * FROM 'file.csv' LIMIT 20;
DESCRIBE SELECT * FROM 'file.csv';
SELECT COUNT(*) FROM 'file.csv';
Onde estão os nulos, já que ir coluna por coluna é tedioso:
SELECT COUNT(*) - COUNT(email) AS missing_email,
COUNT(*) - COUNT(phone) AS missing_phone
FROM 'contacts.csv';
Duplicatas numa chave:
SELECT id, COUNT(*) AS n
FROM 'orders.csv'
GROUP BY id HAVING n > 1
ORDER BY n DESC;
Juntar duas exportações é a consulta que mais justifica todo o exercício, porque fazer isso com PROCV entre duas planilhas é bem pior:
SELECT o.id, o.amount, c.name
FROM 'orders.csv' o
JOIN 'customers.csv' c ON c.id = o.customer_id;
Um total mensal a partir de uma coluna de datas:
SELECT date_trunc('month', ordered_at) AS month, SUM(amount)
FROM 'orders.csv'
GROUP BY month ORDER BY month;
Quando algo dá errado
Tudo veio numa coluna só. O delimitador não é vírgula; ponto e vírgula é o padrão em boa parte da Europa. read_csv('f.csv', delim = ';'). Se o arquivo for mais estranho que isso, ele pode estar realmente malformado e não apenas incomum.
Uma coluna numérica não soma. Ela foi inferida como texto porque aparece algo não numérico nela. Encontre o culpado com SELECT amount FROM 'f.csv' WHERE TRY_CAST(amount AS DOUBLE) IS NULL LIMIT 10;. Geralmente é um símbolo de moeda, um separador de milhar ou a string N/A.
Os zeros à esquerda sumiram. A coluna foi lida como número. Declare-a como VARCHAR em columns e não deixe uma planilha chegar perto dela depois.
A linha de cabeçalho se repete pelo arquivo. Várias exportações foram concatenadas. WHERE id <> 'id' é a correção grosseira e geralmente a correta.
Acabou a memória. O DuckDB processa em fluxo, então isso normalmente significa um ORDER BY ou um DISTINCT sobre o arquivo inteiro. Dê a ele um diretório temporário (SET temp_directory = '/tmp/duckdb') e ele passa a gravar em disco. Ou leia como abrir um arquivo CSV de 1 GB para a questão mais ampla do que dá conta de quê em grande escala.