Ми щось робимо не так чи це помилка SQL Server?
Це помилка з помилковими результатами, про яку слід повідомити через звичайний канал підтримки. Якщо у вас немає угоди про підтримку, можливо, допоможе дізнатися, що оплачувані випадки зазвичай повертаються, якщо Microsoft підтвердить свою поведінку як помилку.
Помилка потребує трьох інгредієнтів:
- Вкладені петлі із зовнішнім посиланням (додаток)
- Внутрішня сторона ледачої вказівної котушки, яка шукає зовнішньої опори
- Внутрішній бік оператора конкатенації
Наприклад, запит у запитанні створює такий зразок плану:
Існує багато способів видалити один з цих елементів, тому помилка більше не відтворюється.
Наприклад, можна створити індекси або статистичні дані, які означають, що оптимізатор вирішив не використовувати котушку Lazy Index. Або можна використовувати підказки, щоб змусити хеш або об'єднати замість Concatenation. Можна також переписати запит, щоб висловити ту саму семантику, але в результаті виходить інша форма плану, коли один або кілька необхідних елементів відсутні.
Детальніше
Шпулька індексу ледачих ліниво кешує внутрішні бічні рядки результатів у робочій таблиці, індексовані значеннями зовнішніх посилань (корельованих параметрів). Якщо у Spool Index Laol запитується зовнішня посилання, яку він бачив раніше, він отримує кешований рядок результатів із своєї робочої таблиці ("перемотування назад"). Якщо у котушки запитується зовнішнє опорне значення, яке вона раніше не бачила, вона запускає своє піддіреве дерево з поточним зовнішнім опорним значенням і кешує результат ("rebind"). Інікат пошуку в котушці Lazy Index вказує ключ (и) для його робочої таблиці.
Проблема виникає в цій специфічній формі плану, коли золотник перевіряє, чи є нова зовнішня посилання такою ж, яку вона бачила раніше. Вкладений цикл приєднання оновлений коректно оновлює свої зовнішні посилання та сповіщає операторів про свій внутрішній вхід за допомогою своїх PrepRecompute
методів інтерфейсу. На початку цієї перевірки внутрішні бічні оператори читають CParamBounds:FNeedToReload
властивість, щоб побачити, чи змінилася зовнішня посилання за останній час. Приклад сліду стека показаний нижче:
Коли піддерево, показане вище, зокрема, де використовується Concatenation, щось піде не так (можливо, проблема ByVal / ByRef / Copy) з прив’язками таким чином, що CParamBounds:FNeedToReload
завжди повертається помилково, незалежно від того, змінилася зовнішня посилання насправді чи ні.
Коли існує одне піддерево, але використовується об'єднання об'єднань або хеш-союз, це істотне властивість встановлюється правильно на кожній ітерації, і шпулька Lazy Index перемотується або відновлюється кожен раз, коли це доречно. До речі, виразний сортування та агрегат потоку бездоганний. Я підозрюю, що Merge and Hash Union роблять копію попереднього значення, тоді як Concatenation використовує посилання. На жаль, неможливо перевірити це без доступу до вихідного коду SQL Server.
Результатом цього є те, що котушка Lazy Index у проблемній формі плану завжди вважає, що вона вже побачила поточну зовнішню посилання, перемотується назад, шукаючи в свою робочу таблицю, як правило, нічого не знаходить, тому жоден рядок не повертається для цієї зовнішньої посилання. Переходячи через виконання у відладчику, котушка виконує лише свій RewindHelper
метод, і ніколи його ReloadHelper
метод (reload = rebind у цьому контексті). Це очевидно в плані виконання, оскільки всі оператори в котушці мають "Кількість виконань = 1".
Виняток, звичайно, є для першої зовнішньої довідки, наданої вказівник Lazy Index Spool. Це завжди виконує піддерево і кешує рядок результатів у робочій таблиці. Усі наступні ітерації призводять до перемотування назад, яке створюватиме рядок (єдиний кешований ряд), коли поточна ітерація має те саме значення для зовнішньої посилання, як і в перший раз.
Отже, для будь-якого заданого набору даних із зовнішньої сторони вкладеного циклу приєднання запит поверне стільки рядків, скільки є дублікатів першого обробленого рядка (плюс один для першого рядка, звичайно).
Демо
Дані таблиці та вибірки:
CREATE TABLE #T1
(
pk integer IDENTITY NOT NULL,
c1 integer NOT NULL,
CONSTRAINT PK_T1
PRIMARY KEY CLUSTERED (pk)
);
GO
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6);
Наступний (тривіальний) запит створює правильну кількість двох для кожного рядка (загалом 18) за допомогою об'єднання об'єднань:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C;
Якщо ми тепер додамо підказку для запиту, щоб змусити об'єднати:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C
OPTION (CONCAT UNION);
План виконання має проблематичну форму:
І результат тепер неправильний, всього три ряди:
Хоча така поведінка не гарантована, перший рядок із кластеризованого індексу сканування має c1
значення 1. Є два інші рядки з цим значенням, тож утворюється три рядки.
Тепер обрізаємо таблицю даних і завантажимо її ще дублікатами першого "ряду":
TRUNCATE TABLE #T1;
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (1), (1), (1), (1), (1);
Тепер план об'єднання:
І, як зазначено, виходить 8 рядів, всі c1 = 1
звичайно:
Я зауважую, що ви відкрили елемент Connect для цієї помилки, але насправді це не місце для повідомлення про проблеми, які впливають на виробництво. Якщо це так, вам дійсно слід звернутися до служби підтримки Microsoft.
Ця помилка з результатами була виправлена на певному етапі. Він більше не відтворюється для мене в жодній версії SQL Server з 2012 року. Вона робить репро для SQL Server 2008 R2 SP3-GDR збірки 10.50.6560.0 (X64).