Перевірте, чи існує рядок, інакше вставити


237

Мені потрібно написати процедуру, що зберігається в T-SQL, яка оновлює рядок у таблиці. Якщо рядок не існує, вставте його. Всі ці кроки завершені транзакцією.

Це для системи бронювання, тому вона повинна бути атомною та надійною . Він повинен повернути істину, якщо транзакція була здійснена і рейс забронювали.

Я новачок у 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?)


пов'язане питання - 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

6
Крім того, подивіться, чому вам може сподобатися З (HOLDLOCK) для цього ПІДГОТОВКИ.
Євген Рябцев

4
Я думаю, що MERGE підтримується після 2005 року (тому 2008+).
Саміс

3
MERGE без З (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 ряд на рейс. Але ваш код виконайте ВИБІР, але не перевіряйте, чи не завершено рейс до ОНОВЛЕННЯ. Як це зробити?

2
Зважаючи на умови перегонів, це правильно лише в тому випадку, якщо поточний рівень ізоляції транзакцій є серіалізаційним.
Jarek Przygódzki

1
@Martin: Відповідь була зосереджена на розглянутому питанні. З власного твердження ОП "Всі ці кроки, завершені транзакцією". Якщо транзакція виконана правильно, безпечна тема потоку не повинна бути проблемою.
Григорій А Бімер

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 якнайшвидше, щоб утримувати блокування, здійснивши транзакцію якнайшвидше (придбайте пізно, випустіть достроково).

Зауважте, що блокування на рівні рядків може бути менш ефективним, якщо ваш ПК є bigint, оскільки внутрішнє хешування на SQL Server вироджене для 64-бітних значень (різні ключові значення можуть хешувати один і той же ідентифікатор блокування).


4
Блокування дуже важливо, щоб уникнути перекуплення книг. Чи правильно вважати, що блокування, оголошене в операторі IF, зберігається до кінця оператора IF, тобто для одного оператора оновлення? Тоді може бути доцільним показати код вище, використовуючи маркери початкового блоку, щоб запобігти копіюванню та вставленню коду новачкам і все-таки помилитися.
Саймон Б.

Чи є проблема, якщо мій ПК є варшаром (хоча не максимум) або комбінацією трьох стовпців VARCHAR?
Паровий

Я поставив запитання, пов'язане з цією відповіддю, на - stackoverflow.com/questions/21945850/… Питання, чи може цей код використовуватись для вставки мільйонів рядків.
Паровий

Це рішення накладає занадто велику кількість фіксуючих накладних витрат у випадках, коли багато потоків часто перевіряють вже наявні рядки. Я думаю, що це можна вирішити за допомогою подвійного перевіреного блокування за допомогою профілактичної додаткової 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 ВІД Таблиці, де col1 = @ par1 AND col2 = @ par2 Він вибирає із значень пошуку TableName

(2) SELECT @ par1, @ par2, де не існує

(3) Вставляє в таблицю значення таблиціName (2)


1
він лише для вставки, а не для оновлення.
Cem

Насправді все ще можливо, що цей метод не може спричинити перевірку існування, перш ніж вставити - див. 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

1

Ви можете використовувати функцію злиття для досягнення. Інакше можна зробити:

declare @rowCount int

select @rowCount=@@RowCount

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

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

0
INSERT INTO [DatabaseName1].dbo.[TableName1] SELECT * FROM [DatabaseName2].dbo.[TableName2]
 WHERE [YourPK] not in (select [YourPK] from [DatabaseName1].dbo.[TableName1])

-2
INSERT INTO table ( column1, column2, column3 )
SELECT $column1, $column2, $column3
EXCEPT SELECT column1, column2, column3
FROM table

ВСТАВИТИ таблицю INTO (колонка1, стовпчик2, стовпець3) ВИБІР $ столб1, $ колонка2, $ стовпчик3 ВИКЛЮЧИТИ ВИБІР стовпця1, стовпця2, стовпця3 з таблиці
Аарон

1
На це питання дуже багато висококваліфікованих відповідей. Не могли б ви пояснити, що ця відповідь додасть до існуючих відповідей?
Францис

-2

Найкращий підхід до цієї проблеми - це спочатку зробити стовпчик бази даних Унікальним

ALTER TABLE table_name ADD UNIQUE KEY

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.