2013-09-30 48 views
6

瀏覽時,我發現following問題/討論關於插入不存在的記錄的「最佳」方法。其中一個令我感到震驚的是[Remus Rusanu]之一,陳述如下:可以INSERT <table>(x)VALUES(@x)WHERE NOT EXISTS(SELECT * FROM <table> WHERE x = @x)會導致重複嗎?

兩種變體都不正確。您將插入一對重複的@ value1,@ value2,保證。

儘管我對INSERT(並且沒有顯式鎖定/事務管理存在)的檢查是'分離'的語法表示同意。我有一個很難理解爲什麼以及何時這將是其他建議語法看起來像這樣

INSERT INTO mytable (x) 
SELECT @x WHERE NOT EXISTS (SELECT * FROM mytable WHERE x = @x); 

真的,我不想啓動(別人)什麼是最好的/最快的討論,我也不認爲語法可以'替換'一個唯一的索引/約束(或PK),但我真的需要知道在什麼情況下這個構造可能會導致雙打,因爲我過去一直使用這種語法,並想知道繼續這樣做是不是安全的在將來。

我認爲發生的是INSERT & SELECT都在同一個(隱式)事務中。查詢將對相關記錄(鍵)執行一次IX鎖定,並且在整個查詢完成之前不會釋放它,因此只有在記錄插入後。 這個鎖阻止所有其他連接進行相同的INSERT,因爲它們在插入完成後才能自己獲取鎖;只有這樣他們才能獲得鎖定,並且如果記錄已經存在或不存在,將自行開始驗證。

至於恕我直言,找出最好的方法是通過測試,我一直在運行下面的代碼在我的筆記本電腦,同時:

創建見下表

CREATE TABLE t_test (x int NOT NULL PRIMARY KEY (x)) 

運行在很多很多在並行連接)

SET NOCOUNT ON 

WHILE 1 = 1 
    BEGIN 
     INSERT t_test (x) 
     SELECT x = DatePart(ms, CURRENT_TIMESTAMP) 
     WHERE NOT EXISTS (SELECT * 
           FROM t_test old 
          WHERE old.x = DatePart(ms, CURRENT_TIMESTAMP)) 
    END 

到目前爲止,唯一需要注意的事情是:

  • 沒有遇到的錯誤(還)
  • CPU正在追趕着那個相當火爆=)
  • 表中保存300條記錄很快(由於3ms的日期時間的「精確」)沒有實際刀片發生任何更多的,符合市場預期。

UPDATE:

原來我上面的例子是沒有做什麼,我打算做的事。我不是試圖同時插入相同的記錄,而是多次連接,而不是 - 在第一秒之後插入已經存在的記錄。由於它可能需要大約一秒鐘的時間來複制粘貼&在下一個連接上執行查詢,因此永遠不會有重複的危險。我會在剩下的時間裏戴上我的驢耳朵......

無論如何,我已經(使用同一個表)

SET NOCOUNT ON 

DECLARE @midnight datetime 
SELECT @midnight = Convert(datetime, Convert(varchar, CURRENT_TIMESTAMP, 106), 106) 

WHILE 1 = 1 
    BEGIN 
     INSERT t_test (x) 
     SELECT x = DateDiff(ms, @midnight, CURRENT_TIMESTAMP) 
     WHERE NOT EXISTS (SELECT * 
           FROM t_test old 
          WHERE old.x = DateDiff(ms, @midnight, CURRENT_TIMESTAMP)) 
    END 

看哪看哪&適應測試更在手頭的事情線,輸出窗口現在持有充足沿着錯誤的的

線消息2627,級別14,狀態1,行8 違反PRIMARY KEY約束 'PK__t_test__3BD019E521C3B7EE' 的。無法在對象'dbo.t_test'中插入>重複鍵。重複鍵值是(57581873)。

FYI:由於Andomar指出,加入HOLDLOCK和/或SERIALIZABLE提示確實「解決了」問題,但隨後被證明是導致大量的死鎖......這是不是很大,但沒有任何意外當我想通了。

想我有相當多的代碼審查的做...

+1

您剛剛在SO中創建了SQL注入,並帶有標題 –

+1

Heheh,等到'我的兒子'報名參加吧! (http://xkcd.com/327/) – deroby

+1

爲了避免死鎖,您可以將'UPDLOCK'添加到混音中。這將序列化訪問'x'上的範圍鎖。 –

回答

4

感謝您發佈單獨的問題。你有幾個誤區:

詢問,直到整個查詢已完成

的INSERT將鎖定行插入,X將在相關記錄的IX鎖(鑰匙),而不是釋放鎖(像IX這樣的意向鎖只能在鎖層級上的父實體上請求,從不在記錄上)。此鎖必須保持到事務提交(嚴格two-phase locking要求X鎖總是隻在事務結束時釋放)。

請注意,由INSERT獲取的鎖不會阻止更多的插入,即使是相同的密鑰。防止重複的唯一方法是唯一索引,強制唯一性的機制不是基於鎖定的。是的,在主鍵上,由於其獨特性,即使鎖定確實起作用,重複也將被阻止,但遊戲中的力量不同。

在您的示例中,將發生的操作將會進行序列化,因爲INSERT上的SELECT塊由於X對S鎖的鎖定而在新插入的行上發生衝突。另一個人認爲要考慮的是INT類型的300條記錄適合於單個頁面,並且會啓動大量優化(例如,使用掃描而不是多次搜索),並且會改變測試結果。請記住,有許多肯定和沒有證據的假設仍然只是一個猜想...

要測試問題,您需要確保INSERT不會阻止併發SELECT。在RCSI下運行或在快照隔離下運行是實現此目的的一種方式(並且可能在生產中非自願地「實現」它並打破做出以上所有假設的應用程序......)WHERE子句是另一種方式。一個顯着的大表和二級索引是另一種方式。

因此,這裏是我測試過它:

set nocount on; 
go 

drop database test; 
go 

create database test; 
go 

use test; 
go 

create table test (id int primary key, filler char(200)); 
go 

-- seed 10000 values, fill some pages 
declare @i int = 0; 
begin transaction 
while @i < 10000 
begin 
    insert into test (id) values (@i); 
    set @i += 1; 
end 
commit; 

現在從幾個並聯運行這個(我用3):

use test; 
go 

set nocount on; 
go 

declare @i int; 
while (1=1) 
begin 
    -- This is not cheating. This ensures that many concurrent SELECT attempt 
    -- to insert the same values, and all of them believe the values are 'free' 
    select @i = max(id) from test with (readpast); 
    insert into test (id) 
    select id 
     from (values (@i), (@i+1), (@i+2), (@i+3), (@i+4), (@i+5)) as t(id) 
     where t.id not in (select id from test); 
end 

下面是一些結果:

Msg 2627, Level 14, State 1, Line 6 
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130076). 
The statement has been terminated. 
Msg 2627, Level 14, State 1, Line 6 
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130096). 
The statement has been terminated. 
Msg 2627, Level 14, State 1, Line 6 
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130106). 
The statement has been terminated. 
Msg 2627, Level 14, State 1, Line 6 
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130121). 
The statement has been terminated. 
Msg 2627, Level 14, State 1, Line 6 
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130141). 
The statement has been terminated. 
Msg 2627, Level 14, State 1, Line 6 
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130151). 
The statement has been terminated. 
Msg 2627, Level 14, State 1, Line 6 
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130176). 
The statement has been terminated. 
Msg 2627, Level 14, State 1, Line 6 

即使鎖定,沒有快照隔離,也沒有RCSI。當每個SELECT嘗試插入@ i + 1 ... @ i + 5時,他們都會發現這些值不存在,然後它們將全部進入INSERT。一個幸運的贏家將會成功,其餘的將會導致PK違規。經常。我故意使用了@i=MAX(id)來大幅增加衝突的追逐,但這不是必需的。我會留下一些問題,弄清楚爲什麼所有違反值都會以值%5 + 1作爲練習發生。

+0

「請注意,由INSERT獲取的鎖不會阻止更多的插入,即使是相同的密鑰」我同意非唯一索引。但是用X範圍鎖定(通常是'XLOCK,ROWLOCK,HOLDLOCK')不能解決這個問題嗎? – usr

+0

@Usr:是的,但是OP(以及開始討論的原始問題)假設它甚至可以在沒有額外提示的情況下工作。我的觀點是證明它沒有。 –

+0

@Remus:謝謝你的精心解答。事實證明,我在很多很多年以前一直是這樣做的錯誤;-(我知道我很久以前就測試了這個,但是可能與我剛剛做的一樣,發生了(同樣的)錯誤,很高興我發現了似乎我們很幸運,因爲這些年來我們從未遇到重複/ PK違規;可能是因爲我們的軟件比OLTP應用程序擁有更多的DWH。 – deroby

3

你是從一個單一的連接測試,這樣你就不會測試併發的。從不同的窗口運行腳本兩次,你會看到衝突。

有針對衝突多發的原因:

  • 默認情況下,鎖不被佔用,直到(隱含的)交易結束。使用with (holdlock)查詢提示更改此行爲。
  • 您的查詢的併發問題稱爲「幻像讀取」。默認的事務隔離級別是「讀取提交」,它不能防止幻像讀取。使用with (serializable)查詢提示來增加隔離級別。 (儘量避免set transaction isolation level命令,因爲isolation level is not cleared當連接返回到連接池。)

主鍵約束總是被強制執行。因此,您的查詢將嘗試插入重複的行,並通過拋出重複的鍵錯誤而失敗。

一個好的方法是用你的查詢(這將工作的99%的時間),並在客戶端處理偶爾重複鍵異常以優雅的方式。

維基百科有很棒的explanation of isolation levels

+0

實際上,正如代碼中的註釋所示:(創建表一次,在許多並行的多個連接上運行) 不知道我已同時運行多少個連接,但已接近20個。 – deroby

+0

不確定如何衝突尋求您的查詢。通過2個窗口在while循環中插入'max(id)+ 1',很容易重現。 – Andomar

+0

顯然它不是;我有大約20個連接並行運行了大約半個小時。他們都沒有提出PK違規。 – deroby

相關問題