Преобразование в datetime не выполняется только в предложении WHERE?

У меня проблема с некоторыми запросами к SQL-серверу. Оказывается, у меня есть таблица с полями «Attibute_Name» и «Attibute_Value», которые могут быть любого типа, хранящиеся в varchar. (Да, я знаю.)

Кажется, что все даты для определенного атрибута хранятся в формате «ГГГГ-ММ-ДД чч: мм: сс» (не уверен на 100%, здесь миллионы записей), поэтому я могу выполнить этот код без проблем. :

select /*...*/ CONVERT(DATETIME, pa.Attribute_Value)
from 
    ProductAttributes pa
    inner join Attributes a on a.Attribute_ID = pa.Attribute_ID
where 
    a.Attribute_Name = 'SomeDate'

Однако, если я выполню следующий код:

select /*...*/ CONVERT(DATETIME, pa.Attribute_Value)
from 
    ProductAttributes pa
    inner join Attributes a on a.Attribute_ID = pa.Attribute_ID
where 
    a.Attribute_Name = 'SomeDate'
    and CONVERT(DATETIME, pa.Attribute_Value) < GETDATE()

Я получаю следующую ошибку: Ошибка преобразования при преобразовании даты и / или времени из символьной строки.

Почему он не работает в предложении where, а не в предложении select?

Еще одна подсказка:

Если вместо фильтрации по Attribute_Name я использую фактический Attribute_ID, хранящийся в базе данных (PK), он будет работать без проблем.

select /*...*/ CONVERT(DATETIME, pa.Attribute_Value)
from 
    ProductAttributes pa
    inner join Attributes a on a.Attribute_ID = pa.Attribute_ID
where 
    a.Attribute_ID = 15
    and CONVERT(DATETIME, pa.Attribute_Value) < GETDATE()

Обновление Спасибо всем за ответы. Мне было трудно выбрать правильный ответ, потому что каждый указывал на то, что было полезно для понимания проблемы. Это определенно было связано с порядком казни. Оказалось, что мой первый запрос работал правильно, потому что сначала было выполнено предложение WHERE, а затем SELECT. Мой второй запрос не удался по той же причине (поскольку атрибуты не были отфильтрованы, преобразование не удалось при выполнении того же предложения WHERE). Мой третий запрос сработал, потому что идентификатор был частью индекса (PK), поэтому он имел приоритет и сначала детализировал результаты по этому условию.

Спасибо!


person Alpha    schedule 31.08.2011    source источник
comment
есть ли еще один атрибут с именем sOmeDaTe - если вы используете сортировку без учета регистра? Наверное, это испортило твои соединения.   -  person YetAnotherUser    schedule 01.09.2011
comment
Кажется, вы предполагаете какую-то оценку короткого замыкания или гарантированный порядок предикатов в предложении WHERE. Это не гарантируется. Когда вы используете смешанные типы данных в таком столбце, единственный безопасный способ справиться с ними - использовать выражение CASE.   -  person Martin Smith    schedule 01.09.2011
comment
Что такое таблица PHA? Если PHA - это таблица, отличная от PA, то может показаться, что в данных PHA есть неконвертируемые записи, а в PA - нет.   -  person NoAlias    schedule 01.09.2011
comment
Я знаю, почему вы не уверены на 100% - если вы используете столбец VARCHAR для хранения значений даты / времени, любой желающий может хранить там любой старый мусор, который ему нужен. Для EAV я всегда предпочитал разделять их на соответствующие столбцы по крайней мере для трех базовых типов, строкового атрибута, атрибута даты / времени и числового атрибута. Это не идеально и не является бесплатным компромиссом, но у вас есть гораздо больше шансов гарантировать, что недействительные данные не попадут в вашу базу данных.   -  person Aaron Bertrand    schedule 01.09.2011
comment
@YetAnotherUser Только что проверил, сортировка столбцов - SQL_Latin1_General_CP1_CI_AS, так что я думаю, что это так, но я не нашел другого значения с другим регистром. Хороший выстрел!   -  person Alpha    schedule 01.09.2011
comment
@Martin Smith: Не совсем, я на самом деле абстрагируюсь с этой точки зрения, поэтому я ожидал, что это сработает или потерпит неудачу независимо от того, где проводятся сравнения. Вот что мотивировало мой вопрос.   -  person Alpha    schedule 01.09.2011
comment
@NoAlias ​​Прости. Я намеренно скрываю реальную структуру БД по двум причинам: 1. чтобы было проще показать проблему, которая у меня возникла 2. не разглашать конфиденциальную информацию, которую я не могу раскрыть по закону. Я уже исправил это в запросах выше.   -  person Alpha    schedule 01.09.2011
comment
@Aaron Bertrand Я согласен, и я знаю, что в этом столбце есть некоторые значения, не относящиеся к дате, но я настраиваю фильтры на то, что должно быть только datetime (и факт того, что один из запросов не завершился ошибкой, должен подтвердите, что все они могут быть правильно преобразованы в datetime). Это не так уж и плохо, поскольку значения поступают из автоматизированной системы, поэтому я могу ожидать, что они будут действительными ... но я осознаю риск.   -  person Alpha    schedule 01.09.2011
comment
@Alpha - Не понимаю, почему вы говорите, что абстрагированы от этой точки зрения. Attribute_Value не содержит дат, вы преобразуете этот столбец в дату. Вы должны верить, что какая-то предварительная фильтрация гарантирована.   -  person Martin Smith    schedule 01.09.2011
comment
@Martin Smith Я только что прочитал статью Ремуса и наконец понял, что вы сказали. Вы правы, в этом есть смысл.   -  person Alpha    schedule 01.09.2011


Ответы (6)


Если преобразование находится в предложении WHERE, оно может быть оценено для гораздо большего количества записей (значений), чем было бы, если бы оно появилось в списке проекций. Я уже говорил об этом раньше в другом контексте, см. Функции T-SQL не подразумевают определенный порядок выполнения и При коротком замыкании логического оператора SQL Server. Ваш случай еще проще, но похож, и в конечном итоге основная причина та же: не предполагайте императивный порядок выполнения при работе с декларативным языком, таким как SQL.

Лучшее решение, с большим отрывом, - очистить данные и изменить тип столбца на тип DATETIME или DATETIME2. Все другие обходные пути будут иметь тот или иной недостаток, поэтому вам может быть лучше просто поступить правильно.

Обновить

После более внимательного рассмотрения (извините, я @VLDB и просматриваю SO только между сессиями) я понимаю, что у вас есть хранилище EAV с присущей семантикой без типов (attribute_value может быть строкой, датой, int и т. Д.). Я считаю, что лучше всего использовать sql_variant в хранилище и вплоть до клиента (например, проекта sql_variant). Вы можете согласовать тип в клиенте, все клиентские API имеют методы для извлечения внутреннего типа из sql_variant, см. Использование данных sql_variant (ну, почти все клиентские API ... Использование типа данных sql_variant в среде CLR). С sql_variant вы можете хранить несколько типов без проблем с просмотром строковых представлений, вы можете использовать _ 6_ для проверки таких вещей, как BaseType в сохраненных значениях, и вы даже можете думать, например, о проверке ограничений, чтобы обеспечить правильность типа данных.

person Remus Rusanu    schedule 31.08.2011
comment
Я бы очень не решился использовать SQL_VARIANT , если вы не выполняете всю презентацию, фильтрацию и сравнение на клиенте. В нашей системе EAV мы быстро отказались от SQL_VARIANT в пользу выделенных столбцов для каждого типа. Итак, у вас есть два NULL в каждой строке, но вам не нужно иметь дело со всеми другими неприятными вещами, которые с этим связаны. Чтобы встряхнуть обе стороны, я немного написал об ограничениях здесь: sqlblog.com/blogs/aaron_bertrand/archive/2009/10/12/ ... можете ли вы показать свой запрос, если столбец были SQL_VARIANT? - person Aaron Bertrand; 01.09.2011
comment
Я понимаю вашу точку зрения. Выполнение и агрегирование по структуре sql_variant EAV станет жертвой проблем с приведением, в то время как выделенный столбец / тип может легко агрегировать значения, потому что он знает, что все они находятся в поле для этого типа, и тип не требует CAST. Действительное возражение. - person Remus Rusanu; 01.09.2011

Кажется, вы предполагаете какую-то оценку короткого замыкания или гарантированный порядок предикатов в предложении WHERE. Это не гарантируется. Когда вы используете смешанные типы данных в таком столбце, единственный безопасный способ справиться с ними - использовать выражение CASE.

Используйте (например,)

CONVERT(DATETIME, 
      CASE WHEN ISDATE(pa.Attribute_Value) = 1 THEN pa.Attribute_Value END)

Нет

CONVERT(DATETIME, pa.Attribute_Value)
person Martin Smith    schedule 31.08.2011

Это связано с порядком обработки SELECT запроса. Предложение WHERE обрабатывается задолго до SELECT. Он должен определить, какие строки включить / исключить. Предложение, использующее имя, должно использовать сканирование, которое исследует все строки, некоторые из которых не содержат действительных данных даты / времени, тогда как ключ, вероятно, ведет к поиску, и ни одна из недопустимых строк не включается в точку. Преобразование в списке SELECT выполняется последним, и очевидно, что к этому времени он не будет пытаться преобразовать недопустимые строки. Поскольку вы смешиваете данные даты и времени с другими данными, вы можете рассмотреть возможность хранения даты или числовых данных в выделенных столбцах с правильными типами данных. А пока отсрочить проверку можно следующим образом:

SELECT /* ... */
FROM
(
  SELECT /* ... */
    FROM ProductAttributes AS pa
    INNER JOIN dbo.Attributes AS a
    ON a.Attribute_ID = pa.Attribute_ID
    WHERE a.Attribute_Name = 'SomeDate'
    AND ISDATE (pa.Attribute_Value) = 1
) AS z
WHERE CONVERT(CHAR(8), AttributeValue, 112) < CONVERT(CHAR(8), GETDATE(), 112);

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

person Aaron Bertrand    schedule 31.08.2011
comment
Это не обязательно сработает. Вычислить скаляр в списке SELECT можно перед фильтром WHERE. См., Например, этот ответ или этот элемент подключения < / а> - person Martin Smith; 01.09.2011
comment
Нет, это не сработает. Вы делаете предположение, что порядок объявления (подзапрос) подразумевает порядок оценки, как в rusanu.com/2011/08/10/ QO может выбрать план, который оценивает ПРЕОБРАЗОВАНИЕ до сравнение attribute_name и вызовет ошибку преобразования. - person Remus Rusanu; 01.09.2011
comment
Нет, я должен был сказать, вы можете попробовать отложить ... лучший ответ - хранить данные в выделенных столбцах с правильным типом данных, вместо того, чтобы помещать все в столбец varchar. - person Aaron Bertrand; 01.09.2011
comment
@Remus, как насчет того, чтобы мы вообще не форсировали преобразование DATETIME. Мы не должны получать плохие данные в результатах из-за ISDATE () внутри подзапроса, и ясно, что этот столбец в любом случае не индексируется, поэтому анализируемость не является проблемой ... В конце концов, что, если OP не не в силах сделать схему идеальной, и ему приходится иметь дело с тем, что у него есть? - person Aaron Bertrand; 01.09.2011
comment
ISDATE() может быть перемещен QO из подзапроса и оценен после CONVERT. QO может сделать это бесплатно. На самом деле этого не произошло бы, если только очень конкретный индекс не ставит «ловушку», слишком приятную для QO. Я видел реальные случаи поддержки продуктов из приложений, которые делали что-то подобное и были сломаны при обновлении до новой версии SQL. на самом деле не существует какого-либо надежного промежуточного подхода, о котором я знаю, который налагал бы строгую систему типов (столбец проекта DATETIME) поверх гибкого хранилища EAV, как в OP. sql_variant - imho самое близкое совпадение (магазин и проект sql_variant) - person Remus Rusanu; 01.09.2011
comment
Но кого волнует, где происходит ISDATE ()? Преобразования DATETIME больше не происходит, поэтому больше не будет ошибки - в худшем случае это приведет к фильтрации строк в менее оптимальном месте. Мы все согласны с тем, что лучшим решением было бы изменить тип данных, но это не единственный ответ, и это вовсе не обязательно приемлемый ответ. - person Aaron Bertrand; 01.09.2011
comment
Да, если преобразование удалено (нет более строго типизированной проекции), то все проблемы, связанные с несоответствующим CAST, исчезнут. - person Remus Rusanu; 01.09.2011

Мне кажется, проблема с данными. Взгляните на данные, когда вы выберете их двумя разными способами, попробуйте найти различные длины, а затем выберите элементы в разных наборах и оцените их. Также проверьте наличие нулей? (Я не уверен, что произойдет, если вы попытаетесь преобразовать null в datetime)

person kmcc049    schedule 31.08.2011
comment
преобразование null в datetime приводит к нулю. - person George Mastros; 01.09.2011

Я думаю, проблема в том, что у вас плохая дата в вашей базе данных (очевидно).

В вашем первом примере, где вы не проверяете дату в предложении WHERE, все даты, где a.attribute.Name = 'SomeDate' действительны, поэтому он никогда не пытается преобразовать неправильную дату.

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

В вашем третьем примере изменение использования Attribute_Id, вероятно, изменяет план запроса так, что он сначала ищет только те, у которых id = 15, а затем проверяет, имеют ли эти записи действительную дату, что они и делают. (Возможно, Attribute_Id индексируется, а Attribute_name нет)

Итак, у вас где-то плохое свидание, но его нет ни в одной записи с Arttribute_id = 15.

person PaulStock    schedule 31.08.2011

Вы можете проверить планы выполнения. Может случиться так, что при первом запросе второй критерий (CONVERT(DATETIME, pa.Attribute_Value) < GETDATE()) сначала оценивается по всем строкам, включая строки с недопустимыми данными (не с датой), а в случае второго - сначала оценивается a.Attribute_ID = 15. Таким образом, исключая строки со значениями, отличными от даты.

кстати, второй тоже может быть быстрее, и если у вас нет ничего из Attributes в списке выбора, вы можете избавиться от inner join Attributes a on a.Attribute_ID = pa.Attribute_ID.

На этой ноте, было бы желательно избавиться от EAV, пока еще не поздно :)

person nad2000    schedule 31.08.2011
comment
вы можете попробовать пересчитать статистику таблицы. Если ProductAttributes содержит миллионы строк, оценка CONVERT(DATETIME, pa.Attribute_Value) < GETDATE() очень неэффективна. Попробуйте: ANALYZE TABLE ProductAttributes; ANALYZE TABLE Attributes; и снова выполните первый запрос. - person nad2000; 01.09.2011