Database
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> Database

Lần cuối cùng, KHÔNG, bạn không thể tin tưởng IDENT_CURRENT ()

Tôi đã có một cuộc thảo luận ngày hôm qua với Kendal Van Dyke (@SQLDBA) về IDENT_CURRENT (). Về cơ bản, Kendal có mã này, mã này anh ấy đã tự kiểm tra và tin tưởng và muốn biết liệu anh ấy có thể dựa vào IDENT_CURRENT () là chính xác trong môi trường quy mô cao, đồng thời hay không:

BEGIN TRANSACTION;
INSERT dbo.TableName(ColumnName) VALUES('Value');
SELECT IDENT_CURRENT('dbo.TableName');
COMMIT TRANSACTION;

Lý do anh ta phải làm điều này là vì anh ta cần trả lại giá trị IDENTITY đã tạo cho máy khách. Những cách điển hình mà chúng tôi thực hiện là:

  • SCOPE_IDENTITY ()
  • Điều khoản OUTPUT
  • @@ BẢN SẮC
  • IDENT_CURRENT ()

Một số trong số này tốt hơn những cái khác, nhưng điều đó đã được thực hiện cho đến chết, và tôi sẽ không đi sâu vào nó ở đây. Trong trường hợp của Kendal, IDENT_CURRENT là giải pháp cuối cùng và duy nhất của anh ấy, bởi vì:

  • TableName có trình kích hoạt INSTEAD OF INSERT, làm cho cả SCOPE_IDENTITY () và mệnh đề OUTPUT trở nên vô dụng đối với trình gọi, bởi vì:
    • SCOPE_IDENTITY () trả về NULL, vì việc chèn thực sự xảy ra trong một phạm vi khác
    • mệnh đề OUTPUT tạo ra lỗi Msg 334 do trình kích hoạt
  • Anh ấy đã loại bỏ @@ IDENTITY; hãy xem xét rằng trình kích hoạt INSTEAD OF INSERT bây giờ có thể (hoặc sau này có thể được thay đổi thành) chèn vào các bảng khác có cột IDENTITY của riêng chúng, điều này sẽ làm rối giá trị được trả về. Điều này cũng sẽ cản trở SCOPE_IDENTITY (), nếu có thể.
  • Và cuối cùng, anh ấy không thể sử dụng mệnh đề OUTPUT (hoặc tập kết quả từ truy vấn thứ hai của bảng giả được chèn sau lần chèn cuối cùng) trong trình kích hoạt, bởi vì khả năng này yêu cầu cài đặt chung và đã không được dùng nữa kể từ đó SQL Server 2005. Có thể hiểu, mã của Kendal cần phải tương thích về phía trước và khi có thể, không dựa hoàn toàn vào một số cài đặt cơ sở dữ liệu hoặc máy chủ nhất định.

Vì vậy, trở lại thực tế của Kendal. Mã của anh ấy có vẻ đủ an toàn - xét cho cùng thì nó cũng đang trong một giao dịch; điều gì có thể xảy ra? Chà, chúng ta hãy xem một vài câu quan trọng từ tài liệu IDENT_CURRENT (tôi nhấn mạnh, vì những cảnh báo này ở đó là có lý do chính đáng):

Trả về giá trị nhận dạng cuối cùng được tạo cho một bảng hoặc chế độ xem được chỉ định. Giá trị nhận dạng cuối cùng được tạo có thể dành cho bất kỳ phiên nào bất kỳ phạm vi nào .

Hãy thận trọng khi sử dụng IDENT_CURRENT để dự đoán giá trị nhận dạng được tạo tiếp theo. giá trị thực tế được tạo có thể khác từ IDENT_CURRENT cộng với IDENT_INCR do các lần chèn được thực hiện bởi các phiên khác .

Các giao dịch hầu như không được đề cập trong phần nội dung của tài liệu (chỉ trong trường hợp thất bại, không đồng thời) và không có giao dịch nào được sử dụng trong bất kỳ mẫu nào. Vì vậy, chúng ta hãy kiểm tra xem Kendal đang làm gì và xem liệu chúng ta có thể làm cho nó không thành công khi nhiều phiên chạy đồng thời hay không. Tôi sẽ tạo một bảng nhật ký để theo dõi các giá trị được tạo bởi mỗi phiên - cả giá trị nhận dạng thực sự được tạo (sử dụng trình kích hoạt sau) và giá trị được xác nhận là được tạo theo IDENT_CURRENT ().

Đầu tiên, các bảng và trình kích hoạt:

-- the destination table:
 
CREATE TABLE dbo.TableName
(
  ID INT IDENTITY(1,1), 
  seq INT
);
 
-- the log table:
 
CREATE TABLE dbo.IdentityLog
(
  SPID INT, 
  seq INT, 
  src VARCHAR(20), -- trigger or ident_current 
  id INT
);
GO
 
-- the trigger, adding my logging:
 
CREATE TRIGGER dbo.InsteadOf_TableName
ON dbo.TableName
INSTEAD OF INSERT
AS
BEGIN
  INSERT dbo.TableName(seq) SELECT seq FROM inserted;
 
  -- this is just for our logging purposes here:
  INSERT dbo.IdentityLog(SPID,seq,src,id)
    SELECT @@SPID, seq, 'trigger', SCOPE_IDENTITY() 
    FROM inserted;
END
GO

Bây giờ, hãy mở một số cửa sổ truy vấn và dán mã này, thực thi chúng gần nhau nhất có thể để đảm bảo chồng chéo nhiều nhất:

SET NOCOUNT ON;
 
DECLARE @seq INT = 0;
 
WHILE @seq <= 100000
BEGIN
  BEGIN TRANSACTION;
 
  INSERT dbo.TableName(seq) SELECT @seq;
  INSERT dbo.IdentityLog(SPID,seq,src,id)
    SELECT @@SPID,@seq,'ident_current',IDENT_CURRENT('dbo.TableName');
 
  COMMIT TRANSACTION;
  SET @seq += 1;
END

Sau khi tất cả các cửa sổ truy vấn đã hoàn tất, hãy chạy truy vấn này để xem một vài hàng ngẫu nhiên trong đó IDENT_CURRENT trả về giá trị sai và tổng số có bao nhiêu hàng bị ảnh hưởng bởi con số được báo cáo sai này:

SELECT TOP (10)
  id_cur.SPID,  
  [ident_current] = id_cur.id, 
  [actual id] = tr.id, 
  total_bad_results = COUNT(*) OVER()
FROM dbo.IdentityLog AS id_cur
INNER JOIN dbo.IdentityLog AS tr
   ON id_cur.SPID = tr.SPID 
   AND id_cur.seq = tr.seq 
   AND id_cur.id <> tr.id
WHERE id_cur.src = 'ident_current' 
   AND tr.src     = 'trigger'
ORDER BY NEWID();

Đây là 10 hàng của tôi cho một lần kiểm tra:

Tôi thấy thật ngạc nhiên khi gần một phần ba số hàng đã bị tắt. Kết quả của bạn chắc chắn sẽ khác nhau và có thể phụ thuộc vào tốc độ ổ đĩa, kiểu khôi phục, cài đặt tệp nhật ký hoặc các yếu tố khác. Trên hai máy khác nhau, tôi có tỷ lệ lỗi rất khác nhau - theo hệ số 10 (máy chậm hơn chỉ có khoảng 10.000 lần hỏng, hoặc khoảng 3%).

Ngay lập tức, rõ ràng rằng một giao dịch không đủ để ngăn IDENT_CURRENT kéo các giá trị IDENTITY được tạo bởi các phiên khác. Làm thế nào về một giao dịch SERIALIZABLE? Đầu tiên, hãy xóa hai bảng:

TRUNCATE TABLE dbo.TableName;
TRUNCATE TABLE dbo.IdentityLog;

Sau đó, thêm mã này vào đầu tập lệnh trong nhiều cửa sổ truy vấn và chạy lại chúng đồng thời càng nhiều càng tốt:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Lần này, khi tôi chạy truy vấn đối với bảng IdentityLog, nó cho thấy rằng SERIALIZABLE có thể đã giúp được một chút, nhưng nó không giải quyết được vấn đề:

Và mặc dù sai là sai, nhưng có vẻ như từ kết quả mẫu của tôi, giá trị IDENT_CURRENT thường chỉ bị sai lệch một hoặc hai. Tuy nhiên, truy vấn này sẽ dẫn đến kết quả rằng nó có thể là * cách * tắt. Trong các lần chạy thử nghiệm của tôi, kết quả này cao tới 236:

SELECT MAX(ABS(id_cur.id - tr.id))
FROM dbo.IdentityLog AS id_cur
INNER JOIN dbo.IdentityLog AS tr
  ON id_cur.SPID = tr.SPID 
  AND id_cur.seq = tr.seq 
  AND id_cur.id <> tr.id
WHERE id_cur.src = 'ident_current' 
  AND tr.src     = 'trigger';

Thông qua bằng chứng này, chúng tôi có thể kết luận rằng IDENT_CURRENT không an toàn cho giao dịch. Nó có vẻ gợi nhớ đến một vấn đề tương tự nhưng gần như ngược lại, trong đó các hàm siêu dữ liệu như OBJECT_NAME () bị chặn - ngay cả khi mức cách ly là ĐỌC KHÔNG ĐƯỢC ĐỀ NGHỊ - vì chúng không tuân theo ngữ nghĩa cách ly xung quanh. (Xem Mục Kết nối # 432497 để biết thêm chi tiết.)

Nhìn bề ngoài, và không biết nhiều hơn về (các) kiến ​​trúc và ứng dụng, tôi không có gợi ý thực sự tốt cho Kendal; Tôi chỉ biết rằng IDENT_CURRENT * không phải * là câu trả lời. :-) Chỉ cần không sử dụng nó. Đối với bất cứ điều gì. Bao giờ. Vào thời điểm bạn đọc giá trị, nó có thể đã bị sai.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Sự khác biệt giữa SQL và NoSQL

  2. Vui vẻ với các tính năng Postgres mới của Django

  3. Cách tạo tài liệu Excel từ chương trình Java bằng Apache POI

  4. Hekaton with a twist:In-memory TVPs - Part 3

  5. Tại sao bạn cần lập mô hình dữ liệu?