Upsert: insertar o cambiar
En resumen: Un upsert inserta una fila o cambia la que ya existe con ON CONFLICT … DO UPDATE cuando se rompe una columna única; DO NOTHING se salta el conflicto.
Problema
El cliente vuelve
Georgi Petrov se mudó a Sofía. Pide sin iniciar sesión, y el formulario envía su nombre, su correo y su ciudad. El sitio web intenta guardarlo como cliente nuevo:
Su correo ya está en la base de datos, y es único. El sitio web tiene que decidir: ¿cliente nuevo o existente? Podría comprobarlo primero con un SELECT y luego elegir entre INSERT y UPDATE. Pero son dos sentencias, y entre ellas otro visitante podría guardar el mismo correo.
La idea
ON CONFLICT … DO UPDATE
ON CONFLICT le dice a INSERT qué hacer si la fila rompe una columna única. Con DO UPDATE cambia la fila que ya está en la tabla:
Las dos filas siguen caminos distintos. Georgi está: su fila conserva el número 2 y la fecha de registro de 2025, y solo cambia la ciudad. Vesela no está: se inserta como cliente 21.
excluded es la fila que la sentencia intentó insertar. excluded.city es la ciudad de VALUES, y city sin nada delante es el valor anterior de la tabla.
Esto se llama upsert, de «update» e «insert».
Más precisamente, para curiosos
En PostgreSQL la sentencia es la misma: ON CONFLICT (email) DO UPDATE SET city = EXCLUDED.city. MySQL tiene su propia forma: INSERT … ON DUPLICATE KEY UPDATE. SQL Server y Oracle usan MERGE, una orden más general que compara dos tablas y decide para cada fila: insertar, cambiar o borrar. PostgreSQL tiene MERGE desde la versión 15. SQLite no tiene MERGE.
Pruébalo
Una dirección nueva
Elena Dimitrova (elena.d@example.com) se mudó a Varna. El formulario envía su nombre, su correo, la ciudad Varna y la fecha de hoy, 2026-10-25. Guárdala de modo que solo cambie la ciudad. Su fecha de registro sigue siendo la anterior.
Solución
INSERT INTO customers (name, email, city, joined_at)
VALUES ('Elena Dimitrova', 'elena.d@example.com', 'Varna', '2026-10-25')
ON CONFLICT (email) DO UPDATE SET city = excluded.city;INSERT INTO customers (name, email, city, joined_at) VALUES ('Elena Dimitrova', 'elena.d@example.com', 'Varna', '2026-10-25') ON CONFLICT (email) DO UPDATE SET city = excluded.city. Si añades también joined_at = excluded.joined_at, Elena parecerá una clienta desde hoy. Se comprueba toda la tabla customers.
La idea
Mercancía del proveedor
Por la mañana llega una entrega: 30 banitsas, 40 pepinos y un producto nuevo, el ayran. El proveedor usa los números de producto de la tienda, y el ayran ya tiene el número 26 en el catálogo. Los productos conocidos deben recibir más existencias, y el nuevo debe aparecer:
El conflicto está en id, la clave primaria. Para la banitsa y los pepinos, products.stock + excluded.stock suma las existencias entregadas a las anteriores. Su fecha added_at se queda como estaba, porque no está en SET. El ayran no está, así que se inserta con todas sus columnas.
Aquí stock lleva products. delante, porque la expresión tiene dos existencias: la de la tabla y la de la entrega.
Pruébalo
Más en el carrito
Georgi añade 2 paquetes de mantequilla (producto 9, 3,20 €) a su pedido nuevo 57. Si ya está, la cantidad debe subir, y si no está, debe añadirse una fila.
Solución
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (57, 9, 2, 3.2)
ON CONFLICT (order_id, product_id) DO UPDATE SET quantity = quantity + excluded.quantity;INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (57, 9, 2, 3.2) ON CONFLICT (order_id, product_id) DO UPDATE SET quantity = quantity + excluded.quantity. En order_items la clave es el par de pedido y producto, así que el conflicto está en las dos columnas. La mantequilla era 3 y pasa a 5.
Míralo
DO NOTHING
A veces ante un conflicto no debe cambiar nada. De una feria llega una lista de personas que quieren ser clientes. A quien ya es cliente se le salta:
Maria se queda como estaba, con la ciudad Sofía. Vesela es una clienta nueva. Sin ON CONFLICT toda la sentencia se habría detenido en Maria, y Vesela no habría entrado.
Más precisamente, para curiosos
SQLite también tiene una forma más antigua propia: INSERT OR IGNORE hace lo mismo que DO NOTHING.
INSERT OR REPLACE parece un upsert, pero no lo es. Ante un conflicto borra la fila anterior e inserta una nueva. Teodora recibe un número nuevo y una fecha de registro nueva:
En el caso de Georgi el borrado ni siquiera pasa, porque sus pedidos apuntan a la fila anterior:
Así que para un upsert usa ON CONFLICT … DO UPDATE. Cambia solo las columnas de SET.
Ejercicio
La entrega
El proveedor envía la entrega como una tabla delivery: número de producto, nombre, categoría, precio y cantidad. Para la lección la tabla ya está en la base de datos. Ejecuta el SELECT inicial para verla.
Guarda la entrega en products. A los productos conocidos se les suma la cantidad entregada a sus existencias y se les pone el precio del proveedor. Los nuevos se insertan con la fecha 2026-10-25.
La entrega en products:
Solución
INSERT INTO products (id, name, category_id, price, stock, added_at)
SELECT product_id, name, category_id, price, quantity, '2026-10-25'
FROM delivery
WHERE true
ON CONFLICT (id) DO UPDATE SET
stock = products.stock + excluded.stock,
price = excluded.price;INSERT INTO products (id, name, category_id, price, stock, added_at) SELECT product_id, name, category_id, price, quantity, '2026-10-25' FROM delivery WHERE true ON CONFLICT (id) DO UPDATE SET stock = products.stock + excluded.stock, price = excluded.price. La mantequilla pasa a 3,40 € con 40 en existencias, del café hay 25, y el té verde es el producto nuevo 26.
WHERE true parece inútil, pero hace falta. Sin él SQLite lee ON como el inicio de JOIN … ON y se detiene con un error. Cuando las filas de un upsert vienen de un SELECT, añade un WHERE, aunque sea solo WHERE true.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Clave primaria y clave foránea · INSERT: insertar filas · UPDATE: cambiar valores · DELETE: borrar filas
- Continúa en: Flujos de eventos · La panadería, etapa 4: los cambios
- Ejercicios sobre el tema: Cambiar los datos