Importar un CSV a una base de datos SQL: SQLite, PostgreSQL y MySQL

El proveedor manda los precios de octubre en un CSV. Para importar un CSV lo más seguro es cargarlo primero en una tabla aparte (de «llegada»), revisarlo, y después actualizar la tabla de verdad con SQL. Cada motor tiene su herramienta de carga, con sus trampas.

El archivo

Con cabecera, un nombre entre comillas porque lleva una coma y otro con comillas dobles escapadas (""), como manda el formato CSV:

id,nombre,precio,stock
3,"Monitor 27"" 4K",399.90,12
6,Ratón inalámbrico,22.99,60
8,SSD 1 TB,89.90,45
16,"Hub USB-C, 7 puertos",34.90,12

SQLite: .import en la consola sqlite3

.import no es SQL: es una orden de la consola sqlite3 (las que empiezan por punto). Con --csv entiende las comillas y con --skip 1 se salta la cabecera. Después, un UPDATE … FROM (SQLite 3.33+) actualiza los que existen y un INSERT … SELECT añade los nuevos:

-- Script para la consola sqlite3: las líneas que empiezan por punto son órdenes de la consola, no SQL
.read /datos/tienda.sql
CREATE TABLE llegada (id INTEGER, nombre TEXT, precio REAL, stock INTEGER);
.import --csv --skip 1 precios-octubre.csv llegada
.mode box
SELECT * FROM llegada;

-- Actualizar precios y stock de los que ya existen
UPDATE productos
SET precio = l.precio, stock = l.stock
FROM llegada l
WHERE productos.id = l.id;

-- Dar de alta los nuevos
INSERT INTO productos (id, nombre, categoria_id, precio, stock)
SELECT id, nombre, NULL, precio, stock FROM llegada
WHERE id NOT IN (SELECT id FROM productos);

SELECT id, nombre, precio, stock FROM productos WHERE id IN (3, 6, 8, 16);
sqlite3 :memory: < importar.sqlite3
salida · sqlite3 3.46.1┌────┬──────────────────────┬────────┬───────┐
│ id │        nombre        │ precio │ stock │
├────┼──────────────────────┼────────┼───────┤
│ 3  │ Monitor 27" 4K       │ 399.9  │ 12    │
│ 6  │ Ratón inalámbrico    │ 22.99  │ 60    │
│ 8  │ SSD 1 TB             │ 89.9   │ 45    │
│ 16 │ Hub USB-C, 7 puertos │ 34.9   │ 12    │
└────┴──────────────────────┴────────┴───────┘
┌────┬──────────────────────┬────────┬───────┐
│ id │        nombre        │ precio │ stock │
├────┼──────────────────────┼────────┼───────┤
│ 3  │ Monitor 27" 4K       │ 399.9  │ 12    │
│ 6  │ Ratón inalámbrico    │ 22.99  │ 60    │
│ 8  │ SSD 1 TB             │ 89.9   │ 45    │
│ 16 │ Hub USB-C, 7 puertos │ 34.9   │ 12    │
└────┴──────────────────────┴────────┴───────┘

Si la tabla no existe, .import la crea usando la primera línea como nombres de columna, pero todas las columnas son de texto. Y como texto, '89.90' va después de '399.90':

-- Sin crear la tabla antes: .import la crea con la cabecera como nombres y todo como TEXT
.import --csv precios-octubre.csv llegada
.mode box
SELECT name, type FROM pragma_table_info('llegada');
SELECT nombre, precio, typeof(precio) AS tipo FROM llegada WHERE id = '3';
-- Como texto, '89.90' va antes que '399.90' al ordenar de mayor a menor
SELECT nombre, precio FROM llegada ORDER BY precio DESC;
salida · sqlite3┌────────┬──────┐
│  name  │ type │
├────────┼──────┤
│ id     │ TEXT │
│ nombre │ TEXT │
│ precio │ TEXT │
│ stock  │ TEXT │
└────────┴──────┘
┌────────────────┬────────┬──────┐
│     nombre     │ precio │ tipo │
├────────────────┼────────┼──────┤
│ Monitor 27" 4K │ 399.90 │ text │
└────────────────┴────────┴──────┘
┌──────────────────────┬────────┐
│        nombre        │ precio │
├──────────────────────┼────────┤
│ SSD 1 TB             │ 89.90  │
│ Monitor 27" 4K       │ 399.90 │
│ Hub USB-C, 7 puertos │ 34.90  │
│ Ratón inalámbrico    │ 22.99  │
└──────────────────────┴────────┘

PostgreSQL: COPY

COPY … FROM lee un archivo del servidor; necesita permisos de superusuario o del rol pg_read_server_files. Desde tu equipo, la orden de la consola psql \copy hace lo mismo con un archivo local, con la misma sintaxis.

CREATE TABLE llegada (id INTEGER, nombre VARCHAR(60), precio NUMERIC(10,2), stock INTEGER);

-- COPY lee el archivo en el servidor; con psql, \copy hace lo mismo con un archivo de tu equipo
COPY llegada FROM '/w/precios-octubre.csv' WITH (FORMAT csv, HEADER true);

SELECT * FROM llegada;

UPDATE productos p
SET precio = l.precio, stock = l.stock
FROM llegada l
WHERE p.id = l.id;

INSERT INTO productos (id, nombre, categoria_id, precio, stock)
SELECT id, nombre, NULL, precio, stock FROM llegada
WHERE id NOT IN (SELECT id FROM productos);

SELECT id, nombre, precio, stock FROM productos WHERE id IN (3, 6, 8, 16) ORDER BY id;
resultado · PostgreSQL id |        nombre        | precio | stock 
----+----------------------+--------+-------
  3 | Monitor 27" 4K       | 399.90 |    12
  6 | Ratón inalámbrico    |  22.99 |    60
  8 | SSD 1 TB             |  89.90 |    45
 16 | Hub USB-C, 7 puertos |  34.90 |    12

 id |        nombre        | precio | stock 
----+----------------------+--------+-------
  3 | Monitor 27" 4K       | 399.90 |    12
  6 | Ratón inalámbrico    |  22.99 |    60
  8 | SSD 1 TB             |  89.90 |    45
 16 | Hub USB-C, 7 puertos |  34.90 |    12

El primer resultado es la tabla de llegada; el segundo, los productos ya actualizados (el hub USB-C es nuevo).

MySQL: LOAD DATA LOCAL INFILE

La forma de MySQL es LOAD DATA. Con LOCAL lee un archivo del equipo del cliente, pero viene desactivado por seguridad, en el servidor y en el cliente:

CREATE TABLE llegada (id INTEGER, nombre VARCHAR(60), precio DECIMAL(10,2), stock INTEGER);

LOAD DATA LOCAL INFILE '/w/precios-octubre.csv' INTO TABLE llegada
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
IGNORE 1 LINES;
resultado · MySQLERROR 3948 (42000) at line 3: Loading local data is disabled; this must be enabled on both the client and server sides

Hay que activarlo en los dos lados: en el servidor, local_infile, y en el cliente, arrancando mysql con --local-infile=1:

mysql --local-infile=1 -u usuario -p tienda < cargar.sql
-- mysql: --local-infile=1
-- Con el cliente arrancado con --local-infile=1 y el servidor con local_infile activado
SET GLOBAL local_infile = 1;

CREATE TABLE llegada (id INTEGER, nombre VARCHAR(60), precio DECIMAL(10,2), stock INTEGER);

LOAD DATA LOCAL INFILE '/w/precios-octubre.csv' INTO TABLE llegada
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
IGNORE 1 LINES;

SELECT * FROM llegada;
resultado · MySQL+------+----------------------+--------+-------+
| id   | nombre               | precio | stock |
+------+----------------------+--------+-------+
|    3 | Monitor 27" 4K       | 399.90 |    12 |
|    6 | Ratón inalámbrico    |  22.99 |    60 |
|    8 | SSD 1 TB             |  89.90 |    45 |
|   16 | Hub USB-C, 7 puertos |  34.90 |    12 |
+------+----------------------+--------+-------+
MotorOrdenLee el archivo de…
SQLite.import --csv (consola sqlite3)Tu equipo
PostgreSQLCOPY … FROMEl servidor
PostgreSQL\copy … FROM (consola psql)Tu equipo
MySQLLOAD DATA INFILEEl servidor (carpeta secure_file_priv)
MySQLLOAD DATA LOCAL INFILETu equipo, si está activado en los dos lados
Revisa antes de tocar la tabla buena. La tabla de llegada permite comprobar con SQL que no haya precios vacíos, ids repetidos o productos desconocidos antes de actualizar nada. Si el CSV viene de fuera, trátalo como datos no fiables. Desde un programa, la alternativa es leer el CSV con el módulo de tu lenguaje e insertar con parámetros, como en SQLite desde Python.

Todas las consultas de esta página se ejecutaron antes de publicarla sobre la base de ejemplo «tienda» del curso, en contenedores limpios: SQLite 3.46.1 (el de Python 3.13), PostgreSQL 17 y MySQL 8.4. Los resultados y los mensajes de error están copiados de esa ejecución. Documentación oficial: COPY en PostgreSQL 17.