НЕДОКУМЕНТИРОВАННАЯ ФУНКЦИЯ при SELECT в VARCHAR с завершающим пробелом SQL Server

Я надеюсь, что это интересная головоломка для эксперта по SQL.

Когда я запускаю следующий запрос, я ожидаю, что он не даст никаких результатов.

-- Create a table variable Note: This same behaviour occurs in standard tables.

DECLARE @TestResults TABLE (Id int IDENTITY(1,1) NOT NULL, Foo VARCHAR(100) NOT NULL, About VARCHAR(1000) NOT NULL)

-- Add some test data Note: Without space, space prefix and space suffix

INSERT INTO @TestResults(Foo, About) VALUES('Bar', 'No spaces')
INSERT INTO @TestResults(Foo, About) VALUES('Bar ', 'Space Suffix')
INSERT INTO @TestResults(Foo, About) VALUES(' Bar', 'Space prefix')

-- SELECT statement that is filtered by a value without a space and also a value with a space suffix

SELECT 
     t.Foo
     , t.About
FROM @TestResults t
WHERE t.Foo like 'Bar '
AND t.Foo like 'Bar'
AND t.Foo = 'Bar '
AND t.Foo = 'Bar'

Результаты возвращают одну строку:

[Foo]  [About]
Bar    Space Suffix

Проблема в том, что люди копируют и вставляют значения из электронных писем и т. д. и каким-то образом попадают в таблицу. Я рассматриваю это как отдельную проблему, поскольку я LTRIM(RTRIM(Foo)) как триггер INSERT и UPDATE, но некоторые каким-то образом проходят через сеть.

Мне нужно больше узнать об этом поведении и о том, как его обойти.

Также стоит отметить, что LEN(Foo) тоже нечетный, а именно:

DECLARE @TestResults TABLE (Id int IDENTITY(1,1) NOT NULL, Foo VARCHAR(100) NOT NULL, About VARCHAR(1000) NOT NULL)
INSERT INTO @TestResults(Foo, About) VALUES('Bar', 'No spaces')
INSERT INTO @TestResults(Foo, About) VALUES('Bar ', 'Space Suffix')
INSERT INTO @TestResults(Foo, About) VALUES(' Bar', 'Space prefix')

SELECT 
     t.Foo
     , LEN(Foo) [Length]
     , t.About
FROM @TestResults t

Дает следующие результаты:

[Foo]   [Length]  [About]
Bar     3         No spaces
Bar     3         Space Suffix
 Bar    4         Space prefix

Без каких-либо боковых размышлений, что мне нужно изменить в моем предложении WHERE, чтобы вернуть 0 результатов, как и ожидалось?


person WonderWorker    schedule 17.03.2020    source источник
comment
когда люди копируют и вставляют... это не обязательно должен быть пробел. Это могут быть и невидимые символы. Возможно, вам придется использовать PATINDEX для фильтрации чего-либо, кроме алфавитов и цифр.   -  person Ganesh Chandrasekaran    schedule 17.03.2020
comment
Отвечает ли это на ваш вопрос? Оператор Like и конечные пробелы в SQL Server   -  person SMor    schedule 17.03.2020
comment
Для оператора равенства (=) конечные пробелы игнорируются. Они не предназначены для начальных пробелов. Поэтому 'abc ' = 'abc' однако, ' abc' != 'abc'. Если вас беспокоит, что пользователи вводят начальные и конечные пробелы в значениях, вам следует очистить значение перед его вставкой в ​​базу данных.   -  person Larnu    schedule 17.03.2020
comment
@SMor Нет, это не так   -  person WonderWorker    schedule 17.03.2020
comment
У Microsoft есть статья в базе знаний об этом, поможет ли это? поддержка. microsoft.com/en-us/help/316626/   -  person Tim Mylott    schedule 17.03.2020
comment
@Larnu Цитата ... Я LTRIM (RTRIM (Foo)) в качестве триггера INSERT и UPDATE, но некоторые каким-то образом проходят через сеть ....   -  person WonderWorker    schedule 17.03.2020
comment
Тогда это не пробел, как предполагает @GaneshChandrasekaran. Возможно, там есть пробелы нулевой длины или разрывы строк. Но в вашем примере вы не обрезаете, эти значения определенно ' Bar' и 'Bar '.   -  person Larnu    schedule 17.03.2020
comment
@Tim Mylott Великолепно, спасибо. Я буду следить за этим.   -  person WonderWorker    schedule 17.03.2020
comment
@Ganesh Chandrasekaran Я проверил их в шестнадцатеричном редакторе, и они содержат 20 символов (стандартный пробел).   -  person WonderWorker    schedule 17.03.2020
comment
Затем TRIM (или LTRIM и RTRIM) удалит их. Покажите нам пример, в котором вы вставляете значение с начальным/конечным пробелом, оборачиваете TRIM, и оно все еще имеет начальный/конечный пробел. Если нет, то мы не можем воспроизвести проблему, поскольку известно, что завершающие пробелы игнорируются, а ведущие — нет.   -  person Larnu    schedule 17.03.2020
comment
@WonderWorker Я считаю, что пробел - это Asc (32). проверьте диаграмму здесь ascii.cl   -  person Ganesh Chandrasekaran    schedule 17.03.2020
comment
@Ganesh Chandrasekaran Это правильно, однако при работе с битами и байтами чаще всего (и полезно из-за кратности 8, т.е. 8 бит в байте) думать в шестнадцатеричном, а не в десятичном виде. Шестнадцатеричный код 32 равен 20, то есть 2 x 16. Вот почему вы склонны видеть %20 в URL, HTML и т. д. Это все еще символ 32, но преобразованный в шестнадцатеричный. Поэкспериментируйте с памятью и бинарными файлами, и вы скоро поймете, почему шестнадцатеричный формат так ценен.   -  person WonderWorker    schedule 19.03.2020


Ответы (3)


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

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

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

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

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

С точки зрения того, почему сравнения переменной длины с переменной длиной игнорируют конечные пробелы, поскольку здесь пробелы в принципе могут иметь смысл, объяснение, вероятно, заключается в сохранении согласованности в поведении сравнения, например, когда задействованы поля фиксированной длины, и избегание наиболее распространенных своего рода ошибка, поскольку конечные пробелы в базах данных гораздо чаще являются ложными, чем значимыми.

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

Чтобы было ясно, это не недокументированная функция, а заметная функция, которая существует по замыслу.

person Steve    schedule 03.02.2021
comment
Отличное объяснение Стив. Интересно, что происходит под капотом, мне интересно, читает ли функция LEN каждый символ в VARCHAR, начиная справа и каждый раз перемещаясь влево, пока не достигнет символа, отличного от пробела, но DATALENGTH может читать слева, но останавливаться, когда он достигает символа, представляющего конец строки. Я не думаю, что вы знаете, как узнать, не так ли? В любом случае, спасибо за ваш блестящий ответ. - person WonderWorker; 03.02.2021
comment
@WonderWorker, DATALENGTH отличается тем, что возвращает длину строки в байтах. Для символов Unicode каждый логический символ может состоять из нескольких байтов. LEN возвращает длину логической строки, включая пробелы в конце. Эти две функции не являются прямым дополнением друг друга. Я не знаю фактических реализаций, но я ожидаю, что строки хранятся с длиной, и Datalength возвращает это необработанное значение, в то время как Len берет значение, но затем работает в обратном направлении справа, вычитая любую серию пробелов, пока не достигнет не пробела персонаж. - person Steve; 03.02.2021

Если вы измените запрос на

SELECT 
     Foo
     , About
     , CASE WHEN Foo LIKE 'Bar ' THEN 'T' ELSE 'F' END As Like_Bar_Space
     , CASE WHEN Foo LIKE 'Bar'  THEN 'T' ELSE 'F' END As Like_Bar
     , CASE WHEN Foo =    'Bar ' THEN 'T' ELSE 'F' END As EQ_Bar_Space
     , CASE WHEN Foo =    'Bar'  THEN 'T' ELSE 'F' END As EQ_Bar
FROM @TestResults

это дает вам лучший обзор, так как вы видите результат различных условий по отдельности:

Foo     About         Like_Bar_Space   Like_Bar   EQ_Bar_Space   EQ_Bar
------  ------------  ---------------  ---------  -------------  ------
Bar     No spaces      F                T          T              T
Bar     Space Suffix   T                T          T              T
 Bar    Space prefix   F                F          F              F

Похоже, что equals = игнорирует конечные пробелы как в искомой строке, так и в шаблоне. LIKE, однако, не игнорирует завершающий пробел в шаблоне, но игнорирует дополнительный завершающий пробел в искомой строке. Ведущие пробелы никогда не игнорируются.

Я не знаю, как туда попали неправильные записи, но вы можете исправить их с помощью

UPDATE @TestResults SET Foo = TRIM(Foo)

Вы можете сделать тест, чувствительный к пробелам, с помощью:

WHERE t.Foo + ";" = pattern + ";" 

Вы можете сделать нечувствительный к пробелу тест с помощью:

WHERE RTRIM(t.Foo) = RTRIM(pattern)
person Olivier Jacot-Descombes    schedule 17.03.2020

Ответ заключается в добавлении следующего пункта:

AND DATALENGTH(t.Foo) = DATALENGTH('Bar')

Выполнение следующего запроса...

DECLARE @Chars TABLE (CharNumber INT NOT NULL)

DECLARE @CharNumber INT = 0

WHILE(@CharNumber <= 255)
    BEGIN
        INSERT INTO @Chars(CharNumber) VALUES(@CharNumber)

        SET @CharNumber = @CharNumber + 1

    END

SELECT 
    CharNumber
    , IIF('Test' = 'Test' + CHAR(CharNumber),1,0) ['Test' = 'Test' + CHAR(CharNumber)]
    , IIF('Test' LIKE 'Test' + CHAR(CharNumber),1,0) ['Test' LIKE 'Test' + CHAR(CharNumber)]
    , IIF(LEN('Test') = LEN('Test' + CHAR(CharNumber)),1,0) [LEN('Test') = LEN('Test' + CHAR(CharNumber))]
    , IIF(DATALENGTH('Test') = DATALENGTH('Test' + CHAR(CharNumber)),1,0) [DATALENGTH('Test') = DATALENGTH('Test' + CHAR(CharNumber))]
FROM @Chars
WHERE ('Test' = 'Test' + CHAR(CharNumber))
OR ('Test' LIKE 'Test' + CHAR(CharNumber))
OR (LEN('Test') = LEN('Test' + CHAR(CharNumber)))
ORDER BY CharNumber

... дает следующие результаты...

CharNumber  'Test' = 'Test' + CHAR(CharNumber)  'Test' LIKE 'Test' + CHAR(CharNumber)   LEN('Test') = LEN('Test' + CHAR(CharNumber))    DATALENGTH('Test') = DATALENGTH('Test' + CHAR(CharNumber))
0           1                                   1                                       0                                               0
32          1                                   0                                       1                                               0
37          0                                   1                                       0                                               0

DATALENGTH можно использовать для проверки равенства двух VARCHAR, поэтому исходный запрос можно исправить следующим образом:

-- Create a table variable Note: This same behaviour occurs in standard tables.

DECLARE @TestResults TABLE (Id int IDENTITY(1,1) NOT NULL, Foo VARCHAR(100) NOT NULL, About VARCHAR(1000) NOT NULL)

-- Add some test data Note: Without space, space prefix and space suffix

INSERT INTO @TestResults(Foo, About) VALUES('Bar', 'No spaces')
INSERT INTO @TestResults(Foo, About) VALUES('Bar ', 'Space Suffix')
INSERT INTO @TestResults(Foo, About) VALUES(' Bar', 'Space prefix')

-- SELECT statement that is filtered by a value without a space and also a value with a space suffix

SELECT 
     t.Foo
     , t.About
FROM @TestResults t
WHERE t.Foo like 'Bar '
AND t.Foo like 'Bar'
AND t.Foo = 'Bar ' 
AND t.Foo = 'Bar' 
AND DATALENGTH(t.Foo) = DATALENGTH('Bar') -- Additional clause

Я также сделал функцию для использования вместо =

ALTER FUNCTION dbo.fVEQ( @VarCharA VARCHAR(MAX), @VarCharB VARCHAR(MAX) ) 
RETURNS BIT 
WITH SCHEMABINDING
AS
BEGIN
    -- Added by WonderWorker on 18th March 2020

    DECLARE @Result BIT = IIF(
        (@VarCharA = @VarCharB AND DATALENGTH(@VarCharA) = DATALENGTH(@VarCharB))

    , 1, 0)

    RETURN @Result

END

.. Вот тест для всех 256 символов, используемых в качестве завершающих символов, чтобы доказать, что это работает.

-- Test fVEQ with all 256 characters

DECLARE @Chars TABLE (CharNumber INT NOT NULL)

DECLARE @CharNumber INT = 0

WHILE(@CharNumber <= 255)
    BEGIN
        INSERT INTO @Chars(CharNumber) VALUES(@CharNumber)

        SET @CharNumber = @CharNumber + 1

    END

SELECT 
    CharNumber
    , dbo.fVEQ('Bar','Bar' + CHAR(CharNumber)) [fVEQ Trailing Char Test]
    , dbo.fVEQ('Bar','Bar') [fVEQ Same test]
    , dbo.fVEQ('Bar',CHAR(CharNumber) + 'Bar') [fVEQ Leading Char Test]
FROM @Chars
WHERE (dbo.fVEQ('Bar','Bar' + CHAR(CharNumber)) = 1)
AND (dbo.fVEQ('Bar','Bar') = 0)
AND (dbo.fVEQ('Bar',CHAR(CharNumber) + 'Bar') = 1)
person WonderWorker    schedule 18.03.2020
comment
WHERE t.foo = 'Bar' AND LEN(t.Foo) = LEN('Bar') должно быть достаточно. - person Olivier Jacot-Descombes; 18.03.2020
comment
Именно поэтому я задал вопрос. Запустите его и посмотрите. Мы обнаружили, что LEN игнорирует конечные пробелы. DATALENGTH нет. - person WonderWorker; 19.03.2020