Saltar al contenido
insert-update-delete.sql · devschool

INSERT, UPDATE y DELETE en SQL

Lección 14 de 22 · 14 min de lectura · Actualizado el

En esta lección
  1. Antes de empezar: practica con una copia
  2. INSERT: añadir filas
  3. Valores por defecto y NULL
  4. INSERT ... SELECT: copiar filas de una consulta
  5. UPDATE: modificar filas
  6. DELETE: borrar filas
  7. No olvides el WHERE
  8. Claves foráneas que impiden borrar
  9. DELETE, TRUNCATE y DROP
  10. Insertar o actualizar: ON DUPLICATE KEY UPDATE
  11. Errores frecuentes
  12. Resumen

Hasta ahora solo has leído datos. Estas tres instrucciones los modifican: INSERT añade filas, UPDATE cambia las que ya existen y DELETE las borra. Juntas forman, con SELECT, lo que se conoce como CRUD (Create, Read, Update, Delete), la base de cualquier aplicación.

Leer datos no tiene riesgo; modificarlos, sí. Por eso en esta lección, además de la sintaxis, verás las costumbres que usan los profesionales para no romper nada.

Antes de empezar: practica con una copia

Los cambios de esta lección son reales: si borras una fila, desaparece. Mientras aprendes, trabaja sobre una copia de las tablas. La forma más rápida es crear una tabla a partir de una consulta:

CREATE TABLE clientes_copia AS SELECT * FROM clientes;
CREATE TABLE pedidos_copia  AS SELECT * FROM pedidos;

Así puedes practicar con clientes_copia sin miedo y, si algo sale mal, borrarla y volver a crearla. Ten en cuenta que esta copia no conserva la clave primaria ni las claves foráneas. En MySQL, si quieres la estructura exacta, usa CREATE TABLE clientes_copia LIKE clientes; y después rellénala con INSERT ... SELECT, que verás más abajo.

En los ejemplos usaremos las tablas del curso. Cada ejemplo parte de los datos originales.

INSERT: añadir filas

INSERT INTO clientes (id, nombre, ciudad, edad)
VALUES (6, 'Sara López', 'Santander', 31);
  • Después del nombre de la tabla, entre paréntesis, las columnas que vas a rellenar.
  • Después de VALUES, los valores, en el mismo orden.
  • Los textos y las fechas van entre comillas simples; los números, sin comillas.

Escribe siempre la lista de columnas

SQL te deja omitirla y dar los valores de todas las columnas en el orden de la tabla:

INSERT INTO clientes VALUES (6, 'Sara López', 'Santander', 31);

Funciona, pero es frágil. Si mañana alguien añade una columna con ALTER TABLE, este INSERT dará error porque faltará un valor. Y si alguien cambia el orden de dos columnas del mismo tipo, guardará cada dato en la columna equivocada sin avisar. Con la lista de columnas, el INSERT dice exactamente qué va en cada sitio.

Varias filas a la vez

INSERT INTO pedidos (id, cliente_id, fecha, importe)
VALUES
  (6, 4, '2026-09-20', 18.00),
  (7, 6, '2026-09-21', 75.40);

Es más rápido que un INSERT por fila, porque la base de datos hace el trabajo en una sola operación. Si una de las filas incumple una regla (un id repetido, por ejemplo), no se inserta ninguna.

Ids automáticos

Normalmente el id lo genera la propia base de datos (con AUTO_INCREMENT en MySQL o GENERATED ... AS IDENTITY en PostgreSQL; lo tienes en CREATE TABLE). En ese caso no lo incluyes:

INSERT INTO clientes (nombre, ciudad, edad)
VALUES ('Sara López', 'Santander', 31);

En MySQL, SELECT LAST_INSERT_ID(); te dice qué id acaba de recibir la fila. En PostgreSQL se usa INSERT ... RETURNING id.

Valores por defecto y NULL

¿Qué pasa con las columnas que no nombras en el INSERT? Reciben su valor por defecto, y si no tienen, NULL:

INSERT INTO clientes (id, nombre) VALUES (7, 'Hugo Sanz');
SELECT * FROM clientes WHERE id = 7;
idnombreciudadedad
7Hugo SanzNULLNULL

Para verlo con valores por defecto, creamos una tabla de productos donde el stock empieza en 0:

CREATE TABLE productos (
  id INT PRIMARY KEY,
  nombre VARCHAR(100) NOT NULL,
  precio DECIMAL(8, 2) NOT NULL,
  stock INT NOT NULL DEFAULT 0,
  descripcion VARCHAR(200)
);

INSERT INTO productos (id, nombre, precio) VALUES (1, 'Mosquetón', 7.50);
INSERT INTO productos (id, nombre, precio, stock, descripcion)
VALUES (2, 'Magnesio', 8.00, 40, 'Bote de 200 g');
INSERT INTO productos (id, nombre, precio, stock) VALUES (3, 'Casco', 60.50, 5);

SELECT * FROM productos;
idnombrepreciostockdescripcion
1Mosquetón7.500NULL
2Magnesio8.0040Bote de 200 g
3Casco60.505NULL

El mosquetón recibe stock = 0 (el DEFAULT) y descripcion = NULL (no tiene valor por defecto). Dos detalles importantes:

  • Si escribes NULL a propósito, no se usa el valor por defecto: VALUES (3, 'Casco', 60.50, NULL) intenta guardar NULL en stock y, como es NOT NULL, falla (NOT NULL constraint failed: productos.stock).
  • En MySQL y PostgreSQL puedes pedir el valor por defecto de forma explícita con la palabra DEFAULT: VALUES (3, 'Casco', 60.50, DEFAULT, NULL). SQLite no lo admite dentro de VALUES; ahí basta con no nombrar la columna.

INSERT ... SELECT: copiar filas de una consulta

En lugar de VALUES, un INSERT puede recibir el resultado de un SELECT. Es la forma de copiar o archivar datos sin sacarlos de la base de datos. Por ejemplo, pasar los pedidos anteriores al 10 de septiembre a una tabla de histórico:

CREATE TABLE pedidos_historico (
  id INT PRIMARY KEY,
  cliente_id INT,
  fecha DATE,
  importe DECIMAL(10, 2)
);

INSERT INTO pedidos_historico (id, cliente_id, fecha, importe)
SELECT id, cliente_id, fecha, importe
FROM pedidos
WHERE fecha < '2026-09-10';

SELECT * FROM pedidos_historico;
idcliente_idfechaimporte
112026-09-0145.00
232026-09-03120.50

No se escribe VALUES: el SELECT ocupa su lugar. Las columnas del SELECT se asignan por posición a las de la lista del INSERT, así que deben coincidir en número y en orden. El SELECT puede ser tan complejo como quieras, con JOIN, GROUP BY o funciones. Por ejemplo, para guardar un resumen de cada cliente en una tabla resumen_clientes (cliente_id, pedidos, total) creada antes:

INSERT INTO resumen_clientes (cliente_id, pedidos, total)
SELECT cliente_id, COUNT(*), SUM(importe)
FROM pedidos
GROUP BY cliente_id;

La diferencia con CREATE TABLE ... AS SELECT es que aquí la tabla ya existe, con su estructura, sus claves y sus restricciones, y las filas se añaden a las que ya tenga.

UPDATE: modificar filas

UPDATE clientes
SET ciudad = 'Bilbao', edad = 35
WHERE id = 1;
  • SET indica qué columnas cambian y a qué valor. Para cambiar varias, sepáralas con comas.
  • WHERE indica qué filas cambian.
idnombreciudadedad
1Ana GarcíaBilbao35

Calcular el nuevo valor a partir del actual

A la derecha del = puedes poner cualquier expresión, incluida la propia columna. Es lo habitual con contadores y precios. Pablo cumple años:

UPDATE clientes SET edad = edad + 1 WHERE id = 4;   -- de 17 a 18

Y la tienda sube un 5 % los pedidos de la primera decena de septiembre, redondeando a dos decimales:

UPDATE pedidos
SET importe = ROUND(importe * 1.05, 2)
WHERE fecha BETWEEN '2026-09-01' AND '2026-09-10';

SELECT id, fecha, importe FROM pedidos;
idfechaimporte
12026-09-0147.25
22026-09-03126.53
32026-09-1013.64
42026-09-1260.00
52026-09-1533.25

En cada fila, importe de la derecha es el valor antes del cambio. Si cambias varias columnas y una usa otra que también cambias, cuidado: PostgreSQL y SQLite usan siempre los valores antiguos, pero MySQL aplica las asignaciones de izquierda a derecha y la segunda ya ve el valor nuevo de la primera. Por ejemplo, con los productos, UPDATE productos SET stock = stock - 1 WHERE id = 3; descontaría una unidad vendida del casco.

Para reglas distintas según la fila (un 10 % a unos, un 15 % a otros) puedes usar un CASE dentro del SET, y para elegir las filas según otra tabla, una subconsulta en el WHERE: WHERE cliente_id IN (SELECT id FROM clientes WHERE ciudad = 'Madrid').

DELETE: borrar filas

DELETE FROM pedidos
WHERE id = 3;

DELETE borra filas completas; no se puede borrar solo una columna. Si quieres vaciar un dato, usa UPDATE ... SET columna = NULL.

No olvides el WHERE

Sin WHERE, UPDATE y DELETE afectan a TODAS las filas de la tabla, y la base de datos no te pregunta si estás seguro:

UPDATE clientes SET ciudad = 'Madrid';  -- todos los clientes pasan a vivir en Madrid
DELETE FROM pedidos;                    -- se borran todos los pedidos

Es uno de los errores más temidos, incluso entre profesionales. Estas costumbres te salvarán:

  1. Escribe primero un SELECT con el mismo WHERE para ver qué filas vas a tocar. Si el resultado es el que esperas, cambia SELECT * por DELETE (o por UPDATE ... SET) sin tocar el WHERE:
SELECT * FROM pedidos WHERE fecha < '2026-09-05';  -- 1.º compruebo: pedidos 1 y 2
DELETE FROM pedidos WHERE fecha < '2026-09-05';    -- 2.º borro esas mismas filas
  1. Mira el número de filas afectadas. Tras un cambio, MySQL responde algo como Query OK, 2 rows affected. Si esperabas 1 y pone 500, algo va mal.

  2. Filtra por la clave primaria (WHERE id = 3) siempre que puedas: garantiza que tocas una sola fila.

  3. Usa una transacción. Si haces el cambio entre START TRANSACTION y COMMIT, puedes comprobar el resultado y deshacerlo con ROLLBACK si no es el que esperabas:

START TRANSACTION;
UPDATE clientes SET ciudad = 'Madrid' WHERE id = 2;
SELECT * FROM clientes;   -- compruebo
COMMIT;                   -- o ROLLBACK si está mal

Consejo: MySQL tiene un modo de seguridad, SET SQL_SAFE_UPDATES = 1;, que rechaza los UPDATE y DELETE sin un WHERE que use una clave. MySQL Workbench lo trae activado por defecto.

Claves foráneas que impiden borrar

Las claves foráneas protegen tus datos también al borrar. Ana tiene pedidos; si la borraras, esos pedidos apuntarían a un cliente que no existe:

DELETE FROM clientes WHERE id = 1;
-- SQLite: FOREIGN KEY constraint failed
-- MySQL:  Cannot delete or update a parent row: a foreign key constraint fails (...)

DELETE FROM clientes WHERE id = 4;   -- funciona: Pablo no tiene pedidos

No es un fallo: es la base de datos evitando que te queden datos huérfanos. Tienes tres salidas:

  • Borrar antes los hijos y después la madre: DELETE FROM pedidos WHERE cliente_id = 1; y luego el cliente. Hazlo dentro de una transacción para que se hagan las dos cosas o ninguna.
  • Definir la clave con ON DELETE CASCADE, para que los hijos se borren solos. Útil para líneas de pedido; peligroso para facturas.
  • No borrar: en muchas aplicaciones reales, en lugar de borrar un cliente se marca como inactivo (UPDATE clientes SET activo = FALSE ...), porque sus pedidos antiguos deben conservarse.

DELETE, TRUNCATE y DROP

Los tres “borran”, pero cosas muy distintas:

DELETE FROM t WHERE ...TRUNCATE TABLE tDROP TABLE t
Qué borraLas filas que cumplan el WHERETodas las filasLa tabla entera: filas y estructura
¿La tabla sigue existiendo?SíSí, vacíaNo
¿Admite WHERE?SíNoNo
Velocidad con muchas filasLento (fila a fila)Muy rápidoMuy rápido
¿Se deshace con ROLLBACK en MySQL?SíNoNo
Tipo de instrucciónDML (datos)DDL (estructura)DDL (estructura)

En el día a día usarás DELETE. TRUNCATE y DROP se explican a fondo en ALTER TABLE y DROP.

Insertar o actualizar: ON DUPLICATE KEY UPDATE

Un caso muy habitual: llega mercancía y quieres sumar unidades a un producto si ya existe, o crearlo si no. Un INSERT normal falla si el id ya está (UNIQUE constraint failed o Duplicate entry). Para esto existe el upsert (update + insert).

En MySQL, con ON DUPLICATE KEY UPDATE:

-- MySQL 8.0.19 o posterior
INSERT INTO productos (id, nombre, precio, stock)
VALUES (1, 'Mosquetón', 7.50, 10), (4, 'Arnés', 60.00, 3) AS nuevo
ON DUPLICATE KEY UPDATE stock = productos.stock + nuevo.stock;

Si el id (o cualquier columna UNIQUE) ya existe, en lugar de fallar se ejecuta el UPDATE. El alias nuevo representa los valores que intentabas insertar. En versiones anteriores se escribía stock = stock + VALUES(stock), que hoy está obsoleto.

En PostgreSQL y SQLite, la forma equivalente es ON CONFLICT, donde excluded son los valores que intentabas insertar:

INSERT INTO productos (id, nombre, precio, stock)
VALUES (1, 'Mosquetón', 7.50, 10), (4, 'Arnés', 60.00, 3)
ON CONFLICT (id) DO UPDATE SET stock = productos.stock + excluded.stock;

SELECT id, nombre, stock FROM productos;
idnombrestock
1Mosquetón10
2Magnesio40
3Casco5
4Arnés3

El mosquetón ya existía con stock 0 y ahora tiene 10; el arnés no existía y se ha creado. Si solo quieres ignorar los duplicados sin cambiar nada, usa ON CONFLICT DO NOTHING (PostgreSQL y SQLite) o INSERT IGNORE (MySQL).

Errores frecuentes

  • Olvidar el WHERE en un UPDATE o DELETE: cambia o borra toda la tabla.
  • Omitir la lista de columnas en el INSERT: se rompe al cambiar la tabla.
  • Escribir NULL esperando el valor por defecto: se guarda NULL, o falla si la columna es NOT NULL.
  • Escribir VALUES en un INSERT ... SELECT: el SELECT sustituye al VALUES.
  • Intentar borrar una fila madre con hijos: la clave foránea lo impide. Borra antes los hijos o replantea si debes borrar.
  • Confundir DELETE con DROP: el primero borra filas; el segundo, la tabla.
  • Practicar en la base de datos real: haz antes una copia.

Resumen

Quieres…Escribe
Añadir una filaINSERT INTO t (a, b) VALUES (1, 'x');
Añadir varias filasINSERT INTO t (a, b) VALUES (1, 'x'), (2, 'y');
Copiar filas de una consultaINSERT INTO t (a, b) SELECT a, b FROM otra WHERE ...;
Modificar filasUPDATE t SET a = 1, b = b + 1 WHERE id = 3;
Borrar filasDELETE FROM t WHERE id = 3;
Insertar o actualizar (MySQL)INSERT ... ON DUPLICATE KEY UPDATE ...
Insertar o actualizar (PostgreSQL, SQLite)INSERT ... ON CONFLICT (id) DO UPDATE SET ...
Practicar sin riesgoCREATE TABLE copia AS SELECT * FROM t;
  • Antes de un UPDATE o un DELETE, ejecuta un SELECT con el mismo WHERE.
  • Filtra por la clave primaria siempre que puedas.
  • Los cambios importantes, dentro de una transacción.

Pon a prueba lo que has aprendido

[SQL] ¿Qué pasa si ejecutas esto?
UPDATE productos SET precio = 0;

[SQL] La columna stock está definida como INT NOT NULL DEFAULT 0. ¿Qué ocurre al ejecutar este INSERT?
INSERT INTO productos (id, nombre, precio, stock)
VALUES (5, 'Cuerda', 139.00, NULL);

[SQL] En Madrid están Pablo (sin pedidos) y Eva (con un pedido). pedidos.cliente_id es una clave foránea sin ON DELETE CASCADE. ¿Qué pasa?
DELETE FROM clientes
WHERE ciudad = 'Madrid';

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.