Là một MERGE với OUTPUT thực hành tốt hơn so với CHỌN và CHỌN có điều kiện?


12

Chúng ta thường gặp phải tình huống "Nếu không tồn tại, chèn". Blog của Dan Guzman có một cuộc điều tra tuyệt vời về cách làm cho quy trình này an toàn.

Tôi có một bảng cơ bản chỉ đơn giản là lập danh mục một chuỗi thành một số nguyên từ a SEQUENCE. Trong một thủ tục được lưu trữ, tôi cần lấy khóa số nguyên cho giá trị nếu nó tồn tại hoặc INSERTsau đó lấy giá trị kết quả. Có một ràng buộc duy nhất trên dbo.NameLookup.ItemNamecột để tính toàn vẹn dữ liệu không gặp rủi ro nhưng tôi không muốn gặp phải các ngoại lệ.

Đó không phải là một thứ IDENTITYtôi không thể có được SCOPE_IDENTITYvà giá trị có thể làNULL trong một số trường hợp nhất định.

Trong tình huống của tôi, tôi chỉ phải đối phó với INSERTsự an toàn trên bàn vì vậy tôi đang cố gắng quyết định xem có nên sử dụng MERGEnhư thế này hay không:

SET NOCOUNT, XACT_ABORT ON;

DECLARE @vValueId INT 
DECLARE @inserted AS TABLE (Id INT NOT NULL)

MERGE 
    dbo.NameLookup WITH (HOLDLOCK) AS f 
USING 
    (SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
        ON f.ItemName= new_item.val
WHEN MATCHED THEN
    UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
    INSERT
      (ItemName)
    VALUES
      (@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s

Tôi có thể thực hiện điều này bằng cách sử dụng MERGEchỉ với một điều kiện INSERTtheo sau bởi SELECT tôi nghĩ rằng cách tiếp cận thứ hai này rõ ràng hơn với người đọc, nhưng tôi không tin rằng đó là cách thực hành "tốt hơn"

SET NOCOUNT, XACT_ABORT ON;

INSERT INTO 
    dbo.NameLookup (ItemName)
SELECT
    @vName
WHERE
    NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)

DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName

Hoặc có lẽ có một cách khác tốt hơn mà tôi chưa xem xét

Tôi đã tìm kiếm và tham khảo các câu hỏi khác. Cái này: /programming/5288283/sql-server-insert-if-not-exists-best-practice là cách thích hợp nhất tôi có thể tìm thấy nhưng dường như không thể áp dụng cho trường hợp sử dụng của tôi. Các câu hỏi khác cho IF NOT EXISTS() THENcách tiếp cận mà tôi không nghĩ là chấp nhận được.


Bạn đã thử trải nghiệm với các bảng lớn hơn bộ đệm của mình chưa, tôi đã có kinh nghiệm khi hiệu suất hợp nhất giảm xuống khi bảng đạt đến một kích thước nhất định.
bình tĩnh

Câu trả lời:


8

Vì bạn đang sử dụng Chuỗi, bạn có thể sử dụng cùng chức năng GIÁ TRỊ TIẾP THEO - mà bạn đã có trong Ràng buộc mặc định trên Idtrường Khóa chính - để tạo Idgiá trị mới trước thời hạn. Tạo giá trị trước tiên có nghĩa là bạn không cần phải lo lắng về việc không có SCOPE_IDENTITY, điều đó có nghĩa là bạn không cần OUTPUTđiều khoản hoặc thực hiện bổ sung SELECTđể có được giá trị mới; bạn sẽ có giá trị trước khi bạn thực hiện INSERTvà thậm chí bạn không cần phải gây rối với SET IDENTITY INSERT ON / OFF:-)

Vì vậy, mà chăm sóc một phần của tình hình tổng thể. Phần khác đang xử lý vấn đề đồng thời của hai quy trình, cùng một lúc, không tìm thấy một hàng hiện có cho cùng một chuỗi chính xác và tiến hành INSERT. Mối quan tâm là về việc tránh vi phạm ràng buộc duy nhất sẽ xảy ra.

Một cách để xử lý các loại vấn đề tương tranh này là buộc hoạt động cụ thể này phải là một luồng. Cách để làm điều đó là bằng cách sử dụng các khóa ứng dụng (hoạt động qua các phiên). Mặc dù hiệu quả, chúng có thể hơi nặng tay đối với một tình huống như thế này khi tần suất va chạm có lẽ khá thấp.

Cách khác để đối phó với các vụ va chạm là chấp nhận rằng đôi khi chúng sẽ xảy ra và xử lý chúng thay vì cố gắng tránh chúng. Sử dụng TRY...CATCHcấu trúc, bạn có thể bẫy một lỗi cụ thể một cách hiệu quả (trong trường hợp này: "vi phạm ràng buộc duy nhất", Msg 2601) và thực hiện lại SELECTđể nhận Idgiá trị vì chúng ta biết rằng nó tồn tại do nằm trong CATCHkhối với cụ thể đó lỗi. Các lỗi khác có thể được xử lý theo cách điển hình RAISERROR/ RETURNhoặc THROW.

Thiết lập thử nghiệm: Trình tự, bảng và chỉ mục duy nhất

USE [tempdb];

CREATE SEQUENCE dbo.MagicNumber
  AS INT
  START WITH 1
  INCREMENT BY 1;

CREATE TABLE dbo.NameLookup
(
  [Id] INT NOT NULL
         CONSTRAINT [PK_NameLookup] PRIMARY KEY CLUSTERED
        CONSTRAINT [DF_NameLookup_Id] DEFAULT (NEXT VALUE FOR dbo.MagicNumber),
  [ItemName] NVARCHAR(50) NOT NULL         
);

CREATE UNIQUE NONCLUSTERED INDEX [UIX_NameLookup_ItemName]
  ON dbo.NameLookup ([ItemName]);
GO

Kiểm tra thiết lập: Thủ tục lưu trữ

CREATE PROCEDURE dbo.GetOrInsertName
(
  @SomeName NVARCHAR(50),
  @ID INT OUTPUT,
  @TestRaceCondition BIT = 0
)
AS
SET NOCOUNT ON;

BEGIN TRY
  SELECT @ID = nl.[Id]
  FROM   dbo.NameLookup nl
  WHERE  nl.[ItemName] = @SomeName
  AND    @TestRaceCondition = 0;

  IF (@ID IS NULL)
  BEGIN
    SET @ID = NEXT VALUE FOR dbo.MagicNumber;

    INSERT INTO dbo.NameLookup ([Id], [ItemName])
    VALUES (@ID, @SomeName);
  END;
END TRY
BEGIN CATCH
  IF (ERROR_NUMBER() = 2601) -- "Cannot insert duplicate key row in object"
  BEGIN
    SELECT @ID = nl.[Id]
    FROM   dbo.NameLookup nl
    WHERE  nl.[ItemName] = @SomeName;
  END;
  ELSE
  BEGIN
    ;THROW; -- SQL Server 2012 or newer
    /*
    DECLARE @ErrorNumber INT = ERROR_NUMBER(),
            @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();

    RAISERROR(N'Msg %d: %s', 16, 1, @ErrorNumber, @ErrorMessage);
    RETURN;
    */
  END;

END CATCH;
GO

Các bài kiểm tra

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT;
SELECT @ItemID AS [ItemID];
GO

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT,
  @TestRaceCondition = 1;
SELECT @ItemID AS [ItemID];
GO

Câu hỏi từ OP

Tại sao điều này tốt hơn MERGE? Tôi sẽ không có được chức năng tương tự mà không cần TRYsử dụng WHERE NOT EXISTSmệnh đề?

MERGEcó nhiều "vấn đề" khác nhau (một số tài liệu tham khảo được liên kết trong câu trả lời của @ SqlZim vì vậy không cần phải sao chép thông tin đó ở đây). Và, không có khóa bổ sung trong phương pháp này (ít tranh chấp hơn), vì vậy nó sẽ tốt hơn về đồng thời. Theo cách tiếp cận này, bạn sẽ không bao giờ bị vi phạm ràng buộc duy nhất, tất cả mà không có HOLDLOCK, v.v. Nó được đảm bảo khá nhiều để hoạt động.

Lý do đằng sau phương pháp này là:

  1. Nếu bạn có đủ thực thi quy trình này để bạn cần lo lắng về va chạm, thì bạn không muốn:
    1. thực hiện bất kỳ bước nào nhiều hơn cần thiết
    2. giữ khóa trên bất kỳ tài nguyên nào lâu hơn cần thiết
  2. Vì các va chạm chỉ có thể xảy ra khi các mục mới (các mục mới được gửi cùng lúc ), tần suất rơi vào CATCHkhối ở vị trí đầu tiên sẽ khá thấp. Sẽ hợp lý hơn khi tối ưu hóa mã sẽ chạy 99% thời gian thay vì mã sẽ chạy 1% thời gian (trừ khi không có chi phí để tối ưu hóa cả hai, nhưng đó không phải là trường hợp ở đây).

Nhận xét từ câu trả lời của @ SqlZim (nhấn mạnh thêm)

Cá nhân tôi thích thử và điều chỉnh một giải pháp để tránh làm điều đó khi có thể . Trong trường hợp này, tôi không cảm thấy rằng sử dụng các khóa từ serializablelà một cách tiếp cận nặng tay và tôi sẽ tự tin rằng nó sẽ xử lý đồng thời cao.

Tôi sẽ đồng ý với câu đầu tiên này nếu nó được sửa đổi thành "và thận trọng". Chỉ vì một cái gì đó có thể về mặt kỹ thuật không có nghĩa là tình huống (nghĩa là trường hợp sử dụng dự định) sẽ được hưởng lợi từ nó.

Vấn đề tôi thấy với cách tiếp cận này là nó khóa nhiều hơn những gì đang được đề xuất. Điều quan trọng là phải đọc lại tài liệu được trích dẫn về "tuần tự hóa", cụ thể như sau (nhấn mạnh thêm):

  • Các giao dịch khác không thể chèn các hàng mới với các giá trị chính sẽ nằm trong phạm vi các khóa được đọc bởi bất kỳ câu lệnh nào trong giao dịch hiện tại cho đến khi giao dịch hiện tại hoàn tất.

Bây giờ, đây là nhận xét trong mã ví dụ:

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */

Từ phẫu thuật có "phạm vi". Khóa được thực hiện không chỉ dựa trên giá trị @vName, mà chính xác hơn là một phạm vi bắt đầu từvị trí nơi giá trị mới này sẽ đi (nghĩa là giữa các giá trị khóa hiện tại ở hai bên của giá trị mới phù hợp), nhưng không phải là giá trị chính nó. Có nghĩa là, các quy trình khác sẽ bị chặn không chèn các giá trị mới, tùy thuộc vào (các) giá trị hiện đang được tra cứu. Nếu việc tra cứu đang được thực hiện ở đầu phạm vi, thì việc chèn bất cứ thứ gì có thể chiếm vị trí đó sẽ bị chặn. Ví dụ: nếu các giá trị "a", "b" và "d" tồn tại, thì nếu một quá trình đang thực hiện CHỌN trên "f", thì sẽ không thể chèn các giá trị "g" hoặc thậm chí là "e" ( vì bất kỳ ai trong số họ sẽ đến ngay sau "d"). Nhưng, việc chèn một giá trị "c" sẽ có thể vì nó sẽ không được đặt trong phạm vi "dành riêng".

Ví dụ sau sẽ minh họa hành vi này:

(Trong tab truy vấn (tức là Phiên) # 1)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'test5');

BEGIN TRAN;

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'test8';

--ROLLBACK;

(Trong tab truy vấn (tức là Phiên) # 2)

EXEC dbo.NameLookup_getset_byName @vName = N'test4';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'test9';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N'test7';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N's';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'u';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

Tương tự, nếu giá trị "C" tồn tại và giá trị "A" đang được chọn (và do đó bị khóa), thì bạn có thể chèn giá trị "D", nhưng không phải là giá trị của "B":

(Trong tab truy vấn (tức là Phiên) # 1)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'testC');

BEGIN TRAN

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'testA';

--ROLLBACK;

(Trong tab truy vấn (tức là Phiên) # 2)

EXEC dbo.NameLookup_getset_byName @vName = N'testD';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'testB';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

Công bằng mà nói, theo cách tiếp cận được đề xuất của tôi, khi có một ngoại lệ, sẽ có 4 mục trong Nhật ký giao dịch sẽ không xảy ra trong phương pháp "giao dịch tuần tự hóa" này. NHƯNG, như tôi đã nói ở trên, nếu ngoại lệ xảy ra 1% (hoặc thậm chí 5%) thời gian, điều đó ít ảnh hưởng hơn nhiều so với trường hợp rất có thể của CHỌN ban đầu tạm thời chặn các hoạt động INSERT.

Một vấn đề khác, mặc dù nhỏ, vấn đề với cách tiếp cận "giao dịch tuần tự + mệnh đề OUTPUT" này là OUTPUTmệnh đề (trong cách sử dụng hiện tại) gửi dữ liệu trở lại dưới dạng tập kết quả. Một tập kết quả đòi hỏi nhiều chi phí hơn (có thể ở cả hai phía: trong SQL Server để quản lý con trỏ bên trong và trong lớp ứng dụng để quản lý đối tượng DataReader) hơn là một OUTPUTtham số đơn giản . Cho rằng chúng ta chỉ đang xử lý một giá trị vô hướng duy nhất và giả định là tần suất thực thi cao, có thể thêm chi phí hoạt động của tập kết quả.

Mặc dù OUTPUTmệnh đề có thể được sử dụng theo cách trả về một OUTPUTtham số, nhưng sẽ yêu cầu các bước bổ sung để tạo một bảng tạm thời hoặc biến bảng, sau đó chọn giá trị từ biến bảng / bảng tạm thời đó vào OUTPUTtham số.

Làm rõ thêm: Trả lời Phản hồi của @ SqlZim (câu trả lời được cập nhật) cho Phản hồi của tôi đối với Phản hồi của @ SqlZim (trong câu trả lời ban đầu) cho tuyên bố của tôi về đồng thời và hiệu suất ;-)

Xin lỗi nếu phần này dài một chút, nhưng tại thời điểm này, chúng tôi chỉ xem xét các sắc thái của hai cách tiếp cận.

Tôi tin rằng cách thông tin được trình bày có thể dẫn đến các giả định sai về số lượng khóa mà người ta có thể gặp phải khi sử dụng serializabletrong kịch bản như được trình bày trong câu hỏi ban đầu.

Vâng, tôi sẽ thừa nhận rằng tôi thiên vị, mặc dù công bằng:

  1. Không thể để con người không bị thiên vị, ít nhất là ở một mức độ nhỏ nào đó, và tôi cố gắng giữ nó ở mức tối thiểu,
  2. Ví dụ đưa ra rất đơn giản, nhưng đó là nhằm mục đích minh họa để truyền đạt hành vi mà không làm phức tạp nó. Ngụ ý tần suất quá mức không nhằm mục đích, mặc dù tôi hiểu rằng tôi cũng không nói rõ ràng bằng cách khác và nó có thể được đọc là ngụ ý một vấn đề lớn hơn thực tế tồn tại. Tôi sẽ cố gắng làm rõ điều đó dưới đây.
  3. Tôi cũng đã bao gồm một ví dụ về việc khóa một phạm vi giữa hai khóa hiện có (bộ thứ hai của các khối "tab truy vấn 1" và "tab truy vấn 2").
  4. Tôi đã tìm thấy (và tình nguyện) "chi phí ẩn" cho cách tiếp cận của mình, đó là bốn mục nhập Nhật ký bổ sung mỗi lần INSERTkhông thành công do vi phạm ràng buộc duy nhất. Tôi chưa thấy điều đó được đề cập trong bất kỳ câu trả lời / bài viết nào khác.

Về cách tiếp cận "JFDI" của @ gbn, bài đăng "Chủ nghĩa thực dụng xấu cho người chiến thắng" của Michael J. Swart, và bình luận của Aaron Bertrand về bài đăng của Michael (liên quan đến các bài kiểm tra của anh ấy cho thấy kịch bản nào đã làm giảm hiệu suất) và nhận xét của bạn về "sự thích nghi của Michael J" Sự thích nghi của Stewart về quy trình Thử bắt JFDI của @ gbn "nêu rõ:

Nếu bạn đang chèn các giá trị mới thường xuyên hơn so với việc chọn các giá trị hiện tại, thì điều này có thể hiệu quả hơn phiên bản của @ srutzky. Nếu không, tôi thích phiên bản của @ srutzky hơn phiên bản này.

Đối với cuộc thảo luận gbn / Michael / Aaron liên quan đến phương pháp "JFDI", sẽ không đúng khi đánh giá đề xuất của tôi với phương pháp "JFDI" của gbn. Do tính chất của hoạt động "Nhận hoặc Chèn", có một nhu cầu rõ ràng là phải thực hiện SELECTđể có được IDgiá trị cho các bản ghi hiện có. CHỌN này hoạt động như IF EXISTSkiểm tra, làm cho cách tiếp cận này tương đương với biến thể "CheckTryCatch" của các thử nghiệm của Aaron. Mã được viết lại của Michael (và bản chuyển thể cuối cùng của bạn về sự thích ứng của Michael) cũng bao gồm một WHERE NOT EXISTSkiểm tra tương tự trước tiên. Do đó, đề xuất của tôi (cùng với mã cuối cùng của Michael và sự điều chỉnh mã cuối cùng của anh ấy) sẽ không thực sự đạt được CATCHkhối đó thường xuyên. Nó chỉ có thể là tình huống trong đó hai phiên,ItemNameINSERT...SELECTtại cùng một thời điểm sao cho cả hai phiên đều nhận được "đúng" cho WHERE NOT EXISTScùng một thời điểm và do đó cả hai đều cố gắng thực hiện INSERTcùng một lúc. Kịch bản rất cụ thể đó xảy ra ít thường xuyên hơn nhiều so với việc chọn một cái hiện có ItemNamehoặc chèn một cái mới ItemNamekhi không có quá trình nào khác đang cố gắng thực hiện vào cùng một thời điểm .

VỚI TẤT CẢ CÁC TRÊN TRÊN MIND: Tại sao tôi thích cách tiếp cận của mình?

Đầu tiên, chúng ta hãy xem những gì khóa diễn ra trong phương pháp "tuần tự hóa". Như đã đề cập ở trên, "phạm vi" bị khóa phụ thuộc vào các giá trị khóa hiện có ở hai bên của giá trị khóa mới sẽ phù hợp. Đầu hoặc cuối của phạm vi cũng có thể là đầu hoặc cuối của chỉ mục, nếu không có giá trị khóa hiện tại theo hướng đó. Giả sử chúng ta có chỉ mục và khóa sau ( ^đại diện cho phần đầu của chỉ mục trong khi $biểu thị phần cuối của chỉ mục):

Range #:    |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value:  ^         C         F         J         $

Nếu phiên 55 cố gắng chèn giá trị chính của:

  • A, sau đó phạm vi # 1 (từ ^đến C) bị khóa: phiên 56 không thể chèn giá trị B, ngay cả khi duy nhất và hợp lệ (chưa). Nhưng phiên 56 có thể chèn các giá trị của D, GM.
  • D, sau đó phạm vi # 2 (từ Cđến F) bị khóa: phiên 56 không thể chèn giá trị E(chưa). Nhưng phiên 56 có thể chèn các giá trị của A, GM.
  • M, sau đó phạm vi # 4 (từ Jđến $) bị khóa: phiên 56 không thể chèn giá trị X(chưa). Nhưng phiên 56 có thể chèn các giá trị của A, DG.

Khi nhiều giá trị khóa được thêm vào, phạm vi giữa các giá trị chính sẽ hẹp hơn, do đó giảm xác suất / tần suất của nhiều giá trị được chèn cùng lúc chiến đấu trên cùng một phạm vi. Phải thừa nhận rằng, đây không phải là một vấn đề lớn và may mắn thay, nó dường như là một vấn đề thực sự giảm dần theo thời gian.

Vấn đề với cách tiếp cận của tôi đã được mô tả ở trên: nó chỉ xảy ra khi hai phiên cố gắng chèn cùng một giá trị khóa cùng một lúc. Về mặt này, điều gì dẫn đến xác suất xảy ra cao hơn: hai giá trị khóa khác nhau nhưng gần nhau được thử cùng một lúc hoặc cùng một giá trị khóa được thử cùng một lúc? Tôi cho rằng câu trả lời nằm trong cấu trúc của ứng dụng khi thực hiện các thao tác chèn, nhưng nói chung tôi sẽ cho rằng nhiều khả năng hai giá trị khác nhau xảy ra để chia sẻ cùng một phạm vi sẽ được chèn vào. Nhưng cách duy nhất để thực sự biết là thử nghiệm cả hai trên hệ thống OP.

Tiếp theo, hãy xem xét hai kịch bản và cách mỗi phương pháp xử lý chúng:

  1. Tất cả các yêu cầu dành cho các giá trị khóa duy nhất:

    Trong trường hợp này, CATCHkhối trong đề xuất của tôi không bao giờ được nhập, do đó không có "vấn đề" (tức là 4 mục nhật ký tran và thời gian cần thiết để làm điều đó). Nhưng, theo cách tiếp cận "tuần tự hóa", ngay cả khi tất cả các phần chèn là duy nhất, sẽ luôn có một số tiềm năng để chặn các phần chèn khác trong cùng phạm vi (mặc dù không quá lâu).

  2. Tần suất cao của các yêu cầu cho cùng một giá trị khóa cùng một lúc:

    Trong trường hợp này - mức độ duy nhất rất thấp về các yêu cầu đến đối với các giá trị khóa không tồn tại - CATCHkhối trong đề xuất của tôi sẽ được nhập thường xuyên. Hiệu quả của việc này là mỗi lần chèn không thành công sẽ cần tự động khôi phục và ghi 4 mục vào Nhật ký giao dịch, đây là một hiệu suất nhẹ đạt được mỗi lần. Nhưng hoạt động tổng thể không bao giờ thất bại (ít nhất là không phải do điều này).

    (Có một vấn đề với phiên bản trước của phương pháp "cập nhật" cho phép nó bị bế tắc. Một updlockgợi ý đã được thêm vào để giải quyết vấn đề này và nó không còn bị bế tắc nữa.)NHƯNG, theo cách tiếp cận "tuần tự hóa" (ngay cả phiên bản cập nhật, tối ưu hóa), hoạt động sẽ bế tắc. Tại sao? Bởi vì serializablehành vi chỉ ngăn các INSERThoạt động trong phạm vi đã được đọc và do đó bị khóa; nó không ngăn cản SELECThoạt động trên phạm vi đó.

    Cách serializabletiếp cận, trong trường hợp này, dường như không có chi phí bổ sung, và có thể thực hiện tốt hơn một chút so với những gì tôi đề xuất.

Cũng như nhiều / hầu hết các cuộc thảo luận liên quan đến hiệu suất, do có quá nhiều yếu tố có thể ảnh hưởng đến kết quả, cách duy nhất để thực sự có ý thức về cách một cái gì đó sẽ thực hiện là thử nó trong môi trường mục tiêu nơi nó sẽ chạy. Tại thời điểm đó, nó sẽ không phải là vấn đề quan điểm :).


7

Cập nhật câu trả lời


Phản hồi với @srutzky

Một vấn đề khác, mặc dù nhỏ, vấn đề với cách tiếp cận "giao dịch tuần tự + mệnh đề OUTPUT" này là mệnh đề OUTPUT (trong cách sử dụng hiện tại) gửi lại dữ liệu dưới dạng tập kết quả. Một tập kết quả đòi hỏi nhiều chi phí hơn (có thể ở cả hai phía: trong SQL Server để quản lý con trỏ bên trong và trong lớp ứng dụng để quản lý đối tượng DataReader) so với tham số OUTPUT đơn giản. Cho rằng chúng ta chỉ đang xử lý một giá trị vô hướng duy nhất và giả định là tần suất thực thi cao, có thể thêm chi phí hoạt động của tập kết quả.

Tôi đồng ý và vì những lý do tương tự, tôi sử dụng các tham số đầu ra khi thận trọng . Đó là sai lầm của tôi khi không sử dụng một tham số đầu ra cho câu trả lời ban đầu của tôi, tôi đã lười biếng.

Dưới đây là quy trình sửa đổi bằng cách sử dụng tham số đầu ra, tối ưu hóa bổ sung, cùng với next value forđó @srutzky giải thích trong câu trả lời của mình :

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50), @vValueId int output) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                                        /* if @vName is empty, return early */
  select  @vValueId = Id                                              /* go get the Id */
    from  dbo.NameLookup
    where ItemName = @vName;
  if @vValueId is not null                                 /* if we got the id, return */
    return;
  begin try;                                  /* if it is not there, then get the lock */
    begin tran;
      select  @vValueId = Id
        from  dbo.NameLookup with (updlock, serializable) /* hold key range for @vName */
        where ItemName = @vName;
      if @@rowcount = 0                    /* if we still do not have an Id for @vName */
      begin;                                         /* get a new Id and insert @vName */
        set @vValueId = next value for dbo.IdSequence;      /* get next sequence value */
        insert into dbo.NameLookup (ItemName, Id)
          values (@vName, @vValueId);
      end;
    commit tran;
  end try
  begin catch;
    if @@trancount > 0 
      begin;
        rollback transaction;
        throw;
      end;
  end catch;
end;

cập nhật lưu ý : Bao gồm updlockvới lựa chọn sẽ lấy các khóa thích hợp trong kịch bản này. Cảm ơn @srutzky, người đã chỉ ra rằng điều này có thể gây ra bế tắc khi chỉ sử dụng serializabletrên select.

Lưu ý: Điều này có thể không phải là trường hợp, nhưng nếu có thể, thủ tục sẽ được gọi với một giá trị cho @vValueId, bao gồm set @vValueId = null;sau set xact_abort on;, nếu không nó có thể được gỡ bỏ.


Liên quan đến các ví dụ của @ srutzky về hành vi khóa phạm vi chính:

@srutzky chỉ sử dụng một giá trị trong bảng của mình và khóa phím "tiếp theo" / "vô cực" cho các thử nghiệm của mình để minh họa việc khóa phạm vi khóa. Trong khi các thử nghiệm của anh ấy minh họa những gì xảy ra trong những tình huống đó, tôi tin rằng cách trình bày thông tin có thể dẫn đến những giả định sai về số lượng khóa mà người ta có thể gặp phải khi sử dụng serializabletrong kịch bản như được trình bày trong câu hỏi ban đầu.

Mặc dù tôi nhận thấy sự thiên vị (có lẽ sai lệch) trong cách anh ấy trình bày lời giải thích và ví dụ về khóa phạm vi khóa, nhưng chúng vẫn đúng.


Sau khi nghiên cứu thêm, tôi tìm thấy một bài viết blog đặc biệt thích hợp từ năm 2011 của Michael J. Swart: Mythbusting: Cập nhật đồng thời / Giải pháp chèn . Trong đó, ông kiểm tra nhiều phương pháp cho độ chính xác và đồng thời. Phương pháp 4: Cách ly tăng + Khóa tinh chỉnh dựa trên bài đăng Chèn hoặc mẫu cập nhật của Sam Saffron cho SQL Server và phương pháp duy nhất trong thử nghiệm ban đầu để đáp ứng mong đợi của anh ấy (được tham gia sau merge with (holdlock)).

Vào tháng 2 năm 2016, Michael J. Swart đã đăng bài Chủ nghĩa thực dụng xấu xí cho người chiến thắng . Trong bài đăng đó, anh ấy đề cập đến một số điều chỉnh bổ sung mà anh ấy đã thực hiện đối với các thủ tục nâng cấp Saffron của mình để giảm khóa (mà tôi đã bao gồm trong quy trình trên).

Sau khi thực hiện những thay đổi đó, Michael không vui khi quy trình của anh bắt đầu có vẻ phức tạp hơn và được hỏi ý kiến ​​với một người có tên là Chris. Chris đọc tất cả các bài đăng trên Mythbuster ban đầu và đọc tất cả các bình luận và hỏi về mẫu TRY CATCH JFDI của @ gbn . Mẫu này tương tự như câu trả lời của @ srutzky, và là giải pháp mà Michael đã kết thúc bằng cách sử dụng trong trường hợp đó.

Michael J Swart:

Hôm qua tôi đã thay đổi suy nghĩ về cách tốt nhất để làm đồng thời. Tôi mô tả một số phương pháp trong Mythbusting: Cập nhật đồng thời / Giải pháp chèn. Phương pháp ưa thích của tôi là tăng mức cách ly và tinh chỉnh ổ khóa.

Ít nhất đó là sở thích của tôi. Gần đây tôi đã thay đổi cách tiếp cận của mình để sử dụng một phương pháp mà gbn đề xuất trong các bình luận. Ông mô tả phương pháp của mình là mô hình JYDI của TRY CATCH JFDI. Thông thường tôi tránh các giải pháp như thế. Có một quy tắc nói rằng các nhà phát triển không nên dựa vào việc bắt lỗi hoặc ngoại lệ cho luồng điều khiển. Nhưng tôi đã phá vỡ quy tắc đó ngày hôm qua.

Nhân tiện, tôi thích mô tả của gbn cho mẫu JFDI 'của mô hình. Nó làm tôi nhớ đến video động lực của Shia Labeouf.


Theo tôi, cả hai giải pháp đều khả thi. Mặc dù tôi vẫn thích tăng mức cô lập và tinh chỉnh khóa, câu trả lời của @ srutzky cũng hợp lệ và có thể hoặc không thể thực hiện nhiều hơn trong tình huống cụ thể của bạn.

Có lẽ trong tương lai tôi cũng sẽ đi đến kết luận tương tự mà Michael J. Swart đã làm, nhưng tôi vẫn chưa ở đó.


Đó không phải là sở thích của tôi, nhưng đây là những gì tôi thích ứng với quy trình Thử bắt JFDI của @ gbn của @ gbn sẽ như thế nào:

create procedure dbo.NameLookup_JFDI (
    @vName nvarchar(50)
  , @vValueId int output
  ) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                     /* if @vName is empty, return early */
  begin try                                                 /* JFDI */
    insert into dbo.NameLookup (ItemName)
      select @vName
      where not exists (
        select 1
          from dbo.NameLookup
          where ItemName = @vName);
  end try
  begin catch        /* ignore duplicate key errors, throw the rest */
    if error_number() not in (2601, 2627) throw;
  end catch
  select  @vValueId = Id                              /* get the Id */
    from  dbo.NameLookup
    where ItemName = @vName
  end;

Nếu bạn đang chèn các giá trị mới thường xuyên hơn so với việc chọn các giá trị hiện tại, điều này có thể hiệu quả hơn phiên bản của @ srutzky . Nếu không, tôi thích phiên bản của @ srutzky hơn phiên bản này.

Nhận xét của Aaron Bertrand về bài đăng của Michael J Swart liên quan đến thử nghiệm có liên quan mà ông đã thực hiện và dẫn đến cuộc trao đổi này. Trích từ phần bình luận về Chủ nghĩa thực dụng xấu xí cho người chiến thắng :

Tuy nhiên, đôi khi, JFDI dẫn đến hiệu suất tổng thể kém hơn, tùy thuộc vào% số cuộc gọi thất bại. Tăng ngoại lệ có chi phí đáng kể. Tôi đã cho thấy điều này trong một vài bài viết:

http://sqlperformance.com/2012/08/t-sql-queries/error-handling

https://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-b Before-entering-sql-server-try-and-catch-logic/

Nhận xét của Aaron Bertrand - ngày 11 tháng 2 năm 2016 @ 11:49 sáng

và trả lời của:

Bạn nói đúng Aaron, và chúng tôi đã kiểm tra nó.

Hóa ra trong trường hợp của chúng tôi, phần trăm các cuộc gọi thất bại là 0 (khi được làm tròn đến phần trăm gần nhất).

Tôi nghĩ rằng bạn minh họa điểm càng nhiều càng tốt, đánh giá mọi thứ trên cơ sở từng trường hợp cụ thể theo quy tắc ngón tay cái.

Đó cũng là lý do tại sao chúng tôi đã thêm mệnh đề WHERE KHÔNG EXISTS không cần thiết nghiêm ngặt.

Nhận xét của Michael J. Swart - ngày 11 tháng 2 năm 2016 @ 11:57 sáng


Liên kết mới:


Câu trả lời gốc


Tôi vẫn thích cách tiếp cận Sam Saffron hơn so với sử dụng merge, đặc biệt là khi giao dịch với một hàng đơn.

Tôi sẽ điều chỉnh phương pháp upert đó cho tình huống như thế này:

declare @vName nvarchar(50) = 'Invader';
declare @vValueId int       = null;

if nullif(@vName,'') is not null /* this gets your where condition taken care of before we start doing anything */
begin tran;
  select @vValueId = Id
    from dbo.NameLookup with (serializable) 
    where ItemName = @vName;
  if @@rowcount > 0 
    begin;
      select @vValueId as id;
    end;
    else
    begin;
      insert into dbo.NameLookup (ItemName)
        output inserted.id
          values (@vName);
      end;
commit tran;

Tôi sẽ phù hợp với cách đặt tên của bạn, và cũng serializablegiống như holdlock, chọn một và nhất quán trong việc sử dụng nó. Tôi có xu hướng sử dụngserializable vì nó cùng tên được sử dụng như khi chỉ định set transaction isolation level serializable.

Bằng cách sử dụng serializablehoặc holdlockkhóa phạm vi được thực hiện dựa trên giá trị @vNamekhiến mọi thao tác khác phải chờ nếu chúng chọn hoặc chèn giá trị vào dbo.NameLookupđó bao gồm giá trị trong wheremệnh đề.

Để khóa phạm vi hoạt động chính xác, cần phải có một chỉ mục trên ItemNamecột, điều này cũng áp dụng khi sử dụng merge.


Đây là những gì thủ tục sẽ trông giống như chủ yếu sau các trang trắng của Erland Sommarskog để xử lý lỗi , sử dụng throw. Nếu throwkhông phải là cách bạn đang nêu ra lỗi của mình, hãy thay đổi nó để phù hợp với phần còn lại của quy trình:

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50) ) as
begin
  set nocount on;
  set xact_abort on;
  declare @vValueId int;
  if nullif(@vName,'') is null /* if @vName is null or empty, select Id as null */
    begin
      select Id = cast(null as int);
    end 
    else                       /* else go get the Id */
    begin try;
      begin tran;
        select @vValueId = Id
          from dbo.NameLookup with (serializable) /* hold key range for @vName */
          where ItemName = @vName;
        if @@rowcount > 0      /* if we have an Id for @vName select @vValueId */
          begin;
            select @vValueId as Id; 
          end;
          else                     /* else insert @vName and output the new Id */
          begin;
            insert into dbo.NameLookup (ItemName)
              output inserted.Id
                values (@vName);
            end;
      commit tran;
    end try
    begin catch;
      if @@trancount > 0 
        begin;
          rollback transaction;
          throw;
        end;
    end catch;
  end;
go

Để tóm tắt những gì đang diễn ra trong quy trình trên: set nocount on; set xact_abort on;như bạn luôn làm , sau đó nếu kết quả đầu vào của chúng tôi biến is nullhoặc trống, select id = cast(null as int)là kết quả. Nếu nó không rỗng hoặc trống, thì hãy lấy Idbiến của chúng ta trong khi giữ vị trí đó trong trường hợp nó không ở đó. Nếu Idcó, gửi nó ra. Nếu nó không có ở đó, chèn nó và gửi cái mới đó Id.

Trong khi đó, các cuộc gọi khác đến quy trình này cố gắng tìm Id cho cùng một giá trị sẽ đợi cho đến khi giao dịch đầu tiên được thực hiện và sau đó chọn và trả lại. Các cuộc gọi khác đến thủ tục này hoặc các câu lệnh khác đang tìm kiếm các giá trị khác sẽ tiếp tục vì điều này không được thực hiện.

Mặc dù tôi đồng ý với @srutzky rằng bạn có thể xử lý các va chạm và nuốt các ngoại lệ cho loại vấn đề này, cá nhân tôi thích thử và điều chỉnh một giải pháp để tránh làm điều đó khi có thể. Trong trường hợp này, tôi không cảm thấy rằng sử dụng các khóa từ serializablelà một cách tiếp cận nặng tay và tôi sẽ tự tin rằng nó sẽ xử lý đồng thời cao.

Trích dẫn từ tài liệu máy chủ sql trên bảng gợi ý serializable/holdlock :

SERIALIZABLE

Tương đương với GIỜ. Làm cho các khóa được chia sẻ hạn chế hơn bằng cách giữ chúng cho đến khi giao dịch được hoàn thành, thay vì giải phóng khóa chia sẻ ngay khi bảng yêu cầu hoặc trang dữ liệu không còn cần thiết, cho dù giao dịch đã được hoàn thành hay chưa. Quá trình quét được thực hiện với cùng một ngữ nghĩa như một giao dịch đang chạy ở mức cô lập SERIALIZABLE. Để biết thêm thông tin về các mức cô lập, hãy xem THIẾT LẬP CẤP PHÂN TÍCH GIAO DỊCH (Transact-SQL).

Trích dẫn từ tài liệu máy chủ sql về mức độ cô lập giao dịchserializable

SERIALIZABLE Chỉ định như sau:

  • Báo cáo không thể đọc dữ liệu đã được sửa đổi nhưng chưa được cam kết bởi các giao dịch khác.

  • Không có giao dịch nào khác có thể sửa đổi dữ liệu đã được đọc bởi giao dịch hiện tại cho đến khi giao dịch hiện tại hoàn tất.

  • Các giao dịch khác không thể chèn các hàng mới với các giá trị chính sẽ nằm trong phạm vi các khóa được đọc bởi bất kỳ câu lệnh nào trong giao dịch hiện tại cho đến khi giao dịch hiện tại hoàn tất.


Liên kết liên quan đến giải pháp trên:

MERGEcó một lịch sử đáng chú ý và dường như cần phải chọc nhiều hơn để đảm bảo rằng mã đang hành xử theo cách bạn muốn theo tất cả cú pháp đó. Các mergebài viết liên quan :

Một liên kết cuối cùng, Kendra Little đã làm một so sánh sơ bộ so mergevới vsinsert with left join , với lời cảnh báo mà cô ấy nói "Tôi đã không thực hiện kiểm tra tải kỹ lưỡng về điều này", nhưng nó vẫn là một bài đọc tốt.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.