SQL Server розбиває A <> B на A <B АБО> B, даючи дивні результати, якщо B недетермінований


26

Ми зіткнулися з цікавою проблемою із SQL Server. Розглянемо наступний приклад докори:

CREATE TABLE #test (s_guid uniqueidentifier PRIMARY KEY);
INSERT INTO #test (s_guid) VALUES ('7E28EFF8-A80A-45E4-BFE0-C13989D69618');

SELECT s_guid FROM #test
WHERE s_guid = '7E28EFF8-A80A-45E4-BFE0-C13989D69618'
  AND s_guid <> NEWID();

DROP TABLE #test;

скрипка

Будь-ласка, забудьте на мить, що s_guid <> NEWID()стан здається абсолютно марним - це лише мінімальний приклад докору. Оскільки ймовірність NEWID()відповідності деякому заданому постійному значенню надзвичайно мала, його слід щоразу оцінювати на ІСТИНА.

Але це не так. Запуск цього запиту зазвичай повертає 1 рядок, але іноді (досить часто, більше 1 разу з 10) повертає 0 рядків. Я відтворив його за допомогою SQL Server 2008 у моїй системі, і ви можете відтворити його в режимі он-лайн із вищезазначеною скрипкою (SQL Server 2014).

Переглядаючи план виконання показує, що аналізатор запитів, очевидно, розбиває умову на s_guid < NEWID() OR s_guid > NEWID():

Скріншот плану запитів

... що повністю пояснює, чому вона іноді виходить з ладу (якщо перший згенерований ідентифікатор менший, а другий більший, ніж даний ідентифікатор).

Чи дозволяється SQL Server оцінювати A <> Bяк A < B OR A > B, навіть якщо один із виразів не є детермінованим? Якщо так, то де це задокументовано? Або ми знайшли помилку?

Цікаво, що AND NOT (s_guid = NEWID())дає той самий план виконання (і той самий випадковий результат).

Ми виявили цю проблему, коли розробник хотів необов’язково виключити певний рядок і використав:

s_guid <> ISNULL(@someParameter, NEWID())

як "ярлик" для:

(@someParameter IS NULL OR s_guid <> @someParameter)

Я шукаю документацію та / або підтвердження помилки. Код є не всім таким релевантним, тому обхідні шляхи не потрібні.


Відповіді:


22

Чи дозволяється SQL Server оцінювати A <> Bяк A < B OR A > B, навіть якщо один із виразів не є детермінованим?

Це дещо суперечливий момент, і відповідь - кваліфіковане "так".

Найкраща дискусія, про яку я знаю, була надана у відповідь на повідомлення про помилку підключення Іціка Бен-Гана Про помилку NEWID та Table Express , яке було закрито, оскільки це не виправлено. З тих пір Connect не вийшов, тому посилання є на веб-архів. На жаль, через користь Коннету було втрачено (або зробити його складніше) багато корисного матеріалу. Як би там не було, найкорисніші цитати від Jim Hogg з Microsoft є:

Це підходить до самої суті проблеми - чи дозволяється оптимізація змінити семантику програми? Тобто, якщо програма дає певні відповіді, але працює повільно, чи правомірним є те, що Оптимізатор запитів змусить цю програму працювати швидше, але також змінити отримані результати?

Перш ніж кричати "НІ!" (теж моя особиста схильність :-), врахуйте: хороша новина полягає в тому, що в 99% випадків відповіді однакові. Тож оптимізація запитів - це явна перемога. Погана новина полягає в тому, що якщо запит містить побічний код, то різні плани МОЖЧЕ дають різні результати. І NEWID () - одна з таких побічних (недетермінованих) 'функцій', яка піддає різниці. [Насправді, якщо ви експериментуєте, ви можете розробити інші - наприклад, коротке замикання клавіш AND: змусити другий додаток кидати арифметичний поділ на нуль - різні оптимізації можуть виконати цей другий пункт до першого пункту] Це відображає Пояснення Крейга, в іншому місці цього потоку, що SqlServer не гарантує, коли виконуються скалярні оператори.

Отже, у нас є вибір: якщо ми хочемо гарантувати певну поведінку за наявності недетермінованого (побічного) коду - щоб результати JOIN, наприклад, дотримувались семантики виконання вкладеного циклу - тоді ми може використовувати відповідні ВАРІАНТИ, щоб змусити цю поведінку - як зазначає UC. Але отриманий код буде працювати повільно - це, по суті, вартість збивання оптимізатора запитів.

Все, що було сказано, ми рухаємо Оптимізатор запитів у напрямку поведінки «як очікувалося» для NEWID () - торгуючи продуктивністю для «результатів, як очікувалося».

Одним із прикладів зміни поведінки в цьому плані з часом NULLIF працює неправильно з недетермінованими функціями, такими як RAND () . Існують також інші подібні випадки, в яких використовується, наприклад, COALESCEпідзапит, який може призвести до несподіваних результатів, і які також вирішуються поступово.

Джим продовжує:

Замикання петлі. . . Я обговорював це питання з командою Dev. І врешті-решт ми вирішили не змінювати поточну поведінку з наступних причин:

1) Оптимізатор не гарантує терміни чи кількість виконання скалярних функцій. Це довготривалий принцип. Це фундаментальне "вільне середовище", яке дозволяє оптимізатору достатньо свободи отримати значні покращення у виконанні плану запитів.

2) Ця "поведінка один раз на рядок" не є новою проблемою, хоча вона широко не обговорюється. Ми почали налаштовувати його поведінку ще у випуску Yukon. Але точно важко точно визначити, у всіх випадках, що саме це означає! Наприклад, чи застосовується це до проміжних рядків, обчислених "на шляху" до кінцевого результату? - у такому випадку це чітко залежить від обраного плану. Або це стосується лише рядків, які з часом з’являться у завершеному результаті? - тут відбувається жахлива рекурсія, адже я впевнений, що ти погодишся!

3) Як я вже згадував раніше, ми за замовчуванням «оптимізуємо продуктивність» - це добре для 99% випадків. 1% випадків, коли це може змінити результати, досить легко помітити побічні «функції», такі як NEWID, і їх легко «виправити» (як наслідок торгівлі перф.). Цей за замовчуванням для "оптимізації продуктивності" знову є давно встановленим та прийнятим. (Так, це не позиція, яку обирають компілятори для звичайних мов програмування, але так і нехай).

Отже, наші рекомендації:

a) Уникайте покладатися на негарантовану семантику часу та кількості виконань. b) Уникайте використання NEWID () глибоких табличних виразів. в) Використовуйте ОПЦІЮ, щоб змусити певну поведінку (торгувати perf)

Сподіваємось, що це пояснення допомагає з’ясувати наші причини закриття цієї помилки як «не виправлено».


Цікаво, що AND NOT (s_guid = NEWID())дає той самий план виконання

Це наслідок нормалізації, що відбувається дуже рано під час складання запитів. Обидва вирази складаються в точно однаковій нормованій формі, тому створюється однаковий план виконання.


У цьому випадку, якщо ми хочемо примусити певний план, який, здається, уникне проблеми, ми можемо використовувати С (FORCESCAN). Напевно, ми повинні використовувати змінну для зберігання результату NEWID () перед виконанням запиту.
Разван Сокол

11

Це задокументовано (свого роду) тут:

Кількість разів, коли функція, визначена в запиті, реально виконується, може змінюватись між планами виконання, побудованими оптимізатором. Приклад - функція, яку викликає підзапит у пункті WHERE. Кількість виконаних запитів та його функції може змінюватися залежно від різних шляхів доступу, обраних оптимізатором.

Функції, визначені користувачем

Це не єдина форма запиту, де план запитів виконає NEWID () кілька разів та змінить результат. Це заплутано, але насправді важливо, щоб NEWID () був корисним для генерації ключів та випадкового сортування.

Найбільше бентежить те, що не всі недетерміновані функції насправді ведуть себе так. Наприклад, RAND () та GETDATE () виконуватимуться лише один раз за запитом.


Чи є повідомлення в блозі чи подібне, що пояснює, чому / коли двигун перетворить "не дорівнює" в діапазон?
Містер Магуо

3
Не те, що я знаю. Це може бути звичайним, тому що =, <і >може бути ефективно оцінено проти BTree.
Девід Браун - Майкрософт

5

Для чого варто, якщо подивитися цей старий стандартний документ SQL 92 , вимоги навколо нерівності описані в розділі " 8.2 <comparison predicate>" так:

1) Нехай X і Y - будь-які два відповідні <конструкторні значення конструктора елемента> s. Нехай XV і YV - значення, представлені відповідно X і Y, відповідно.

[...]

ii) "X <> Y" вірно, якщо і лише тоді, коли XV і YV не рівні.

[...]

7) Нехай Rx і Ry є двома <конструкторами значень рядків> s <предиката порівняння>, а RXi і RYi - i-го <елемент елемента конструктора рядків> s Rx і Ry відповідно. "Rx <comp op> Ry" є істинним, хибним або невідомим так:

[...]

б) "x <> Ry" вірно, якщо і тільки тоді, коли RXi <> RYi для деяких i.

[...]

h) "x <> Ry" є хибним, якщо і лише тоді, коли "Rx = Ry" є істинним.

Примітка: я включив 7b і 7h для повноти, оскільки вони говорять про <>порівняння - я не думаю, що порівняння конструкторів значень рядків з декількома значеннями реалізовано в T-SQL, якщо я просто масово не розумію, що це говорить - що цілком можливо

Це купа заплутаного сміття. Але якщо ви хочете продовжити дайвінг дайвінг ...

Я думаю, що 1.ii - це елемент, який застосовується в цьому сценарії, оскільки ми порівнюємо значення "елементів конструктивних значень рядків".

ii) "X <> Y" вірно, якщо і лише тоді, коли XV і YV не рівні.

В основному це X <> Yтвердження вірно, якщо значення, представлені X і Y, не рівні. Оскільки X < Y OR X > Yє логічно еквівалентним переписати цей присудок, оптимізатор може використовувати це.

Стандарт не ставить жодних обмежень щодо цього визначення, пов'язаного з детермінованістю (або що б там не було) елементів конструктора значень рядків з будь-якої сторони <>оператора порівняння. Відповідальність коду користувача полягає в тому, щоб вирішити той факт, що значення значення на одній стороні може бути недетермінованим.


1
Я утримаюся від голосування (вгору чи вниз), але не переконаний. У цитатах, які ви надаєте, згадується "значення" . Я розумію, що порівняння полягає в двох значеннях, по одному на кожній стороні. Не між двома (або більше) даними значення кожної сторони. Плюс до цього, стандарт (принаймні, 92, який ви цитуєте) взагалі не згадує про недетерміновані функції. Аналогічним міркуванням, як і ваше, можна вважати, що продукт SQL, який відповідає стандарту, не забезпечує жодної недетермінованої функції, а лише ті, що згадуються в стандарті.
ypercubeᵀᴹ

@yper дякую за відгук! Я думаю, що ваше тлумачення безумовно справедливе. Це перший раз, коли я читав цього документа. Це згадування значень у контексті значення, представленого "конструктором значень рядків", що в інших місцях документа, за яким він каже, може бути скалярним підзапитом (серед багатьох інших речей). Зокрема, скалярний запит здається, що він може бути недетермінованим. Але я дійсно не знаю, про що я говорю =)
Джош Дарнелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.