Мы делаем что-то не так или это ошибка SQL Server?
Это ошибка с ошибочными результатами, о которой вы должны сообщать через обычный канал поддержки. Если у вас нет соглашения о поддержке, это может помочь узнать, что платные инциденты обычно возвращаются, если Microsoft подтверждает поведение как ошибку.
Ошибка требует трех ингредиентов:
- Вложенные циклы с внешней ссылкой (применяется)
- Внутренняя ленивая индексная шпуля, которая ищет внешнюю ссылку
- Оператор конкатенации на внутренней стороне
Например, запрос в вопросе создает план, подобный следующему:
Есть много способов удалить один из этих элементов, поэтому ошибка больше не воспроизводится.
Например, можно создать индексы или статистику, которые означают, что оптимизатор решает не использовать Lazy Index Spool. Или можно использовать подсказки для принудительного объединения хеша или объединения вместо использования конкатенации. Можно также переписать запрос, чтобы выразить ту же семантику, но в результате получается другая форма плана, в которой один или несколько обязательных элементов отсутствуют.
Подробнее
Lazy Index Spool лениво кэширует строки результатов внутренней стороны в рабочей таблице, индексируемой значениями внешних ссылок (коррелированных параметров). Если в Lazy Index Spool запрашивается внешняя ссылка, которую он видел ранее, он извлекает кэшированную строку результатов из своей рабочей таблицы («перемотка назад»). Если в буфере запрашивается внешнее эталонное значение, которого он не видел раньше, оно запускает свое поддерево с текущим внешним эталонным значением и кэширует результат («повторная привязка»). Предикат поиска в Lazy Index Spool указывает ключ (и) для его рабочей таблицы.
Проблема возникает в этой конкретной форме плана, когда катушка проверяет, является ли новая внешняя ссылка такой же, как и ранее. Объединение вложенных циклов корректно обновляет свои внешние ссылки и уведомляет операторов о своем внутреннем вводе через их PrepRecompute
методы интерфейса. В начале этой проверки операторы внутренней стороны читают CParamBounds:FNeedToReload
свойство, чтобы увидеть, изменилась ли внешняя ссылка с прошлого раза. Пример трассировки стека показан ниже:
Когда показанное выше поддерево существует, особенно там, где используется конкатенация, что-то идет не так (возможно, проблема ByVal / ByRef / Copy) с привязками, которые CParamBounds:FNeedToReload
всегда возвращают false независимо от того, изменилась ли внешняя ссылка на самом деле или нет.
Когда существует одно и то же поддерево, но используется объединение слиянием или объединение хэшей, это важное свойство устанавливается правильно на каждой итерации, и ленивый индексный пул перематывает или перематывает каждый раз при необходимости. Между прочим, отличная сортировка и совокупность потоков безупречны. Я подозреваю, что Merge и Hash Union делают копию предыдущего значения, тогда как Concatenation использует ссылку. К сожалению, почти невозможно проверить это без доступа к исходному коду SQL Server.
Конечным результатом является то, что Lazy Index Spool в форме проблемного плана всегда думает, что он уже видел текущую внешнюю ссылку, перематывает, просматривая свою рабочую таблицу, обычно ничего не находит, поэтому для этой внешней ссылки строка не возвращается. Выполняя выполнение в отладчике, спул выполняет только свой 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).