Menu

SQLite UPSERT: ON CONFLICT DO UPDATE y DO NOTHING

Cómo funciona UPSERT en SQLite: la cláusula ON CONFLICT, la tabla excluded, DO NOTHING vs DO UPDATE y por qué conviene más que INSERT OR REPLACE.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Insertar, o actualizar si ya existe

Una necesidad muy común: insertar una fila, pero si ya hay otra con la misma clave, actualizarla en lugar de fallar. Sin UPSERT, tendrías que hacer primero un SELECT y luego decidir entre INSERT o UPDATE: dos viajes a la base de datos y una bonita condición de carrera entre medio.

El UPSERT de SQLite resuelve todo eso en una sola sentencia:

La primera vez que lo ejecutas, se inserta la fila. Si lo vuelves a correr con un precio distinto y el mismo sku, la fila existente se actualiza en su lugar. Sin duplicados, sin errores.

Anatomía de ON CONFLICT en SQLite

La forma completa es así:

INSERT INTO table (...) VALUES (...)
ON CONFLICT(conflict_target) DO UPDATE SET col = expr, ...
WHERE condition;

Hay tres piezas que importan:

  • conflict_target — la columna o columnas con una restricción UNIQUE o PRIMARY KEY donde esperas que ocurra el choque. SQLite la usa para decidir qué índice vigilar.
  • DO UPDATE SET ... — qué cambiar en la fila existente cuando se produce el conflicto. (O DO NOTHING para ignorarlo sin hacer ruido.)
  • WHERE opcional — una condición extra que debe cumplirse para que el UPDATE se ejecute de verdad.

El destino del conflicto tiene que coincidir con una restricción única real. ON CONFLICT(price) no va a compilar si price no es única: SQLite no tiene contra qué detectar el conflicto.

DO NOTHING: insertar si no existe, si no, omitir

Es la variante más sencilla. Resulta útil cuando estás precargando datos o registrando eventos y los duplicados simplemente deben ignorarse en silencio:

El segundo INSERT choca con el mismo event_id y, en condiciones normales, lanzaría UNIQUE constraint failed. Con DO NOTHING, SQLite simplemente lo ignora: ni excepción, ni filas afectadas.

Esta es la típica "inserción idempotente" para la que mucha gente recurre a INSERT OR IGNORE. El DO NOTHING del UPSERT cumple la misma función y, además, se combina mucho mejor con cláusulas WHERE y RETURNING.

La pseudo-tabla excluded

Cuando salta un conflicto, tienes dos filas en juego al mismo tiempo: la que ya existe en la tabla y la nueva que intentaste insertar. SQLite te da una forma de referirte a ambas.

  • Los nombres de columna a secas (price, name) hacen referencia a la fila existente.
  • excluded.column hace referencia a la fila entrante que fue rechazada.

quantity = quantity + excluded.quantity se lee como "la cantidad existente más la nueva". Tras dos INSERT, A-100 queda con una cantidad de 8. Este patrón —ir acumulando sobre una fila ya existente— es uno de los trucos más útiles del UPSERT en SQLite.

UPSERT condicional con WHERE en SQLite

El WHERE final te permite omitir la actualización salvo que se cumpla cierta condición. Se evalúa sobre la fila existente (y puede hacer referencia a excluded.* para los datos entrantes):

La fila nueva trae un updated_at más antiguo, por lo que el WHERE da falso y la actualización se omite. La fila existente conserva su precio más reciente. Si inviertes las fechas, la actualización sí se ejecuta. Es el patrón clásico de "solo sobrescribir con datos más frescos".

Upsert de varias filas en SQLite

VALUES admite varias filas a la vez, y ON CONFLICT se aplica a cada una de forma independiente:

A-100 choca con una fila existente y se actualiza. A-200 y A-300 son nuevos y se insertan. Una sola sentencia, resultado mixto entre insert y update. Es una forma limpia de sincronizar un lote de registros desde una fuente externa.

UPSERT vs INSERT OR REPLACE en SQLite

A primera vista, INSERT OR REPLACE parece hacer lo mismo. Pero no.

notes desapareció. INSERT OR REPLACE eliminó por completo la fila 1 e insertó una nueva: cualquier columna que no incluyas vuelve a NULL o a su valor por defecto. Además dispara los triggers DELETE y propaga las cascadas de las foreign keys con ON DELETE.

En cambio, UPSERT conserva la fila:

notes sigue ahí. Solo cambian las columnas que aparecen en el SET. Por defecto, tira de UPSERT; deja INSERT OR REPLACE solo para cuando realmente quieras la semántica de borrar y volver a insertar.

Varios objetivos de conflicto

Si una fila pudiera chocar contra más de una restricción, puedes encadenar varias cláusulas ON CONFLICT:

Gana la primera restricción que se dispare, y se ejecuta el DO UPDATE de esa rama. En la práctica, la mayoría de las tablas tienen un único objetivo de conflicto evidente —la clave primaria o una sola columna UNIQUE— y rara vez necesitarás más de una cláusula.

Errores típicos al usar UPSERT en SQLite

Hay un par de cosas que suelen pillar desprevenido a más de uno:

  • Sin índice único correspondiente, no hay UPSERT. ON CONFLICT(col) exige que col sea PRIMARY KEY o tenga una restricción UNIQUE. Si no, SQLite lanza el error "no such constraint".
  • DO UPDATE no se dispara si no hay conflicto. Es una alternativa al insert, no un comportamiento adicional. La primera vez que aparece una clave, solo se ejecuta el insert.
  • excluded es de solo lectura. Puedes leer de ahí, pero no escribir. El destino del SET siempre es la fila existente.
  • Rowids autogenerados con INTEGER PRIMARY KEY. Si no proporcionas el id, cada insert recibe uno nuevo y no hay nada con lo que entrar en conflicto. UPSERT solo tiene sentido cuando la columna en conflicto recibe un valor determinista desde quien hace la llamada.

Lo que viene: RETURNING

UPSERT no te dice qué filas se insertaron, cuáles se actualizaron ni cómo quedaron al final. Para eso está la cláusula RETURNING: te devuelve las filas afectadas en la misma sentencia, sin necesidad de un SELECT posterior. Lo vemos a continuación.

Preguntas frecuentes

¿Qué es UPSERT en SQLite?

UPSERT es un INSERT que se convierte en UPDATE (o directamente no hace nada) cuando violaría una restricción UNIQUE o PRIMARY KEY. Se escribe como INSERT ... ON CONFLICT(columna) DO UPDATE SET ... o DO NOTHING. SQLite lo soporta desde la versión 3.24.0 (2018).

¿Qué es la tabla excluded en un UPSERT de SQLite?

excluded es una pseudo-tabla especial que contiene la fila que intentaste insertar. Dentro de DO UPDATE SET ..., la fila ya existente se referencia por nombre de columna y la rechazada como excluded.columna. Por ejemplo, SET price = excluded.price significa: 'sobrescribe el precio con el valor que traía el INSERT nuevo'.

¿Cuál es la diferencia entre INSERT OR REPLACE y UPSERT?

INSERT OR REPLACE borra la fila en conflicto e inserta una nueva — eso dispara los triggers de DELETE, rompe las claves foráneas con ON DELETE CASCADE y resetea todas las columnas a sus valores por defecto. UPSERT, en cambio, actualiza la fila existente in situ, así que solo cambian las columnas que listas en SET. Mejor usar UPSERT, salvo que de verdad quieras el efecto de borrar y reinsertar.

¿Se pueden hacer upsert de varias filas a la vez en SQLite?

Sí. INSERT INTO t(...) VALUES (...), (...), (...) ON CONFLICT(col) DO UPDATE SET ... funciona sin problemas. Cada fila se comprueba contra el conflict target de forma individual, y la fila excluded dentro de DO UPDATE hace referencia a la fila entrante concreta que provocó el conflicto.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR