简体   繁体   English

SQL Server : ISNULL(compound NULL condition, 'a string') 在某些情况下只返回第一个字符

[英]SQL Server : ISNULL(compound NULL condition, 'a string') returns only the 1st character, under certain circumstance(s)

I'm a self-taught, vaguely competent SQL user.我是一个自学成才的 SQL 用户。 For a view that I'm writing, I'm trying to develop a 'conditional LEFT ' string-splitting command (presumably later to be joined by a 'conditional RIGHT ' - whereby:对于我正在编写的视图,我正在尝试开发一个“有条件的LEFT ”字符串拆分命令(大概稍后会加入一个“有条件的RIGHT ”——由此:

  • If a string (let's call it 'haystack') contains a particular pattern (let's call it 'needle'), it will be pruned to the left of that pattern如果一个字符串(我们称之为“haystack”)包含一个特定的模式(我们称之为“needle”),它将被修剪到该模式的左侧
  • Otherwise, the entire string will be passed unaltered.否则,整个字符串将原封不动地传递。

So, if our pattern is ' - ',所以,如果我们的模式是' - ',

  • 'A long string - containing the pattern' will output as 'A long string' 'A long string - contains the pattern' 将输出为 'A long string'
  • 'A string without the pattern' will be returned as-is. “没有模式的字符串”将按原样返回。

Rather than using the crudest ways to do this, I'm trying to come up with a way that avoids having to repeat any clause (such as if 0 < CHARINDEX , then take CHARINDEX - 1, etc.) and instead leverages conditional NULL ing.我没有使用最粗略的方法来做到这一点,而是试图想出一种方法来避免重复任何子句(例如 if 0 < CHARINDEX ,然后采用CHARINDEX - 1 等),而是利用条件NULL ing .

Yet - here's what I get for trying to be creative - I've hit what seems to be a really basic stumbling block.然而——这就是我尝试发挥创造力的结果——我遇到了一个看似非常基本的绊脚石。 Please observe the following code and results, and let me know whether you can replicate it - and hence whether it's a bug or I've missed something peculiar.请观察以下代码和结果,让我知道您是否可以复制它 - 因此它是一个错误还是我错过了一些特殊的东西。 I have tested this on SQL Server both 2008 R2 and 2014, both Express editions.我已经在 SQL Server 2008 R2 和 2014(两个 Express 版本)上对此进行了测试。

select
    -- ISNULL: returns 'a big old string'
    ISNULL(null, 'a big old string'),

    -- NULLIF: returns NULL
    left(
        'a big old string',
        nullif
        (
            CHARINDEX
            (
                'needle',
                'haystack'
            ), 0
        ) - 1
    ),

    -- combined: returns just 'a' (1st character of ISNULL condition)
    ISNULL(
        left
        (
            'a big old string', -- the input string. In reality, this would be a column alias, etc.
            nullif
            (
                CHARINDEX       -- Search for the splitting pattern
                (
                    'needle',
                    'haystack'
                ), 0            -- If it's not found, return NULL instead of the usual 0
            ) - 1               -- so that this subtraction produces a NULL, not an invalid negative index
        ),
        'a big old string'      -- If the pattern was not found, we should return the input unaltered
    );

/*
---------------- ---- ----
a big old string NULL a

(1 row(s) affected)
*/

Why do these 2 clauses work as expected in isolation, but when I combine them, rather than getting the sum of their effects, I get only the 1st character of the ISNULL string - 'a'?为什么这 2 个子句可以单独按预期工作,但是当我将它们组合起来时,我没有得到它们效果的总和,而是只得到ISNULL字符串的第一个字符- 'a'?

Is there some kind of implicit CAST to varchar(1) ?是否有某种隐式CASTvarchar(1) Deliberately cast ing to varchar(max) made no difference.故意cast ing 转换为varchar(max)没有任何区别。 What else could be going on here?这里还能发生什么?

Am I just doing something really stupid?我只是在做一些非常愚蠢的事情吗? Because from here, I can't figure out what I'm doing wrong, and so it really seems like a bug.因为从这里开始,我无法弄清楚我做错了什么,所以它真的看起来像是一个错误。 I hoped testing on 2014 would prove it to be a bug in the old 2008 R2, but alas, they act identically (or, rather, don't).我希望 2014 年的测试将证明它是旧的 2008 R2 中的一个错误,但唉,它们的行为相同(或者更确切地说,不是)。

Thanks in advance for, hopefully, saving me from what would presumably be an evening of baffled existential crisis.提前感谢您,希望能让我免于陷入困惑的生存危机之夜。

This is the difference between isnull and coalesce -- and since your first parameter to isnull is char(1), that will be the type of return value of the statement.这就是isnullcoalesce之间的区别——因为isnull 的第一个参数是char(1),这将是语句返回值的类型。 With coalesce you'll get the correct result.使用coalesce 你会得到正确的结果。

Isnull :是空的

Returns the same type as check_expression.返回与 check_expression 相同的类型。 If a literal NULL is provided as check_expression, returns the datatype of the replacement_value.如果提供文字 NULL 作为 check_expression,则返回 replacement_value 的数据类型。 If a literal NULL is provided as check_expression and no replacement_value is provided, returns an int.如果提供文字 NULL 作为 check_expression 并且没有提供 replacement_value,则返回一个 int。

Coalesce :合并

Returns the data type of expression with the highest data type precedence.返回具有最高数据类型优先级的表达式的数据类型。 If all expressions are nonnullable, the result is typed as nonnullable.如果所有表达式都不可为空,则结果类型为不可为空。

There are two parts to this problem, the first is the nature of the ISNULL operator, it will use the datatype and length of the first argument.这个问题有两个部分,第一个是ISNULL运算符的性质,它将使用第一个参数的数据类型和长度。 A simple example would be:一个简单的例子是:

DECLARE @A CHAR(1) = NULL,
        @B VARCHAR(MAX) =  'This is a test';

SELECT TOP 1 Test = ISNULL(@A, @B);

This returns T and checking the execution plan XML we can see the implicit conversion of "This is a Test" to CHAR(1) :这将返回T并检查执行计划 XML 我们可以看到"This is a Test"CHAR(1)的隐式转换:

<ScalarOperator ScalarString="isnull([@A],CONVERT_IMPLICIT(char(1),[@B],0))">
    <Intrinsic FunctionName="isnull">
    <ScalarOperator>
        <Identifier>
        <ColumnReference Column="@A" />
        </Identifier>
    </ScalarOperator>
    <ScalarOperator>
        <Convert DataType="char" Length="1" Style="0" Implicit="true">
        <ScalarOperator>
            <Identifier>
            <ColumnReference Column="@B" />
            </Identifier>
        </ScalarOperator>
        </Convert>
    </ScalarOperator>
    </Intrinsic>
</ScalarOperator>

Your example is not quite so straightforward since you don't have your types nicely defined like above, but if we do define the dataypes:您的示例并不是那么简单,因为您没有像上面那样很好地定义类型,但是如果我们确实定义了数据类型:

DECLARE @A VARCHAR(MAX) =  'a big old string',
        @B VARCHAR(MAX) = 'needle',
        @C VARCHAR(MAX) = 'haystack';

SELECT TOP 1 ISNULL(LEFT(@A, NULLIF(CHARINDEX(@B, @C), 0) - 1), @A);

We get the result as expected.我们得到了预期的结果。 So something else is happening under the hood.因此,幕后正在发生其他事情。 The query plan does not delve into the inner workings of the constant evaluation, but the following demonstrates what is happening:查询计划没有深入研究常量评估的内部工作原理,但以下内容演示了正在发生的事情:

SELECT  Test = LEFT('a big old string', NULLIF(CHARINDEX('needle', 'haystack'), 0) - 1)
INTO    #T;

SELECT  t.name, c.max_length
FROM    tempdb.sys.columns AS c
        INNER JOIN sys.types AS t
            ON t.system_type_id = c.system_type_id
            AND t.user_type_id = c.user_type_id
WHERE   [object_id] = OBJECT_ID(N'tempdb..#T');

----------------
name        max_length
varchar     1

Basically, by using the SELECT INTO sytax with your left expression shows that a when the NULL length is passed to LEFT the resulting datatype is VARCHAR(1) , however , this is not always the case.基本上,通过将SELECT INTO语法与左表达式一起使用,表明当将 NULL 长度传递给LEFT ,结果数据类型为VARCHAR(1)但是,情况并非总是如此。 If I simply hard code NULL into the LEFT function:如果我只是将NULL硬编码到LEFT函数中:

SELECT  Test = LEFT('a big old string', NULL)
INTO    #T;

--------------------
name        max_length
varchar     16

Then you get the length of the string passed, but a case expression that should be optimised away to the same thing, yields a length of 1 again:然后你得到传递的字符串的长度,但是一个应该优化为相同内容的 case 表达式,再次产生长度为 1:

SELECT  TOP 1 Test = LEFT('a big old string', CASE WHEN 1 = 1 THEN NULL ELSE 1 END)
INTO    #T;

----------------
name        max_length
varchar     1

I suspect it is related to the default behaviour of VARCHAR , where the default length is 1, eg:我怀疑这与VARCHAR的默认行为有关,其中默认长度为 1,例如:

DECLARE @A VARCHAR = 'This is a Test';

SELECT  Value = @A,                                         -- T
        MaxLength = SQL_VARIANT_PROPERTY(@A, 'MaxLength')   -- 1

But I can't tell you why you would see different behaviour for NULL and CASE WHEN 1 = 1 THEN NULL ELSE 1 END .但我不能告诉你为什么你会看到NULLCASE WHEN 1 = 1 THEN NULL ELSE 1 END不同行为。 If you wanted to get the bottom of what is going on in the constant evaluation I think you would probably need to re-ask on the DBA site and hope that one of the real SQL Server Gurus picks it up.如果您想了解持续评估中发生的事情,我认为您可能需要在 DBA 站点上重新询问,并希望真正的 SQL Server 专家之一能够了解它。

In summary, LEFT(<constant>, <constant expression>) where <constant expression> yields NULL is implicitly typed as VARCHAR(1) , and this implicit type is used in ISNULL evaluation.总之, LEFT(<constant>, <constant expression>)其中<constant expression>产生NULL被隐式类型化为VARCHAR(1) ,并且在ISNULL评估中使用此隐式类型。

For what it is worth, if you explicitly type the result of your LEFT function then you get the expected result:值得一提的是,如果您明确键入LEFT函数的结果,那么您会得到预期的结果:

SELECT ISNULL(
            CAST(
                LEFT(
                    'a big old string', 
                    NULLIF(CHARINDEX('needle', 'haystack'), 0) - 1
                    ) 
                AS VARCHAR(MAX))
                , 'a big old string');

An additional point is that when you say you don't want to repeat any expressions (If 0 < CHARINDEX, then take CHARINDEX - 1, etc.), there are two things you should know, the first is that NULLIF(<expression>, <value>) expands to a case expression - CASE WHEN <expression> = <value> THEN NULL ELSE <expression> END , so is repeated, the second is that this doesn't matter, SQL Server can identify that this is the same expression used twice, and will evaluate it once and refer to the same result each time it is used.另外一点是,当你说你不想重复任何表达式(如果 0 < CHARINDEX,然后取 CHARINDEX - 1 等)时,你应该知道两件事,第一是NULLIF(<expression>, <value>)扩展为 case 表达式 - CASE WHEN <expression> = <value> THEN NULL ELSE <expression> END ,所以是重复的,第二个是这个没关系,SQL Server 可以识别这是相同的表达式使用了两次,每次使用时都会计算一次并引用相同的结果。

Seems to me like you are over complicating a simple thing.在我看来,你把一件简单的事情复杂化了。

This sql code should do what you describe:这个 sql 代码应该做你所描述的:

Declare @SomeString varchar(max) = 'asdf asdf - cvbncvbn',
        @Needle varchar(100) = '-'


DECLARE @NeedlePattern varchar(102) = '%' + @Needle + '%'

SELECT CASE WHEN PATINDEX(@NeedlePattern, @SomeString) > 0 THEN
         LEFT(@SomeString, PATINDEX(@NeedlePattern, @SomeString) - LEN(@NeedlePattern)+1)
       ELSE
         @SomeString
       END

See sql fiddle here在这里查看 sql 小提琴

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM