Проверьте, существует ли строка, в противном случае вставьте

237

Мне нужно написать хранимую процедуру T-SQL, которая обновляет строку в таблице. Если строка не существует, вставьте ее. Все эти шаги обернуты транзакцией.

Это для системы бронирования, поэтому она должна быть атомарной и надежной . Он должен вернуть значение true, если транзакция была совершена и рейс забронирован.

Я новичок в T-SQL , и не уверен, как его использовать @@rowcount. Это то, что я написал до сих пор. Я на правильном пути? Я уверен, что это простая проблема для вас.

-- 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
источник
1
возможный дубликат Решений для ВСТАВКИ ИЛИ ОБНОВЛЕНИЯ на SQL Server
Алекс Ангас
связанный вопрос - stackoverflow.com/questions/21889843/…
Steam

Ответы:

158

Посмотрите на команду MERGE . Вы можете сделать UPDATE, INSERTи DELETEв одном заявлении.

Вот рабочая реализация по использованию MERGE
- она ​​проверяет, заполнен ли полет перед обновлением, иначе делает вставку.

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

А потом ...

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
источник
6
Также, посмотрите, почему вам может понравиться WITH (HOLDLOCK) для этого MERGE.
Евгений Рябцев
4
Я думаю, что MERGE поддерживается после 2005 года (поэтому 2008+).
Самис
3
MERGE без WITH (UPDLOCK) может иметь нарушения первичного ключа, что в данном случае будет плохо. См. [Является ли MERGE атомарным оператором в SQL2008?] ( Stackoverflow.com/questions/9871644/… )
Джеймс
156

Я предполагаю одну строку для каждого полета? Если так:

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

Я предполагаю, что я сказал, так как ваш способ действий может перебить рейс, так как он вставит новый ряд, когда максимум 10 билетов, а вы бронируете 20.

Грегори Бимер
источник
Да. Есть 1 ряд на рейс. Но ваш код делает SELECT, но не проверяет, полон полон, прежде чем ОБНОВИТЬ. Как это сделать?
2
Из-за условий гонки корректно, только если текущий уровень изоляции транзакции является Сериализуемым.
Ярек Пшигодзки
1
@ Мартин: Ответ был сосредоточен на вопросе под рукой. Из собственного заявления ОП «Все эти шаги обернуты транзакцией». Если транзакция реализована правильно, проблема с безопасностью потока не должна быть проблемой.
Григорий Бимер
14
@GregoryABeamer - простое закрепление на BEGIN TRAN ... COMMITуровне изоляции по умолчанию не решит проблему. ОП уточнил, что атомные и надежные требования. Ваш ответ не может решить эту проблему в любой форме или форме.
Мартин Смит
2
Будет ли это поточно-если (UPDLOCK, HOLDLOCK) был добавлен к SELECT: IF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id)?
Джим
67

Пропускать намеки на блокировку строк, блокировку строк и блокировку при тестировании на наличие строки.

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 */

Подсказка при обновлении заставляет запрос принять блокировку обновления для строки, если она уже существует, не позволяя другим транзакциям изменять ее, пока вы не подтвердите или не откатитесь.

Подсказка удержания заставляет запрос принять блокировку диапазона, не позволяя другим транзакциям добавлять строку, соответствующую критериям вашего фильтра, пока вы не подтвердите или не откатитесь.

Подсказка о блокировке строки вынуждает детализацию блокировки на уровень строки вместо уровня страницы по умолчанию, поэтому ваша транзакция не будет блокировать другие транзакции, пытающиеся обновить несвязанные строки на той же странице (но помните о компромиссе между уменьшением конкуренции и увеличением накладные расходы на блокировку - вы должны избегать принятия большого количества блокировок на уровне строк в одной транзакции).

См. Http://msdn.microsoft.com/en-us/library/ms187373.aspx для получения дополнительной информации.

Обратите внимание, что блокировки воспринимаются как выполняемые операторы - вызов метода begin tran не дает вам иммунитета против другой транзакции, накладывающей блокировки на что-либо до того, как вы доберетесь до нее. Вы должны попытаться использовать свой SQL-код для удержания блокировок в кратчайшие сроки, совершая транзакцию как можно скорее (запоздайте поздно, выпустите раньше).

Обратите внимание, что блокировки на уровне строк могут быть менее эффективными, если ваш PK является bigint, так как внутреннее хеширование в SQL Server вырождено для 64-битных значений (разные значения ключа могут хешироваться с одним и тем же идентификатором блокировки).

Кассий Поркус
источник
4
Блокировка очень важна, чтобы избежать перебронирования. Правильно ли предположить, что блокировка, объявленная в операторе IF, сохраняется до конца инструкции IF, т.е. для одного оператора обновления? Тогда, возможно, было бы целесообразно показать приведенный выше код, используя маркеры начала и конца блока, чтобы новички не копировали и не вставляли ваш код и все еще получали его неправильно.
Саймон Б.
Есть ли проблема, если мой PK - это varchar (но не max) или комбинация из трех столбцов VARCHAR?
Steam
Я задал вопрос, связанный с этим ответом, по адресу - stackoverflow.com/questions/21945850/… Вопрос в том, может ли этот код использоваться для вставки миллионов строк.
Steam
Это решение будет накладывать слишком много накладных расходов на блокировку в случаях, когда многие потоки часто проверяют уже существующие строки. Я предполагаю, что это можно обойти с помощью своего рода двойной проверки блокировки с помощью превентивной дополнительной existsпроверки без подсказок блокировки.
Вадим
38

Я пишу свое решение. мой метод не стоит «если» или «слить». Мой метод прост.

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

Например:

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

Объяснение:

(1) ВЫБЕРИТЕ col1, col2 ОТ ИМЕНИ TableName, ГДЕ col1 = @ par1 И col2 = @ par2 Выбирает из найденных значений TableName

(2) SELECT @ par1, @ par2, ГДЕ НЕ СУЩЕСТВУЕТ. Требуется, если не существует, из (1) подзапроса.

(3) Вставляет в TableName (2) значения шага

Cem
источник
1
это только для вставки, а не обновления.
Сем
Фактически все еще возможно, что этот метод потерпит неудачу, потому что проверка на наличие выполняется перед вставкой - см. Stackoverflow.com/a/3790757/1744834
Роман Пекар
3

Я наконец смог вставить строку при условии, что она еще не существует, используя следующую модель:

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 
    )
)

который я нашел в:

http://www.postgresql.org/message-id/87hdow4ld1.fsf@stark.xeocode.com

Пол Г
источник
1
Это только ответ с копией-вставкой ... лучше подходит для комментариев.
Ян
2

Это то, что я только недавно должен был сделать:

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
источник
1

Вы можете использовать функцию слияния для достижения. В противном случае вы можете сделать:

declare @rowCount int

select @rowCount=@@RowCount

if @rowCount=0
begin
--insert....
JoshBerke
источник
0

Полное решение приведено ниже (включая структуру курсора). Большое спасибо Кассиусу Поркусу за begin trans ... commitкод, опубликованный выше.

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
user2836818
источник
0
INSERT INTO [DatabaseName1].dbo.[TableName1] SELECT * FROM [DatabaseName2].dbo.[TableName2]
 WHERE [YourPK] not in (select [YourPK] from [DatabaseName1].dbo.[TableName1])
Almamun
источник
-2
INSERT INTO table ( column1, column2, column3 )
SELECT $column1, $column2, $column3
EXCEPT SELECT column1, column2, column3
FROM table
Аарон
источник
INSERT INTO таблица (column1, column2, column3) SELECT $ column1, $ column2, $ column3 ИСКЛЮЧИТЬ SELECT column1, column2, column3 из таблицы
Аарон,
1
На этот вопрос очень много ответов. Не могли бы вы объяснить, что этот ответ добавить к существующим ответам?
Фрэнсис
-2

Лучший подход к этой проблеме - сначала сделать столбец базы данных УНИКАЛЬНЫМ.

ALTER TABLE table_name ADD UNIQUE KEY

THEN INSERT IGNORE INTO table_name значение не будет вставлено, если оно приведет к дублированию ключа / уже существует в таблице.

Морис Элагу
источник