INSERT, UPDATE y DELETE en SQL
En esta lección
- Antes de empezar: practica con una copia
- INSERT: añadir filas
- Valores por defecto y NULL
- INSERT ... SELECT: copiar filas de una consulta
- UPDATE: modificar filas
- DELETE: borrar filas
- No olvides el WHERE
- Claves foráneas que impiden borrar
- DELETE, TRUNCATE y DROP
- Insertar o actualizar: ON DUPLICATE KEY UPDATE
- Errores frecuentes
- 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;
| id | nombre | ciudad | edad |
|---|---|---|---|
| 7 | Hugo Sanz | NULL | NULL |
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;
| id | nombre | precio | stock | descripcion |
|---|---|---|---|---|
| 1 | Mosquetón | 7.50 | 0 | NULL |
| 2 | Magnesio | 8.00 | 40 | Bote de 200 g |
| 3 | Casco | 60.50 | 5 | NULL |
El mosquetón recibe stock = 0 (el DEFAULT) y descripcion = NULL (no tiene valor por defecto). Dos detalles importantes:
- Si escribes
NULLa propósito, no se usa el valor por defecto:VALUES (3, 'Casco', 60.50, NULL)intenta guardarNULLenstocky, como esNOT 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 deVALUES; 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;
| id | cliente_id | fecha | importe |
|---|---|---|---|
| 1 | 1 | 2026-09-01 | 45.00 |
| 2 | 3 | 2026-09-03 | 120.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;
SETindica qué columnas cambian y a qué valor. Para cambiar varias, sepáralas con comas.WHEREindica qué filas cambian.
| id | nombre | ciudad | edad |
|---|---|---|---|
| 1 | Ana García | Bilbao | 35 |
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;
| id | fecha | importe |
|---|---|---|
| 1 | 2026-09-01 | 47.25 |
| 2 | 2026-09-03 | 126.53 |
| 3 | 2026-09-10 | 13.64 |
| 4 | 2026-09-12 | 60.00 |
| 5 | 2026-09-15 | 33.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:
- Escribe primero un
SELECTcon el mismoWHEREpara ver qué filas vas a tocar. Si el resultado es el que esperas, cambiaSELECT *porDELETE(o porUPDATE ... SET) sin tocar elWHERE:
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
-
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.
-
Filtra por la clave primaria (
WHERE id = 3) siempre que puedas: garantiza que tocas una sola fila. -
Usa una transacción. Si haces el cambio entre
START TRANSACTIONyCOMMIT, puedes comprobar el resultado y deshacerlo conROLLBACKsi 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 losUPDATEyDELETEsin unWHEREque 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 t | DROP TABLE t | |
|---|---|---|---|
| Qué borra | Las filas que cumplan el WHERE | Todas las filas | La tabla entera: filas y estructura |
| ¿La tabla sigue existiendo? | Sí | Sí, vacía | No |
¿Admite WHERE? | Sí | No | No |
| Velocidad con muchas filas | Lento (fila a fila) | Muy rápido | Muy rápido |
¿Se deshace con ROLLBACK en MySQL? | Sí | No | No |
| Tipo de instrucción | DML (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;
| id | nombre | stock |
|---|---|---|
| 1 | Mosquetón | 10 |
| 2 | Magnesio | 40 |
| 3 | Casco | 5 |
| 4 | Arnés | 3 |
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
WHEREen unUPDATEoDELETE: cambia o borra toda la tabla. - Omitir la lista de columnas en el
INSERT: se rompe al cambiar la tabla. - Escribir
NULLesperando el valor por defecto: se guardaNULL, o falla si la columna esNOT NULL. - Escribir
VALUESen unINSERT ... SELECT: elSELECTsustituye alVALUES. - Intentar borrar una fila madre con hijos: la clave foránea lo impide. Borra antes los hijos o replantea si debes borrar.
- Confundir
DELETEconDROP: 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 fila | INSERT INTO t (a, b) VALUES (1, 'x'); |
| Añadir varias filas | INSERT INTO t (a, b) VALUES (1, 'x'), (2, 'y'); |
| Copiar filas de una consulta | INSERT INTO t (a, b) SELECT a, b FROM otra WHERE ...; |
| Modificar filas | UPDATE t SET a = 1, b = b + 1 WHERE id = 3; |
| Borrar filas | DELETE 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 riesgo | CREATE TABLE copia AS SELECT * FROM t; |
- Antes de un
UPDATEo unDELETE, ejecuta unSELECTcon el mismoWHERE. - Filtra por la clave primaria siempre que puedas.
- Los cambios importantes, dentro de una transacción.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.