SQL Server разделяет A <> B на A <B ИЛИ A> B, получая странные результаты, если B недетерминирован

26

Мы столкнулись с интересной проблемой с SQL Server. Рассмотрим следующий пример repro:

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)

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

Heinzi
источник

Ответы:

22

Можно ли оценивать SQL Server A <> Bкак A < B OR A > B, даже если одно из выражений недетерминировано?

Это несколько спорный момент, и ответом является квалифицированное «да».

Лучшее обсуждение, о котором я знаю, было дано в ответе на сообщение об ошибке Connect Ицик Бен-Гана «Ошибка с NEWID» и «Выражения таблиц» , которое было закрыто, так как не будет исправлено. С тех пор Connect был удален, поэтому есть ссылка на веб-архив. К сожалению, много полезного материала было потеряно (или стало труднее найти) после кончины Connect. Во всяком случае, самые полезные цитаты из Джима Хогга из Microsoft:

Это затрагивает самую суть проблемы - позволяет ли оптимизация изменять семантику программы? То есть: если программа дает определенные ответы, но работает медленно, то является ли законным для оптимизатора запросов заставить эту программу работать быстрее, но также изменить полученные результаты?

Прежде чем кричать "НЕТ!" (мой личный уклон тоже :-), учтите: хорошая новость в том, что в 99% случаев ответы одинаковы. Таким образом, оптимизация запросов - явная победа. Плохая новость заключается в том, что если запрос содержит код с побочными эффектами, тогда разные планы МОГУТ действительно давать разные результаты. И NEWID () - одна из таких побочных (недетерминированных) «функций», которая выявляет разницу. [На самом деле, если вы экспериментируете, вы можете придумать другие - например, оценку короткого замыкания предложений AND: заставьте второе предложение выбросить арифметическое деление на ноль - различные оптимизации могут выполнить это второе предложение ДО первого предложения] Это отражает Объяснение Крейга, в другом месте в этой теме, что SqlServer не гарантирует, что скалярные операторы выполняются.

Таким образом, у нас есть выбор: если мы хотим гарантировать определенное поведение при наличии недетерминированного (побочного) кода - так, чтобы результаты JOIN, например, следовали семантике выполнения вложенного цикла - тогда мы может использовать соответствующие ОПЦИИ для форсирования такого поведения - как отмечает UC. Но полученный код будет работать медленно - это, по сути, стоимость работы с оптимизатором запросов.

Все это говорит о том, что мы перемещаем Оптимизатор запросов в направлении «как ожидается» поведения для NEWID () - компенсируя производительность для «результатов как ожидалось».

Одним из примеров изменения поведения в этом отношении во времени является то, что NULLIF некорректно работает с недетерминированными функциями, такими как RAND () . Существуют также другие подобные случаи, использующие, например, COALESCEподзапрос, который может привести к неожиданным результатам и который также решается постепенно.

Джим продолжает:

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

1) Оптимизатор не гарантирует время или количество выполнений скалярных функций. Это давно установленный принцип. Это фундаментальная «свобода действий», которая дает оптимизатору достаточно свободы, чтобы добиться значительных улучшений в выполнении плана запросов.

2) Это «поведение по одному разу в строке» не является новой проблемой, хотя и широко не обсуждается. Мы начали настраивать его поведение еще в выпуске Yukon. Но довольно сложно точно определить, во всех случаях, что именно это означает! Например, применимо ли это к промежуточным строкам, рассчитанным «по пути» к конечному результату? - в этом случае это явно зависит от выбранного плана. Или это относится только к строкам, которые в конечном итоге появятся в готовом результате? - здесь идет отвратительная рекурсия, я уверен, вы согласитесь!

3) Как я упоминал ранее, мы по умолчанию «оптимизируем производительность», что хорошо в 99% случаев. 1% случаев, когда это может изменить результаты, довольно легко обнаружить - «побочные» «функции», такие как NEWID, - и их легко «исправить» (следствие торговли). Это значение по умолчанию для «оптимизации производительности» снова, давно установлено и принято. (Да, это не позиция, выбранная компиляторами для обычных языков программирования, но так и будет).

Итак, наши рекомендации:

а) Избегайте использования негарантированной синхронизации и семантики количества выполнений. б) Избегайте использования NEWID () глубоко в табличных выражениях. c) Используйте OPTION, чтобы вызвать определенное поведение (торговля перф)

Надеюсь, что это объяснение поможет прояснить причины, по которым мы можем закрыть эту ошибку, поскольку "не исправлю".


Интересно, что AND NOT (s_guid = NEWID())дает тот же план выполнения

Это является следствием нормализации, которая происходит очень рано во время компиляции запроса. Оба выражения компилируются в одну и ту же нормализованную форму, поэтому создается один и тот же план выполнения.

Пол Уайт говорит, что GoFundMonica
источник
В этом случае, если мы хотим форсировать конкретный план, который, кажется, избегает проблемы, мы можем использовать WITH (FORCESCAN). Чтобы быть уверенным, мы должны использовать переменную для хранения результата NEWID () перед выполнением запроса.
Разван Соколь
11

Это задокументировано (вроде) здесь:

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

Пользовательские функции

Это не единственная форма запроса, в которой план запроса будет выполнять NEWID () несколько раз и изменять результат. Это сбивает с толку, но на самом деле очень важно, чтобы NEWID () был полезен для генерации ключей и случайной сортировки.

Больше всего сбивает с толку то, что не все недетерминированные функции на самом деле ведут себя так. Например, RAND () и GETDATE () будут выполняться только один раз за запрос.

Дэвид Браун - Microsoft
источник
Есть ли в блоге или аналогичном сообщении, объясняющее, почему / когда движок будет конвертировать "не равно" в диапазон?
Мистер Магу
3
Не то, что я знаю о. Может быть рутиной, потому что =, <и >может быть эффективно оценена с BTree.
Дэвид Браун - Microsoft
5

Для чего стоит, если вы посмотрите на этот старый стандартный документ SQL 92 , требования к неравенству описаны в разделе « 8.2 <comparison predicate>» следующим образом:

1) Пусть X и Y - любые два соответствующих <элемента конструктора значения строки> s. Пусть XV и YV будут значениями, представленными X и Y соответственно.

[...]

ii) «X <> Y» истинно тогда и только тогда, когда XV и YV не равны.

[...]

7) Пусть Rx и Ry будут двумя <конструкторами значений строк <предиката сравнения>, а RXi и RYi будут i-тыми <элементами конструкторов значений строк Rx и Ry соответственно. «Rx <comp op> Ry» имеет значение true, false или unknown, как указано ниже:

[...]

б) «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 спасибо за отзыв! Я думаю, что ваша интерпретация определенно верна. Это первый раз, когда я читаю этот документ. Он упоминает значения в контексте значения, представленного «конструктором значения строки», который в других местах документа может быть скалярным подзапросом (среди многих других вещей). В частности, скалярный подзапрос кажется недетерминированным. Но я действительно не знаю, о чем говорю =)
Джош Дарнелл