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

Існує дуже багато дивної поведінки:

  • Після серії крапель, створення, скорочення та вставок на таблицю "чоловіки" це іноді працює (повертає 12 рядків)
  • Якщо ви зміните "union select men.wife_id" на "union all select men.wife_id" або "union select isnull (men.wife_id, null)" (!!!), він повертає 12 рядків (як очікувалося).
  • Дивна поведінка, схоже, не пов'язана з типом даних стовпця "wife_id". Ми спостерігали це в системі розробок із значно більшими наборами даних.
  • "де wife_id> 0" повертає 6 рядків
  • ми також спостерігаємо дивну поведінку поглядів з таким висловом. SELECT * повертає підмножину рядків, SELECT TOP 1000 повертає всі

Відповіді:


35

Ми щось робимо не так чи це помилка SQL Server?

Це помилка з помилковими результатами, про яку слід повідомити через звичайний канал підтримки. Якщо у вас немає угоди про підтримку, можливо, допоможе дізнатися, що оплачувані випадки зазвичай повертаються, якщо Microsoft підтвердить свою поведінку як помилку.

Помилка потребує трьох інгредієнтів:

  1. Вкладені петлі із зовнішнім посиланням (додаток)
  2. Внутрішня сторона ледачої вказівної котушки, яка шукає зовнішньої опори
  3. Внутрішній бік оператора конкатенації

Наприклад, запит у запитанні створює такий зразок плану:

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

Існує багато способів видалити один з цих елементів, тому помилка більше не відтворюється.

Наприклад, можна створити індекси або статистичні дані, які означають, що оптимізатор вирішив не використовувати котушку Lazy Index. Або можна використовувати підказки, щоб змусити хеш або об'єднати замість Concatenation. Можна також переписати запит, щоб висловити ту саму семантику, але в результаті виходить інша форма плану, коли один або кілька необхідних елементів відсутні.

Детальніше

Шпулька індексу ледачих ліниво кешує внутрішні бічні рядки результатів у робочій таблиці, індексовані значеннями зовнішніх посилань (корельованих параметрів). Якщо у Spool Index Laol запитується зовнішня посилання, яку він бачив раніше, він отримує кешований рядок результатів із своєї робочої таблиці ("перемотування назад"). Якщо у котушки запитується зовнішнє опорне значення, яке вона раніше не бачила, вона запускає своє піддіреве дерево з поточним зовнішнім опорним значенням і кешує результат ("rebind"). Інікат пошуку в котушці Lazy Index вказує ключ (и) для його робочої таблиці.

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

CParamBounds: FNeedToReload

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

Коли існує одне піддерево, але використовується об'єднання об'єднань або хеш-союз, це істотне властивість встановлюється правильно на кожній ітерації, і шпулька Lazy Index перемотується або відновлюється кожен раз, коли це доречно. До речі, виразний сортування та агрегат потоку бездоганний. Я підозрюю, що Merge and Hash Union роблять копію попереднього значення, тоді як Concatenation використовує посилання. На жаль, неможливо перевірити це без доступу до вихідного коду SQL Server.

Результатом цього є те, що котушка Lazy Index у проблемній формі плану завжди вважає, що вона вже побачила поточну зовнішню посилання, перемотується назад, шукаючи в свою робочу таблицю, як правило, нічого не знаходить, тому жоден рядок не повертається для цієї зовнішньої посилання. Переходячи через виконання у відладчику, котушка виконує лише свій 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).


-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
Так, це працює, але і моя версія повинна працювати. Наведений вище абстрактний приклад - це набагато спрощена версія нашого виробничого запиту, що має набагато більше сенсу.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.