Непредсказуемые результаты выбора SQL Server (ошибка dbms?)

37

Ниже приведен простой пример, который возвращает странные результаты, которые непредсказуемы, и мы не можем объяснить это в нашей команде. Мы делаем что-то не так или это ошибка SQL Server?

После некоторого исследования мы сократили область поиска до условия объединения в подзапросе , который выбирает одну запись из таблицы "men"

Он работает, как и ожидалось, в SQL Server 2000 (возвращает 12 строк), но в 2008 и 2012 годах он возвращает только одну строку.

create table dual (dummy int)

insert into dual values (0)

create table men (
man_id int,
wife_id int )

-- there are 12 men, 6 married 
insert into men values (1, 1)
insert into men values (2, 2)
insert into men values (3, null)
insert into men values (4, null)
insert into men values (5, null)
insert into men values (6, 3)
insert into men values (7, 5)
insert into men values (8, 7)
insert into men values (9, null)
insert into men values (10, null)
insert into men values (11, null)
insert into men values (12, 9)

Это возвращает только одну строку: 1 1 2

select 
man_id,
wife_id,
(select count( * ) from 
    (select dummy from dual
     union select men.wife_id  ) family_members
) as family_size
from men
--where wife_id = 2 -- uncomment me and try again

Раскомментируйте последнюю строку и получите: 2 2 2

Есть много странных поведений:

  • После ряда выпадений, создания, усечения и вставки в таблицу "men" это иногда работает (возвращает 12 строк)
  • Когда вы меняете «union select men.wife_id» на «union all select men.wife_id» или «union select isnull (men.wife_id, null)» (!!!), возвращается 12 строк (как и ожидалось).
  • Странное поведение, похоже, не связано с типом данных столбца "woman_id ". Мы наблюдали это в системе разработки с гораздо большими наборами данных.
  • «whereouse_id> 0» возвращает 6 строк
  • мы также наблюдаем странное поведение взглядов с такими утверждениями. SELECT * возвращает подмножество строк, SELECT TOP 1000 возвращает все
Ризард Боциан
источник

Ответы:

35

Мы делаем что-то не так или это ошибка SQL Server?

Это ошибка с ошибочными результатами, о которой вы должны сообщать через обычный канал поддержки. Если у вас нет соглашения о поддержке, это может помочь узнать, что платные инциденты обычно возвращаются, если Microsoft подтверждает поведение как ошибку.

Ошибка требует трех ингредиентов:

  1. Вложенные циклы с внешней ссылкой (применяется)
  2. Внутренняя ленивая индексная шпуля, которая ищет внешнюю ссылку
  3. Оператор конкатенации на внутренней стороне

Например, запрос в вопросе создает план, подобный следующему:

Аннотированный план

Есть много способов удалить один из этих элементов, поэтому ошибка больше не воспроизводится.

Например, можно создать индексы или статистику, которые означают, что оптимизатор решает не использовать Lazy Index Spool. Или можно использовать подсказки для принудительного объединения хеша или объединения вместо использования конкатенации. Можно также переписать запрос, чтобы выразить ту же семантику, но в результате получается другая форма плана, в которой один или несколько обязательных элементов отсутствуют.

Подробнее

Lazy Index Spool лениво кэширует строки результатов внутренней стороны в рабочей таблице, индексируемой значениями внешних ссылок (коррелированных параметров). Если в Lazy Index Spool запрашивается внешняя ссылка, которую он видел ранее, он извлекает кэшированную строку результатов из своей рабочей таблицы («перемотка назад»). Если в буфере запрашивается внешнее эталонное значение, которого он не видел раньше, оно запускает свое поддерево с текущим внешним эталонным значением и кэширует результат («повторная привязка»). Предикат поиска в Lazy Index Spool указывает ключ (и) для его рабочей таблицы.

Проблема возникает в этой конкретной форме плана, когда катушка проверяет, является ли новая внешняя ссылка такой же, как и ранее. Объединение вложенных циклов корректно обновляет свои внешние ссылки и уведомляет операторов о своем внутреннем вводе через их PrepRecomputeметоды интерфейса. В начале этой проверки операторы внутренней стороны читают CParamBounds:FNeedToReloadсвойство, чтобы увидеть, изменилась ли внешняя ссылка с прошлого раза. Пример трассировки стека показан ниже:

CParamBounds: FNeedToReload

Когда показанное выше поддерево существует, особенно там, где используется конкатенация, что-то идет не так (возможно, проблема ByVal / ByRef / Copy) с привязками, которые CParamBounds:FNeedToReloadвсегда возвращают false независимо от того, изменилась ли внешняя ссылка на самом деле или нет.

Когда существует одно и то же поддерево, но используется объединение слиянием или объединение хэшей, это важное свойство устанавливается правильно на каждой итерации, и ленивый индексный пул перематывает или перематывает каждый раз при необходимости. Между прочим, отличная сортировка и совокупность потоков безупречны. Я подозреваю, что Merge и Hash Union делают копию предыдущего значения, тогда как Concatenation использует ссылку. К сожалению, почти невозможно проверить это без доступа к исходному коду SQL Server.

Конечным результатом является то, что Lazy Index Spool в форме проблемного плана всегда думает, что он уже видел текущую внешнюю ссылку, перематывает, просматривая свою рабочую таблицу, обычно ничего не находит, поэтому для этой внешней ссылки строка не возвращается. Выполняя выполнение в отладчике, спул выполняет только свой RewindHelperметод, но не ReloadHelperметод (в этом контексте reload = rebind). Это очевидно в плане выполнения, потому что все операторы под катушкой имеют «Число выполнений = 1».

RewindHelper

Исключением, конечно, является первая внешняя ссылка, которая дается 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 рядов

И, как указано, получается 8 рядов, все с c1 = 1конечно:

Результат 8 строки

Я заметил, что вы открыли элемент Connect для этой ошибки, но на самом деле это не то место, где можно сообщать о проблемах, влияющих на производительность. Если это так, вам действительно следует обратиться в службу поддержки Microsoft.


Эта ошибка с ошибочными результатами была исправлена ​​на каком-то этапе. Он больше не воспроизводится для меня ни в одной версии SQL Server, начиная с 2012 года. Он воспроизводится в SQL Server 2008 R2 SP3-GDR, сборка 10.50.6560.0 (X64).

Пол Уайт говорит, что GoFundMonica
источник
-3

Почему вы используете подзапрос без оператора from? Я думаю, что это может привести к разнице в 2005 и 2008 серверах. Может быть, вы могли бы пойти с явным объединением?

select 
m1.man_id,
m1.wife_id,
(select count( * ) from 
    (select dummy from dual
     union
     select m2.wife_id
     from men m2
     where m2.man_id = m1.man_id) family_members
) as family_size
from men m1

источник
3
Да, это работает, но моя версия тоже должна работать. Выше абстрактный пример представляет собой значительно упрощенную версию нашего производственного запроса, которая имеет гораздо больше смысла.