[ Част 1 | Част 2 | Част 3 | Част 4 ]
В първата част от тази поредица видяхме как проблемът с Хелоуин се прилага към UPDATE
запитвания. За да обобщим накратко, проблемът беше, че индексът, използван за намиране на записи за актуализиране, имаше своите ключове, променени от самата операция за актуализиране (друга добра причина да се използват включени колони в индекс, вместо разширяване на ключовете). Оптимизаторът на заявки въведе оператор Eager Table Spool за разделяне на страните за четене и писане на плана за изпълнение, за да се избегне проблемът. В тази публикация ще видим как същият основен проблем може да засегне INSERT
и DELETE
изявления.
Вмъкване на изявления
Сега знаем малко за условията, които изискват защита на Хелоуин, доста е лесно да създадете INSERT
пример, който включва четене от и запис в ключовете на една и съща индексна структура. Най-простият пример е дублирането на редове в таблица (където добавянето на нови редове неизбежно променя ключовете на клъстерирания индекс):
CREATE TABLE dbo.Demo ( SomeKey integer NOT NULL, CONSTRAINT PK_Demo PRIMARY KEY (SomeKey) ); INSERT dbo.Demo SELECT SomeKey FROM dbo.Demo;
Проблемът е, че нововмъкнатите редове могат да бъдат срещнати от страната за четене на плана за изпълнение, което потенциално води до цикъл, който добавя редове завинаги (или поне докато се достигне някакъв ресурсен лимит). Оптимизаторът на заявки разпознава този риск и добавя Eager Table Spool, за да осигури необходимото разделяне на фазите :
По-реалистичен пример
Вероятно не пишете често заявки, за да дублирате всеки ред в таблица, но вероятно пишете заявки, където целевата таблица за INSERT
също се появява някъде в SELECT
клауза. Един пример е добавянето на редове от промеждуваща таблица, които все още не съществуват в местоназначението:
CREATE TABLE dbo.Staging ( SomeKey integer NOT NULL ); -- Sample data INSERT dbo.Staging (SomeKey) VALUES (1234), (1234); -- Test query INSERT dbo.Demo SELECT s.SomeKey FROM dbo.Staging AS s WHERE NOT EXISTS ( SELECT 1 FROM dbo.Demo AS d WHERE d.SomeKey = s.SomeKey );
Планът за изпълнение е:
Проблемът в този случай е малко по-различен, но все пак е пример за същия основен проблем. В целевата демонстрационна таблица няма стойност „1234“, но таблицата за етапи съдържа два такива записа. Без разделяне на фазите, първата срещана стойност „1234“ ще бъде вмъкната успешно, но втората проверка ще установи, че стойността „1234“ вече съществува и няма да се опита да я вмъкне отново. Изявлението като цяло ще завърши успешно.
Това може да доведе до желан резултат в този конкретен случай (и дори може да изглежда интуитивно правилно), но не е правилно изпълнение. Стандартът SQL изисква заявките за промяна на данни да се изпълняват така, сякаш трите фази на ограниченията за четене, писане и проверка се случват напълно отделно (вижте първа част).
Търсейки всички редове за вмъкване като една операция, трябва да изберем и двата реда „1234“ от таблицата за етапи, тъй като тази стойност все още не съществува в целта. Следователно планът за изпълнение трябва да се опита да вмъкне и двете „1234“ редове от таблицата за етапи, което води до нарушение на първичния ключ:
Съобщение 2627, ниво 14, състояние 1, ред 1Нарушение на ограничението на PRIMARY KEY 'PK_Demo'.
Не може да се вмъкне дублиран ключ в обект 'dbo.Demo'.
Дублираната стойност на ключа е ( 1234).
Изявлението е прекратено.
Разделянето на фазите, осигурено от Table Spool, гарантира, че всички проверки за съществуване са завършени, преди да бъдат направени каквито и да било промени в целевата таблица. Ако изпълните заявката в SQL Server с примерните данни по-горе, ще получите (правилното) съобщение за грешка.
Защитата за Хелоуин се изисква за изрази INSERT, където целевата таблица също е посочена в клаузата SELECT.
Изтриване на изявления
Може да очакваме проблемът с Хелоуин да не се отнася за DELETE
изявления, тъй като не би трябвало да има значение, ако се опитаме да изтрием ред няколко пъти. Можем да модифицираме нашия пример за табличка, за да премахнем редове от демонстрационната таблица, които не съществуват в Staging:
TRUNCATE TABLE dbo.Demo; TRUNCATE TABLE dbo.Staging; INSERT dbo.Demo (SomeKey) VALUES (1234); DELETE dbo.Demo WHERE NOT EXISTS ( SELECT 1 FROM dbo.Staging AS s WHERE s.SomeKey = dbo.Demo.SomeKey );
Този тест изглежда потвърждава нашата интуиция, защото в плана за изпълнение няма Table Spool:
Този тип DELETE
не изисква разделяне на фази, тъй като всеки ред има уникален идентификатор (RID, ако таблицата е купчина, клъстериран индексен ключ(и) и евентуално унификатор в противен случай). Този уникален локатор на редове е стабилен ключ – няма механизъм, чрез който може да се промени по време на изпълнението на този план, така че проблемът с Хелоуин не възниква.
ИЗТРИВАНЕ на защитата за Хелоуин
Въпреки това има поне един случай, когато DELETE
изисква защита за Хелоуин:когато планът препраща към ред в таблицата, различен от този, който се изтрива. Това изисква самостоятелно присъединяване, често срещано при моделиране на йерархични връзки. По-долу е показан опростен пример:
CREATE TABLE dbo.Test ( pk char(1) NOT NULL, ref char(1) NULL, CONSTRAINT PK_Test PRIMARY KEY (pk) ); INSERT dbo.Test (pk, ref) VALUES ('B', 'A'), ('C', 'B'), ('D', 'C');
Наистина би трябвало да има препратка към външния ключ в същата таблица, дефинирана тук, но нека пренебрегнем този дизайн, който се проваля за момент – структурата и данните са все пак валидни (и за съжаление е доста често срещано да се намират външни ключове, пропуснати в реалния свят). Както и да е, задачата е да изтриете всеки ред, където ref колона сочи към несъществуващ pk стойност. Естественият DELETE
заявка, отговаряща на това изискване, е:
DELETE dbo.Test WHERE NOT EXISTS ( SELECT 1 FROM dbo.Test AS t2 WHERE t2.pk = dbo.Test.ref );
Планът на заявката е:
Забележете, че този план сега включва скъпа Eager Table Spool. Тук се изисква разделяне на фази, защото в противен случай резултатите могат да зависят от реда, в който се обработват редовете:
Ако машината за изпълнение стартира с реда, където pk =B, няма да намери съвпадащ ред (ref =A и няма ред, където pk =А). Ако изпълнението се преминава към реда, където pk =C, той също ще бъде изтрит, защото току-що премахнахме ред B, посочен от неговия ref колона. Крайният резултат би бил, че итеративната обработка в този ред ще изтрие всички редове от таблицата, което очевидно е неправилно.
От друга страна, ако машината за изпълнение е обработила реда с pk =D първо, ще намери съвпадащ ред (ref =C). Ако приемем, че изпълнението е продължило в обратен pk ред, единственият ред, изтрит от таблицата, ще бъде този, където pk =B. Това е правилният резултат (не забравяйте, че заявката трябва да се изпълни, сякаш фазите на четене, запис и валидиране са се случили последователно и без припокривания).
Разделяне на фази за валидиране на ограничение
Като настрана, можем да видим друг пример за разделяне на фази, ако добавим ограничение за външен ключ за същата таблица към предишния пример:
DROP TABLE dbo.Test; CREATE TABLE dbo.Test ( pk char(1) NOT NULL, ref char(1) NULL, CONSTRAINT PK_Test PRIMARY KEY (pk), CONSTRAINT FK_ref_pk FOREIGN KEY (ref) REFERENCES dbo.Test (pk) ); INSERT dbo.Test (pk, ref) VALUES ('B', NULL), ('C', 'B'), ('D', 'C');
Планът за изпълнение на INSERT е:
Самата вложка не изисква защита за Хелоуин, тъй като планът не чете от същата таблица (източникът на данни е виртуална таблица в паметта, представена от оператора Constant Scan). Стандартът SQL обаче изисква фаза 3 (проверка на ограниченията) да настъпи след завършване на фазата на писане. Поради тази причина към плана след се добавя фазово разделяне Eager Table Spool Clustered Index Index и точно преди всеки ред да бъде проверен, за да се уверите, че ограничението на външния ключ остава валидно.
Ако започвате да мислите, че превеждането на базирана на набор заявка за декларативна SQL модификация към стабилен итеративен план за физическо изпълнение е труден бизнес, започвате да разбирате защо обработката на актуализации (от която защитата на Хелоуин е много малка част) е най-сложната част от процесора на заявки.
Директивите DELETE изискват защита за Хелоуин, където присъства самообединяване на целевата таблица.
Резюме
Защитата на Хелоуин може да бъде скъпа (но необходима) функция в планове за изпълнение, които променят данни (където „промяната“ включва целия синтаксис на SQL, който добавя, променя или премахва редове). За UPDATE
е необходима защита за Хелоуин планове, където ключовете на обща индексна структура се четат и модифицират, за INSERT
планове, където целевата таблица е посочена от страната за четене на плана, а за DELETE
планове, при които се извършва самостоятелно присъединяване към целевата таблица.
Следващата част от тази поредица ще обхване някои специални оптимизации на проблеми за Хелоуин, които се отнасят само за MERGE
изявления.
[ Част 1 | Част 2 | Част 3 | Част 4 ]