【问题标题】:UNDOCUMENTED FEATURE when SELECT in VARCHAR with trailing whitespace SQL Server在 VARCHAR 中选择带有尾随空格 SQL Server 时的未记录功能
【发布时间】:2022-05-12 04:37:32
【问题描述】:

我希望这对 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

我需要更多地了解这种行为以及我应该如何解决它。

另外值得注意的是,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 个结果?

【问题讨论】:

  • 当人们复制粘贴时..它不一定是空格。它也可以是隐形字符。您可能必须使用 PATINDEX 来过滤除字母、数字以外的任何内容。
  • 这能回答你的问题吗? Like operator and Trailing spaces in SQL Server
  • 对于等号 (=) 运算符,将忽略尾随空格。它们不适用于 前导 空格。因此'abc ' = 'abc' 然而' abc' != 'abc'。如果您担心用户在值中输入前导和尾随空格,则应在将值插入数据库之前“清理”该值。
  • @SMor 不,它没有
  • 微软有一篇关于它的知识库文章,这有帮助吗? support.microsoft.com/en-us/help/316626/…

标签: sql-server varchar trailing-whitespace


【解决方案1】:

答案是添加以下子句:

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)

【讨论】:

  • WHERE t.foo = 'Bar' AND LEN(t.Foo) = LEN('Bar') 应该足够了。
  • 正是我问这个问题的原因。运行它看看。我们发现 LEN 忽略了尾随空格。 DATALENGTH 没有。
【解决方案2】:

在字符串比较中忽略尾随空格的原因是因为固定长度字符串字段的概念,其中任何短于固定长度的内容都会自动用空格右填充。这种固定长度的字段无法区分有意义的尾随空格和填充。

为什么存在固定长度的字符串字段的理由是,它们在许多情况下显着提高了性能,并且在设计 SQL 时,基于字符的终端很常见(通常将尾随空格处理为等同于填充),报告使用等宽字体(使用尾随空格进行填充和对齐)以及数据存储和交换格式(使用固定长度字段代替广泛且昂贵的分隔符和复杂的解析逻辑)打印,都围绕固定长度字段,因此在处理的所有阶段都与此概念紧密集成。

当比较两个具有相同固定长度的固定长度字段时,字面比较当然是可能的,并且会产生正确的结果。

但是,当比较给定固定长度的固定长度字段与不同固定长度的固定长度字段时,所需的行为永远不会是在比较中包含尾随空格,因为两个这样的字段永远不会仅仅凭借它们不同的固定长度来匹配字面意思。较短的字段可以强制转换并填充到较长的字段的长度(如果不是物理上至少在概念上),但尾随空格仍将被视为填充而不是有意义的。

将固定长度字段与可变长度字段进行比较时,期望的行为也可能永远不会在比较中包含尾随空格。尝试将含义归因于比较的可变长度侧的尾随空格的更复杂的方法只会以较慢的比较逻辑和额外的概念复杂性和潜在的错误为代价。

关于为什么可变长度到可变长度的比较忽略尾随空格,因为这里的空格原则上是有意义的,其基本原理可能是在涉及固定长度字段时保持比较行为的一致性,并且避免最常见的错误类型,因为在数据库中尾随空格的虚假程度远远超过它们的意义。

如今,从头开始设计的各个方面的数据库系统可能会放弃固定长度的字段,并可能按字面意思执行所有比较,让开发人员明确处理虚假的尾随空格,但根据我的经验,这将导致额外的开发比 SQL 中的当前安排更费力且更频繁的错误,其中涉及静默忽略尾随空格的程序逻辑错误通常仅在设计用于处理非规范化数据(这是一种数据SQL 并未专门针对处理进行优化)。

所以要明确的是,这不是一个未记录的功能,而是一个设计存在的突出功能。

【讨论】:

  • 很好的解释史蒂夫。我想知道引擎盖下发生了什么,我想知道 LEN 函数是否读取 VARCHAR 中的每个字符,每次从右开始向左移动,直到它到达一个非空格字符,但是 DATALENGTH 可能从左侧读取但在它时停止到达代表字符串结尾的字符。我想你不会知道如何找出答案吧?不管怎样,谢谢你的精彩回答。
  • @WonderWorker,DATALENGTH 的不同之处在于它返回字符串的字节长度。对于 Unicode 字符,每个逻辑字符可能由多个字节组成。 LEN 返回逻辑字符串长度,包括忽略尾随空格。这两个功能不是直接互补的。我不知道实际的实现,但我希望字符串存​​储有一个长度,并且 Datalength 返回这个原始值,而 Len 取这个值,然后从右边向后工作,减去任何一系列空格,直到它到达一个非空格字符。
【解决方案3】:

如果您将查询更改为

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

看起来等于= 忽略了搜索字符串和模式中的尾随空格。然而,LIKE 不会忽略模式中的尾随空格,而是忽略搜索字符串中的额外尾随空格。前导空格永远不会被忽略。

我不知道那里的错误条目是如何出现的,但是您可以使用以下方法修复它们

UPDATE @TestResults SET Foo = TRIM(Foo)

您可以使用以下命令进行尾随空格敏感测试:

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

您可以使用以下方法进行尾随空格不敏感测试:

WHERE RTRIM(t.Foo) = RTRIM(pattern)

【讨论】:

  • 您错误地认为值中的尾随空格需要修复。它们是完全体面的值,所以如果试图通过修剪它们来修复它们,不幸的是你已经破坏了它们
  • @WonderWorker,在许多情况下不需要前导和尾随空格。例如,人名和地址。
猜你喜欢
  • 1970-01-01
  • 2010-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-24
  • 1970-01-01
相关资源
最近更新 更多