Database
 sql >> база данни >  >> RDS >> Database

За последен път, НЕ, не можете да се доверите на IDENT_CURRENT()

Вчера имах дискусия с Кендъл Ван Дайк (@SQLDBA) относно IDENT_CURRENT(). По принцип Кендъл имаше този код, който беше тествал и на който се довери сам и искаше да знае дали може да разчита, че IDENT_CURRENT() е точен във високомащабна, едновременна среда:

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

Причината, поради която трябваше да направи това, е, че трябва да върне генерираната стойност на IDENTITY на клиента. Типичните начини, по които правим това са:

  • SCOPE_IDENTITY()
  • клауза OUTPUT
  • @@IDENTITY
  • IDENT_CURRENT()

Някои от тях са по-добри от други, но това е направено до смърт и няма да навлизам тук. В случая на Кендъл, IDENT_CURRENT беше последното му и единствено средство, защото:

  • Име на таблица имаше задействане INSTEAD OF INSERT, което прави както SCOPE_IDENTITY(), така и клаузата OUTPUT безполезни от повикващия, защото:
    • SCOPE_IDENTITY() връща NULL, тъй като вмъкването всъщност се е случило в различен обхват
    • клаузата OUTPUT генерира грешка Msg 334 поради тригера
  • Той елиминира @@IDENTITY; имайте предвид, че тригерът INSTEAD OF INSERT сега може (или може да бъде променен на) в други таблици, които имат свои собствени колони IDENTITY, което би объркало върнатата стойност. Това също би попречило на SCOPE_IDENTITY(), ако беше възможно.
  • И накрая, той не можа да използва клаузата OUTPUT (или набор от резултати от втора заявка на вмъкнатата псевдотаблица след евентуалното вмъкване) в рамките на тригера, тъй като тази възможност изисква глобална настройка и е отхвърлена от SQL Server 2005. Разбираемо е, че кодът на Kendal трябва да бъде съвместим напред и, когато е възможно, да не разчита изцяло на определени бази данни или настройки на сървъра.

И така, обратно към реалността на Кендъл. Кодът му изглежда достатъчно безопасен – все пак е в транзакция; какво може да се обърка? Е, нека да разгледаме няколко важни изречения от документацията IDENT_CURRENT (подчертавам моето, защото тези предупреждения са там с добра причина):

Връща последната генерирана стойност на идентичност за определена таблица или изглед. Последната генерирана стойност на идентичност може да бъде за всяка сесия и всякакъв обхват .

Бъдете внимателни при използването на IDENT_CURRENT за прогнозиране на следващата генерирана стойност на идентичност. действителната генерирана стойност може да е различна от IDENT_CURRENT плюс IDENT_INCR поради вмъквания, извършени от други сесии .

Транзакциите почти не се споменават в основната част на документа (само в контекста на неуспех, а не на едновременност) и в нито една от извадките не се използват транзакции. И така, нека да проверим какво прави Kendal и да видим дали можем да го накараме да се провали, когато няколко сесии се изпълняват едновременно. Ще създам регистрационна таблица, за да следя стойностите, генерирани от всяка сесия – както стойността на идентичността, която действително е генерирана (с помощта на след тригер), така и стойността, за която се твърди, че е генерирана според IDENT_CURRENT().

Първо, таблиците и тригерите:

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

Сега отворете няколко прозореца за заявка и поставете този код, като ги изпълните възможно най-близо един до друг, за да осигурите най-голямо припокриване:

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

След като всички прозорци на заявка са завършени, изпълнете тази заявка, за да видите няколко произволни реда, където IDENT_CURRENT върна грешна стойност и брой на общото количество реда, засегнати от този погрешно отчетен номер:

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();

Ето моите 10 реда за един тест:

За мен беше изненадващо, че почти една трета от редовете бяха изключени. Резултатите ви със сигурност ще варират и може да зависят от скоростта на вашите дискове, модела за възстановяване, настройките на регистрационните файлове или други фактори. На две различни машини имах значително различни проценти на откази – с коефициент 10 (по-бавна машина имаше само около 10 000 отказа, или приблизително 3%).

Веднага става ясно, че една транзакция не е достатъчна, за да попречи на IDENT_CURRENT да изтегли стойностите на IDENTITY, генерирани от други сесии. Какво ще кажете за СЕРИАЛИЗИРУЕМАТА транзакция? Първо изчистете двете таблици:

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

След това добавете този код към началото на скрипта в няколко прозореца за заявка и ги стартирайте отново възможно най-едновременно:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Този път, когато стартирам заявката към таблицата IdentityLog, тя показва, че SERIALIZABLE може да е помогнал малко, но не е решил проблема:

И въпреки че грешното е грешно, от моите примерни резултати изглежда, че стойността IDENT_CURRENT обикновено се отклонява само с един или два. Въпреки това, тази заявка трябва да доведе до това, че може да бъде *надалече*. При моите тестови тестове този резултат беше до 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';

Чрез това доказателство можем да заключим, че IDENT_CURRENT не е безопасен за транзакции. Изглежда напомня за подобен, но почти противоположен проблем, при който функциите на метаданните като OBJECT_NAME() се блокират – дори когато нивото на изолация е ПРОЧЕТЕНЕ НЕ ОТКАЗВАНО – защото не се подчиняват на семантиката на заобикалящата изолация. (Вижте Connect Item #432497 за повече подробности.)

На пръв поглед и без да знам много повече за архитектурата и приложението(ите), нямам наистина добро предложение за Kendal; Просто знам, че IDENT_CURRENT *не е* отговорът. :-) Просто не го използвайте. За каквото и да е. някога. Докато прочетете стойността, тя вече може да е грешна.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Използване на ODBC със Salesforce и Azure Active Directory (AD) Единичен вход (SSO)

  2. Понякога МОЖЕТЕ да увеличите колона на място

  3. Топ 10 причини защо трябва да научите SQL

  4. SQL не е равен на () оператор за начинаещи

  5. Влизане с външни услуги