行が存在するかどうかを確認し、存在しない場合は挿入します


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


回答:


158

MERGEコマンドを見てください。あなたは行うことができますUPDATEINSERTDELETE1つの文インチ

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
また、そのMERGEに対してWITH(HOLDLOCK)が必要になる理由も参照してください。
Eugene Ryabtsev 2013年

4
MERGEは2005年以降にサポートされると思います(2008年以降)。
samis

3
WITH(UPDLOCK)を使用しないMERGEは、主キー違反を引き起こす可能性があり、これはこの場合は悪いことです。[MERGEはSQL2008の原子文のですか?]を参照してください(stackoverflow.com/questions/9871644/...
ジェームズ・

156

フライトごとに1つの行を想定していますか?その場合:

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

チケットの最大数が10で、20を予約すると新しい行が挿入されるため、あなたのやり方でフライトをオーバーブッキングできるので、私が言ったことを前提としています。


はい。フライトごとに1行あります。しかし、コードはSELECTを実行しますが、更新する前にフライトが満員かどうかをチェックしません。これを行う方法?

2
競合状態のため、現在のトランザクション分離レベルがSerializableである場合のみ正しいです。
JarekPrzygódzki11年

1
@Martin:答えは目の前の質問に集中していました。OP自身のステートメント「トランザクションによってラップされるこのすべてのステップ」から。トランザクションが正しく実装されている場合、スレッドセーフの問題は問題になりません。
グレゴリーAビーマー、2011

14
@GregoryABeamer-単にBEGIN TRAN ... COMMITデフォルトの分離レベルに固定しても問題は解決しません。OPは、アトミックかつ信頼性が要件であることを明記しました。あなたの答えは、これをどのような形や形でも扱うことができません。
マーティン・スミス

2
(UPDLOCK、HOLDLOCK)がSELECTに追加された場合、これはスレッドセーフになりますIF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id)か?
ジム

67

行の存在をテストするときにupdlock、rowlock、holdlockヒントを渡します。

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

updlockヒントは、行がすでに存在する場合、その行に更新ロックをかけるようにクエリを強制し、コミットまたはロールバックするまで他のトランザクションが行を変更できないようにします。

holdlockヒントは、クエリに範囲ロックを強制し、コミットまたはロールバックするまで、他のトランザクションがフィルター基準に一致する行を追加するのを防ぎます。

行ロックヒントは、ロックの粒度をデフォルトのページレベルではなく行レベルに強制するため、トランザクションは、同じページ内の無関係な行を更新しようとする他のトランザクションをブロックしません(ただし、競合の減少と増加のトレードオフに注意してください)ロックのオーバーヘッド-単一のトランザクションで多数の行レベルのロックを取得することは避けてください)。

詳細については、http://msdn.microsoft.com/en-us/library/ms187373.aspxを参照してください

ロックは、それらを実行するステートメントが実行されるときに取得されることに注意してください。begintranを呼び出しても、何かに到達する前に、別のトランザクションが何かをロックしていることに対する耐性はありません。できるだけ早くトランザクションをコミットすることにより、SQLをできるだけ短い時間ロックを保持するようにSQLを試してみてください(取得は遅く、リリースは早くします)。

SQL Serverの内部ハッシュは64ビット値に対して縮退しているため(異なるキー値が同じロックIDにハッシュする可能性があるため)、PKがbigintである場合、行レベルのロックはあまり効果的ではないことに注意してください。


4
ロックはオーバーブッキングを回避するために非常に重要です。IFステートメントで宣言されたロックがIFステートメントの終わりまで、つまり1つの更新ステートメントまで保持されると想定することは正しいですか?次に、初心者がコードをコピー&貼り付けして、それでも間違いを犯さないように、開始ブロックマーカーを使用して上記のコードを表示するのが賢明な場合があります。
Simon B.

私のPKがvarchar(ただし最大ではない)または3つのVARCHAR列の組み合わせである場合、問題はありますか?
Steam

この回答に関連する質問を-stackoverflow.com/questions/21945850/…で行いました。問題は、このコードを使用して何百万もの行を挿入できることです。
スチーム

このソリューションでは、多くのスレッドが既存の行を頻繁にテストする場合に、ロックのオーバーヘッドがかかりすぎます。これは、existsロックのヒントなしの予防的な追加チェックによる一種のダブルチェックロックで回避できると思います。
Vadzim

38

私は私の解決策を書いています。私のメソッドは「if」または「merge」に対応していません。私の方法は簡単です。

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)SELECT col1、col2 FROM TableName WHERE col1 = @ par1 AND col2 = @ par2それはTableName検索値から選択します

(2)SELECT @ par1、@ par2 WHERE NOT EXISTS(1)サブクエリから存在しない場合は、

(3)TableNameへの挿入(2)ステップ値


1
更新のみではなく、挿入専用です。
Cem

参照-このメソッドが失敗することが存在する原因チェックを挿入する前に行われ、まだ実際に可能だstackoverflow.com/a/3790757/1744834
ローマPekar

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
これはリンク貼り付けのみの回答です...コメントとして適しています。
Ian

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上記の投稿からのコードについて、Cassius Porcusに感謝します。

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

INSERT INTO table(column1、column2、column3)SELECT $ column1、$ column2、$ column3 EXCEPT SELECT column1、column2、column3 from table
Aaron

1
この質問には非常に賛成の回答がたくさんあります。この回答が既存の回答に何を追加するかを詳しく説明していただけますか?
francis

-2

この問題への最善のアプローチは、最初にデータベース列を一意にすることです

ALTER TABLE table_name ADD UNIQUE KEY

THEN INSERT IGNORE INTO table_name 、キーが重複するか、テーブルにすでに存在する場合、値は挿入されません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.