Вчера имах дискусия с Кендъл Ван Дайк (@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 *не е* отговорът. :-) Просто не го използвайте. За каквото и да е. някога. Докато прочетете стойността, тя вече може да е грешна.