La versión corta: apunta DuckDB al archivo.
duckdb -c "SELECT region, SUM(amount) FROM 'sales.csv' GROUP BY region"
Sin esquema, sin importar, sin servidor, sin CREATE TABLE. El CSV se queda exactamente donde está y la fila de encabezados pasa a ser los nombres de las columnas. Si hoy instalas una sola cosa, instala esta.
El resto de esta página explica por qué funciona, qué hacer cuando no puedes usar DuckDB y cómo resolver los dos problemas con los que tropieza todo el mundo: poner comillas al nombre del archivo y las columnas cuyos nombres tienen espacios.
Por qué no necesitas cargar el archivo en ningún lado
El reflejo de «primero meter los datos en una base de datos» viene de cuando los motores SQL solo sabían leer su propio almacenamiento. Los motores modernos ya no tienen esa limitación. DuckDB, SQLite y varias herramientas pequeñas de línea de comandos tratan un CSV como una tabla durante una sola consulta, infieren los tipos tomando una muestra, responden y se olvidan de todo.
Eso cambia por completo la cuenta. Una pregunta puntual sobre un archivo que te llegó por correo ya no justifica un esquema, un script de carga y un paso de limpieza. Justifica una línea.
Cargar los datos en una base de datos de verdad sigue siendo lo correcto en exactamente dos casos: los datos los van a consultar varias personas de forma repetida, o necesitan restricciones y relaciones que un solo archivo plano no puede expresar. Ninguno describe «cuál es el total por región en esta exportación».
Las opciones, con honestidad
| Herramienta | Instalación | Ideal para | Débil en |
|---|---|---|---|
| DuckDB | Un solo binario, sin dependencias | Todo hasta decenas de GB | Casi nada en esta tarea |
SQLite + .import | Ya viene en macOS y Linux | Conservar la tabla para después | Tipos: todo entra como TEXT |
q | pip install q-text-as-data | Comandos rápidos de una línea en un pipeline de shell | Archivos grandes; es más lento |
csvsql (csvkit) | pip install csvkit | Generar un CREATE TABLE para una base de datos real | Velocidad: es el más lento de aquí |
Pandas + pandasql | Ya tienes pandas | Cuando el resultado alimenta más código en Python | El dialecto SQL es el de SQLite, no el de Postgres |
| App de celular con SQL | App Store / Play | El archivo llegó a tu correo, en el celular | Cualquier cosa que quieras automatizar |
El ranking honesto es corto: DuckDB para casi todo, SQLite cuando no puedes instalar nada, y una app de celular cuando no estás frente a una computadora. Las demás están bien y tienen sus momentos, pero ninguna le gana a DuckDB en esta tarea concreta.
DuckDB a fondo
La instalación es un solo binario: brew install duckdb, o descárgalo y ponlo en tu path. Luego:
duckdb
D SELECT * FROM 'orders.csv' LIMIT 10;
Hay dos cosas que debes saber desde el principio:
El nombre del archivo necesita comillas simples. FROM orders.csv se interpreta como un esquema llamado orders y una tabla llamada csv, y el mensaje de error no lo deja claro. FROM 'orders.csv' es lo correcto.
Los nombres de columna con espacios necesitan comillas dobles. SELECT "Order Date" FROM …. Con comillas simples obtienes la cadena literal, en silencio, en todas las filas, lo que parece que la consulta funcionó.
Usa un glob para leer todo un directorio cuando la exportación llegó en partes mensuales:
SELECT * FROM 'exports/*.csv'
Cuando la inferencia de tipos se equivoca, normalmente en una columna de IDs con ceros a la izquierda o en fechas con un formato que no reconoce, toma el control de la lectura en lugar de pelear con ella:
SELECT * FROM read_csv('orders.csv',
header = true,
columns = {'id': 'VARCHAR', 'ordered_at': 'DATE', 'amount': 'DOUBLE'},
dateformat = '%d/%m/%Y'
)
read_csv también acepta delim, quote, escape, ignore_errors = true y sample_size = -1 (escanear todo el archivo antes de decidir los tipos, en vez de las primeras 20 480 filas). Esta última es la solución cuando una columna parece numérica durante diez mil filas y luego se topa con N/A.
Guarda la respuesta en un archivo sin salir de SQL:
COPY (SELECT * FROM 'orders.csv' WHERE amount > 1000)
TO 'large-orders.csv' (HEADER, DELIMITER ',');
Y si vas a consultar el archivo más de un puñado de veces, conviértelo una vez con COPY (SELECT * FROM 'orders.csv') TO 'orders.parquet' y consulta el Parquet. Tiene tipos, está comprimido y normalmente se escanea de cinco a veinte veces más rápido.
SQLite, cuando no puedes instalar nada
sqlite3 viene con macOS y con la mayoría de las distribuciones de Linux, así que es la opción que funciona en una máquina restringida.
sqlite3 shop.db
sqlite> .mode csv
sqlite> .import orders.csv orders
sqlite> SELECT region, COUNT(*) FROM orders GROUP BY region;
El inconveniente, y es importante: .import convierte todas las columnas en TEXT. El orden pasa a ser lexicográfico, así que 100 va antes que 20, y SUM() da sorpresas. Haz el cast al consultar (CAST(amount AS REAL)), o crea la tabla con tipos reales antes de importar.
A diferencia de DuckDB, esto deja un shop.db atrás, lo cual es justo lo que quieres si vas a necesitar la tabla mañana, y un estorbo si no.
q, para un pipeline de shell
Cuando la consulta es un paso dentro de una cadena de comandos de shell, q lee la entrada estándar como cualquier otra herramienta de Unix:
q -H -d, "SELECT region, SUM(amount) FROM - GROUP BY region" < orders.csv
-H indica que la primera fila es un encabezado, -d, fija el delimitador y - es stdin. Por debajo es SQLite, así que el dialecto es el de SQLite. También es la opción razonable más lenta con un archivo grande: bien para unos cientos de miles de filas, penosa más allá de eso.
Cuando el archivo está en tu celular
Nada de lo anterior sirve cuando la exportación es un adjunto en tu correo y no estás en un escritorio. Pasa seguido, y la respuesta habitual, «envíatelo y míralo después», es mala.
Ejecuta SQL de verdad sobre el archivo en iOS y Android, con un constructor visual de filtros para cuando prefieres no escribir una consulta en el teclado del celular, y un asistente en lenguaje normal para cuando todavía no conoces los nombres de las columnas.
Qué haceEl dialecto es el de SQLite, así que las consultas de este artículo se trasladan con las salvedades de siempre: no hay funciones de ventana como en los ejemplos de DuckDB, y se aplican los mismos hábitos con CAST.
Consultas para tener a mano
Qué hay en este archivo:
SELECT * FROM 'file.csv' LIMIT 20;
DESCRIBE SELECT * FROM 'file.csv';
SELECT COUNT(*) FROM 'file.csv';
Dónde están los nulos, porque ir columna por columna es tedioso:
SELECT COUNT(*) - COUNT(email) AS missing_email,
COUNT(*) - COUNT(phone) AS missing_phone
FROM 'contacts.csv';
Duplicados en una clave:
SELECT id, COUNT(*) AS n
FROM 'orders.csv'
GROUP BY id HAVING n > 1
ORDER BY n DESC;
Unir dos exportaciones es la consulta que más a menudo justifica todo el ejercicio, porque hacerlo con BUSCARV entre dos hojas es mucho peor:
SELECT o.id, o.amount, c.name
FROM 'orders.csv' o
JOIN 'customers.csv' c ON c.id = o.customer_id;
Un total mensual a partir de una columna de fechas:
SELECT date_trunc('month', ordered_at) AS month, SUM(amount)
FROM 'orders.csv'
GROUP BY month ORDER BY month;
Cuando algo sale mal
Todo llegó en una sola columna. El delimitador no es una coma; el punto y coma es lo habitual en buena parte de Europa. read_csv('f.csv', delim = ';'). Si el archivo es más raro que eso, quizá esté realmente dañado y no solo sea inusual.
Una columna numérica no suma. Se infirió como texto porque aparece algo no numérico en ella. Encuentra al culpable con SELECT amount FROM 'f.csv' WHERE TRY_CAST(amount AS DOUBLE) IS NULL LIMIT 10;. Suele ser un símbolo de moneda, un separador de miles o la cadena N/A.
Desaparecieron los ceros a la izquierda. La columna se leyó como número. Declárala VARCHAR en columns, y no dejes que una hoja de cálculo se acerque después.
La fila de encabezados se repite a lo largo del archivo. Se concatenaron varias exportaciones. WHERE id <> 'id' es la solución burda y normalmente la correcta.
Se quedó sin memoria. DuckDB procesa en flujo, así que normalmente esto significa un ORDER BY o un DISTINCT sobre todo el archivo. Dale un directorio temporal (SET temp_directory = '/tmp/duckdb') y escribirá en disco. O lee cómo abrir un archivo CSV de 1 GB para ver la pregunta más amplia de qué aguanta qué a gran escala.