Ya tienes dónde poner los datos. Toca ponerlos. INSERT es la instrucción que añade filas a una tabla y, en apariencia, es la más simple del módulo: nombre de tabla, columnas, valores, punto. Pero detrás de esa simplicidad hay decisiones que separan un script que aguanta cinco años de uno que se rompe en la primera refactorización: listar o no listar las columnas, insertar una fila o cuarenta en una sola sentencia, dejar que el motor genere el id o forzarlo (y qué desastre silencioso provoca lo segundo), y cómo recuperar el id recién creado para poder insertar las líneas del pedido que acabas de registrar.

En esta lección aprenderás las cinco formas de INSERT, entenderás por fin qué hace el bloque de setval con el que termina tiendaverde.sql, sabrás diagnosticar de un vistazo cada uno de los cuatro errores que un INSERT puede darte, y terminarás registrando un pedido completo con sus tres líneas usando RETURNING, que es exactamente lo que hace una tienda online cada vez que alguien pulsa "Confirmar compra".

Contenido

  1. La sintaxis, y por qué hay que listar siempre las columnas
  2. Insertar varias filas en una sola sentencia
  3. DEFAULT y DEFAULT VALUES
  4. La columna de identidad: omitirla, forzarla y setval
  5. RETURNING: recuperar lo que acabas de insertar
  6. INSERT ... SELECT: insertar el resultado de una consulta
  7. Los cuatro errores de un INSERT y su diagnóstico
  8. El orden de inserción cuando hay claves foráneas
  9. ON CONFLICT DO NOTHING, de pasada
  10. Carga masiva: COPY y \copy frente a INSERT
  11. Ejemplo completo: alta de producto y registro de un pedido
  12. Errores Comunes y Consejos
  13. Ejercicios
  14. Conclusión

  1. La sintaxis, y por qué hay que listar siempre las columnas

INSERT INTO tabla (columna1, columna2, ...)
VALUES (valor1, valor2, ...);

Un alta de proveedor:

INSERT INTO proveedores (nombre, pais, email, activo)
VALUES ('Cooperativa La Safor', 'España', '[email protected]', TRUE);
INSERT 0 1

Esa salida de psql merece una explicación, porque desconcierta a todo el mundo la primera vez:

Parte Significado
INSERT El tipo de sentencia
0 El OID de la fila insertada. Es una reliquia: hoy siempre vale 0
1 El número de filas insertadas. Este es el número que importa

Cada vez que ejecutes un INSERT, mira el segundo número. Si esperabas 5 filas y pone INSERT 0 3, algo va mal.

La forma sin columnas, y por qué no debes usarla

SQL permite omitir la lista de columnas si das un valor para todas, en el orden exacto de la tabla:

-- ⚠️ INCORRECTA como práctica, aunque funcione hoy
INSERT INTO proveedores
VALUES (7, 'Cooperativa La Safor', 'España', '[email protected]', TRUE);
INSERT 0 1

Funciona. Y es una bomba de relojería, por cuatro motivos:

Problema Qué pasa
Dependes del orden físico Si alguien reordena las columnas con un ALTER TABLE, tus INSERT empiezan a meter el país en el email
Se rompe al añadir columnas El día que proveedores gane una columna telefono, todos estos INSERT fallarán con INSERT has more target columns than expressions… o peor, seguirán funcionando y desplazarán los datos
Ilegible ('España', 'ventas@…', TRUE) no dice qué es qué. Con quince columnas es directamente indescifrable
Obliga a dar el id Al listar todas las columnas incluyes la de identidad, y ahí empieza el problema del apartado 4

La regla, sin excepciones: lista siempre las columnas. Cuesta veinte caracteres y te ahorra una clase entera de bugs. Es, con diferencia, el hábito más rentable de esta lección.

Con la lista explícita puedes además omitir las columnas que tengan DEFAULT o admitan nulos, y ponerlas en el orden que quieras:

INSERT INTO proveedores (pais, nombre)
VALUES ('Portugal', 'Quinta Bio Douro');
INSERT 0 1

id lo genera la identidad, email queda NULL (es nulable) y activo toma su DEFAULT TRUE. Tres columnas rellenas sin escribirlas.

  1. Insertar varias filas en una sola sentencia

Basta separar las tuplas por comas:

INSERT INTO categorias (nombre, descripcion) VALUES
('Despensa a granel', 'Legumbres, cereales y frutos secos sin envase'),
('Bebés',             'Cuidado infantil con certificación ecológica'),
('Mascotas',          'Alimentación y accesorios para animales de compañía');
INSERT 0 3

Es la forma que usa tiendaverde.sql en sus nueve bloques de carga, y no es solo cuestión de comodidad: es muchísimo más rápido.

Por qué es más rápido

Un INSERT de N filas no cuesta lo mismo que N INSERT de una fila:

Coste N sentencias separadas 1 sentencia con N tuplas
Viajes de red cliente↔servidor N 1
Análisis y planificación de la consulta N veces 1 vez
Transacciones implícitas (sin BEGIN) N confirmaciones a disco 1
Comprobación de restricciones N (inevitable) N (inevitable)

Los tres primeros costes son fijos por sentencia y suelen dominar. En la práctica, insertar 1 000 filas con una sola sentencia frente a 1 000 sentencias sueltas puede ser entre 10 y 50 veces más rápido, y la mayor parte de esa diferencia viene del tercer punto: sin una transacción explícita, cada INSERT suelto se confirma a disco por su cuenta.

Límites prácticos: no hagas una sentencia con 100 000 tuplas. El texto de la consulta se vuelve enorme y el consumo de memoria del servidor también. Lotes de 500 a 5 000 filas son el punto dulce habitual. Y si hablamos de millones, la herramienta ya no es INSERT sino COPY (apartado 10).

  1. DEFAULT y DEFAULT VALUES

La palabra clave DEFAULT puede aparecer como valor, y significa "usa el valor por omisión de esta columna":

INSERT INTO productos (nombre, categoria_id, proveedor_id, precio, coste, stock, activo, fecha_alta)
VALUES ('Lentejas pardinas ecológicas 500 g', 1, 1, 2.45, 1.05, DEFAULT, DEFAULT, DEFAULT);
INSERT 0 1

stock toma 0, activo toma TRUE y fecha_alta toma CURRENT_DATE. Es idéntico a haber omitido las tres columnas, y de hecho omitirlas es preferible: se lee mejor. DEFAULT como valor solo resulta útil cuando construyes la sentencia desde un programa y te sale más cómodo mantener fija la lista de columnas.

DEFAULT VALUES inserta una fila con todos los valores por omisión:

CREATE TABLE demo_defaults (
    id     INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    estado VARCHAR(20) NOT NULL DEFAULT 'pendiente',
    fecha  DATE        NOT NULL DEFAULT CURRENT_DATE
);

INSERT INTO demo_defaults DEFAULT VALUES;
INSERT 0 1
SELECT * FROM demo_defaults;
id estado fecha
1 pendiente 2026-03-01

(La fecha será la del día en que lo ejecutes.) Es un caso raro, pero existe: tablas de "reserva de identificador" o de estado inicial que se rellenan después.

  1. La columna de identidad: omitirla, forzarla y setval

Este apartado explica, por fin, el bloque más críptico de tiendaverde.sql.

Lo normal: omitir el id

INSERT INTO categorias (nombre, descripcion)
VALUES ('Despensa a granel', 'Legumbres, cereales y frutos secos sin envase');

Como no has dado id, PostgreSQL pide el siguiente valor a la secuencia asociada. Es lo correcto y lo que harás el 99 % de las veces.

Forzarlo: legal con BY DEFAULT, y peligroso

Como TiendaVerde declara sus identidades GENERATED BY DEFAULT, puedes dar el id a mano:

INSERT INTO categorias (id, nombre, descripcion)
VALUES (7, 'Bebés', 'Cuidado infantil con certificación ecológica');
INSERT 0 1

Funciona… y no toca la secuencia. La secuencia sigue creyendo que el último valor entregado es el 6. Así que el siguiente INSERT sin id:

INSERT INTO categorias (nombre, descripcion)
VALUES ('Mascotas', 'Alimentación y accesorios para animales');
ERROR:  duplicate key value violates unique constraint "categorias_pkey"
DETAIL:  Key (id)=(7) already exists.

La secuencia ha entregado el 7, que ya estaba ocupado. Ese es el desajuste de la secuencia, y es uno de los fallos más desconcertantes que existen: la sentencia que falla es correcta y el error apunta a un id que tú no has escrito.

flowchart TD
    A["Secuencia en 6<br/>categorias tiene ids 1..6"] --> B["INSERT con id = 7<br/>explícito"]
    B --> C["Tabla: ids 1..7<br/>Secuencia: sigue en 6 ❌"]
    C --> D["INSERT sin id"]
    D --> E["La secuencia entrega 7"]
    E --> F["💥 duplicate key value<br/>violates categorias_pkey"]

El arreglo: setval

setval reposiciona la secuencia. La forma robusta, que no exige saber cómo se llama la secuencia:

SELECT setval(pg_get_serial_sequence('categorias', 'id'),
              (SELECT MAX(id) FROM categorias));
setval
7

Ahora el siguiente INSERT sin id pedirá el 8 y funcionará. Y aquí está la explicación de las nueve líneas finales del script del curso:

SELECT setval(pg_get_serial_sequence('categorias',    'id'), (SELECT MAX(id) FROM categorias));
SELECT setval(pg_get_serial_sequence('proveedores',   'id'), (SELECT MAX(id) FROM proveedores));
SELECT setval(pg_get_serial_sequence('productos',     'id'), (SELECT MAX(id) FROM productos));
...

tiendaverde.sql inserta todos los id a mano —para que las lecciones puedan hablar del "cliente 7" o del "pedido 12" con ids estables y reproducibles—, así que al terminar la carga las nueve secuencias están en 1. Sin ese bloque, el primer INSERT sin id de cualquier tabla fallaría por clave duplicada. Ese es el detalle que 01-06 dejó anunciado "para el módulo 5".

Tres funciones útiles alrededor de las secuencias:

Función Qué hace
pg_get_serial_sequence('tabla','columna') Devuelve el nombre de la secuencia asociada ('public.categorias_id_seq')
currval('secuencia') Último valor entregado en tu sesión. Falla si aún no has pedido ninguno
setval('secuencia', n) Reposiciona: el siguiente valor será n + 1

Cómo evitar todo esto: declara las identidades GENERATED ALWAYS AS IDENTITY (05-01) y no las fuerces nunca. TiendaVerde usa BY DEFAULT por una razón didáctica concreta; tu esquema no tiene por qué.

Y un detalle que sorprende: las secuencias no se deshacen con un ROLLBACK. Si una transacción consume el valor 21 y luego aborta, ese 21 se pierde para siempre. Es deliberado —de lo contrario dos sesiones concurrentes tendrían que esperarse—, y significa que los id autogenerados tienen huecos. No son un contador de filas ni deben usarse como tal.

  1. RETURNING: recuperar lo que acabas de insertar

Un INSERT normal solo te dice cuántas filas ha metido. RETURNING, extensión de PostgreSQL, te devuelve las filas insertadas, con sus valores generados incluidos:

INSERT INTO productos (nombre, categoria_id, proveedor_id, precio, coste, stock)
VALUES ('Garbanzos ecológicos 500 g', 1, 1, 2.60, 1.15, 140)
RETURNING id, nombre, precio, stock, activo, fecha_alta;
id nombre precio stock activo fecha_alta
21 Garbanzos ecológicos 500 g 2.60 140 true 2026-03-01
INSERT 0 1

Ahí tienes el id que ha generado el motor (21, el siguiente al 20), el activo que ha puesto el DEFAULT y la fecha_alta que ha puesto CURRENT_DATE. Todo eso lo habrías tenido que ir a buscar con un SELECT posterior… que, además, no sería fiable en un sistema con varios usuarios: entre tu INSERT y tu SELECT puede haber entrado otro pedido.

RETURNING admite lo mismo que un SELECT: columnas, expresiones, alias y *.

INSERT INTO productos (nombre, categoria_id, proveedor_id, precio, coste, stock)
VALUES ('Quinoa real ecológica 500 g', 1, 3, 6.80, 3.40, 90)
RETURNING id,
          nombre,
          precio,
          coste,
          precio - coste                      AS margen,
          ROUND((precio - coste) / precio, 4) AS margen_relativo;
id nombre precio coste margen margen_relativo
22 Quinoa real ecológica 500 g 6.80 3.40 3.40 0.5000

Por qué RETURNING es imprescindible

El caso canónico es insertar un padre y sus hijos: un pedido y sus líneas. Sin RETURNING, la secuencia sería:

  1. INSERT INTO pedidos ...
  2. SELECT MAX(id) FROM pedidosaquí está el fallo
  3. INSERT INTO lineas_pedido (pedido_id, ...) VALUES (ese_id, ...)

El paso 2 es una condición de carrera: si otro cliente confirma su compra entre tu paso 1 y tu paso 2, MAX(id) te devuelve el pedido de otro y le cuelgas tus líneas. En una tienda con tráfico real esto no es una posibilidad teórica: pasa. RETURNING lo resuelve de raíz porque devuelve tu fila, no la última de la tabla.

Equivalentes por motor

Motor Cómo se recupera el id generado
PostgreSQL INSERT ... RETURNING id (también en UPDATE y DELETE)
MySQL / MariaDB SELECT LAST_INSERT_ID(); tras el INSERT, en la misma conexión
SQL Server SCOPE_IDENTITY(), o la cláusula OUTPUT INSERTED.id en la propia sentencia
SQLite SELECT last_insert_rowid(); o RETURNING desde la versión 3.35
Oracle INSERT ... RETURNING id INTO :variable (en PL/SQL o con parámetro de salida)

Dos avisos sobre esta tabla. LAST_INSERT_ID() de MySQL es por conexión, así que es seguro frente a otros usuarios pero se pisa a sí mismo si haces dos INSERT seguidos. Y en SQL Server hay que preferir SCOPE_IDENTITY() a @@IDENTITY: el segundo devuelve el id generado por cualquier ámbito, incluidos los triggers, lo cual es una fuente clásica de errores.

RETURNING funciona igual en UPDATE (05-03) y en DELETE (05-04), y ahí es todavía más útil: te deja ver exactamente qué has cambiado o qué has borrado.

  1. INSERT ... SELECT: insertar el resultado de una consulta

En lugar de VALUES, un INSERT puede alimentarse de una consulta. Es DML puro: no estás anidando una subconsulta (eso es el módulo 7), estás conectando la salida de un SELECT con la entrada de un INSERT.

INSERT INTO tabla_destino (col1, col2, ...)
SELECT expr1, expr2, ...
FROM ...
WHERE ...;

Las columnas se emparejan por posición, no por nombre. Es la trampa clásica: si el SELECT devuelve (nombre, precio) y la lista de destino dice (precio, nombre), PostgreSQL se quejará por tipos incompatibles… o no se quejará y te dejará los datos cruzados.

Caso 1: poblar una tabla de histórico

Antes de archivar los pedidos entregados, guardamos una foto:

CREATE TABLE pedidos_historico (
    id            INTEGER       PRIMARY KEY,
    cliente_id    INTEGER       NOT NULL,
    fecha_pedido  DATE          NOT NULL,
    estado        VARCHAR(20)   NOT NULL,
    gastos_envio  NUMERIC(10,2) NOT NULL,
    fecha_archivo DATE          NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE
INSERT INTO pedidos_historico (id, cliente_id, fecha_pedido, estado, gastos_envio)
SELECT pe.id,
       pe.cliente_id,
       pe.fecha_pedido,
       pe.estado,
       pe.gastos_envio
FROM pedidos AS pe
WHERE pe.estado = 'entregado';
INSERT 0 14

14 filas, los 14 pedidos entregados. fecha_archivo no aparece en la lista de destino, así que toma su DEFAULT CURRENT_DATE en todas.

Caso 2: duplicar el catálogo de un proveedor

El proveedor 5 (EcoNordic Supplies) está inactivo. Verde Atlántico (proveedor 3) se ofrece a suministrar los mismos artículos con un 8 % de recargo en el coste. En lugar de teclear cuatro altas:

INSERT INTO productos (nombre, categoria_id, proveedor_id, precio, coste, stock, activo, fecha_alta)
SELECT p.nombre || ' (Verde Atlántico)',
       p.categoria_id,
       3,
       p.precio,
       ROUND(p.coste * 1.08, 2),
       0,
       TRUE,
       DATE '2026-03-01'
FROM productos AS p
WHERE p.proveedor_id = 5
ORDER BY p.id;
INSERT 0 4
SELECT id, nombre, categoria_id, proveedor_id, precio, coste, stock
FROM productos
WHERE id > 20
ORDER BY id;
id nombre categoria_id proveedor_id precio coste stock
21 Detergente ecológico concentrado 1 L (Verde Atlántico) 3 3 11.20 6.48 0
22 Velas de cera de soja (pack 2) (Verde Atlántico) 3 3 13.75 7.45 0
23 Cepillo de dientes de bambú (Verde Atlántico) 5 3 3.50 1.30 0
24 Cápsulas de espirulina 120 uds (Verde Atlántico) 6 3 16.40 9.40 0

Cuatro altas con una sentencia. Fíjate en lo que hace el SELECT: transforma mientras copia. Concatena un sufijo al nombre, sustituye el proveedor_id por una constante, recalcula el coste y pone el stock a cero. Ese es el verdadero valor de INSERT ... SELECT: no es copiar, es copiar transformando.

Comprobación de los costes, redondeados a dos decimales: 6,00 × 1,08 = 6,48; 6,90 × 1,08 = 7,452 → 7,45; 1,20 × 1,08 = 1,296 → 1,30; 8,70 × 1,08 = 9,396 → 9,40.

Caso 3: INSERT ... SELECT con LIMIT

Todo lo que sabes del módulo 2 sirve aquí, ORDER BY y LIMIT incluidos:

CREATE TEMP TABLE top_productos (
    posicion INTEGER,
    id       INTEGER,
    nombre   VARCHAR(150),
    precio   NUMERIC(10,2)
);

INSERT INTO top_productos (posicion, id, nombre, precio)
SELECT ROW_NUMBER() OVER (ORDER BY p.precio DESC, p.id),
       p.id,
       p.nombre,
       p.precio
FROM productos AS p
ORDER BY p.precio DESC, p.id
LIMIT 5;
INSERT 0 5
SELECT * FROM top_productos ORDER BY posicion;
posicion id nombre precio
1 15 Té verde matcha ceremonial 30 g 22.00
2 6 Crema facial de aloe vera 50 ml 18.90
3 20 Cápsulas de espirulina 120 uds 16.40
4 8 Aceite corporal de almendras 200 ml 14.25
5 13 Velas de cera de soja (pack 2) 13.75

(ROW_NUMBER() es una función de ventana del módulo 10; aquí solo numera las filas del ranking.)

Y ojo con un matiz: ORDER BY dentro de un INSERT ... SELECT no garantiza el orden físico de las filas en la tabla destino, porque en el modelo relacional una tabla es un conjunto sin orden (01-05). Solo determina qué filas entran cuando hay LIMIT. Para leerlas ordenadas necesitarás un ORDER BY en el SELECT, siempre.

  1. Los cuatro errores de un INSERT y su diagnóstico

Un INSERT puede fallar por cuatro motivos, uno por cada tipo de restricción. Conocer los mensajes literales te ahorra horas.

7.1. Violación de NOT NULL

INSERT INTO productos (nombre, categoria_id, precio)
VALUES (NULL, 1, 4.50);
ERROR:  null value in column "nombre" of relation "productos" violates not-null constraint
DETAIL:  Failing row contains (23, null, 1, null, 4.50, null, 0, t, 2026-03-01).

El DETAIL te muestra la fila completa tal como habría quedado, con los valores por omisión ya aplicados. Es muy útil: ahí ves qué columnas se rellenaron solas.

7.2. Violación de UNIQUE (o de la PK)

INSERT INTO categorias (nombre, descripcion)
VALUES ('Bebidas', 'Duplicada');
ERROR:  duplicate key value violates unique constraint "categorias_nombre_key"
DETAIL:  Key (nombre)=(Bebidas) already exists.

El nombre de la restricción te dice qué columna. Si fuera categorias_pkey, sería el id: probablemente el desajuste de secuencia del apartado 4.

7.3. Violación de CHECK

INSERT INTO resenas (producto_id, cliente_id, puntuacion, comentario, fecha)
VALUES (1, 14, 7, 'Genial', '2026-03-01');
ERROR:  new row for relation "resenas" violates check constraint "resenas_puntuacion_check"
DETAIL:  Failing row contains (13, 1, 14, 7, Genial, 2026-03-01).

El nombre de la restricción identifica la regla. Aquí es la puntuación de 1 a 5, y por eso 05-01 insistía tanto en nombrarlas: chk_resenas_puntuacion habría sido igual de claro; resenas_check1 no lo habría sido.

7.4. Violación de FOREIGN KEY

INSERT INTO pedidos (cliente_id, fecha_pedido, estado, metodo_pago, gastos_envio)
VALUES (999, '2026-03-01', 'pendiente', 'tarjeta', 4.95);
ERROR:  insert or update on table "pedidos" violates foreign key constraint "pedidos_cliente_id_fkey"
DETAIL:  Key (cliente_id)=(999) is not present in table "clientes".

is not present in table es la firma inconfundible de una FK rota: has referenciado algo que no existe.

Tabla de diagnóstico rápido

Fragmento del mensaje Restricción rota Qué mirar
violates not-null constraint NOT NULL Falta un valor obligatorio; míralo en el DETAIL
duplicate key value violates unique constraint "..._pkey" PRIMARY KEY ¿Estás forzando el id? ¿Secuencia desajustada? → setval
duplicate key value violates unique constraint "..._key" UNIQUE Ya existe una fila con ese valor. ¿Querías un UPSERT? → 05-05
violates check constraint CHECK Valor fuera de dominio o de rango. El nombre te dice cuál
violates foreign key constraint + is not present in table FOREIGN KEY El padre no existe. ¿Orden de inserción? → apartado 8
violates foreign key constraint + is still referenced from table FOREIGN KEY Esto no es un INSERT: es un DELETE → 05-04
column "x" of relation "y" does not exist Errata en el nombre de columna. \d y
INSERT has more expressions than target columns Descuadre entre la lista de columnas y la de valores
invalid input syntax for type numeric: "12,50" Coma decimal en lugar de punto (01-03)

Un detalle importante: cuando un INSERT multifila falla, no se inserta ninguna fila. Una sentencia es atómica: o entra entera o no entra nada. Si insertas 500 tuplas y la 337 viola un CHECK, las 499 restantes tampoco se guardan.

  1. El orden de inserción cuando hay claves foráneas

La integridad referencial impone el mismo orden que imponía a CREATE TABLE en 05-01: primero los padres, después los hijos.

flowchart TD
    A["1 · categorias<br/>proveedores"] --> B["2 · productos"]
    A2["1 · clientes<br/>empleados"] --> C["3 · pedidos"]
    B --> D["4 · lineas_pedido"]
    C --> D
    B --> E["4 · resenas"]
    A2 --> E
    C --> F["4 · devoluciones"]

Intenta saltártelo y verás por qué:

-- ⚠️ INCORRECTA: la categoría 9 aún no existe
INSERT INTO productos (nombre, categoria_id, proveedor_id, precio, coste, stock)
VALUES ('Pan de espelta congelado', 9, 1, 3.20, 1.60, 40);
ERROR:  insert or update on table "productos" violates foreign key constraint "productos_categoria_id_fkey"
DETAIL:  Key (categoria_id)=(9) is not present in table "categorias".

Primero la categoría, después el producto:

-- ✅ CORRECTA
INSERT INTO categorias (id, nombre, descripcion)
VALUES (9, 'Panadería', 'Pan y bollería ecológica congelada');

INSERT INTO productos (nombre, categoria_id, proveedor_id, precio, coste, stock)
VALUES ('Pan de espelta congelado', 9, 1, 3.20, 1.60, 40);
INSERT 0 1
INSERT 0 1

Las relaciones reflexivas tienen un matiz propio. clientes.referido_por_id apunta a la misma tabla, así que el referidor debe existir antes que el referido. En el script del curso funciona porque los clientes están ordenados por id y ninguno es referido por otro de id mayor. Si no fuera así, tendrías dos opciones: insertar primero con referido_por_id a NULL y actualizarlo después (05-03), o usar una restricción diferida (DEFERRABLE INITIALLY DEFERRED), que pospone la comprobación al final de la transacción. Esta última pertenece de lleno al modelo transaccional del módulo 9.

  1. ON CONFLICT DO NOTHING, de pasada

Si intentas insertar una fila que viola un UNIQUE, el INSERT falla. A veces lo que quieres es que no falle y simplemente no haga nada:

INSERT INTO categorias (nombre, descripcion)
VALUES ('Bebidas', 'Intento de duplicado')
ON CONFLICT DO NOTHING;
INSERT 0 0

Cero filas insertadas, cero errores. Es tremendamente útil para scripts que deben poder reejecutarse (cargas de datos maestros, semillas de desarrollo).

Su hermana mayor, ON CONFLICT ... DO UPDATE, resuelve el clásico "insértalo si no existe y actualízalo si existe". Eso es el UPSERT, y tiene lección propia: 05-05.

  1. Carga masiva: COPY y \copy frente a INSERT

Para volúmenes grandes, INSERT no es la herramienta. PostgreSQL tiene COPY, diseñada específicamente para mover datos entre un fichero y una tabla:

-- COPY se ejecuta en el SERVIDOR: la ruta es del servidor
-- y hace falta ser superusuario o tener el rol pg_read_server_files
COPY productos (nombre, categoria_id, proveedor_id, precio, coste, stock)
FROM '/var/lib/postgresql/import/productos.csv'
WITH (FORMAT csv, HEADER true, DELIMITER ',');
COPY 4500

\copy es el comando equivalente de psql, que lee el fichero en tu máquina y lo envía por la conexión. No necesita permisos especiales y es el que usarás casi siempre:

tiendaverde=> \copy productos (nombre, categoria_id, proveedor_id, precio, coste, stock) FROM 'productos.csv' WITH (FORMAT csv, HEADER true)
COPY 4500

También funciona en sentido inverso, para exportar:

tiendaverde=> \copy (SELECT id, nombre, precio FROM productos ORDER BY id) TO 'catalogo.csv' WITH (FORMAT csv, HEADER true)
COPY 20

Comparativa honesta:

INSERT multifila COPY / \copy
Origen de los datos Texto SQL Fichero CSV/texto o flujo
Velocidad relativa Referencia 5 a 20 veces más rápido
Análisis de la sentencia Una vez por sentencia Ninguno: es un protocolo binario/textual
Comprobación de restricciones Sí, todas
ON CONFLICT No
Transformación de datos Sí (con INSERT ... SELECT) No: entra tal cual
Si falla una fila Falla la sentencia entera Falla la carga entera
Estándar SQL No: es de PostgreSQL

La regla: decenas o cientos de filas → INSERT multifila. Miles o millones → COPY. Y si necesitas transformar mientras cargas, el patrón profesional es de dos pasos: COPY a una tabla de staging sin restricciones, y de ahí INSERT ... SELECT transformando hacia la tabla definitiva. Ese patrón reaparecerá en 05-05 y en el módulo 11.

Equivalentes por motor: MySQL tiene LOAD DATA INFILE, SQL Server tiene BULK INSERT y la utilidad bcp, Oracle tiene SQL*Loader y las external tables, y SQLite tiene el comando .import de su consola. Ninguno es compatible con los demás.

  1. Ejemplo completo: alta de producto y registro de un pedido

Cerramos con lo que hace una tienda de verdad. Trabajamos sobre la base recién cargada.

11.1. Dar de alta un producto nuevo

Huerta del Turia (proveedor 1) incorpora garbanzos a la categoría Alimentación:

INSERT INTO productos (nombre, categoria_id, proveedor_id, precio, coste, stock)
VALUES ('Garbanzos ecológicos 500 g', 1, 1, 2.60, 1.15, 140)
RETURNING id, nombre, precio, coste, stock, activo, fecha_alta;
id nombre precio coste stock activo fecha_alta
21 Garbanzos ecológicos 500 g 2.60 1.15 140 true 2026-03-01
INSERT 0 1

Tres columnas omitidas y tres columnas rellenas solas: id por la identidad, activo y fecha_alta por sus DEFAULT. Exactamente el trabajo que 05-01 dejó declarado en el DDL.

11.2. Registrar un pedido con sus líneas

Pau Llorens Vidal (cliente 6) llama por teléfono. Le atiende Óscar Peris Blasco (empleado 4). Quiere dos aceites, una miel y tres infusiones, con portes de 4,95 €.

Paso 1: la cabecera, con RETURNING.

INSERT INTO pedidos (cliente_id, empleado_id, fecha_pedido, estado, metodo_pago, gastos_envio)
VALUES (6, 4, DATE '2026-03-02', 'pendiente', 'tarjeta', 4.95)
RETURNING id, cliente_id, empleado_id, fecha_pedido, estado, gastos_envio;
id cliente_id empleado_id fecha_pedido estado gastos_envio
21 6 4 2026-03-02 pendiente 4.95
INSERT 0 1

El pedido es el 21. Ese número no lo hemos elegido nosotros: nos lo ha dicho el motor.

Paso 2: las tres líneas, en una sola sentencia.

Los precios se copian del catálogo en el momento de la venta —es la desnormalización deliberada de precio_unitario (01-05)— y aquí eso significa: 12,50 €, 9,75 € y 3,25 €.

INSERT INTO lineas_pedido (pedido_id, producto_id, cantidad, precio_unitario, descuento) VALUES
(21,  1, 2, 12.50, 0.00),
(21,  3, 1,  9.75, 0.00),
(21, 14, 3,  3.25, 0.00)
RETURNING id, pedido_id, producto_id, cantidad, precio_unitario,
          cantidad * precio_unitario * (1 - descuento) AS importe;
id pedido_id producto_id cantidad precio_unitario importe
48 21 1 2 12.50 25.0000
49 21 3 1 9.75 9.7500
50 21 14 3 3.25 9.7500
INSERT 0 3

Las líneas 48, 49 y 50, a continuación de las 47 que ya había.

Paso 3: comprobar el pedido completo.

SELECT pe.id                             AS pedido,
       c.nombre || ' ' || c.apellidos    AS cliente,
       e.nombre || ' ' || e.apellidos    AS comercial,
       COUNT(*)                          AS lineas,
       SUM(lp.cantidad)                  AS unidades,
       ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2) AS productos,
       pe.gastos_envio                   AS portes,
       ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)) + pe.gastos_envio, 2) AS total
FROM pedidos       AS pe
JOIN lineas_pedido AS lp ON lp.pedido_id  = pe.id
JOIN clientes      AS c  ON pe.cliente_id = c.id
JOIN empleados     AS e  ON pe.empleado_id = e.id
WHERE pe.id = 21
GROUP BY pe.id, c.nombre, c.apellidos, e.nombre, e.apellidos, pe.gastos_envio;
pedido cliente comercial lineas unidades productos portes total
21 Pau Llorens Vidal Óscar Peris Blasco 3 6 44.50 4.95 49.45

44,50 € de producto más 4,95 € de portes: 49,45 €. Todo el módulo 4 aplicado a una fila que has creado tú.

Lo que falta y por qué. Un sistema real haría estas dos inserciones dentro de una misma transacción, para que sea imposible que quede una cabecera sin líneas si algo falla en medio. Y descontaría el stock de los tres productos. Lo primero lo empezarás a usar en la próxima lección y lo estudiarás a fondo en el módulo 9; lo segundo es un UPDATE, y también es de la próxima lección.

Recuerda recargar. Si has ejecutado estos ejemplos, tu base ya no coincide con la del curso: hay 21 o más productos, 21 pedidos y 50 líneas. Vuelve a lanzar tiendaverde.sql antes de continuar.

Errores Comunes y Consejos

  • Omitir la lista de columnas. Funciona hoy y se rompe el día que alguien toque la tabla. Lista siempre las columnas.
  • Insertar el id a mano en una identidad BY DEFAULT. La secuencia se queda atrás y el siguiente INSERT automático falla con duplicate key ... _pkey. Arréglalo con setval o, mejor, no lo hagas.
  • Usar SELECT MAX(id) para saber qué acabas de insertar. Es una condición de carrera. Usa RETURNING.
  • Emparejar mal las columnas en un INSERT ... SELECT. Se emparejan por posición. Léelas dos veces.
  • Insertar hijos antes que padres. is not present in table. Padres primero, siempre.
  • Creer que un INSERT multifila inserta lo que puede. Es atómico: si falla una tupla, no entra ninguna.
  • Confundir NULL con DEFAULT. VALUES (NULL) guarda NULL (o falla con NOT NULL); VALUES (DEFAULT) aplica el valor por omisión. Omitir la columna equivale a lo segundo.
  • Escribir los decimales con coma. 12,50 no es un número en SQL: es invalid input syntax for type numeric (01-03).
  • Cargar un millón de filas con INSERT. Para eso está COPY. Y si necesitas transformar, COPY a staging y luego INSERT ... SELECT.
  • Copiar productos.precio en precio_unitario desde la tabla en lugar de fijarlo. El precio de la línea es el del momento de la venta; si lo enlazas al catálogo, mañana cambiarán tus facturas antiguas.
  • Consejo: mira siempre el segundo número de INSERT 0 N. Es el único que te dice si ha hecho lo que esperabas.
  • Consejo: usa RETURNING incluso cuando no lo necesites. Ver la fila resultante con sus DEFAULT aplicados es la forma más rápida de comprobar que el DDL hace lo que crees.
  • Consejo: para datos maestros reejecutables, ON CONFLICT DO NOTHING. Convierte un script frágil en uno idempotente.

Ejercicios

Ejercicio 1

TiendaVerde incorpora un proveedor y dos productos suyos. Escribe las sentencias necesarias, en el orden correcto, cumpliendo estos requisitos:

  1. Alta del proveedor Cooperativa La Safor, de España, con email [email protected], activo. Recupera su id con RETURNING.
  2. Alta de dos productos suyos en una sola sentencia, ambos de la categoría Alimentación (id 1):
    • Almendra marcona ecológica 250 g, precio 7,90 €, coste 4,20 €, stock 60.
    • Naranjas de Valencia ecológicas 5 kg, precio 11,50 €, coste 6,00 €, stock 35.
    • En los dos, activo y fecha_alta deben quedar en sus valores por omisión, sin escribirlos.
  3. Una consulta que muestre el proveedor nuevo con sus dos productos y el margen de cada uno.

Ejercicio 2

Predice qué ocurre con cada una de estas sentencias sobre la base recién recargada, y si falla, di qué restricción se rompe y cuál sería el mensaje. Después compruébalo.

-- a)
INSERT INTO lineas_pedido (pedido_id, producto_id, cantidad, precio_unitario)
VALUES (1, 5, 0, 1.95);

-- b)
INSERT INTO clientes (nombre, apellidos, email, pais)
VALUES ('Lucía', 'Martínez Soler', '[email protected]', 'España');

-- c)
INSERT INTO empleados (nombre, apellidos, puesto, jefe_id, fecha_contratacion)
VALUES ('Nerea', 'Blasco Tur', 'Comercial', 2, '2026-03-01');

-- d)
INSERT INTO pedidos (cliente_id, fecha_pedido, estado, metodo_pago)
VALUES (14, '2026-03-01', 'preparando', 'tarjeta');

-- e)
INSERT INTO devoluciones (pedido_id, motivo, fecha, importe)
VALUES (21, 'Producto defectuoso', '2026-03-05', 12.00);

Ejercicio 3

El equipo de análisis quiere una tabla de resumen mensual de ventas de 2025 para no recalcularla en cada informe.

  1. Crea la tabla ventas_mensuales con: mes (texto AAAA-MM, clave primaria), pedidos (entero, obligatorio), lineas (entero, obligatorio), unidades (entero, obligatorio), facturacion (NUMERIC(10,2), obligatorio, no negativo) y fecha_calculo (DATE, obligatorio, por omisión hoy).
  2. Puéblala con un INSERT ... SELECT a partir de los pedidos de 2025.
  3. Comprueba el resultado y verifica que la suma de la columna facturacion coincide con la facturación de producto de 2025.

Soluciones

Solución 1

-- 1) El proveedor primero: es el padre
INSERT INTO proveedores (nombre, pais, email, activo)
VALUES ('Cooperativa La Safor', 'España', '[email protected]', TRUE)
RETURNING id, nombre, pais, email, activo;
id nombre pais email activo
6 Cooperativa La Safor España [email protected] true
INSERT 0 1
-- 2) Los dos productos, en una sola sentencia
INSERT INTO productos (nombre, categoria_id, proveedor_id, precio, coste, stock) VALUES
('Almendra marcona ecológica 250 g',    1, 6,  7.90, 4.20, 60),
('Naranjas de Valencia ecológicas 5 kg', 1, 6, 11.50, 6.00, 35)
RETURNING id, nombre, precio, coste, stock, activo, fecha_alta;
id nombre precio coste stock activo fecha_alta
21 Almendra marcona ecológica 250 g 7.90 4.20 60 true 2026-03-01
22 Naranjas de Valencia ecológicas 5 kg 11.50 6.00 35 true 2026-03-01
INSERT 0 2
-- 3) Comprobación
SELECT pr.id   AS proveedor_id,
       pr.nombre AS proveedor,
       p.id     AS producto_id,
       p.nombre AS producto,
       p.precio,
       p.coste,
       p.precio - p.coste AS margen
FROM productos   AS p
JOIN proveedores AS pr ON p.proveedor_id = pr.id
WHERE pr.nombre = 'Cooperativa La Safor'
ORDER BY p.id;
proveedor_id proveedor producto_id producto precio coste margen
6 Cooperativa La Safor 21 Almendra marcona ecológica 250 g 7.90 4.20 3.70
6 Cooperativa La Safor 22 Naranjas de Valencia ecológicas 5 kg 11.50 6.00 5.50

Tres decisiones que había que tomar bien: el orden (proveedor antes que productos, porque es el padre de la FK), una sola sentencia para los dos productos, y omitir activo y fecha_alta en lugar de escribirlos. Y un detalle: el id 6 del proveedor no se ha escrito en ningún sitio; se ha recuperado con RETURNING y se ha usado en el INSERT siguiente. En una aplicación real, ese valor viajaría en una variable.

Solución 2

a) Falla. CHECK de cantidad:

ERROR:  new row for relation "lineas_pedido" violates check constraint "lineas_pedido_cantidad_check"
DETAIL:  Failing row contains (48, 1, 5, 0, 1.95, 0.00).

CHECK (cantidad > 0): una línea de cero unidades no es una venta. Fíjate en que descuento sí se ha rellenado con su DEFAULT 0.00 antes de comprobar la restricción.

b) Falla. UNIQUE del email:

ERROR:  duplicate key value violates unique constraint "clientes_email_key"
DETAIL:  Key (email)=([email protected]) already exists.

Es la protección de la clave natural de 01-05. El id sería distinto, pero el email es único: la base impide registrar dos veces a la misma persona. Este es exactamente el escenario que resolverá el UPSERT de 05-05.

c) Funciona.

INSERT 0 1

Se crea la empleada 9, con jefe_id 2 (Andrés Company Talens, que existe), salario a NULL (la columna es nulable) y ciudad a NULL. Ninguna restricción se viola: salario no es obligatorio y su CHECK (salario >= 0) da UNKNOWN con un nulo, lo que se considera satisfecho (05-01).

d) Falla. CHECK del estado:

ERROR:  new row for relation "pedidos" violates check constraint "pedidos_estado_check"
DETAIL:  Failing row contains (21, 14, null, 2026-03-01, preparando, tarjeta, 0.00).

'preparando' no está en el dominio ('pendiente','pagado','enviado','entregado','cancelado'). Observa el DETAIL: empleado_id ha quedado a NULL (pedido web) y gastos_envio ha tomado su DEFAULT 0.00. La fila estaba casi bien.

e) Falla. FOREIGN KEY:

ERROR:  insert or update on table "devoluciones" violates foreign key constraint "devoluciones_pedido_id_fkey"
DETAIL:  Key (pedido_id)=(21) is not present in table "pedidos".

Sobre la base recién recargada solo hay 20 pedidos. El pedido 21 es el que creamos en el apartado 11, no el que hay ahora. Es un recordatorio de por qué conviene recargar el script entre bloques de ejercicios.

Solución 3

-- 1) La tabla
CREATE TABLE ventas_mensuales (
    mes           CHAR(7)       NOT NULL,
    pedidos       INTEGER       NOT NULL,
    lineas        INTEGER       NOT NULL,
    unidades      INTEGER       NOT NULL,
    facturacion   NUMERIC(10,2) NOT NULL,
    fecha_calculo DATE          NOT NULL DEFAULT CURRENT_DATE,

    CONSTRAINT pk_ventas_mensuales      PRIMARY KEY (mes),
    CONSTRAINT chk_ventas_facturacion   CHECK (facturacion >= 0)
);
CREATE TABLE
-- 2) La carga
INSERT INTO ventas_mensuales (mes, pedidos, lineas, unidades, facturacion)
SELECT TO_CHAR(pe.fecha_pedido, 'YYYY-MM'),
       COUNT(DISTINCT pe.id),
       COUNT(*),
       SUM(lp.cantidad),
       ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2)
FROM lineas_pedido AS lp
JOIN pedidos AS pe ON lp.pedido_id = pe.id
WHERE pe.fecha_pedido >= DATE '2025-01-01'
  AND pe.fecha_pedido <  DATE '2026-01-01'
GROUP BY TO_CHAR(pe.fecha_pedido, 'YYYY-MM');
INSERT 0 10
-- 3) Comprobación
SELECT mes, pedidos, lineas, unidades, facturacion
FROM ventas_mensuales
ORDER BY mes;
mes pedidos lineas unidades facturacion
2025-03 2 5 10 68.80
2025-04 2 5 14 61.28
2025-05 2 5 6 58.85
2025-06 2 5 14 95.48
2025-07 1 3 9 44.60
2025-08 1 2 3 48.27
2025-09 1 2 13 32.76
2025-10 2 5 13 97.20
2025-11 1 3 6 31.70
2025-12 2 5 7 64.58
SELECT SUM(facturacion) AS total_2025,
       SUM(pedidos)     AS pedidos_2025,
       SUM(lineas)      AS lineas_2025,
       SUM(unidades)    AS unidades_2025
FROM ventas_mensuales;
total_2025 pedidos_2025 lineas_2025 unidades_2025
603.52 16 40 95

Diez meses, 16 pedidos y 603,52 € de facturación de producto en 2025. Los otros 4 pedidos y 124,43 € corresponden a 2026: 603,52 + 124,43 = 727,95 €, la cifra canónica del módulo 4. Cuadra.

Dos observaciones sobre el diseño de esta tabla. La primera: mes como CHAR(7) en formato AAAA-MM es clave primaria natural y ordena alfabéticamente igual que cronológicamente, que es justo la razón por la que ese formato se usa tanto. La segunda, más importante: esta tabla es un agregado precalculado, es decir, desnormalización deliberada (01-05, sección 8). Su gran problema es la sincronización: si mañana entra un pedido con fecha de 2025, la tabla queda obsoleta y nadie se entera. Las soluciones —vistas materializadas y triggers— son del módulo 10.

Conclusión

INSERT es la puerta de entrada de los datos, y ya la dominas:

  • La sintaxis INSERT INTO tabla (columnas) VALUES (...), y la regla que no admite excepciones: lista siempre las columnas. La forma sin lista depende del orden físico de la tabla y se rompe en cuanto alguien la toca.
  • La salida INSERT 0 N: el 0 es una reliquia, la N es lo que importa.
  • Varias filas en una sentencia, que es lo que hace el script del curso y lo que debes hacer tú: entre 10 y 50 veces más rápido, porque el coste dominante es fijo por sentencia.
  • DEFAULT como valor y DEFAULT VALUES para una fila entera por omisión; y la equivalencia entre poner DEFAULT y simplemente omitir la columna.
  • La columna de identidad: omitirla es lo normal, forzarla es legal con BY DEFAULT y desajusta la secuencia, y el arreglo es setval(pg_get_serial_sequence(...), MAX(id)) — que es exactamente lo que hacen las nueve líneas finales de tiendaverde.sql. Las secuencias no se deshacen con ROLLBACK: los id tienen huecos y no cuentan filas.
  • RETURNING, la extensión que devuelve la fila insertada con sus valores generados, y que elimina la condición de carrera de SELECT MAX(id). Con sus equivalentes por motor: LAST_INSERT_ID(), SCOPE_IDENTITY(), OUTPUT.
  • INSERT ... SELECT para poblar históricos y duplicar catálogos transformando mientras copia; las columnas se emparejan por posición.
  • Los cuatro errores de un INSERT, con su mensaje literal y su tabla de diagnóstico; y el hecho de que un INSERT multifila es atómico.
  • El orden de inserción: padres antes que hijos, siempre.
  • COPY y \copy para carga masiva, de 5 a 20 veces más rápidos, y el patrón profesional de staging + INSERT ... SELECT.
  • Y el caso completo: alta de producto y registro del pedido 21 con sus tres líneas y sus 49,45 € de total, encadenando dos INSERT con RETURNING.

Sabes crear tablas y llenarlas. Lo que aún no sabes es corregir. En la siguiente lección, Instrucción UPDATE, cambia el tono: hasta ahora, un error tuyo creaba una fila de más que podías borrar; a partir de ahora, un error tuyo puede sobrescribir veinte filas correctas y dejarte sin forma de saber qué había antes. Verás el protocolo profesional para que eso no ocurra —escribir primero el SELECT, contar las filas, y solo entonces convertirlo en UPDATE—, el hábito de trabajar con BEGIN y ROLLBACK como red de seguridad, y todo lo que UPDATE sabe hacer: varias columnas a la vez, cálculos sobre el valor anterior, actualizaciones a partir de otra tabla con FROM, y RETURNING para ver exactamente qué has cambiado.

Curso de SQL

Módulo 1: Introducción a SQL

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados