Insertar actualización almacenada proc en SQL Server

104

He escrito un proceso almacenado que hará una actualización si existe un registro, de lo contrario, hará una inserción. Se parece a esto:

update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)

Mi lógica detrás de escribirlo de esta manera es que la actualización realizará una selección implícita usando la cláusula where y si eso devuelve 0, entonces se llevará a cabo la inserción.

La alternativa a hacerlo de esta manera sería hacer una selección y luego, según el número de filas devueltas, hacer una actualización o una inserción. Esto lo consideré ineficiente porque si va a hacer una actualización, causará 2 selecciones (la primera llamada de selección explícita y la segunda implícita en el dónde de la actualización). Si el proceso hiciera un inserto, no habría diferencia en la eficiencia.

¿Mi lógica suena aquí? ¿Es así como combinaría una inserción y una actualización en un proceso almacenado?

Chico
fuente

Respuestas:

61

Su suposición es correcta, esta es la forma óptima de hacerlo y se llama upsert / merge .

Importancia de UPSERT - de sqlservercentral.com :

Para cada actualización en el caso mencionado anteriormente, estamos eliminando una lectura adicional de la tabla si usamos UPSERT en lugar de EXISTS. Desafortunadamente para un encarte, tanto el método UPSERT como el SI EXISTE usan el mismo número de lecturas en la tabla. Por lo tanto, la verificación de existencia solo debe realizarse cuando haya una razón muy válida para justificar la E / S adicional. La forma optimizada de hacer las cosas es asegurarse de tener la menor cantidad de lecturas posibles en la base de datos.

La mejor estrategia es intentar la actualización. Si no hay filas afectadas por la actualización, inserte. En la mayoría de las circunstancias, la fila ya existirá y solo se requerirá una E / S.

Editar : consulte esta respuesta y la publicación del blog vinculada para conocer los problemas con este patrón y cómo hacer que funcione de manera segura.

binOr
fuente
1
Bueno, creo que al menos respondió una pregunta. Y no agregué código porque el código en la pregunta ya me parecía adecuado. Aunque lo pondría en una transacción, no tomé en cuenta el nivel de aislamiento para la actualización. ¡Gracias por señalar eso en tu respuesta!
binOr
54

Lea la publicación en mi blog para obtener un patrón bueno y seguro que pueda usar. Hay muchas consideraciones y la respuesta aceptada a esta pregunta está lejos de ser segura.

Para una respuesta rápida, pruebe el siguiente patrón. Funcionará bien en SQL 2000 y superior. SQL 2005 le brinda manejo de errores que abre otras opciones y SQL 2008 le brinda un comando MERGE.

begin tran
   update t with (serializable)
   set hitCount = hitCount + 1
   where pk = @id
   if @@rowcount = 0
   begin
      insert t (pk, hitCount)
      values (@id,1)
   end
commit tran
Sam Saffron
fuente
1
En la publicación de su blog, concluye con el uso de la sugerencia WITH (updlock, serializable) en la verificación de existencia. Sin embargo, la lectura de MSDN indica: "UPDLOCK: especifica que los bloqueos de actualización se deben tomar y mantener hasta que se complete la transacción". ¿Significa esto que la sugerencia serializable es superflua ya que el bloqueo de actualización se mantendrá durante el resto de la transacción de todos modos, o he entendido mal algo?
Dan Def
10

Si se va a utilizar con SQL Server 2000/2005, el código original debe incluirse en la transacción para asegurarse de que los datos permanezcan consistentes en un escenario concurrente.

BEGIN TRANSACTION Upsert
update myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
insert into myTable (Col1, Col2) values (@col1, @col2)
COMMIT TRANSACTION Upsert

Esto supondrá un coste de rendimiento adicional, pero garantizará la integridad de los datos.

Agregue, como ya se sugirió, MERGE debe usarse donde esté disponible.

Dima Malenko
fuente
8

MERGE es una de las nuevas características de SQL Server 2008, por cierto.

Jon Galloway
fuente
y absolutamente deberías usarlo en lugar de esta tontería casera difícil de leer. Un buen ejemplo está aquí - mssqltips.com/sqlservertip/1704/…
Rich Bryant
6

No solo necesita ejecutarlo en una transacción, también necesita un alto nivel de aislamiento. De hecho, el nivel de aislamiento predeterminado es Lectura confirmada y este código necesita serializable.

SET transaction isolation level SERIALIZABLE
BEGIN TRANSACTION Upsert
UPDATE myTable set Col1=@col1, Col2=@col2 where ID=@ID
if @@rowcount = 0
  begin
    INSERT into myTable (ID, Col1, Col2) values (@ID @col1, @col2)
  end
COMMIT TRANSACTION Upsert

Tal vez agregar también la verificación de error @@ y la reversión podría ser una buena idea.

Tomás Tintera
fuente
@Munish Goyal Porque en la base de datos múltiples comandos y procedimientos se ejecutan en paralelo. Luego, otro hilo puede insertar una fila justo después de ejecutar la actualización y antes de que se ejecute la inserción.
Tomas Tintera
5

Si no está haciendo una fusión en SQL 2008, debe cambiarlo a:

si @@ rowcount = 0 y @@ error = 0

de lo contrario, si la actualización falla por alguna razón, lo intentará y luego lo insertará porque el recuento de filas en una declaración fallida es 0

Simón Munro
fuente
3

Gran fan de UPSERT, realmente reduce el código para administrar. Aquí hay otra forma en que lo hago: uno de los parámetros de entrada es ID, si el ID es NULL o 0, sabes que es un INSERT, de lo contrario es una actualización. Asume que la aplicación sabe si hay una identificación, por lo que no funcionará en todas las situaciones, pero reducirá las ejecuciones a la mitad si lo hace.

Soda
fuente
2

Publicación de Dima Malenko modificada:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 

BEGIN TRANSACTION UPSERT 

UPDATE MYTABLE 
SET    COL1 = @col1, 
       COL2 = @col2 
WHERE  ID = @ID 

IF @@rowcount = 0 
  BEGIN 
      INSERT INTO MYTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

IF @@Error > 0 
  BEGIN 
      INSERT INTO MYERRORTABLE 
                  (ID, 
                   COL1, 
                   COL2) 
      VALUES      (@ID, 
                   @col1, 
                   @col2) 
  END 

COMMIT TRANSACTION UPSERT 

Puede capturar el error y enviar el registro a una tabla de inserción fallida.
Necesitaba hacer esto porque tomamos los datos que se envían a través de WSDL y, si es posible, los arreglamos internamente.

matones78013
fuente
1

Su lógica parece sólida, pero es posible que desee considerar agregar algún código para evitar la inserción si ha pasado una clave primaria específica.

De lo contrario, si siempre está haciendo una inserción si la actualización no afectó ningún registro, ¿qué sucede cuando alguien elimina el registro antes de que se ejecute "UPSERT"? Ahora, el registro que estaba intentando actualizar no existe, por lo que creará un registro en su lugar. Probablemente ese no sea el comportamiento que estabas buscando.

Kevin Fairchild
fuente