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

UPDATE es 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 lanzar tiendaverde.sql.
  • En un sistema real, un UPDATE sobre datos vivos debe ir revisado por otra persona y ejecutado dentro de una transacción.

Contenido

  1. La sintaxis, y el WHERE como red de seguridad
  2. Qué pasa exactamente si olvidas el WHERE
  3. El protocolo de los cinco pasos
  4. BEGINROLLBACK / COMMIT: la red de verdad
  5. Actualizar varias columnas a la vez
  6. Calcular sobre el valor anterior: subir precios un 5 %
  7. Por qué el orden de las asignaciones no importa
  8. UPDATE ... FROM: actualizar a partir de otra tabla
  9. RETURNING: ver qué has cambiado
  10. UPDATE que viola una restricción
  11. Actualizar a NULL, y el efecto de ON UPDATE CASCADE
  12. Idempotencia: por qué importa al reejecutar un script
  13. Cuatro casos de negocio de TiendaVerde
  14. Errores Comunes y Consejos
  15. Ejercicios
  16. Conclusión

  1. La sintaxis, y el WHERE como red de seguridad

UPDATE tabla
SET    columna1 = valor1,
       columna2 = valor2,
       ...
WHERE  condición;

Tres 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 productos
SET    activo = FALSE
WHERE  id = 13;
UPDATE 1

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:

UPDATE productos
SET    stock = 0
WHERE  id = 13;
UPDATE 1

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.

  1. Qué pasa exactamente si olvidas el WHERE

No 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;
UPDATE 20

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.

SELECT id, nombre, precio FROM productos ORDER BY id LIMIT 5;
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 UPDATE o un DELETE sin WHERE, y psql tiene incluso una opción para ello (\set ON_ERROR_STOP on no hace esto, pero herramientas como pgcli o DBeaver sí lo hacen). Pero el estándar SQL no lo contempla: un UPDATE sin WHERE es 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.

  1. 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.

SELECT id, nombre, categoria_id, precio
FROM   productos
WHERE  categoria_id = 4
ORDER BY id;
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

SELECT COUNT(*) AS filas_afectadas
FROM   productos
WHERE  categoria_id = 4;
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.

UPDATE productos
SET    precio = precio * 1.05
WHERE  categoria_id = 4;
UPDATE 4

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

SELECT id, nombre, precio
FROM   productos
WHERE  categoria_id = 4
ORDER BY id;
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 el UPDATE y aun así los precios han quedado con dos decimales. El motivo es que precio es NUMERIC(10,2): al asignar 3.25 * 1.05 = 3.4125 a 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 el ROUND explícito para que el lector de tu código sepa que la decisión fue tuya y no del sistema de tipos.

  1. BEGINROLLBACK / COMMIT: la red de verdad

El 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:

BEGIN;

UPDATE productos
SET    precio = precio * 1.05
WHERE  categoria_id = 4;
BEGIN
UPDATE 4

Ahora, antes de confirmar, verifica:

SELECT id, nombre, precio FROM productos WHERE categoria_id = 4 ORDER BY id;
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:

COMMIT;
COMMIT

Y si está mal —o si ese UPDATE 4 hubiera sido un UPDATE 20—:

ROLLBACK;
ROLLBACK
SELECT id, nombre, precio FROM productos WHERE categoria_id = 4 ORDER BY id;
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.

CREATE TABLE productos_backup_20260302 AS
SELECT * FROM productos;
SELECT 20

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.

  1. Actualizar varias columnas a la vez

Se separan por comas dentro del mismo SET:

UPDATE productos
SET    activo = FALSE,
       stock  = 0
WHERE  id = 20;
UPDATE 1
SELECT id, nombre, precio, stock, activo FROM productos WHERE id = 20;
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í.

  1. 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 gratis

La 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.

UPDATE productos
SET    precio = ROUND(coste * 2.2, 2)
WHERE  categoria_id = 5
  AND  coste IS NOT NULL;
UPDATE 2
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.

  1. 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.

UPDATE productos
SET    precio = coste * 2,
       coste  = coste * 1.1
WHERE  id = 5;
UPDATE 1

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:

SELECT id, nombre, precio, coste FROM productos WHERE id = 5;
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;
UPDATE 1
SELECT id, nombre, precio, coste FROM productos 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:

  1. Puedes escribir las asignaciones en cualquier orden. SET a = ..., b = ... y SET b = ..., a = ... son equivalentes.
  2. No puedes encadenar cálculos dentro del mismo UPDATE. SET precio = precio * 1.1, iva = precio * 0.21 calcularía el IVA sobre el precio viejo, no sobre el recién subido. Si necesitas encadenar, hacen falta dos UPDATE (dentro de la misma transacción).
  3. No puedes asignar dos veces la misma columna. PostgreSQL lo rechaza:
-- ⚠️ INCORRECTA
UPDATE productos SET precio = precio * 1.05, precio = precio + 1 WHERE id = 1;
ERROR:  multiple assignments to same column "precio"

  1. UPDATE ... FROM: actualizar a partir de otra tabla

Hasta 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:

-- Antes
SELECT id, nombre, stock FROM productos WHERE id IN (1, 3, 14) ORDER BY id;
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;
UPDATE 3
SELECT id, nombre, stock FROM productos WHERE id IN (1, 3, 14) ORDER BY id;
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 de productos empareja con varias filas de lineas_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, este UPDATE descontaría solo una de ellas.

Es exactamente la multiplicación de filas del módulo 3, pero mucho más peligrosa: en un SELECT la 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 el FROM — módulo 7. Mientras tanto: antes de un UPDATE ... 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;
(0 filas)

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
UPDATE 1

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 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.

  1. RETURNING: ver qué has cambiado

Igual 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
UPDATE 2

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.columna devuelve 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.

  1. UPDATE que viola una restricción

Un 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:

UPDATE productos SET stock = stock - 1 WHERE id = 13;
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:

UPDATE pedidos SET estado = 'devuelto' WHERE id = 6;
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:

UPDATE pedidos SET cliente_id = 999 WHERE id = 1;
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:

UPDATE pedidos SET cliente_id = NULL WHERE id = 1;
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.

  1. Actualizar a NULL, y el efecto de ON UPDATE CASCADE

Poner 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
UPDATE 1

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);
CREATE TABLE
CREATE TABLE
INSERT 0 3
INSERT 0 4

Ahora la empresa decide migrar a códigos ISO de dos letras:

UPDATE paises SET codigo = 'ES' WHERE codigo = 'ESP';
UPDATE 1
SELECT id, pais_codigo, peso_max_kg, importe FROM tarifas_envio ORDER BY id;
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.

  1. 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
3.41
3.58
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 Fija un estado absoluto
SET col = otra_columna 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:

  1. 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.

  1. Registrar que ya se hizo, en una tabla de control o con una columna de marca, y comprobarlo antes.

  2. 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.

  1. 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
UPDATE 2
SELECT estado, COUNT(*) AS pedidos FROM pedidos GROUP BY estado ORDER BY pedidos DESC, estado;
estado pedidos
entregado 16
pagado 2
cancelado 1
pendiente 1
COMMIT;

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:

SELECT id, nombre, apellidos, email, pais FROM clientes WHERE id = 8;
id nombre apellidos email 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 email
8 Tiago Almeida Nunes [email protected]
UPDATE 1

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 el WHERE no tocarías ninguna fila (UPDATE 0) y podrías creer que ya estaba bien. La PK es siempre el filtro más seguro.
  • El UNIQUE sigue vigilando. Si la dirección nueva ya perteneciera a otro cliente, el UPDATE fallaría con duplicate 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
UPDATE 1

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 WHERE de memoria al pasar del SELECT al UPDATE. 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. Un BEGIN cuesta seis letras.
  • Dejar una transacción abierta. Bloquea filas para el resto de sesiones. Confirma o deshaz antes de levantarte.
  • Esperar que RETURNING devuelva 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.21 usa el a viejo para calcular b. Si necesitas encadenar, dos UPDATE.
  • Usar UPDATE ... FROM con un emparejamiento 1:N. No suma: elige una coincidencia arbitraria y no avisa. Comprueba con un GROUP BY ... HAVING COUNT(*) > 1 antes.
  • Repetir la tabla destino en el FROM de 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 * 2 con coste nulo da NULL y revienta contra el NOT NULL.
  • Escribir UPDATE no idempotentes en scripts automáticos. precio = precio * 1.05 ejecutado dos veces sube un 10,25 %. Fija valores absolutos o acota con el WHERE.
  • Actualizar una columna que representa un histórico. precio_unitario es 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 WHERE más seguro que existe.
  • Consejo: previsualiza el SET como 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 tabla primero. Treinta segundos que valen por una noche entera.

Ejercicios

Trabaja sobre la base recién recargada y usa BEGINROLLBACK 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:

  1. Subir el precio un 8 %, redondeando a dos decimales.
  2. Subir el coste un 3 %, redondeando a dos decimales.
  3. 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';
  1. Encuentra el error y explica qué hace realmente esta sentencia.
  2. Reescríbela correctamente, con alias, y di cuántas filas debería tocar sobre la base recién recargada.
  3. Explica por qué esta operación sería catastrófica si el WHERE incluyera pedidos.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);
  1. Escribe el UPDATE ... FROM que suma las unidades recibidas al stock de cada producto.
  2. Comprueba antes que el emparejamiento es 1:1.
  3. Muestra el estado antes y después, y explica qué le ocurre al producto 13.
  4. ¿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
UPDATE 4
-- 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
COMMIT;

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;
UPDATE 0

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;
(0 filas)

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
UPDATE 4
COMMIT;

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
160
200 ← incorrecto

Las tres formas de evitarlo, de menos a más robusta:

  1. Marcar las recepciones ya procesadas. Añadir a recepcion_almacen una columna procesada BOOLEAN NOT NULL DEFAULT FALSE, filtrar por WHERE NOT r.procesada en el UPDATE y marcarla a TRUE en la misma transacción. La segunda ejecución no encuentra nada.
  2. Registrar cada movimiento en una tabla movimientos_stock con 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.
  3. 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 el WHERE hace el mismo trabajo que en un SELECT pero con consecuencias distintas. UPDATE N cuenta 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: SELECT con el WHERE definitivo → contar filas → previsualizar los valores nuevos como columna calculada → convertir en UPDATE copiando el WHERE literalmente → verificar. Y la comparación entre el recuento previo y el UPDATE N como alarma.
  • BEGIN … verificar … COMMIT / ROLLBACK como 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 dos UPDATE seguidos.
  • 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 eso SET a = b, b = a intercambia dos columnas sin variable temporal, y por eso no se pueden encadenar cálculos.
  • UPDATE ... FROM para 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.
  • RETURNING para ver el resultado, que no puede devolver el valor anterior (salvo el OUTPUT de SQL Server).
  • Los cinco errores de restricción, idénticos a los de INSERT, con la fila original intacta cuando fallan.
  • Actualizar a NULL y su semántica, y ON UPDATE CASCADE, que solo hace falta con claves naturales mutables.
  • La idempotencia: SET col = valor_fijo sí, SET col = col * k no; y el patrón que lo resuelve, acotar el WHERE a las filas "aún no procesadas" para que la segunda ejecución devuelva UPDATE 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

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