SQLite UPSERT / ACTUALIZAR O INSERTAR

102

Necesito realizar UPSERT / INSERT O UPDATE contra una base de datos SQLite.

Existe el comando INSERT OR REPLACE que en muchos casos puede ser útil. Pero si desea mantener sus ID con autoincrement en su lugar debido a claves externas, no funciona ya que elimina la fila, crea una nueva y, en consecuencia, esta nueva fila tiene una nueva ID.

Esta sería la mesa:

jugadores - (clave principal en la identificación, nombre de usuario único)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |
bgusach
fuente

Respuestas:

51

Esta es una respuesta tardía. A partir de SQLIte 3.24.0, lanzado el 4 de junio de 2018, finalmente hay soporte para la cláusula UPSERT siguiendo la sintaxis de PostgreSQL.

INSERT INTO players (user_name, age)
  VALUES('steven', 32) 
  ON CONFLICT(user_name) 
  DO UPDATE SET age=excluded.age;

Nota: Para aquellos que tengan que usar una versión de SQLite anterior a 3.24.0, consulte esta respuesta a continuación (publicada por mí, @MarqueIV).

Sin embargo, si tiene la opción de actualizar, se le recomienda encarecidamente que lo haga ya que, a diferencia de mi solución, la que se publica aquí logra el comportamiento deseado en una sola declaración. Además, obtienes todas las demás funciones, mejoras y correcciones de errores que generalmente vienen con una versión más reciente.

prapin
fuente
Por ahora, esta versión aún no está en el repositorio de Ubuntu.
bl79
¿Por qué no puedo usar esto en Android? Lo intenté db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Me da un error de sintaxis en la palabra "on"
Bastian Voigt
1
@BastianVoigt Debido a que las bibliotecas SQLite3 instaladas en varias versiones de Android son anteriores a la 3.24.0. Consulte: developer.android.com/reference/android/database/sqlite/… Lamentablemente, si necesita una nueva función de SQLite3 (o cualquier otra biblioteca del sistema) en Android o iOS, debe incluir una versión específica de SQLite en su aplicación en lugar de depender del sistema instalado.
prapin
En lugar de UPSERT, ¿no es más un INDATE ya que primero intenta el inserto? ;)
Mark A. Donohoe
@BastianVoigt, consulte mi respuesta a continuación (vinculada en la pregunta anterior) que es para versiones anteriores a 3.24.0.
Mark A. Donohoe
105

Estilo de preguntas y respuestas

Bueno, después de investigar y luchar con el problema durante horas, descubrí que hay dos formas de lograr esto, dependiendo de la estructura de su tabla y si tiene restricciones de claves externas activadas para mantener la integridad. Me gustaría compartir esto en un formato limpio para ahorrar algo de tiempo a las personas que puedan estar en mi situación.


Opción 1: puede permitirse eliminar la fila

En otras palabras, no tiene una clave externa, o si las tiene, su motor SQLite está configurado para que no haya excepciones de integridad. El camino a seguir es INSERTAR O REEMPLAZAR . Si está intentando insertar / actualizar un reproductor cuya ID ya existe, el motor SQLite eliminará esa fila e insertará los datos que está proporcionando. Ahora surge la pregunta: ¿qué hacer para mantener asociada la antigua ID?

Digamos que queremos UPSERT con los datos user_name = 'steven' y age = 32.

Mira este código:

INSERT INTO players (id, name, age)

VALUES (
    coalesce((select id from players where user_name='steven'),
             (select max(id) from drawings) + 1),
    32)

El truco está en la fusión. Devuelve la identificación del usuario 'steven' si la hay, y de lo contrario, devuelve una nueva identificación nueva.


Opción 2: no puede permitirse eliminar la fila

Después de jugar con la solución anterior, me di cuenta de que en mi caso eso podría terminar destruyendo datos, ya que este ID funciona como una clave externa para otra tabla. Además, creé la tabla con la cláusula ON DELETE CASCADE , lo que significaría que eliminaría los datos en silencio. Peligroso.

Entonces, primero pensé en una cláusula IF, pero SQLite solo tiene CASE . Y este CASE no se puede usar (o al menos no lo administré) para realizar una consulta de ACTUALIZACIÓN si EXISTE (seleccione la identificación de los jugadores donde user_name = 'steven'), e INSERT si no lo hizo. No vayas.

Y luego, finalmente usé la fuerza bruta, con éxito. La lógica es que, para cada UPSERT que desee realizar, primero ejecute INSERT OR IGNORE para asegurarse de que haya una fila con nuestro usuario, y luego ejecute una consulta UPDATE con exactamente los mismos datos que intentó insertar.

Los mismos datos que antes: user_name = 'steven' y age = 32.

-- make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

-- make sure it has the right data
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven'; 

¡Y eso es todo!

EDITAR

Como ha comentado Andy, intentar insertar primero y luego actualizar puede provocar que se activen los activadores con más frecuencia de lo esperado. En mi opinión, esto no es un problema de seguridad de los datos, pero es cierto que disparar eventos innecesarios tiene poco sentido. Por tanto, una solución mejorada sería:

-- Try to update any existing row
UPDATE players SET age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 
bgusach
fuente
10
Ditto ... la opción 2 es genial. Excepto que lo hice al revés: intente una actualización, verifique si las filas afectadas> 0, si no, haga una inserción.
Tom Spencer
Ese también es un enfoque bastante bueno, el único pequeño inconveniente es que no tiene solo un SQL para el "upsert".
bgusach
2
no es necesario restablecer el nombre de usuario en la declaración de actualización en la última muestra de código. Es suficiente para establecer la edad.
Serg Stetsuk
72

Aquí hay un enfoque que no requiere el 'ignorar' de fuerza bruta que solo funcionaría si hubiera una violación clave. De esta forma funciona según las condiciones que especifique en la actualización.

Prueba esto...

-- Try to update any existing row
UPDATE players
SET age=32
WHERE user_name='steven';

-- If no update happened (i.e. the row didn't exist) then insert one
INSERT INTO players (user_name, age)
SELECT 'steven', 32
WHERE (Select Changes() = 0);

Cómo funciona

La 'salsa mágica' aquí se usa Changes()en la Wherecláusula. Changes()representa el número de filas afectadas por la última operación, que en este caso es la actualización.

En el ejemplo anterior, si no hay cambios desde la actualización (es decir, el registro no existe), entonces Changes()= 0 para que la Wherecláusula en la Insertdeclaración se evalúe como verdadera y se inserte una nueva fila con los datos especificados.

Si la Update hicieron actualización de una fila existente, entonces Changes()= 1 (o más exactamente, no cero si se actualiza más de una fila), por lo que el 'Dónde' cláusula en el Insertahora se evalúa como falsa y por lo tanto ninguna inserción se llevará a cabo.

La belleza de esto es que no se necesita fuerza bruta, ni eliminar innecesariamente y luego volver a insertar datos, lo que puede resultar en alterar las claves descendentes en las relaciones de clave externa.

Además, dado que es solo una Wherecláusula estándar , puede basarse en cualquier cosa que defina, no solo en violaciones clave. Del mismo modo, puede usarlo Changes()en combinación con cualquier otra cosa que desee / necesite en cualquier lugar donde se permitan expresiones.

Mark A. Donohoe
fuente
1
Esto funciono muy bien para mi. No he visto esta solución en ningún otro lugar junto con todos los ejemplos INSERT OR REPLACE, es mucho más flexible para mi caso de uso.
csab
@MarqueIV y ¿qué pasa si hay dos elementos que deben actualizarse o insertarse? por ejemplo, el primero se actualizó y el segundo no existe. en tal caso Changes() = 0, devolverá falso y dos filas harán INSERT OR REEMPLAZAR
Andriy Antonov
Por lo general, se supone que un UPSERT actúa sobre un registro. Si está diciendo que sabe con certeza que está actuando en más de un registro, cambie la verificación de recuento en consecuencia.
Mark A. Donohoe
Lo malo es que si la fila existe, el método de actualización debe ejecutarse independientemente de si la fila ha cambiado o no.
Jimi
1
Por qué es eso algo malo? Y si los datos no han cambiado, ¿por qué llamas UPSERTen primer lugar? Pero aun así, es bueno que la actualización ocurra, la configuración Changes=1o, de lo contrario, la INSERTdeclaración se dispararía incorrectamente, lo que no desea.
Mark A. Donohoe
25

El problema con todas las respuestas presentadas es la falta total de tener en cuenta los desencadenantes (y probablemente otros efectos secundarios). Solución como

INSERT OR IGNORE ...
UPDATE ...

lleva a que se ejecuten ambos disparadores (para insertar y luego para actualizar) cuando la fila no existe.

La solución adecuada es

UPDATE OR IGNORE ...
INSERT OR IGNORE ...

en ese caso, solo se ejecuta una instrucción (cuando la fila existe o no).

Andy
fuente
1
Entiendo tu argumento. Actualizaré mi pregunta. Por cierto, no sé por qué UPDATE OR IGNOREes necesario, ya que la actualización no se bloqueará si no se encuentran filas.
bgusach
1
¿legibilidad? Puedo ver lo que hace el código de Andy de un vistazo. Suya, tuve que estudiar un minuto para averiguarlo.
Brandan
6

Para tener un UPSERT puro sin agujeros (para programadores) que no se relacionan con claves únicas y de otro tipo:

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();

SELECT changes () devolverá el número de actualizaciones realizadas en la última consulta. Luego verifique si el valor de retorno de changes () es 0, si es así, ejecute:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 
Gilco
fuente
Esto es equivalente a lo que propuso @fiznool en su comentario (aunque yo iría por su solución). Está bien y en realidad funciona bien, pero no tiene una declaración SQL única. UPSERT no basado en PK u otras claves únicas tiene poco o ningún sentido para mí.
bgusach
4

También puede simplemente agregar una cláusula ON CONFLICT REPLACE a su restricción única de nombre de usuario y luego simplemente INSERTAR, dejando que SQLite averigüe qué hacer en caso de un conflicto. Ver: https://sqlite.org/lang_conflict.html .

También tenga en cuenta la oración sobre los activadores de eliminación: cuando la estrategia de resolución de conflictos REPLACE elimina filas para satisfacer una restricción, los activadores de eliminación se activan si y solo si los activadores recursivos están habilitados.

Maximiliano Tyrtania
fuente
1

Opción 1: Insertar -> Actualizar

Si desea evitar ambos changes()=0e INSERT OR IGNOREincluso si no puede permitirse eliminar la fila, puede utilizar esta lógica;

Primero, inserte (si no existe) y luego actualice filtrando con la clave única.

Ejemplo

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Insert if NOT exists
INSERT INTO players (user_name, age)
SELECT 'johnny', 20
WHERE NOT EXISTS (SELECT 1 FROM players WHERE user_name='johnny' AND age=20);

-- Update (will affect row, only if found)
-- no point to update user_name to 'johnny' since it's unique, and we filter by it as well
UPDATE players 
SET age=20 
WHERE user_name='johnny';

Respecto a los disparadores

Aviso: no lo he probado para ver qué desencadenadores se están llamando, pero supongo lo siguiente:

si la fila no existe

  • ANTES DE INSERTAR
  • INSERTAR usando EN VEZ DE
  • DESPUÉS DE INSERTAR
  • ANTES DE ACTUALIZAR
  • ACTUALIZAR usando EN VEZ DE
  • DESPUÉS DE LA ACTUALIZACIÓN

si la fila existe

  • ANTES DE ACTUALIZAR
  • ACTUALIZAR usando EN VEZ DE
  • DESPUÉS DE LA ACTUALIZACIÓN

Opción 2: Insertar o reemplazar - conserve su propia identificación

de esta manera puede tener un solo comando SQL

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Single command to insert or update
INSERT OR REPLACE INTO players 
(id, user_name, age) 
VALUES ((SELECT id from players WHERE user_name='johnny' AND age=20),
        'johnny',
        20);

Editar: opción agregada 2.

itsho
fuente