Shorthand CASE with additional conditions - sql

Up until today I held as true the notion that shorthand CASE doesn't accept additional conditions, as in
CASE evaluate_this
WHEN TRUE AND another_field ... THEN ...
would never work.
Today, as I was distractingly writing some code, I violated this rule just to realise that one CASE expression still worked as intended, the other didn't.
Assuming the following data:
id
boolean_one
boolean_two
field_one
field_two
1
TRUE
TRUE
NULL
'a'
2
TRUE
FALSE
2
'a'
3
FALSE
TRUE
8
NULL
4
FALSE
FALSE
NULL
NULL
The one that did not work looked like this:
CASE boolean_one
WHEN TRUE AND field_one IS NOT NULL THEN field_one * UNIFORM(1, 10, RANDOM())
ELSE field_one
END AS new_field_one
The idea is that in most circumstances the value for new_field_one should match field_one, the only exception being when boolean_one = TRUE AND field_one IS NOT NULL in which case it should be assigned the product of field_one and a random integer.
Below the desired and actual values for new_field_one based on the above
id
desired
actual
1
NULL
NULL
2
20
20
3
8
8
4
NULL
5
So it applies the randomisation also when boolean_one = FALSE AND field_one IS NULL.
Fixing the expression to
CASE
WHEN boolean_one = TRUE AND field_one IS NOT NULL THEN...
solves the issue. What I find more interesting, and confusing, is that the second expression still did what it was meant to do:
CASE boolean_two
WHEN TRUE AND field_two = 'b' THEN 'a'
WHEN TRUE AND field_two = 'a' THEN 'b'
ELSE field_two
END AS new_field_two
which in this case means that new_field_two = 'b' for the row with ID = 1, and all other remain equal to field_two.
So, this doesn't seem to be an issue of adding an extra condition to a shorthand CASE per se, I would guess it must have something to do specifically with the fact that the second condition is a NULL check. Am I on the right track?

With the shortcut syntax the "when expression" is evaluated as a whole and then that value is compared against the "case expression". It's not matching the value as true and then running an extra check
In fact your true and is essentially ignored because true and X is just equivalent to X in Boolean logic. The test then is whether boolean_one matches the non-null state of field_one. The second and fourth rows correspond to true = true and false = false which is when you're seeing the random result values.
You could certainly nest case if there were a real benefit to legibility:
case boolean_one
when true then
case field_one is not null ... end
else field_one
end

Shawnt00 answer is good,
I like to think of the two forms as
chained If expression
CASE
WHEN check1 THEN r1
WHEN check2 THEN r2
WHEN check3 THEN r3
END
if the same as
IF(check1)
r1
ELSE IF (check2)
r2
...
where-as this "shortcut" version is the Switch statement form:
CASE expression
WHEN val1 THEN r1
WHEN val2 THEN r2
WHEN val3 THEN r2
END
is the same as
SWITCH(expression){
CASE val1: r1
CASE val2: r2
CASE val3: r3
}
which is to say the VAL things need to evaluate down, thus for you stumbled upon cases, you are writing extra boolean logic, which resolves down to a boolean answer. And as the doc's note, for a expression to match a value they need to be of the same type of can be auto converted.
thus this gives a SQL error:
select
case false
when true then 1
when 'bob' then 2
end;
Boolean value 'bob' is not recognized
because the second value cannot be auto converted to the same type as the case expression, which is a boolean value.

Related

Is there boolean type for column in Oracle SQL?

I tried to:
select 1>2 from dual;
but got:
ORA-00923: FROM keyword not found where expected
Is there boolean type for column expression in Oracle SQL?
I able to do:
select case when 1>2 then 'T' else 'F' end from dual;
Originally I tried to compare date fields and the quickest way I found was getting difference and look to sign...
UPDATE I tried SIGN function, I don't know if it is vendor specific extension:
select SIGN(1-2) from dual;
select SIGN(DATE '2017-01-02' - DATE '2017-02-12') from dual;
but this trick doesn't work for strings...
No there is not, you can use 0 and 1 just as yes/no.
If you need to get the result 1 if something is true and 0 if it is false, you can use a case expression:
select case when (any_logical_condition_here) then 1 else 0 end as my_col
from ....
where ....
For example:
select case when 1 > 2 then 1 else 0 end as bool_result
from dual;
BOOL_RESULT
---------------------------------------
0
NOTE though - "Boolean" refers strictly to the TRUE/FALSE logic, it has no place for UNKNOWN. When you deal with null, as you must in SQL, you need three-valued logic. The case expression as written above returns 1 when the logical condition is true and 0 otherwise. Try it with 1 > null - the truth value is UNKNOWN, the case expression will return 0.

Why does this case statement not work?

Why does this case statement not work? My table is defined as below. I want the case to return true when M = "M" or false if it is not. Something does not seem to look right with this CASE statement but this is the simple case statement. But I thought that could be as many WHEN values as needed but in this cases there is only True or False and it does not appear that there could be anything different.
The input should be all of the M column in the row. I am expecting the output to be one row since I only have one row that has an M value in the column. I am not certain if the case statement should return false in this case.
CASE M = "M <-- is this is what the criteria is based on
I found that this is not the case however from the post below. When i meant that this did not look right I was referring two the fact that the WHEN portion of the code
WHEN true THEN something
just did not look like it normally does.
Code:
select CASE M = "M"
WHEN true THEN "PPP"
WHEN false THEN "False -- Look Again!"
ELSE "Not Found"
END
from Bat;
Image:
New Code:
select *,
CASE
WHEN M = 'M' THEN
Case
when O = 'O' then 'you found a double (MM)' <--Compiles but does not show result
End
WHEN M is null THEN 'False -- Look Again!'
END as 'What is this value'
from Bat;
That fixed the first problem but you can embed case statements N deep I would think so if i search again on O it should return a correct. But the first case returns only one record. So my second case would have to search in the record correct.
The normal way to write this is:
select (CASE WHEN M = 'M' THEN 'PPP'
WHEN M <> 'M' THEN 'False -- Look Again!'
ELSE 'Not Found'
END)
from Bat;
or:
select (CASE WHEN M is null then 'Not Found'
WHEN M = 'M' THEN 'PPP'
ELSE 'False -- Look Again!'
END)
from Bat;
If I had to guess, your version has unexpected behavior for NULL values, but it is hard to say without knowing what you expect or what it is doing.
Change it like this
select CASE
WHEN M = 'M' THEN 'PPP'
WHEN M is null THEN 'False -- Look Again!'
ELSE "Not Found"
END
from Bat;

NULL IN (1,2,NULL) returns false

Why does this SQL-statement return 0?
SELECT CASE WHEN NULL IN (9,1,NULL) THEN 1 ELSE 0 END
SQL is based on three-valued logic, where there are three truth values: TRUE, FALSE, and UNKNOWN. The special NULL value is a placeholder for "missing data" and if you compare something, anything, with missing data the result is unknown.
For example, is <missing data> equal to <missing data>? It's impossible to know, so the result is UNKNOWN.
In this particular case, you are trying to find out if <missing data> is in a given list: since the data is missing, it's impossible to know if it's in the list, and the query returns 0.
SELECT CASE WHEN NULL IN (9,1,NULL) THEN 1 ELSE 0 END
I don't know what RDBMS you are using since some of them has configuration on how NULL will be treated.
NULL IN (9, 1, NULL)
can be written as
(NULL = 9) OR (NULL = 1) OR (NULL = NULL)
and not of them were TRUE nor FALSE. They are all NULL. Since there are only two paths in the CASE statement, it falls under ELSE block.
It depends on how NULL is treated in the specific server.
For instance in SQL SERVER you may set ANSI_NULLS off and have it return 1:
SET ANSI_NULLS OFF
SELECT CASE WHEN NULL IN (9,1,NULL) THEN 1 ELSE 0 END
For further info you should read the remarks section of SET ANSI_NULLS
Because you can't compare null values using comparison operators. You can only use is null or is not null or using functions like COALESCE(), ISNULL(). For example
SELECT CASE WHEN COALESCE(NULL,-1) IN (9,1,-1) THEN 1 ELSE 0 END
Null mean not defined.
NUll object is not equal to other NULL
NULL==1 return false
NUll==2 return false
NULL==NULL return also false

Dynamic where clause based on variable

I am working on a select statement in SQL and am running into issues trying to create a where clause that includes a case statement or an if else statement. I want to select records based on the value of a variable. If the variable is 'True' then only return records from the select statement where a column is null. If the variable is not 'True' then return all records regardless if that columns is null.
Any tips on how to do this?
Below is a simple example of what i am trying to do:
declare #option1 as varchar(5)
--This can be True or False so to test i just put the Set option below
set #option1 = 'True'
Select a,b,c,d...
from ...
where d = case when #option1 = 'True' then NULL End
This is the part where i do not know what to do. I only need to filter out the records if the variable is 'True' so not sure what to put in the else section of the case.
You can't test for d = NULL as your CASE statement does because that will always return false since NULL is not equal to NULL (unless you set ANSI_NULLS to 'off').
The simplest thing to do would be to change the WHERE clause to this:
WHERE #option1 = 'False' OR d IS NULL
If you prefer to use a CASE statement for some reason, you can write it like this:
WHERE 1 = CASE WHEN #option1 = 'False' THEN 1
WHEN #option1 = 'True' AND d IS NULL THEN 1
ELSE 0
END
This:
UPDATE: PinnyM has straightened me out on this. I am leaving my embarrassing logically flawed argument here for the education of the masses. The solution I propose below after "Try this" is certainly still valid, but PinnyM's solutions is by far more elegant and should be used.
WHERE #option1 = 'False' OR d IS NULL
Will always return all the results given his current select statement (assuming #Option1 is simply a flag parameter passed in).
Try this:
SELECT a, b, c, d
WHERE
-- Returns only rows where d is null (if #Option1 is True)
(#Option1 = 'True' AND d IS NULL)
OR
-- returns all the rows (if #Option1 is False)
(#Option1 = 'False')

How do I map true/false/unknown to -1/0/null without repetition?

I am currently working on a tool to help my users port their SQL code to SQL-Server 2005. For this purpose, I parse the SQL into a syntax tree, analyze it for constructs which need attentions, modify it and transform it back into T-SQL.
On thing that I want to support, is the "bools are values too" semantics of other RDBMS. For example, MS-Access allows me to write select A.x and A.y as r from A, which is impossible in T-SQL because:
Columns can't have boolean type (column values can't be and'ed)
Logical predicates can not be used where expressions are expected.
Therefore, my transformation routine converts the above statement into this:
select case
when (A.x<>0) and (A.y<>0)
then -1
when not((A.x<>0) and (A.y<>0))
then 0
else
null
end as r
from A;
Which works, but is annoying, because I have to duplicate the logical expression (which can be very complex or contain subqueries etc.) in order to distinguish between true, false and unknown - the latter shall map to null. So I wonder if the T-SQL pro's here know a better way to achieve this?
UPDATE:
I would like to point out, that solutions which try to keep the operands in the integer domain have to take into account, that some operands may be logical expressions in the first place. This means that a efficient solution to convert a bool to a value is stil required. For example:
select A.x and exists (select * from B where B.y=A.y) from A;
I don't think there's a good answer, really it's a limitation of TSQL.
You could create a UDF for each boolean expression you need
CREATE FUNCTION AndIntInt
(
#x as int,#y as int
)
RETURNS int
AS
BEGIN
if (#x<>0) and (#y<>0)
return -1
if not((#x<>0) and (#y<>0))
return 0
return null
END
used via
select AndIntInt(A.x,A.y) as r from A
Boolean handling
Access seems to use the logic that given 2 booleans
Both have to be true to return true
Either being false returns false (regardless of nulls)
Otherwise return null
I'm not sure if this is how other DBMS (Oracle, DB2, PostgreSQL) deal with bool+null, but this answer is based on the Access determination (MySQL and SQLite agree). The table of outcomes is presented below.
X Y A.X AND B.Y
0 0 0
0 -1 0
0 (null) 0
-1 0 0
-1 -1 -1
-1 (null) (null)
(null) 0 0
(null) -1 (null)
(null) (null) (null)
SQL Server helper 1: function for boolean from any "single value"
In SQL Server in general, this function will fill the gap for the missing any value as boolean functionality. It returns a ternary result, either 1/0/null - 1 and 0 being the SQL Server equivalent of true/false (without actually being boolean).
drop function dbo.BoolFromAny
GO
create function dbo.BoolFromAny(#v varchar(max)) returns bit as
begin
return (case
when #v is null then null
when isnumeric(#v) = 1 and #v like '[0-9]%' and (#v * 1.0 = 0) then 0
else 1 end)
end
GO
Note: taking Access as a starting point, only the numeric value 0 evaluates to FALSE
This uses some SQL Server tricks
everything is convertible to varchar. Therefore only one function taking varchar input is required.
isnumeric is not comprehensive, '.' returns 1 for isnumeric but will fail at #v * 1.0, so an explicit test for LIKE [0-9]%`` is required to "fix" isnumeric.
#v * 1.0 is required to overcome some arithmetic issues. If you pass the string "1" into the function without *1.0, it will bomb
Now we can test the function.
select dbo.BoolFromAny('abc')
select dbo.BoolFromAny(1)
select dbo.BoolFromAny(0) -- the only false
select dbo.BoolFromAny(0.1)
select dbo.BoolFromAny(-1)
select dbo.BoolFromAny('')
select dbo.BoolFromAny('.')
select dbo.BoolFromAny(null) -- the only null
You can now safely use it in a query against ANY SINGLE COLUMN, such as
SELECT dbo.BoolFromAny(X) = 1
SQL Server helper 2: function to return result of BOOL AND BOOL
Now the next part is creating the same truth table in SQL Server. This query shows you how two bit columns interact and the simple CASE statement to produce the same table as Access and your more complicated one.
select a.a, b.a,
case
when a.a = 0 or b.a = 0 then 0
when a.a = b.a then 1
end
from
(select 1 A union all select 0 union all select null) a,
(select 1 A union all select 0 union all select null) b
order by a.a, b.a
This is easily expressed as a function
create function dbo.BoolFromBits(#a bit, #b bit) returns bit as
begin
return case
when #a = 0 or #b = 0 then 0
when #a = #b then 1
end
end
SQL Server conversion of other expressions (not of a single value)
Due to lack of support for bit-from-boolean conversion, expressions that are already [true/false/null] in SQL Server require repetition in a CASE statement.
An example is a "true boolean" in SQL Server, which cannot be the result for a column.
select A > B -- A=B resolves to one of true/false/null
from C
Needs to be expressed as
select case when A is null or B is null then null when A > B then 1 else 0 end
from C
But if A is not a scalar value but a subquery like (select sum(x)...), then as you can see A will appear twice and be evaluated twice in the CASE statement (repeated).
FINAL TEST
Now we put all the conversion rules to use in this long expression
SELECT X AND Y=Z AND C FROM ..
( assume X is numeric 5, and C is varchar "H" )
( note C contributes either TRUE or NULL in Access )
This translates to SQL Server (chaining the two functions and using CASE)
SELECT dbo.BoolFromBits(
dbo.BoolFromBits(dbo.BoolFromAny(X), CASE WHEN Y=Z then 1 else 0 end),
dbo.BoolFromAny(C))
FROM ...
Access Bool or bool
For completeness, here is the truth table for Access bool OR bool. Essentially, it is the opposite of AND, so
Both have to be false to return false
Either being true returns true (regardless of nulls)
Otherwise return null
The SQL SERVER case statement would therefore be
case
when a.a = 1 or b.a = 1 then 1
when a.a = b.a then 0
end
(the omission of an ELSE clause is intentional as the result is NULL when omitted)
EDIT: Based on additional information added to the Question and comments made on one of the suggested Answers, I am reformulating this answer:
If you are porting to SQL Server then I would expect that you are also transforming the data to match SQL Server types. If you have a boolean field then True, False, and Unknown map to 1, 0, and NULL as a NULLable BIT field.
With this in mind, you only need to worry about transforming pure boolean values. Expressions such as:
exists (select * from B where B.y=A.y)
and:
A.x in (1,2,3)
are already in a workable form. Meaning, statements like:
IF (EXISTS(SELECT 1 FROM Table))
and:
IF (value IN (list))
are already correct. So you just need to worry about the fact that "1" for "True" is not by itself enough. Hence, you can convert "1" values to boolean expressions by testing if they are in fact equal to "1". For example:
IF (value = 1)
is the equivalent of what you previously had as:
IF (value)
Putting all of this together, you should be able to simply translate all instances of pure boolean values of the old code into boolean expressions in the form of "value = 1" since a 1 will produce a True, 0 will produce False, and NULL will give you False.
HOWEVER, the real complexity is that SELECTing the value vs. testing with it via a WHERE condition is different. Boolean expressions evaluate correctly in WHERE conditions but have no direct representation to SELECT (especially since NULL / Unknown isn't really boolean). So, you can use the "Value = 1" translation in WHERE conditions but you will still need a CASE statement if you want to SELECT it as a result.
As mentioned briefly a moment ago, since NULL / Unknown isn't truly boolean, it is meaningless trying to convert "NULL AND NULL" to NULL for the purposes of a WHERE condition. In effect, NULL is truly FALSE since it cannot be determined to be TRUE. Again, this might be different for your purposes in a SELECT statement which is again why the CASE statement is your only choice there.