ตรวจสอบว่ามีแถวอยู่หรือไม่


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

1
อาจเป็นไปได้ซ้ำกันของโซลูชันสำหรับ INSERT หรือ UPDATE บน SQL Server
Alex Angas

คำถามที่เกี่ยวข้อง - 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)สำหรับการรวมเข้าด้วยกัน
Eugene Ryabtsev

4
ฉันคิดว่า MERGE ได้รับการสนับสนุนหลังจากปี 2005 (เช่นปี 2008+)
samis

3
การรวมกันโดยไม่มี (UPDLOCK) อาจมีการละเมิดคีย์หลักซึ่งจะไม่ดีในกรณีนี้ ดู [รวมเป็นคำแถลงอะตอมมิกใน SQL2008 หรือไม่] ( stackoverflow.com/questions/9871644/… )
James

156

ฉันถือว่าแถวเดียวสำหรับแต่ละเที่ยวบินหรือไม่ ถ้าเป็นเช่นนั้น:

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

ฉันถือว่าสิ่งที่ฉันพูดเนื่องจากวิธีการทำสิ่งต่างๆของคุณสามารถจองตั๋วเครื่องบินได้มากเกินไปเพราะจะเป็นการแทรกแถวใหม่เมื่อมีตั๋วสูงสุด 10 ใบและคุณจอง 20


ใช่. มี 1 แถวต่อเที่ยวบิน แต่รหัสของคุณทำการเลือก แต่ไม่ได้ตรวจสอบว่าเที่ยวบินนั้นเต็มก่อนถึง UPDATE หรือไม่ ทำอย่างไร

2
เนื่องจากสภาพการแข่งขันจะถูกต้องเฉพาะในกรณีที่ระดับการแยกรายการปัจจุบันเป็น Serializable
Jarek Przygódzki

1
@ มาร์ติน: คำตอบคือมุ่งเน้นไปที่คำถามที่อยู่ในมือ จากคำสั่งของ OP "ทุกขั้นตอนนี้ห่อด้วยธุรกรรม" ถ้าธุรกรรมถูกนำไปใช้อย่างถูกต้องปัญหาเธรดที่ปลอดภัยไม่ควรเป็นปัญหา
Gregory A Beamer

14
@GregoryABeamer - เพียงติดไว้ในBEGIN TRAN ... COMMITระดับการแยกเริ่มต้นจะไม่สามารถแก้ไขปัญหาได้ สหกรณ์ระบุว่าอะตอมและมีความน่าเชื่อถือมีความต้องการ คำตอบของคุณไม่สามารถระบุได้ในรูปแบบหรือรูปแบบใด ๆ
Martin Smith

2
นี้จะเป็นด้ายปลอดภัยถ้า (UPDLOCK, HOLDLOCK) เพิ่มที่เลือก: IF EXISTS (SELECT * FROM Bookings (UPDLOCK, HOLDLOCK) WHERE FLightID = @Id)?
Jim

67

ผ่าน updlock, rowlock, holdlock hint เมื่อทดสอบการมีอยู่ของแถว

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 บังคับให้แบบสอบถามทำการล็อคการอัปเดตในแถวหากมีอยู่แล้วทำให้ธุรกรรมอื่นไม่สามารถแก้ไขได้จนกว่าคุณจะยอมรับหรือย้อนกลับ

คำแนะนำของโฮลด์ล็อกจะบังคับให้คิวรีล็อกช่วงเพื่อป้องกันไม่ให้ธุรกรรมอื่น ๆ เพิ่มแถวที่ตรงกับเกณฑ์การกรองของคุณจนกว่าคุณจะยอมรับหรือย้อนกลับ

คำใบ้ rowlock จะบังคับให้ล็อคระดับเป็นระดับแทนที่จะเป็นระดับหน้าเริ่มต้นดังนั้นการทำธุรกรรมของคุณจะไม่ปิดกั้นการทำธุรกรรมอื่น ๆ ที่พยายามปรับปรุงแถวที่ไม่เกี่ยวข้องในหน้าเดียวกัน ล็อคค่าใช้จ่าย - คุณควรหลีกเลี่ยงการล็อกระดับแถวจำนวนมากในการทำธุรกรรมเดียว)

ดูhttp://msdn.microsoft.com/en-us/library/ms187373.aspxสำหรับข้อมูลเพิ่มเติม

โปรดทราบว่าการล็อกถูกนำมาใช้เป็นคำสั่งที่จะถูกดำเนินการการเรียกใช้ tran เริ่มต้นไม่ได้ให้อิสระกับคุณในการทำธุรกรรมการบีบการล็อกในบางสิ่งบางอย่างก่อนที่คุณจะไปถึง คุณควรลองใช้ตัวประกอบ SQL ของคุณเพื่อล็อคตัวเองในเวลาที่สั้นที่สุดเท่าที่จะเป็นไปได้โดยการทำธุรกรรมโดยเร็วที่สุดเท่าที่จะเป็นไปได้

โปรดทราบว่าการล็อกระดับแถวอาจมีประสิทธิภาพน้อยลงหาก PK ของคุณเป็นเรื่องใหญ่เนื่องจากการแฮ็ชภายในบน SQL Server นั้นเสื่อมลงสำหรับค่า 64- บิต (ค่าคีย์ที่แตกต่างกันอาจแฮชกับรหัสล็อคเดียวกัน)


4
การล็อคเป็นสิ่งสำคัญมากเพื่อหลีกเลี่ยงการจองมากกว่าจำนวนที่มี มันถูกต้องหรือไม่ที่จะถือว่าการล็อกที่ประกาศในคำสั่ง IF นั้นถูกเก็บไว้จนกว่าจะสิ้นสุดคำสั่ง IF นั่นคือสำหรับคำสั่งอัพเดตหนึ่งคำหรือไม่? จากนั้นก็ควรที่จะแสดงรหัสด้านบนโดยใช้เครื่องหมายเริ่มต้นบล็อกปิดกั้นเพื่อป้องกันมือใหม่จากการคัดลอกและวางรหัสของคุณและยังคงทำให้มันผิด
Simon B.

มีปัญหาหรือไม่หาก PK ของฉันเป็น varchar (ไม่ใช่สูงสุด) หรือการรวมกันของสามคอลัมน์ VARCHAR?
Steam

ฉันสร้างคำถามเกี่ยวกับคำตอบนี้ที่ - stackoverflow.com/questions/21945850/…คำถามนี้สามารถใช้รหัสนี้เพื่อแทรกแถวนับล้านได้หรือไม่
Steam

วิธีการแก้ปัญหานี้จะกำหนดค่าใช้จ่ายการล็อคมากเกินไปในกรณีที่หลายกระทู้มักจะทดสอบแถวที่มีอยู่แล้ว ฉันคิดว่านี่สามารถใช้งานได้กับการตรวจสอบการล็อคสองครั้งผ่านการexistsตรวจสอบเพิ่มเติมเชิงป้องกันโดยไม่ต้องใส่คำแนะนำ
Vadzim

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) SELECT col1, col2 จาก TableName WHERE col1 = @ par1 AND col2 = @ par2 มันเลือกจาก TableName ค่าการค้นหา

(2) SELECT @ par1, @ par2 หากไม่มีอยู่จะใช้เวลาหากไม่มีอยู่จากแบบสอบถามย่อย (1)

(3) แทรกลงใน TableName (2) ค่าขั้นตอน


1
มันเป็นเพียงการแทรกไม่อัปเดต
Cem

จริงๆแล้วมันยังคงเป็นไปได้สำหรับวิธีนี้ที่จะล้มเหลวทำให้เกิดการตรวจสอบการมีอยู่ก่อนที่จะแทรก - ดูstackoverflow.com/a/3790757/1744834
Roman 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

โซลูชันทั้งหมดอยู่ด้านล่าง (รวมถึงโครงสร้างเคอร์เซอร์) ขอบคุณ Cassius Porcus สำหรับ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

INSERT INTO ตาราง (คอลัมน์ 1, คอลัมน์ 2, คอลัมน์ 3) เลือก $ Column1, $ Column2, $ Column3 ยกเว้นเลือกคอลัมน์ 1, คอลัมน์ 2, คอลัมน์ 3 จากตาราง
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.