Compruebe si existe una fila, de lo contrario, inserte

237

Necesito escribir un procedimiento almacenado T-SQL que actualice una fila en una tabla. Si la fila no existe, insértela. Todos estos pasos envueltos por una transacción.

Esto es para un sistema de reserva, por lo que debe ser atómico y confiable . Debe devolver verdadero si la transacción fue confirmada y el vuelo reservado.

Soy nuevo en T-SQL y no estoy seguro de cómo usarlo @@rowcount. Esto es lo que he escrito hasta ahora. ¿Estoy en el camino correcto? Estoy seguro de que es un problema fácil para ti.

-- BEGIN TRANSACTION (HOW TO DO?)

UPDATE Bookings
 SET TicketsBooked = TicketsBooked + @TicketsToBook
 WHERE FlightId = @Id AND TicketsMax < (TicketsBooked + @TicketsToBook)

-- Here I need to insert only if the row doesn't exists.
-- If the row exists but the condition TicketsMax is violated, I must not insert 
-- the row and return FALSE

IF @@ROWCOUNT = 0 
BEGIN

 INSERT INTO Bookings ... (omitted)

END

-- END TRANSACTION (HOW TO DO?)

-- Return TRUE (How to do?)
Whymarrh
fuente
1
posible duplicado de soluciones para INSERTAR O ACTUALIZAR en SQL Server
Alex Angas
pregunta relacionada - stackoverflow.com/questions/21889843/…
Steam

Respuestas:

158

Echa un vistazo al comando MERGE . Puedes hacerlo UPDATE, INSERTy DELETEen una sola declaración.

Aquí hay una implementación funcional sobre el uso MERGE
: comprueba si el vuelo está lleno antes de realizar una actualización; de lo contrario, realiza una inserción.

if exists(select 1 from INFORMATION_SCHEMA.TABLES T 
              where T.TABLE_NAME = 'Bookings') 
begin
    drop table Bookings
end
GO

create table Bookings(
  FlightID    int identity(1, 1) primary key,
  TicketsMax    int not null,
  TicketsBooked int not null
)
GO

insert  Bookings(TicketsMax, TicketsBooked) select 1, 0
insert  Bookings(TicketsMax, TicketsBooked) select 2, 2
insert  Bookings(TicketsMax, TicketsBooked) select 3, 1
GO

select * from Bookings

Y entonces ...

declare @FlightID int = 1
declare @TicketsToBook int = 2

--; This should add a new record
merge Bookings as T
using (select @FlightID as FlightID, @TicketsToBook as TicketsToBook) as S
    on  T.FlightID = S.FlightID
      and T.TicketsMax > (T.TicketsBooked + S.TicketsToBook)
  when matched then
    update set T.TicketsBooked = T.TicketsBooked + S.TicketsToBook
  when not matched then
    insert (TicketsMax, TicketsBooked) 
    values(S.TicketsToBook, S.TicketsToBook);

select * from Bookings
dance2die
fuente
66
Además, vea por qué le puede gustar WITH (HOLDLOCK) para ese MERGE.
Eugene Ryabtsev
44
Creo que MERGE es compatible después de 2005 (por lo tanto, 2008+).
samis
3
MERGE sin WITH (UPDLOCK) puede tener violaciones de clave principal, lo que sería malo en este caso. Consulte [¿Es MERGE una declaración atómica en SQL2008?] ( Stackoverflow.com/questions/9871644/… )
James
156

¿Asumo una sola fila para cada vuelo? Si es así:

IF EXISTS (SELECT * FROM Bookings WHERE FLightID = @Id)
BEGIN
    --UPDATE HERE
END
ELSE
BEGIN
   -- INSERT HERE
END

Supongo lo que dije, ya que su forma de hacer las cosas puede sobrecargar un vuelo, ya que insertará una nueva fila cuando haya un máximo de 10 boletos y usted reserve 20.

Gregory A Beamer
fuente
Si. Hay 1 fila por vuelo. Pero su código hace la SELECCIÓN pero no verifica si el vuelo está lleno antes de ACTUALIZAR. ¿Como hacer esto?
2
Debido a las condiciones de carrera, solo es correcto si el nivel de aislamiento de la transacción actual es serializable.
Jarek Przygódzki
1
@ Martin: La respuesta se centró en la pregunta en cuestión. De la propia declaración del OP "Todos estos pasos envueltos por una transacción". Si la transacción se implementa correctamente, el problema de seguridad de subprocesos no debería ser un problema.
Gregory A Beamer
14
@GregoryABeamer: simplemente pegarlo en un BEGIN TRAN ... COMMITnivel de aislamiento por defecto no resolverá el problema. El OP especificó que los requisitos atómicos y confiables eran requisitos. Su respuesta no aborda esto de ninguna forma o forma.
Martin Smith
2
¿Sería seguro para subprocesos si se agrega (UPDLOCK, HOLDLOCK) a SELECT IF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id):?
Jim
67

Pase pistas de updlock, rowlock, holdlock cuando pruebe la existencia de la fila.

begin tran /* default read committed isolation level is fine */

if not exists (select * from Table with (updlock, rowlock, holdlock) where ...)
    /* insert */
else
    /* update */

commit /* locks are released here */

La sugerencia de updlock obliga a la consulta a tomar un bloqueo de actualización en la fila si ya existe, evitando que otras transacciones la modifiquen hasta que se confirme o retroceda.

La sugerencia de bloqueo obliga a la consulta a tomar un bloqueo de rango, evitando que otras transacciones agreguen una fila que coincida con sus criterios de filtro hasta que se confirme o retroceda.

La sugerencia de bloqueo de fila fuerza la granularidad de bloqueo al nivel de fila en lugar del nivel de página predeterminado, por lo que su transacción no bloqueará otras transacciones que intenten actualizar filas no relacionadas en la misma página (pero tenga en cuenta la compensación entre la contención reducida y el aumento en gastos generales de bloqueo: debe evitar tomar grandes cantidades de bloqueos a nivel de fila en una sola transacción).

Consulte http://msdn.microsoft.com/en-us/library/ms187373.aspx para obtener más información.

Tenga en cuenta que los bloqueos se toman a medida que se ejecutan las declaraciones que los toman: invocar begin tran no le da inmunidad contra otra transacción que aprieta los bloqueos en algo antes de llegar a él. Debe intentar factorizar su SQL para mantener los bloqueos durante el menor tiempo posible al confirmar la transacción lo antes posible (adquirir tarde, liberar temprano).

Tenga en cuenta que los bloqueos de nivel de fila pueden ser menos efectivos si su PK es un bigint, ya que el hashing interno en SQL Server se degenera para valores de 64 bits (diferentes valores de clave pueden dividirse en el mismo ID de bloqueo).

Cassius Porcus
fuente
44
El bloqueo es MUY importante para evitar la sobreventa. ¿Es correcto suponer que un bloqueo declarado en la declaración IF se mantiene hasta el final de la declaración IF, es decir, para una declaración de actualización? Entonces, sería aconsejable mostrar el código anterior utilizando los marcadores de inicio y final de bloque para evitar que los novatos copien y peguen su código y sigan equivocándolo.
Simon B.
¿Hay algún problema si mi PK es un varchar (aunque no máximo) o una combinación de tres columnas VARCHAR?
Steam
Hice una pregunta relacionada con esta respuesta en stackoverflow.com/questions/21945850/… La pregunta es si este código se puede usar para insertar millones de filas.
Steam
Esta solución impondría demasiada sobrecarga de bloqueo en los casos en que muchos subprocesos a menudo prueban filas ya existentes. Supongo que esto se puede solucionar con una especie de bloqueo de doble verificación a través de una existsverificación adicional preventiva sin sugerencias de bloqueo.
Vadzim
38

Estoy escribiendo mi solución. mi método no soporta 'if' o 'merge'. Mi método es fácil.

INSERT INTO TableName (col1,col2)
SELECT @par1, @par2
   WHERE NOT EXISTS (SELECT col1,col2 FROM TableName
                     WHERE col1=@par1 AND col2=@par2)

Por ejemplo:

INSERT INTO Members (username)
SELECT 'Cem'
   WHERE NOT EXISTS (SELECT username FROM Members
                     WHERE username='Cem')

Explicación:

(1) SELECCIONE col1, col2 DESDE TableName DONDE col1 = @ par1 Y col2 = @ par2 Selecciona de los valores buscados de TableName

(2) SELECCIONE @ par1, @ par2 DONDE NO EXISTE Se necesita si no existe desde (1) subconsulta

(3) Se inserta en TableName (2) valores de paso

Cem
fuente
1
es solo para insertar, no para actualizar.
Cem
En realidad, todavía es posible que este método falle porque la verificación de existencia se realiza antes de la inserción: consulte stackoverflow.com/a/3790757/1744834
Roman Pekar
3

Finalmente pude insertar una fila, con la condición de que aún no existiera, usando el siguiente modelo:

INSERT INTO table ( column1, column2, column3 )
(
    SELECT $column1, $column2, $column3
      WHERE NOT EXISTS (
        SELECT 1
          FROM table 
          WHERE column1 = $column1
          AND column2 = $column2
          AND column3 = $column3 
    )
)

que encontré en:

http://www.postgresql.org/message-id/[email protected]

Paul G
fuente
1
Esta es una respuesta de enlace de copiar y pegar solo ... más adecuada como comentario.
Ian
2

Esto es algo que recientemente tuve que hacer:

set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[cjso_UpdateCustomerLogin]
    (
      @CustomerID AS INT,
      @UserName AS VARCHAR(25),
      @Password AS BINARY(16)
    )
AS 
    BEGIN
        IF ISNULL((SELECT CustomerID FROM tblOnline_CustomerAccount WHERE CustomerID = @CustomerID), 0) = 0
        BEGIN
            INSERT INTO [tblOnline_CustomerAccount] (
                [CustomerID],
                [UserName],
                [Password],
                [LastLogin]
            ) VALUES ( 
                /* CustomerID - int */ @CustomerID,
                /* UserName - varchar(25) */ @UserName,
                /* Password - binary(16) */ @Password,
                /* LastLogin - datetime */ NULL ) 
        END
        ELSE
        BEGIN
            UPDATE  [tblOnline_CustomerAccount]
            SET     UserName = @UserName,
                    Password = @Password
            WHERE   CustomerID = @CustomerID    
        END

    END
TheTXI
fuente
1

Puede usar la Funcionalidad de fusión para lograr. De lo contrario, puede hacer:

declare @rowCount int

select @rowCount=@@RowCount

if @rowCount=0
begin
--insert....
JoshBerke
fuente
0

La solución completa está debajo (incluida la estructura del cursor). Muchas gracias a Cassius Porcus por el begin trans ... commitcódigo publicado anteriormente.

declare @mystat6 bigint
declare @mystat6p varchar(50)
declare @mystat6b bigint

DECLARE mycur1 CURSOR for

 select result1,picture,bittot from  all_Tempnogos2results11

 OPEN mycur1

 FETCH NEXT FROM mycur1 INTO @mystat6, @mystat6p , @mystat6b

 WHILE @@Fetch_Status = 0
 BEGIN

 begin tran /* default read committed isolation level is fine */

 if not exists (select * from all_Tempnogos2results11_uniq with (updlock, rowlock, holdlock)
                     where all_Tempnogos2results11_uniq.result1 = @mystat6 
                        and all_Tempnogos2results11_uniq.bittot = @mystat6b )
     insert all_Tempnogos2results11_uniq values (@mystat6 , @mystat6p , @mystat6b)

 --else
 --  /* update */

 commit /* locks are released here */

 FETCH NEXT FROM mycur1 INTO @mystat6 , @mystat6p , @mystat6b

 END

 CLOSE mycur1

 DEALLOCATE mycur1
 go
usuario2836818
fuente
0
INSERT INTO [DatabaseName1].dbo.[TableName1] SELECT * FROM [DatabaseName2].dbo.[TableName2]
 WHERE [YourPK] not in (select [YourPK] from [DatabaseName1].dbo.[TableName1])
Almamun
fuente
-2
INSERT INTO table ( column1, column2, column3 )
SELECT $column1, $column2, $column3
EXCEPT SELECT column1, column2, column3
FROM table
Aaron
fuente
INSERTAR EN la tabla (columna1, columna2, columna3) SELECCIONAR $ columna1, $ columna2, $ columna3 EXCEPTO SELECCIONAR columna1, columna2, columna3 de la tabla
Aaron
1
Hay muchas respuestas altamente votadas a esta pregunta. ¿Podría elaborar para explicar lo que esta respuesta agrega a las respuestas existentes?
francis
-2

El mejor enfoque para este problema es hacer que la columna de la base de datos sea ÚNICA

ALTER TABLE table_name ADD UNIQUE KEY

THEN INSERT IGNORE INTO table_name , el valor no se insertará si da como resultado una clave duplicada / ya existe en la tabla.

Maurice Elagu
fuente