UPDATE modifica filas que ya existen. Es la instrucción más peligrosa que has visto hasta ahora, y la razón es aritmética: si te equivocas en un INSERT creas una fila de más y la borras; si te equivocas en un UPDATE sobrescribes datos correctos con datos incorrectos y el valor anterior deja de existir. No hay papelera. No hay "deshacer". Solo hay la copia de seguridad de anoche, si es que la hay.
Por eso esta lección dedica su primera mitad a algo que no es sintaxis: el protocolo profesional para ejecutar un UPDATE sin destrozar nada. Escribir primero el SELECT, contar las filas, envolverlo en una transacción, comprobar y solo entonces confirmar. Es un hábito de treinta segundos que separa a quien lleva años tocando bases de datos de quien lleva un mes. Después vendrá 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, RETURNING para ver exactamente qué has cambiado, y la propiedad —la idempotencia— que determina si un script se puede reejecutar sin miedo.
⚠️ Aviso de seguridad
UPDATEes una operación destructiva: sustituye datos existentes sin conservar el valor anterior.
- Ejecuta todos los ejemplos de esta lección sobre tu base de datos de prácticas (
tiendaverde), nunca sobre un sistema en producción.- Haz una copia de seguridad previa antes de cualquier prueba:
pg_dump -U curso_sql -d tiendaverde -f copia.sql. Recuperar el estado inicial es tan fácil como volver a lanzartiendaverde.sql.- En un sistema real, un
UPDATEsobre datos vivos debe ir revisado por otra persona y ejecutado dentro de una transacción.
Contenido
- La sintaxis, y el
WHEREcomo red de seguridad - Qué pasa exactamente si olvidas el
WHERE - El protocolo de los cinco pasos
BEGIN…ROLLBACK/COMMIT: la red de verdad- Actualizar varias columnas a la vez
- Calcular sobre el valor anterior: subir precios un 5 %
- Por qué el orden de las asignaciones no importa
UPDATE ... FROM: actualizar a partir de otra tablaRETURNING: ver qué has cambiadoUPDATEque viola una restricción- Actualizar a
NULL, y el efecto deON UPDATE CASCADE - Idempotencia: por qué importa al reejecutar un script
- Cuatro casos de negocio de TiendaVerde
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- La sintaxis, y el
WHERE como red de seguridad
WHERE como red de seguridadTres piezas: qué tabla, qué columnas cambian y a qué valor, y qué filas. Solo la tercera es opcional para el motor; ninguna lo es para ti.
Un ejemplo mínimo: el producto 13 (Velas de cera de soja) lleva meses con stock 0 y se decide descatalogarlo.
UPDATE N te dice cuántas filas ha modificado. Ese número es tu primera línea de defensa: si esperabas una fila y pone UPDATE 17, algo ha salido muy mal — pero al menos lo sabes.
Un matiz sobre ese contador: PostgreSQL cuenta las filas que cumplían el WHERE, no las que han cambiado de valor. Si actualizas una columna al mismo valor que ya tenía, la fila cuenta igual:
Una fila "modificada" aunque el stock ya fuera 0. Es importante saberlo: UPDATE 1 significa "una fila cumplía la condición", no "una fila ha cambiado".
Y el punto crítico, ya anunciado al final del módulo 4: en el orden lógico de ejecución, el WHERE de un UPDATE hace exactamente el mismo trabajo que el de un SELECT. Determina el conjunto de filas afectadas.
flowchart LR
A["1 · FROM<br/>la tabla (y el FROM extra)"] --> B["2 · WHERE<br/>selecciona las FILAS<br/>que se van a tocar"]
B --> C["3 · SET<br/>calcula los valores nuevos<br/>a partir de los antiguos"]
C --> D["4 · restricciones<br/>NOT NULL · CHECK · UNIQUE · FK"]
D --> E["5 · RETURNING<br/>(opcional)"]
Esa es la razón de que el módulo 5 sea la continuación natural del 4: el WHERE que llevas cuatro módulos afinando es el mismo. Lo que cambia es la consecuencia de equivocarse.
- Qué pasa exactamente si olvidas el
WHERE
WHERENo hay misterio: el UPDATE se aplica a todas las filas de la tabla.
-- ⚠️ INCORRECTA: no hagas esto fuera de una transacción
UPDATE productos
SET precio = precio * 1.05;Los veinte productos del catálogo acaban de subir un 5 %. No solo los de Bebidas: todos. Y el precio anterior ya no está en ninguna parte.
| id | nombre | precio |
|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 13.13 |
| 2 | Arroz integral ecológico 1 kg | 4.10 |
| 3 | Miel de azahar cruda 500 g | 10.24 |
| 4 | Pasta de espelta 500 g | 2.94 |
| 5 | Tomate triturado ecológico 400 g | 2.05 |
(5 primeras de 20 filas.)
Y aquí está lo verdaderamente insidioso: PostgreSQL no ha protestado. No hay error, no hay aviso, no hay confirmación. La sentencia es sintácticamente perfecta y semánticamente legal. El motor ha hecho exactamente lo que le has pedido. El único indicio es ese UPDATE 20 en lugar de UPDATE 4, y para verlo hay que estar mirando.
Ahora imagina la misma sentencia sobre una tabla productos de 40 000 referencias en una tienda real, a las siete de la tarde de un viernes. No es una historia inventada: es uno de los incidentes más repetidos de la industria.
Por qué SQL no te protege. Algunos clientes gráficos avisan si detectan un
UPDATEo unDELETEsinWHERE, ypsqltiene incluso una opción para ello (\set ON_ERROR_STOP onno hace esto, pero herramientas como pgcli o DBeaver sí lo hacen). Pero el estándar SQL no lo contempla: unUPDATEsinWHEREes una operación legítima —a veces es justo lo que quieres, por ejemplo al rellenar una columna nueva—. La protección tiene que ponerla tu proceso, no el lenguaje.
- El protocolo de los cinco pasos
Este es el hábito. Cinco pasos, treinta segundos, cero incidentes.
El encargo: "sube un 5 % el precio de todos los productos de Bebidas".
Paso 1 — Escribe el SELECT con el WHERE definitivo
Antes de escribir la palabra UPDATE, escribe la consulta que localiza exactamente las filas que quieres tocar.
| id | nombre | categoria_id | precio |
|---|---|---|---|
| 14 | Infusión de manzanilla ecológica 20 uds | 4 | 3.25 |
| 15 | Té verde matcha ceremonial 30 g | 4 | 22.00 |
| 16 | Kombucha de jengibre 750 ml | 4 | 4.95 |
| 17 | Zumo de naranja prensado en frío 1 L | 4 | 5.40 |
Paso 2 — Cuenta las filas y compara con lo que esperabas
| filas_afectadas |
|---|
| 4 |
Cuatro. Coincide con lo esperado (Bebidas tiene 4 productos, como comprobaste en 04-06). Si el número te sorprende, para. Ese asombro es la señal más valiosa que vas a recibir.
Paso 3 — Previsualiza los valores nuevos
Escribe la expresión del SET como una columna calculada del SELECT y míralos:
SELECT id,
nombre,
precio AS precio_actual,
ROUND(precio * 1.05, 2) AS precio_nuevo,
ROUND(precio * 0.05, 2) AS subida
FROM productos
WHERE categoria_id = 4
ORDER BY id;| id | nombre | precio_actual | precio_nuevo | subida |
|---|---|---|---|---|
| 14 | Infusión de manzanilla ecológica 20 uds | 3.25 | 3.41 | 0.16 |
| 15 | Té verde matcha ceremonial 30 g | 22.00 | 23.10 | 1.10 |
| 16 | Kombucha de jengibre 750 ml | 4.95 | 5.20 | 0.25 |
| 17 | Zumo de naranja prensado en frío 1 L | 5.40 | 5.67 | 0.27 |
Todos los precios son razonables. Ningún redondeo raro, ningún negativo, ninguna sorpresa.
Paso 4 — Convierte el SELECT en UPDATE sin tocar el WHERE
Copia el WHERE literalmente. No lo reescribas de memoria: cópialo.
Cuatro, el mismo número del paso 2. Si hubiera puesto UPDATE 20, sabrías al instante que el WHERE se ha perdido por el camino.
Paso 5 — Verifica
| id | nombre | precio |
|---|---|---|
| 14 | Infusión de manzanilla ecológica 20 uds | 3.41 |
| 15 | Té verde matcha ceremonial 30 g | 23.10 |
| 16 | Kombucha de jengibre 750 ml | 5.20 |
| 17 | Zumo de naranja prensado en frío 1 L | 5.67 |
Idénticos a la previsualización del paso 3.
Detalle de tipos que conviene entender. No hemos escrito
ROUND(...)en elUPDATEy aun así los precios han quedado con dos decimales. El motivo es queprecioesNUMERIC(10,2): al asignar3.25 * 1.05 = 3.4125a esa columna, PostgreSQL redondea al almacenar (3.41). Funciona, pero es redondeo implícito. Si el resultado te importa —y con dinero siempre importa—, escribe elROUNDexplícito para que el lector de tu código sepa que la decisión fue tuya y no del sistema de tipos.
BEGIN … ROLLBACK / COMMIT: la red de verdad
BEGIN … ROLLBACK / COMMIT: la red de verdadEl protocolo del apartado anterior reduce muchísimo el riesgo, pero no lo elimina. La red de seguridad completa es la transacción.
En PostgreSQL, cada sentencia suelta se ejecuta y se confirma sola (autocommit). Con BEGIN abres un bloque donde nada es definitivo hasta que digas COMMIT, y donde ROLLBACK lo deshace todo:
Ahora, antes de confirmar, verifica:
| id | nombre | precio |
|---|---|---|
| 14 | Infusión de manzanilla ecológica 20 uds | 3.41 |
| 15 | Té verde matcha ceremonial 30 g | 23.10 |
| 16 | Kombucha de jengibre 750 ml | 5.20 |
| 17 | Zumo de naranja prensado en frío 1 L | 5.67 |
Si está bien:
Y si está mal —o si ese UPDATE 4 hubiera sido un UPDATE 20—:
| id | nombre | precio |
|---|---|---|
| 14 | Infusión de manzanilla ecológica 20 uds | 3.25 |
| 15 | Té verde matcha ceremonial 30 g | 22.00 |
| 16 | Kombucha de jengibre 750 ml | 4.95 |
| 17 | Zumo de naranja prensado en frío 1 L | 5.40 |
Como si nunca hubiera pasado. Ese es el poder que llevas cuatro módulos sin usar.
El flujo completo, que a partir de aquí deberías aplicar siempre que toques datos:
flowchart TD
A["SELECT con el WHERE<br/>y contar filas"] --> B{"¿El número<br/>es el esperado?"}
B -->|No| A
B -->|Sí| C["BEGIN"]
C --> D["UPDATE"]
D --> E{"¿UPDATE N coincide<br/>con el recuento?"}
E -->|No| F["ROLLBACK"]
E -->|Sí| G["SELECT de verificación"]
G --> H{"¿Los datos<br/>son correctos?"}
H -->|No| F
H -->|Sí| I["COMMIT"]
F --> A
Tres advertencias prácticas sobre las transacciones, lo justo para usarlas hoy:
| Advertencia | Detalle |
|---|---|
| Una transacción abierta bloquea filas | Mientras no hagas COMMIT o ROLLBACK, las filas que has tocado quedan bloqueadas para otras sesiones. No te vayas a comer con un BEGIN abierto |
| Un error aborta la transacción entera | Si una sentencia falla dentro del bloque, PostgreSQL entra en estado 25P02 y rechaza todo hasta que hagas ROLLBACK. El mensaje es current transaction is aborted, commands ignored until end of transaction block |
| Las secuencias no se deshacen | Como viste en 05-02, un ROLLBACK no devuelve los id consumidos |
Esto es solo la punta. El modelo completo de las transacciones —las propiedades ACID, los niveles de aislamiento, los bloqueos y los interbloqueos— es el módulo 9 entero. Aquí lo usas como herramienta de trabajo: abre, comprueba, confirma o deshaz. Con eso ya eres mucho más seguro de lo que eras hace diez minutos.
Y una alternativa cuando la transacción no basta, porque el cambio es grande o irreversible: haz una copia de la tabla antes.
Es el uso legítimo de CTAS que anunciaba 05-01. Recuerda que no copia restricciones (05-01, apartado 8): sirve para restaurar valores, no para sustituir la tabla.
- Actualizar varias columnas a la vez
Se separan por comas dentro del mismo SET:
| id | nombre | precio | stock | activo |
|---|---|---|---|---|
| 20 | Cápsulas de espirulina 120 uds | 16.40 | 0 | false |
Un solo UPDATE con dos asignaciones es siempre mejor que dos UPDATE, y no solo por escribir menos:
Dos UPDATE separados |
Uno con dos asignaciones | |
|---|---|---|
| Recorridos de la tabla | 2 | 1 |
| Versiones de fila creadas | 2 | 1 |
| Estado intermedio visible | Sí: entre ambos, la fila está a medias | No |
Atomicidad sin BEGIN |
No garantizada | Garantizada |
Ese "estado intermedio visible" es el argumento de peso: entre el primer UPDATE y el segundo existe un instante en que el producto está inactivo pero con 55 unidades de stock. Otra sesión puede leerlo justo ahí.
- Calcular sobre el valor anterior: subir precios un 5 %
En el lado derecho del = puedes usar cualquier expresión, incluidas las columnas de la propia fila:
UPDATE productos SET precio = precio * 1.05 WHERE categoria_id = 4; -- subir un 5 %
UPDATE productos SET stock = stock + 50 WHERE id = 13; -- reponer 50 unidades
UPDATE pedidos SET gastos_envio = 0 WHERE gastos_envio < 5; -- portes gratisLa regla, y es la clave de todo el apartado siguiente: el lado derecho se evalúa con los valores que la fila tenía ANTES del UPDATE.
Un ejemplo con CASE… bueno, con CASE no todavía (módulo 6). Un ejemplo con aritmética y una columna de otra fila de la misma tabla tampoco: eso es una subconsulta (módulo 7). Con lo que tienes hoy, las expresiones útiles son las de 02-02: aritmética, concatenación y funciones básicas.
SELECT id, nombre, coste, precio, ROUND((precio - coste) / precio, 4) AS margen_relativo
FROM productos WHERE categoria_id = 5 ORDER BY id;| id | nombre | coste | precio | margen_relativo |
|---|---|---|---|---|
| 18 | Cepillo de dientes de bambú | 1.20 | 2.64 | 0.5455 |
| 19 | Desodorante natural en barra 50 g | 3.30 | 7.26 | 0.5455 |
El margen relativo sale idéntico en los dos, y no es casualidad: al fijar el precio como coste * 2.2, el margen relativo es siempre 1 − 1/2,2 = 0,5455, independientemente del coste. Es la comprobación de que la fórmula hace lo que se pretendía.
Fíjate en el AND coste IS NOT NULL. En TiendaVerde todos los productos tienen coste, pero la columna es nulable (05-01): sin ese filtro, un producto sin coste habría recibido precio = NULL, y como precio es NOT NULL la sentencia entera habría fallado. Cuando el SET hace aritmética con una columna nulable, el WHERE debe protegerte — es la lógica de tres valores de 04-03 aplicada a la escritura.
- Por qué el orden de las asignaciones no importa
Esta es la diferencia más profunda entre UPDATE y un lenguaje imperativo, y sorprende a todo el mundo que llega desde Java, Python o C.
En Python, precio = coste * 2 seguido de coste = coste * 1.1 usaría el coste original en la primera línea y lo modificaría en la segunda. En SQL ocurre lo mismo, pero por una razón distinta y más fuerte: no es que las asignaciones se ejecuten en orden, es que todas se evalúan simultáneamente sobre la fila anterior.
El producto 5 tenía precio = 1.95 y coste = 0.90:
| id | nombre | precio | coste |
|---|---|---|---|
| 5 | Tomate triturado ecológico 400 g | 1.80 | 0.99 |
precio = 0,90 × 2 = 1,80 (con el coste antiguo, no con 0,99). coste = 0,90 × 1,1 = 0,99.
Y ahora la prueba definitiva, que en un lenguaje imperativo necesitaría una variable temporal:
-- Intercambiar dos columnas: en SQL funciona directamente
UPDATE productos
SET precio = coste,
coste = precio
WHERE id = 3;| id | nombre | precio | coste |
|---|---|---|---|
| 3 | Miel de azahar cruda 500 g | 5.40 | 9.75 |
Estaban en 9,75 y 5,40; ahora están en 5,40 y 9,75. Se han intercambiado. En Python habrías necesitado tmp = precio o una asignación múltiple; en SQL es el comportamiento por defecto.
Tres consecuencias prácticas:
- Puedes escribir las asignaciones en cualquier orden.
SET a = ..., b = ...ySET b = ..., a = ...son equivalentes. - No puedes encadenar cálculos dentro del mismo
UPDATE.SET precio = precio * 1.1, iva = precio * 0.21calcularía el IVA sobre el precio viejo, no sobre el recién subido. Si necesitas encadenar, hacen falta dosUPDATE(dentro de la misma transacción). - No puedes asignar dos veces la misma columna. PostgreSQL lo rechaza:
UPDATE ... FROM: actualizar a partir de otra tabla
UPDATE ... FROM: actualizar a partir de otra tablaHasta ahora, los valores nuevos salían de la propia fila. A menudo salen de otra tabla. PostgreSQL lo resuelve con una cláusula FROM en el UPDATE, que funciona igual que el FROM de un SELECT: aporta tablas adicionales y se relaciona con la que actualizas mediante el WHERE.
UPDATE tabla_destino AS d
SET columna = expresión_que_usa_o
FROM otra_tabla AS o
WHERE d.clave = o.clave
AND otras_condiciones;Caso 1: descontar el stock de un pedido
Retomamos el pedido 21 que registramos en 05-02 (cliente 6, tres líneas: 2 aceites, 1 miel, 3 infusiones). Falta descontar el stock:
| id | nombre | stock |
|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 120 |
| 3 | Miel de azahar cruda 500 g | 80 |
| 14 | Infusión de manzanilla ecológica 20 uds | 180 |
UPDATE productos AS p
SET stock = p.stock - lp.cantidad
FROM lineas_pedido AS lp
WHERE lp.producto_id = p.id
AND lp.pedido_id = 21;| id | nombre | stock |
|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 118 |
| 3 | Miel de azahar cruda 500 g | 79 |
| 14 | Infusión de manzanilla ecológica 20 uds | 177 |
120 − 2, 80 − 1, 180 − 3. Exacto.
⚠️ La trampa mortal de
UPDATE ... FROM. Si una fila deproductosempareja con varias filas delineas_pedido, PostgreSQL no suma: elige una de las coincidencias, de forma no determinista, y aplica esa. No da error, no avisa. Si el pedido 21 tuviera dos líneas del mismo producto, esteUPDATEdescontaría solo una de ellas.Es exactamente la multiplicación de filas del módulo 3, pero mucho más peligrosa: en un
SELECTla ves porque salen filas de más; aquí es invisible. La solución correcta pasa por agregar primero (SUM(lp.cantidad)agrupado por producto) y actualizar contra ese resultado, lo cual requiere una subconsulta en elFROM— módulo 7. Mientras tanto: antes de unUPDATE ... FROM, comprueba siempre que el emparejamiento es 1:1.
Esa comprobación se hace así:
SELECT lp.producto_id, COUNT(*) AS lineas
FROM lineas_pedido AS lp
WHERE lp.pedido_id = 21
GROUP BY lp.producto_id
HAVING COUNT(*) > 1;Cero filas: no hay productos repetidos y el emparejamiento es 1:1. Ahora sí.
Caso 2: actualizar precios de pedidos aún no enviados
Acabamos de subir un 5 % las Bebidas (apartado 3). Los pedidos que ya se enviaron o entregaron deben conservar su precio histórico —esa es toda la razón de ser de precio_unitario (01-05)—, pero los que aún no han salido deben facturar la tarifa nueva.
-- Paso 1: ver qué se va a tocar
SELECT lp.id, lp.pedido_id, pe.estado, p.nombre,
lp.precio_unitario AS precio_linea,
p.precio AS precio_catalogo
FROM lineas_pedido AS lp
JOIN pedidos AS pe ON lp.pedido_id = pe.id
JOIN productos AS p ON lp.producto_id = p.id
WHERE pe.estado IN ('pendiente', 'pagado')
AND lp.precio_unitario <> p.precio
ORDER BY lp.id;| id | pedido_id | estado | nombre | precio_linea | precio_catalogo |
|---|---|---|---|---|---|
| 45 | 19 | pagado | Kombucha de jengibre 750 ml | 4.95 | 5.20 |
Una sola línea. El resto de líneas de los pedidos 18, 19 y 20 ya coincide con el catálogo.
-- Paso 2: el UPDATE
UPDATE lineas_pedido AS lp
SET precio_unitario = p.precio
FROM productos AS p,
pedidos AS pe
WHERE lp.producto_id = p.id
AND lp.pedido_id = pe.id
AND pe.estado IN ('pendiente', 'pagado')
AND lp.precio_unitario <> p.precio
RETURNING lp.id, lp.pedido_id, lp.producto_id, lp.precio_unitario;| id | pedido_id | producto_id | precio_unitario |
|---|---|---|---|
| 45 | 19 | 16 | 5.20 |
Fíjate en dos cosas. Primero: en el FROM de un UPDATE puedes poner varias tablas, separadas por comas o con JOIN entre ellas. Segundo: la tabla que se actualiza (lineas_pedido) no se repite en el FROM. Si la pusieras, PostgreSQL la trataría como una segunda instancia independiente y el resultado sería un producto cartesiano silencioso.
Equivalencias por motor
Esta es una de las divergencias más grandes del módulo:
| Motor | Sintaxis |
|---|---|
| PostgreSQL | UPDATE d SET c = o.c FROM otra o WHERE d.k = o.k |
| MySQL / MariaDB | UPDATE d JOIN otra o ON d.k = o.k SET d.c = o.c — el JOIN va antes del SET |
| SQL Server | UPDATE d SET c = o.c FROM destino d JOIN otra o ON d.k = o.k — la tabla destino sí se repite en el FROM |
| SQLite | UPDATE d SET c = (...) FROM otra o WHERE d.k = o.k desde la versión 3.33; antes, subconsulta correlacionada |
| Oracle | No tiene UPDATE ... FROM. Se usa MERGE (05-05) o una subconsulta correlacionada |
Y el estándar SQL, en realidad, no define ninguna de las cuatro: la forma portable es una subconsulta correlacionada en el SET, que verás en el módulo 7.
RETURNING: ver qué has cambiado
RETURNING: ver qué has cambiadoIgual que en INSERT, RETURNING devuelve las filas afectadas. En UPDATE es todavía más útil, porque te enseña el resultado:
UPDATE pedidos
SET estado = 'entregado'
WHERE estado = 'enviado'
AND fecha_pedido < DATE '2026-02-01'
RETURNING id, cliente_id, fecha_pedido, estado, metodo_pago;| id | cliente_id | fecha_pedido | estado | metodo_pago |
|---|---|---|---|---|
| 16 | 4 | 2025-12-19 | entregado | tarjeta |
| 17 | 7 | 2026-01-13 | entregado | paypal |
Dos pedidos que llevaban meses "enviado" pasan a "entregado". Y lo ves sin necesidad de un SELECT posterior.
Lo que RETURNING no puede hacer: devolver el valor anterior. Solo ve la fila ya actualizada. Si necesitas conservar el valor viejo, tienes tres opciones: copiar la tabla antes (apartado 4), un INSERT ... SELECT a una tabla de auditoría antes del UPDATE (05-02), o un trigger de auditoría (módulo 10).
Nota de dialecto: SQL Server sí puede: su cláusula
OUTPUT DELETED.columna, INSERTED.columnadevuelve el antes y el después en la misma sentencia. Es una de las pocas cosas en las que su sintaxis supera a la de PostgreSQL.
UPDATE que viola una restricción
UPDATE que viola una restricciónUn UPDATE está sujeto a exactamente las mismas restricciones que un INSERT, y da los mismos errores. La diferencia es que ahora la fila ya existía y era correcta.
CHECK:
ERROR: new row for relation "productos" violates check constraint "productos_stock_check" DETAIL: Failing row contains (13, Velas de cera de soja (pack 2), 3, 5, 13.75, 6.90, -1, t, 2025-03-01).
El producto 13 tenía stock 0 y CHECK (stock >= 0) impide bajarlo a −1. Ese CHECK, que en 05-01 parecía burocracia, acaba de impedir un stock negativo.
CHECK de dominio:
ERROR: new row for relation "pedidos" violates check constraint "pedidos_estado_check" DETAIL: Failing row contains (6, 5, 4, 2025-05-23, devuelto, tarjeta, 4.95).
UNIQUE:
UPDATE clientes SET email = '[email protected]' WHERE id = 2;ERROR: duplicate key value violates unique constraint "clientes_email_key" DETAIL: Key (email)=([email protected]) already exists.
FOREIGN KEY:
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".
NOT NULL:
ERROR: null value in column "cliente_id" of relation "pedidos" violates not-null constraint DETAIL: Failing row contains (1, null, null, 2025-03-04, entregado, tarjeta, 4.95).
En los cinco casos, la fila original queda intacta: un UPDATE que falla no modifica nada. Y como en el INSERT, si el UPDATE afectaba a diez filas y una viola una restricción, no se actualiza ninguna.
- Actualizar a
NULL, y el efecto de ON UPDATE CASCADE
NULL, y el efecto de ON UPDATE CASCADEPoner una columna a NULL
Se hace con = NULL, no con IS NULL (que es un operador de comparación, 04-03):
UPDATE pedidos
SET empleado_id = NULL
WHERE id = 2
RETURNING id, cliente_id, empleado_id, fecha_pedido, estado;| id | cliente_id | empleado_id | fecha_pedido | estado |
|---|---|---|---|---|
| 2 | 2 | (null) | 2025-03-12 | entregado |
El pedido 2 deja de tener comercial asignado: pasa a comportarse como los diez pedidos web. Ahora habría 11 pedidos con empleado_id IS NULL.
Solo puedes hacerlo si la columna admite nulos. Y una advertencia semántica que arrastra 04-03: NULL significa "desconocido", no "ninguno". Poner empleado_id a NULL para significar "lo gestionó la web" es correcto porque el esquema define ese nulo así. Poner salario = NULL para significar "cobra 0" sería un error de diseño: 0 y "no lo sé" son cosas distintas.
ON UPDATE CASCADE
Cuando cambia el valor de la clave primaria de un padre, ON UPDATE decide qué pasa con los hijos. TiendaVerde no lo usa porque sus PK son subrogadas y no cambian nunca (01-05). Con una tabla de ejemplo se ve bien:
-- Ejemplo puntual: tablas con clave natural, fuera del esquema de TiendaVerde
CREATE TABLE paises (
codigo VARCHAR(3) PRIMARY KEY,
nombre VARCHAR(60) NOT NULL
);
CREATE TABLE tarifas_envio (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
pais_codigo VARCHAR(3) NOT NULL REFERENCES paises(codigo)
ON UPDATE CASCADE ON DELETE RESTRICT,
peso_max_kg NUMERIC(5,2) NOT NULL,
importe NUMERIC(10,2) NOT NULL
);
INSERT INTO paises (codigo, nombre) VALUES ('ESP', 'España'), ('PRT', 'Portugal'), ('FRA', 'Francia');
INSERT INTO tarifas_envio (pais_codigo, peso_max_kg, importe) VALUES
('ESP', 2.00, 4.95), ('ESP', 10.00, 6.50), ('PRT', 2.00, 9.90), ('FRA', 2.00, 12.50);Ahora la empresa decide migrar a códigos ISO de dos letras:
| id | pais_codigo | peso_max_kg | importe |
|---|---|---|---|
| 1 | ES | 2.00 | 4.95 |
| 2 | ES | 10.00 | 6.50 |
| 3 | PRT | 2.00 | 9.90 |
| 4 | FRA | 2.00 | 12.50 |
Las dos filas hijas se han actualizado solas. Sin ON UPDATE CASCADE, el UPDATE habría fallado:
ERROR: update or delete on table "paises" violates foreign key constraint "tarifas_envio_pais_codigo_fkey" on table "tarifas_envio" DETAIL: Key (codigo)=(ESP) is still referenced from table "tarifas_envio".
Y aquí está el argumento de 01-05 en su forma más nítida: con claves subrogadas este problema no existe. ON UPDATE CASCADE es la respuesta a una pregunta que solo se hace quien eligió una clave natural mutable.
- Idempotencia: por qué importa al reejecutar un script
Una operación es idempotente si ejecutarla dos veces da el mismo resultado que ejecutarla una. Es una propiedad crucial en cualquier proceso automatizado, porque tarde o temprano un script se ejecuta dos veces: falla a mitad y se relanza, se despliega dos veces, alguien no está seguro de si llegó a correr.
Compara:
-- ❌ NO idempotente
UPDATE productos SET precio = precio * 1.05 WHERE categoria_id = 4;
-- ✅ Idempotente
UPDATE productos SET precio = 3.41 WHERE id = 14;Con el primero, sobre el precio original de 3,25 €:
| Ejecución | precio resultante |
|---|---|
| 1ª | 3.41 |
| 2ª | 3.58 |
| 3ª | 3.76 |
El precio se dispara sin que nadie lo note. Con el segundo, el resultado es 3,41 € las tres veces.
La regla que se deduce:
Forma del SET |
¿Idempotente? | Por qué |
|---|---|---|
SET col = valor_fijo |
Sí | Fija un estado absoluto |
SET col = otra_columna |
Sí | El origen no cambia |
SET col = col * k, col + k, col - k |
No | Cada ejecución parte del resultado anterior |
SET col = col + 1 (contadores) |
No | Es justo lo contrario de lo que se busca |
Y las tres formas de convivir con un UPDATE no idempotente cuando el cálculo relativo es lo que necesitas:
- Hacerlo idempotente con el
WHERE. Si puedes identificar las filas "aún no procesadas", el segundo pase no encuentra nada:
UPDATE pedidos
SET estado = 'entregado'
WHERE estado = 'enviado'
AND fecha_pedido < DATE '2026-02-01';La primera ejecución devuelve UPDATE 2; la segunda, UPDATE 0, porque ya no queda ningún pedido en estado enviado con esa fecha. El WHERE bien elegido convierte una operación relativa en una idempotente. Es el patrón más importante de este apartado.
-
Registrar que ya se hizo, en una tabla de control o con una columna de marca, y comprobarlo antes.
-
Envolverlo en una migración versionada, que por definición se aplica una sola vez. Es el tema de 05-06.
Consejo de oro: cuando escribas un script que vaya a ejecutarse sin supervisión, pregúntate siempre "¿qué pasa si esto corre dos veces?". Si la respuesta es "un desastre silencioso", reescríbelo.
- Cuatro casos de negocio de TiendaVerde
Todos sobre la base recién recargada, con el protocolo completo.
13.1. Subir el precio de una categoría
Ya resuelto en el apartado 3, con su SELECT previo, su recuento de 4 filas, su previsualización y su verificación. Es el caso canónico.
13.2. Marcar como entregados los pedidos enviados hace más de un mes
BEGIN;
SELECT COUNT(*) AS afectados
FROM pedidos
WHERE estado = 'enviado'
AND fecha_pedido < DATE '2026-02-01';| afectados |
|---|
| 2 |
UPDATE pedidos
SET estado = 'entregado'
WHERE estado = 'enviado'
AND fecha_pedido < DATE '2026-02-01'
RETURNING id, cliente_id, fecha_pedido, estado;| id | cliente_id | fecha_pedido | estado |
|---|---|---|---|
| 16 | 4 | 2025-12-19 | entregado |
| 17 | 7 | 2026-01-13 | entregado |
| estado | pedidos |
|---|---|
| entregado | 16 |
| pagado | 2 |
| cancelado | 1 |
| pendiente | 1 |
De 14 entregados a 16, y el estado enviado desaparece. Coincide con las 2 filas anunciadas. Observa el uso del rango de fechas cerrado por abajo (< DATE '2026-02-01') siguiendo la convención del curso.
13.3. Corregir un email mal escrito
El cliente 8 avisa de que su dirección está mal:
| id | nombre | apellidos | pais | |
|---|---|---|---|---|
| 8 | Tiago | Almeida Nunes | [email protected] | Portugal |
UPDATE clientes
SET email = '[email protected]'
WHERE id = 8
RETURNING id, nombre, apellidos, email;| id | nombre | apellidos | |
|---|---|---|---|
| 8 | Tiago | Almeida Nunes | [email protected] |
Corrección de una sola fila, la operación más frecuente de todas. Dos observaciones:
- Se filtra por
id, no por email. Filtrar por el valor que vas a cambiar funciona, pero si hubiera una errata en elWHEREno tocarías ninguna fila (UPDATE 0) y podrías creer que ya estaba bien. La PK es siempre el filtro más seguro. - El
UNIQUEsigue vigilando. Si la dirección nueva ya perteneciera a otro cliente, elUPDATEfallaría conduplicate key value violates unique constraint "clientes_email_key".
13.4. Descatalogar un producto: borrado lógico
El producto 13 (Velas de cera de soja) no se ha vendido nunca y lleva meses a cero:
UPDATE productos
SET activo = FALSE,
stock = 0
WHERE id = 13
RETURNING id, nombre, precio, stock, activo;| id | nombre | precio | stock | activo |
|---|---|---|---|---|
| 13 | Velas de cera de soja (pack 2) | 13.75 | 0 | false |
Esto es un borrado lógico, y es la alternativa correcta a DELETE FROM productos WHERE id = 13. El producto desaparece del catálogo de venta pero conserva su fila, su histórico y su integridad referencial. La columna activo existe exactamente para esto, y el tema tiene desarrollo completo en la próxima lección.
Lo que hay que recordar hoy: a partir de ahora, toda consulta de catálogo necesita WHERE activo:
SELECT COUNT(*) FILTER (WHERE activo) AS en_venta,
COUNT(*) FILTER (WHERE NOT activo) AS descatalogados,
COUNT(*) AS total
FROM productos;| en_venta | descatalogados | total |
|---|---|---|
| 18 | 2 | 20 |
Dieciocho a la venta (el 13 acaba de sumarse al 20, que ya estaba inactivo). Ese FILTER es el de 04-04, ahora útil para auditar el resultado de una escritura.
Errores Comunes y Consejos
- Olvidar el
WHERE. Modifica todas las filas sin previo aviso. Es el error emblemático del módulo y el motivo del protocolo de cinco pasos. - Reescribir el
WHEREde memoria al pasar delSELECTalUPDATE. Cópialo literalmente. La mitad de los incidentes vienen de una condición que "era casi la misma". - No mirar el
UPDATE N. Es el único aviso que vas a recibir. Compáralo siempre con el recuento previo. - Trabajar sin
BEGIN. En autocommit no hay marcha atrás. UnBEGINcuesta seis letras. - Dejar una transacción abierta. Bloquea filas para el resto de sesiones. Confirma o deshaz antes de levantarte.
- Esperar que
RETURNINGdevuelva el valor anterior. Solo ve el nuevo. Para el antes, copia la tabla o audita. - Encadenar cálculos en un mismo
SET.SET a = a * 1.1, b = a * 0.21usa elaviejo para calcularb. Si necesitas encadenar, dosUPDATE. - Usar
UPDATE ... FROMcon un emparejamiento 1:N. No suma: elige una coincidencia arbitraria y no avisa. Comprueba con unGROUP BY ... HAVING COUNT(*) > 1antes. - Repetir la tabla destino en el
FROMde PostgreSQL. Produce un producto cartesiano silencioso. En SQL Server es al revés: hay que repetirla. - Aritmética sobre una columna nulable sin protegerla en el
WHERE.precio = coste * 2concostenulo daNULLy revienta contra elNOT NULL. - Escribir
UPDATEno idempotentes en scripts automáticos.precio = precio * 1.05ejecutado dos veces sube un 10,25 %. Fija valores absolutos o acota con elWHERE. - Actualizar una columna que representa un histórico.
precio_unitarioes el precio del momento de la venta. Tocarlo en pedidos ya entregados falsifica la contabilidad. - Consejo: filtra por la clave primaria siempre que puedas. Es el
WHEREmás seguro que existe. - Consejo: previsualiza el
SETcomo columna calculada. Ver los valores nuevos antes de escribirlos detecta redondeos, negativos y nulos inesperados. - Consejo: para cambios grandes,
CREATE TABLE copia AS SELECT * FROM tablaprimero. Treinta segundos que valen por una noche entera.
Ejercicios
Trabaja sobre la base recién recargada y usa BEGIN … ROLLBACK para no arrastrar cambios de un ejercicio al siguiente.
Ejercicio 1
Dirección aprueba una revisión de tarifas de Cosmética natural (categoría 2) con estas reglas:
- Subir el precio un 8 %, redondeando a dos decimales.
- Subir el coste un 3 %, redondeando a dos decimales.
- Solo debe afectar a los productos activos.
Aplica el protocolo completo: consulta previa, recuento, previsualización con el margen relativo antes y después, UPDATE dentro de una transacción, verificación y COMMIT. Después responde: ¿ha mejorado o ha empeorado el margen relativo? ¿Por qué?
Ejercicio 2
Un compañero te pasa este UPDATE con el comentario "quiero poner al día el precio de las líneas de todos los pedidos que aún no se han enviado, y me sale un número raro":
-- ⚠️ INCORRECTA
UPDATE lineas_pedido
SET precio_unitario = productos.precio
FROM productos, pedidos, lineas_pedido
WHERE lineas_pedido.producto_id = productos.id
AND lineas_pedido.pedido_id = pedidos.id
AND pedidos.estado = 'pendiente';- Encuentra el error y explica qué hace realmente esta sentencia.
- Reescríbela correctamente, con alias, y di cuántas filas debería tocar sobre la base recién recargada.
- Explica por qué esta operación sería catastrófica si el
WHEREincluyerapedidos.estado = 'entregado'.
Ejercicio 3
El almacén ha recibido una entrega y hay que reponer stock. Los datos vienen en esta tabla auxiliar:
CREATE TEMP TABLE recepcion_almacen (
producto_id INTEGER NOT NULL,
unidades INTEGER NOT NULL CHECK (unidades > 0)
);
INSERT INTO recepcion_almacen (producto_id, unidades) VALUES
(1, 40), (5, 100), (13, 25), (16, 30);- Escribe el
UPDATE ... FROMque suma las unidades recibidas al stock de cada producto. - Comprueba antes que el emparejamiento es 1:1.
- Muestra el estado antes y después, y explica qué le ocurre al producto 13.
- ¿Es idempotente este
UPDATE? Si no lo es, ¿qué pasaría si el almacén ejecutara el script dos veces por error, y cómo lo evitarías?
Soluciones
Solución 1
-- Paso 1 y 2: qué filas y cuántas
SELECT id, nombre, precio, coste, activo
FROM productos
WHERE categoria_id = 2
AND activo
ORDER BY id;| id | nombre | precio | coste | activo |
|---|---|---|---|---|
| 6 | Crema facial de aloe vera 50 ml | 18.90 | 9.50 | true |
| 7 | Champú sólido de romero 80 g | 8.40 | 3.60 | true |
| 8 | Aceite corporal de almendras 200 ml | 14.25 | 7.10 | true |
| 9 | Bálsamo labial de caléndula 15 ml | 4.60 | 1.80 | true |
4 filas. Los cuatro productos de Cosmética natural están activos.
-- Paso 3: previsualización
SELECT id,
nombre,
precio,
coste,
ROUND((precio - coste) / precio, 4) AS margen_antes,
ROUND(precio * 1.08, 2) AS precio_nuevo,
ROUND(coste * 1.03, 2) AS coste_nuevo,
ROUND((ROUND(precio * 1.08, 2) - ROUND(coste * 1.03, 2))
/ ROUND(precio * 1.08, 2), 4) AS margen_despues
FROM productos
WHERE categoria_id = 2 AND activo
ORDER BY id;| id | nombre | precio | coste | margen_antes | precio_nuevo | coste_nuevo | margen_despues |
|---|---|---|---|---|---|---|---|
| 6 | Crema facial de aloe vera 50 ml | 18.90 | 9.50 | 0.4974 | 20.41 | 9.79 | 0.5203 |
| 7 | Champú sólido de romero 80 g | 8.40 | 3.60 | 0.5714 | 9.07 | 3.71 | 0.5910 |
| 8 | Aceite corporal de almendras 200 ml | 14.25 | 7.10 | 0.5018 | 15.39 | 7.31 | 0.5250 |
| 9 | Bálsamo labial de caléndula 15 ml | 4.60 | 1.80 | 0.6087 | 4.97 | 1.85 | 0.6278 |
-- Paso 4: el UPDATE, en transacción
BEGIN;
UPDATE productos
SET precio = ROUND(precio * 1.08, 2),
coste = ROUND(coste * 1.03, 2)
WHERE categoria_id = 2
AND activo
RETURNING id, nombre, precio, coste;| id | nombre | precio | coste |
|---|---|---|---|
| 6 | Crema facial de aloe vera 50 ml | 20.41 | 9.79 |
| 7 | Champú sólido de romero 80 g | 9.07 | 3.71 |
| 8 | Aceite corporal de almendras 200 ml | 15.39 | 7.31 |
| 9 | Bálsamo labial de caléndula 15 ml | 4.97 | 1.85 |
-- Paso 5: verificar y confirmar
SELECT ROUND(AVG((precio - coste) / precio), 4) AS margen_medio
FROM productos WHERE categoria_id = 2 AND activo;| margen_medio |
|---|
| 0.5660 |
El margen relativo mejora en los cuatro productos (de una media de 0,5448 a 0,5660), y la razón es aritmética: el precio sube un 8 % y el coste solo un 3 %, así que la diferencia crece más deprisa que el precio. Es la comprobación de que la revisión de tarifas hace lo que dirección pretendía.
Dos decisiones del enunciado que había que respetar: ROUND explícito (aunque NUMERIC(10,2) redondearía igual, escribirlo hace la intención visible) y el filtro AND activo, que aquí no descarta ninguna fila pero blinda la sentencia contra un futuro producto descatalogado.
Solución 2
1. El error. lineas_pedido aparece dos veces: una como tabla destino del UPDATE y otra dentro del FROM. PostgreSQL las trata como dos instancias independientes, así que la del FROM no está correlacionada con la que se actualiza. El resultado es un producto cartesiano silencioso: la condición lineas_pedido.producto_id = productos.id se resuelve contra la instancia del FROM, y el UPDATE acaba tocando todas las líneas de la tabla, poniéndoles un precio arbitrario. De ahí "el número raro".
PostgreSQL incluso avisa de algo parecido en la documentación, pero no da error: la sentencia es legal.
2. La versión correcta:
-- ✅ CORRECTA
UPDATE lineas_pedido AS lp
SET precio_unitario = p.precio
FROM productos AS p,
pedidos AS pe
WHERE lp.producto_id = p.id
AND lp.pedido_id = pe.id
AND pe.estado = 'pendiente'
AND lp.precio_unitario <> p.precio
RETURNING lp.id, lp.pedido_id, lp.producto_id, lp.precio_unitario;Cero filas sobre la base recién recargada. El único pedido pendiente es el 20, y sus dos líneas (productos 2 y 18, a 3,90 € y 3,50 €) ya coinciden con el catálogo. Ese UPDATE 0 es información valiosa, no un fallo: confirma que no hay nada desfasado.
Tres cambios respecto al original: se han puesto alias, se ha quitado lineas_pedido del FROM, y se ha añadido AND lp.precio_unitario <> p.precio para no tocar filas que ya están bien (lo cual, de paso, hace la sentencia idempotente).
3. Por qué sería catastrófico con 'entregado'. precio_unitario guarda el precio del momento de la venta: es la desnormalización deliberada de 01-05 y la base de toda la contabilidad histórica. Reescribirlo con el precio actual significaría que las facturas emitidas hace un año cambian de importe retroactivamente.
El daño concreto sobre TiendaVerde: las líneas 1 y 4 llevan precios históricos (11,95 € y 17,50 €) frente a los actuales (12,50 € y 18,90 €). Este UPDATE los reescribiría y la facturación total pasaría de 727,95 € a otra cifra distinta, invalidando todas las cantidades publicadas en los módulos 3 y 4. Y no habría forma de recuperar los valores antiguos: ya no estarían en ninguna parte.
Es el ejemplo perfecto de un UPDATE que se ejecuta sin error, sin aviso, y destruye información irrecuperable.
Solución 3
-- 2) Comprobar el emparejamiento 1:1 ANTES
SELECT producto_id, COUNT(*) AS veces
FROM recepcion_almacen
GROUP BY producto_id
HAVING COUNT(*) > 1;Ningún producto repetido: cada fila de productos emparejará con una sola de recepcion_almacen.
-- 3) Estado antes
SELECT p.id, p.nombre, p.stock, r.unidades AS recibidas
FROM productos AS p
JOIN recepcion_almacen AS r ON r.producto_id = p.id
ORDER BY p.id;| id | nombre | stock | recibidas |
|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 120 | 40 |
| 5 | Tomate triturado ecológico 400 g | 300 | 100 |
| 13 | Velas de cera de soja (pack 2) | 0 | 25 |
| 16 | Kombucha de jengibre 750 ml | 60 | 30 |
-- 1) El UPDATE
BEGIN;
UPDATE productos AS p
SET stock = p.stock + r.unidades
FROM recepcion_almacen AS r
WHERE r.producto_id = p.id
RETURNING p.id, p.nombre, p.stock;| id | nombre | stock |
|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 160 |
| 5 | Tomate triturado ecológico 400 g | 400 |
| 13 | Velas de cera de soja (pack 2) | 25 |
| 16 | Kombucha de jengibre 750 ml | 90 |
Qué le ocurre al producto 13. Pasa de 0 a 25 unidades: deja de estar agotado. Es un cambio con consecuencias más allá de la tabla, porque el producto 13 es uno de los "huecos deliberados" del conjunto de datos del curso (01-06): tenía stock 0 y no se había vendido nunca. Si dejas este cambio confirmado, algunos ejemplos de módulos anteriores dejarán de coincidir. Recarga el script antes de seguir.
4. Idempotencia. No lo es. SET stock = stock + unidades es una operación relativa: ejecutarlo dos veces sumaría las unidades dos veces, y el almacén tendría 200 unidades de aceite en el sistema y 160 en la estantería. Un descuadre de inventario que nadie detectaría hasta el recuento anual.
| Ejecución | stock del producto 1 |
|---|---|
| 0 (inicial) | 120 |
| 1ª | 160 |
| 2ª | 200 ← incorrecto |
Las tres formas de evitarlo, de menos a más robusta:
- Marcar las recepciones ya procesadas. Añadir a
recepcion_almacenuna columnaprocesada BOOLEAN NOT NULL DEFAULT FALSE, filtrar porWHERE NOT r.procesadaen elUPDATEy marcarla aTRUEen la misma transacción. La segunda ejecución no encuentra nada. - Registrar cada movimiento en una tabla
movimientos_stockcon su propia clave única, y calcular el stock como la suma de los movimientos en lugar de guardarlo como valor mutable. Es el enfoque de libro mayor, el más robusto y el que usan los sistemas de inventario serios. - Convertirlo en una migración versionada, que por construcción se aplica una sola vez (05-06).
Y la propiedad que hace que la opción 1 funcione: siempre que puedas identificar en el WHERE las filas "aún no procesadas", una operación relativa se vuelve idempotente. Es el mismo patrón del caso 13.2, donde WHERE estado = 'enviado' garantiza que la segunda pasada devuelva UPDATE 0.
Conclusión
UPDATE es la primera instrucción del curso que puede destruir información, y ya sabes manejarla:
- La sintaxis
UPDATE tabla SET col = valor WHERE ..., donde elWHEREhace el mismo trabajo que en unSELECTpero con consecuencias distintas.UPDATE Ncuenta las filas que cumplían la condición, no las que cambiaron de valor. - Sin
WHERE, se modifican todas las filas: 20 productos en lugar de 4, sin error, sin aviso y sin vuelta atrás. - El protocolo de los cinco pasos:
SELECTcon elWHEREdefinitivo → contar filas → previsualizar los valores nuevos como columna calculada → convertir enUPDATEcopiando elWHEREliteralmente → verificar. Y la comparación entre el recuento previo y elUPDATE Ncomo alarma. BEGIN… verificar …COMMIT/ROLLBACKcomo red de seguridad real, con sus tres advertencias prácticas: bloquea filas, un error aborta el bloque entero, y las secuencias no se deshacen. El modelo completo —ACID, aislamiento, bloqueos— es el módulo 9.- Varias columnas en un solo
SET, que evita el estado intermedio visible de dosUPDATEseguidos. - Cálculos sobre el valor anterior (
SET precio = precio * 1.05) y la regla de oro: todas las asignaciones se evalúan simultáneamente sobre la fila antigua. Por eso el orden da igual, por esoSET a = b, b = aintercambia dos columnas sin variable temporal, y por eso no se pueden encadenar cálculos. UPDATE ... FROMpara actualizar a partir de otra tabla, con su trampa mortal: un emparejamiento 1:N no suma, elige una coincidencia arbitraria y no avisa. Y las cuatro sintaxis incompatibles de PostgreSQL, MySQL, SQL Server y Oracle.RETURNINGpara ver el resultado, que no puede devolver el valor anterior (salvo elOUTPUTde SQL Server).- Los cinco errores de restricción, idénticos a los de
INSERT, con la fila original intacta cuando fallan. - Actualizar a
NULLy su semántica, yON UPDATE CASCADE, que solo hace falta con claves naturales mutables. - La idempotencia:
SET col = valor_fijosí,SET col = col * kno; y el patrón que lo resuelve, acotar elWHEREa las filas "aún no procesadas" para que la segunda ejecución devuelvaUPDATE 0. - Y los casos reales: subida de tarifas por categoría, cierre de pedidos antiguos, corrección de un email y descatalogación con
activo = FALSE, el borrado lógico.
Ese último caso es la puerta de la siguiente lección. En Instrucción DELETE aprenderás a eliminar filas de verdad —con el mismo protocolo, reforzado—, verás la diferencia entre DELETE y TRUNCATE, comprobarás con recuentos qué ocurre al borrar una fila referenciada según su ON DELETE sea RESTRICT, CASCADE o SET NULL, y llegarás a la pregunta de fondo: si activo = FALSE conserva el histórico y DELETE lo destruye, ¿por qué borrar algo alguna vez? La respuesta tiene que ver con la auditoría, con el rendimiento y con el derecho de supresión de los datos personales.
Curso de SQL
Módulo 1: Introducción a SQL
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
