Not allowing Multiple CASE Subquery Results when using IN - sql-server

I'm working with the following query:
SELECT *
FROM TableA a
WHERE a.FieldA IN (
CASE
--select subquery returns a single value
WHEN a.FieldB = 'Value1'
THEN (select b.ID from TableB b where b.FK_Field = '123')
--select subquery returns multiple values
WHEN a.FieldB = 'Value2'
THEN (select c.ID from TableC c where c.FK_Field = '123')
END
)
The first case select statement returns only a single b.ID. If I just have that statement, my code works.
The second case statement, however, returns multiple c.IDs. When I add that check, I get the following error:
Subquery returned more than 1 value. This is not permitted when the
subquery follows =, !=, <, <= , >, >= or when the subquery is used as
an expression.
If I would have WHERE a.FieldA =, then I understand that the subquery can only return 1 value. I however have WHERE a.FieldA IN, so why is it complaining if there are multiple values returned?
How can I implement this kind of check?

As #marc_s explained in a comment:
CASE in T-SQL is an expression (like a+b) which returns a single,
atomic value - you cannot use it to selectively run SQL snippets that
return entire result sets
In order to resolve this error, I removed the CASE statement and instead used a bunch of AND and OR statements to accomplish the same kind of check.
SELECT *
FROM TableA a
WHERE
(a.FieldB = 'Value1'
AND a.FieldA IN (select b.ID from TableB b where b.FK_Field = '123'))
OR (a.FieldB = 'Value2'
AND a.FieldA IN (select c.ID from TableC c where c.FK_Field = '123'))
This code is a bit messier than a CASE statement, but it works.

Lots of ways of doing this, here is one way using union all and a correlated EXISTS statement
;WITH cte AS (
SELECT 'Value1' as FieldB, b.Id
FROM
TableB
WHERE
b.FK_FieldId = '123'
UNION ALL
SELECT 'Value1' as FieldB, c.Id
FROM
TableB
WHERE
c.FK_FieldId = '123'
)
SELECT *
FROM
TableA a
WHERE
EXISTS (SELECT 1
FROM
cte c
WHERE
a.FieldB = c.FieldB
AND a.FieldA = c.Id)
The problem with the way you have written it is that you are getting a non-scalar value (meaning more than 1 row) where sql is expecting a scalar value. In the case expression only scalar values can be used in the THEN part as well as some rules in WHEN as well. To solve you need to break apart your case expression to multiple where statements and/or use some other technique such as the one above.
Or you could write your case expression like this:
SELECT *
FROM
TableA a
WHERE
(CASE
WHEN a.FieldB = 'Value1' AND a.FieldA IN (select b.ID from TableB b where b.FK_Field = '123') THEN 1
WHEN a.FieldB = 'Value2' AND a.FieldA IN (select c.ID from TableC c where c.FK_Field = '123') THEN 1
ELSE 0
END) = 1

Don't use case in predicates if it is not necessary - using case make your argument non-SARG (you qry will not use index).
SELECT *
FROM TableA a
WHERE EXISTS(
SELECT NULL
FROM TableB b
WHERE a.FieldB = 'Value1'
AND b.FK_Field = '123'
AND a.FieldA = b.ID))
OR EXISTS(
SELECT NULL
FROM TableC c
WHERE a.FieldB = 'Value2'
AND c.FK_Field = '123'
AND a.FieldB = c.ID))
Use indexes. And try to make your qry readable.
Or if you would like use semi-join:
SELECT *
FROM TableA a
WHERE a.FieldA IN (
SELECT b.ID
FROM TableB b
WHERE a.FieldB = 'Value1'
AND b.FK_Field = '123'))
OR a.FieldB IN (
SELECT c.ID
FROM TableC c
WHERE a.FieldB = 'Value2'
AND c.FK_Field = '123'))
Both of this solutions are with SARG.

Related

SQL multi-part identifier can't be bound in SELECT

Is it possible to add a case using a column from other tables than first in the from section?
I can't use anything like C.code or Y.anything in the SELECT part:
SELECT
fromTableA, fromTableA, fromTableA,
CASE
WHEN fromTableA = anyValue THEN 'is_ok'
WHEN B.fromTableB = anyValue THEN 'couldnt.be.bound'
WHEN fromTableB = anyValue THEN 'invalid.column.name'
END AS X
FROM
(SELECT fromTableA,
SUM(fromTableA),
CASE WHEN A.fromTableA = 'anything' THEN 'still ok'
WHEN C.fromTableC = 'allowed' THEN 'no problem'
FROM tableA A
JOIN tableB B ON A.id = B.id
JOIN tableC C ON C.id = B.id
Having SUM(fromTableA) > 0
) AS Y
Edit: What I need is to use columns from table B or C in the outer Select ( I just can't remove the inner select because i would lost cases and aggregation operations there are in the inner select).
The following should be all you need, a derived table here doesn't accomplish anything.
select A.Cols...,
case
when A.col1 = anyValue then 'is_ok'
when B.Col1 = anyValue then 'couldnt.be.bound'
when B.Col2 = anyValue then 'invalid.column.name'
end as X
from tableA A
join tableB B on A.id = B.id
join tableC C on C.id = B.id;

Use IF in SELECT part of query

I need to create something like this
SELECT x.id
, x.name
, x.type
,(
IF x.type = 1
(SELECT SUM(Col1) FROM TableA WHERE ... etc)
ELSE IF x.type = 2
(SELECT SUM(Col2) FROM TableB WHERE ... etc)
) AS Total
FROM TableX as x
So I am trying to select a different sub query according to the value of x.type
Wing
Try to use LEFT JOIN and COALESCE. Use your conditions of x.type to join the tables.
COALESCE (Transact-SQL): Evaluates the arguments in order and returns the current value of the first expression that initially does not evaluate to NULL.
https://msdn.microsoft.com/en-us/library/ms190349.aspx
SELECT x.id
, x.name
, x.type
, COALESCE(SUM(TableA.Column), SUM(TableB.Column)) as column_xyz
FROM TableX as x
LEFT JOIN TableA ON x.type = 1 AND ...
LEFT JOIN TableB ON x.type = 2 AND ...
You can also use CASE WHEN ... THEN ... instead of COALESCE to define which column to use.
Use CASE statement
SELECT x.id,
x.name,
x.type,
CASE
WHEN x.type = 1 THEN (SELECT Sum(Col1)
FROM TableA Where...)
WHEN x.type = 2 THEN (SELECT Sum(Col2)
FROM TableB Where .. )
END AS Total
FROM TableX AS x
You can use CASE WHEN as the below:
SELECT
x.id,
x.name,
x.type,
CASE
WHEN x.type = 1 THEN (SELECT SUM(A.Col1) FROM TableA A WHERE 1 = 1)
WHEN x.type = 2 THEN (SELECT SUM(B.Col2) FROM TableB B WHERE 1 = 1)
ELSE NULL END AS Total
FROM
TableX as x
You can use case expression:
select t.* ,
Case when t.type = 1 then (select sum(col1) ... TableA)
when t.type = 2 then (select sum(col2) ... TableB)
End as Total
From tableX t
By using variable -
DECLARE #SumA INT = SELECT SUM(Col1) FROM TableA WHERE ... etc
DECLARE #SumB INT = SELECT SUM(Col2) FROM TableB WHERE ... etc
SELECT x.id
, x.name
, x.type
,( CASE x.type
WHEN 1 THEN #SumA
WHEN 2 THEN #SumB
END
) AS Total
FROM TableX as x
Choose the datatype for Sum variable accordingly (if Decimal).

How to write where condition on the basis of selected value

I have a multi select drop down containing 20 values.
User can select all or few.
I want to write a query
if user has selected all the values then I don't want to pass all the selected values. (I will pass 0 to identify that all values are selected)
If use has selected few(or not all) then I want to pass only those selected values. (I will be passing comma separated values ex 101,102,103,104)
On the basis of the values I want write a join table or use it in a where condition
I have done something like as follows but it seem not working.
WHERE: Following throwing error (Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.)
DECLARE #EntitiyIds AS varchar(2500) = '0'
SELECT *
FROM Tb_CompanyType
WHERE Id IN (
CASE WHEN #EntitiyIds <> '0' THEN (SELECT Item FROM dbo.Split(#EntitiyIds, ','))
WHEN #EntitiyIds = '0' THEN (SELECT Id FROM Tb_CompanyType) END
)
INNER JOIN: (Following is not giving proper output)
DECLARE #EntitiyIds AS VARCHAR(2500) = '101'
SELECT * FROM Tb_CompanyType CT
INNER JOIN Tb_Company TC ON
CASE WHEN #EntitiyIds = '0' AND TC.CompanyId = CT.Id THEN 1
WHEN #EntitiyIds <> '0' AND TC.CompanyId = (SELECT Item FROM dbo.Split(#EntitiyIds, ',')) THEN 1 END = 1
dbo.Split returns table containing comma separated values
Kindly help
You can do it this way.
SELECT *
FROM Tb_CompanyType
WHERE #EntitiyIds = '0'
OR Id IN (SELECT Item
FROM dbo.Split(#EntitiyIds, ','))
Something like this perhaps.
SELECT Id FROM Tb_CompanyType where #EntitiyIds = '0'
UNION ALL
SELECT Item FROM dbo.Split(#EntitiyIds, ',') s where #EntitiyIds <> '0'
From the error message your provided, it seems that your sub-query is returning more than one rows. You last query segment's last line seems problematic:
DECLARE #EntitiyIds AS VARCHAR(2500) = '101'
SELECT * FROM Tb_CompanyType CT
INNER JOIN Tb_Company TC ON
CASE WHEN #EntitiyIds = '0' AND TC.CompanyId = CT.Id THEN 1
WHEN #EntitiyIds <> '0' AND TC.CompanyId = (SELECT Item FROM dbo.Split(#EntitiyIds, ',')) THEN 1 END = 1
Change the last
SELECT Item FROM dbo.Split(#EntitiyIds, ',')
to
SELECT top 1 Item FROM dbo.Split(#EntitiyIds, ',')

LEFT JOIN with CASE WHEN

I am trying to implement a way to replace "dynamic SQL" of WHERE clause.
That is, to create a temp table, "#FilterTable", which contains my WHERE's name and value.
Then, I use "left join" to map my target table #Hotel with #FilterTable.
However, I got an error when using the following code:
DECLARE #filterName varchar(50)='Id',
#filterValue int =13
CREATE TABLE #Hotel (Id varchar(50), DisplayName varchar(100))
CREATE TABLE #FilterTable (Name varchar(50), Value varchar(100))
INSERT INTO #Hotel(Id,DisplayName) VALUES ('1','California Inn'), ('13','Hilton Hotel')
INSERT INTO #FilterTable(Name,Value) VALUES ('Id','13'),('DisplayName','Hotel')
SELECT a.*
FROM #Hotel a WITH(NOLOCK)
LEFT JOIN #FilterTable b WITH(NOLOCK)
ON #filterName= b.Name
AND
CASE b.Name
WHEN 'Id' THEN CONVERT(varchar(10), a.Id) = b.Value
WHEN 'DisplayName' THEN a.DisplayName like b.Value END
DROP Table #Hotel
DROP Table #FilterTable
It always shows error in
WHEN 'Id' THEN CONVERT(varchar(10), a.Id) = b.Value
WHEN 'DisplayName' THEN a.DisplayName like b.Value END
I know if I change to
WHEN 'DisplayName' THEN CONVERT(varchar(10), a.Id) END = b.Value
, it will pass, but there is no way to add the second condition in "WHEN" clause under this situation.
Does anyone know what is the correct syntax to put those two "WHEN" clause togather?
My db version is SQL SERVER 2014 Enterprise.
Thank you.
Your syntax for CASE is incorrect. You're using it like a switch statement in C, Java, etc. You don't say case against a value, then switch on the value. Rather, it's like an if statement where each condition is independent.
SELECT a.*
FROM #Hotel a WITH(NOLOCK)
LEFT JOIN #FilterTable b WITH(NOLOCK)
ON #filterName= b.Name
AND
CASE
WHEN b.Name = 'Id' AND CONVERT(varchar(10), a.Id) = b.Value THEN 1
WHEN b.Name = 'DisplayName' AND a.DisplayName like b.Value THEN 1
ELSE 0
END = 1
This says, if b.Name is ID and the convert pattern matches, the row matches, or if b.Name is DisplayName and the display name is like b.Value, the row matches.
Give that a try.
I do have to point out that this is likely to not scale well given multiple rows. You should really have a separate query for each case so that SQL Server can use the appropriate indexes and statistics.
In your code:
WHEN 'Id' THEN CONVERT(varchar(10), a.Id) = b.Value
WHEN 'DisplayName' THEN a.DisplayName like b.Value END
= b.Value and like b.Value equals = b.Value. If you want to use LIKE you have to add: like '%' + b.Value + '%'
But if it doesn't mean then you can use:
b.Value = CASE b.Name
WHEN 'Id' THEN CONVERT(varchar(10), a.Id)
WHEN 'DisplayName' THEN a.DisplayName END

Case statement in sql using other selected columns in the same statement

I would like to know if the following is possible in SQL server 2005. Column A and B are calculated using other case statements in my actual stored proc. I don't want to repeat the same for another field unnecessarily. If this is not syntactically possible, any other ideas?
SELECT A, B, CASE WHEN column1='1' THEN A ELSE B END Col1.
Modified version of actual query provided as requested. CTE kind of seems to be tough in this model. WANNABE is the column I want to accomplish in the sub select statement.
SELECT 1 AS Region, 'Test',
CAST(Work AS NUMERIC(18,2)) Work,
Work + 2 AS Work2,
WANNABE
FROM
(
SELECT
ROW_NUMBER() OVER(PARTITION BY G.Value, C.C, FR.Mod1 ORDER BY FR.Date DESC, FG.Date DESC, FC.Date DESC) ROW,
CASE WHEN COALESCE(FR.Mod1, '') = '' THEN '' ELSE FR.Mod1 END Mod1,
CASE WHEN #var1=1 AND #var2 = 1 THEN FR.Col1 * G.Value
WHEN #var1=1 AND #var2 = 0 THEN FP.Col1 * G.Value END Work,
CASE WHEN 1=1 THEN Work ELSE 1 END WANNABE,
(
SELECT Col3
FROM Table2
WHERE c = FR.Value
) AS Custom
FROM MainTable FR
JOIN #C C ON FR.Col2 = C.Col2
LEFT JOIN Function1(#VersionDate) cv ON cv.Code = C.Code
LEFT JOIN Function2(#VersionDate) hv ON hv.Code = C.Code
LEFT JOIN #G G ON 1 = 1
LEFT JOIN SubTable1 FG ON FG.Number = G.Value, 2 AND FG.Date = #VersionDate
LEFT JOIN SubTable2 FO ON FO.Number = G.Value
AND FO.Date = #VersionDate AND FO.Code = FR.Code AND FR.Mod1 = FO.Mod1
LEFT JOIN SubTable3 FP ON FP.Code = FR.Code AND FP.VersionDate = #Versiondate
AND CASE WHEN DATALENGTH(FR.Mod1) = 0 THEN '00' ELSE FR.Mod1 END = CASE WHEN DATALENGTH(FP.Mod1) = 0 THEN '00' ELSE FP.Mod1 END
LEFT JOIN SubTable4 FC ON FC.Date = #VersionDate
WHERE FR.Date = #VersionDate
) x
WHERE x.Row = 1
AND RTRIM(LTRIM(x.Col1)) IN ('', '2')
You can define the A,B column aliases in a CTE then reference them in an outer select.
;WITH CTE AS
(
SELECT CASE ... END AS A,
CASE ... END AS B,
column1
FROM your_table
)
SELECT A,
B,
CASE WHEN column1='1' THEN A ELSE B END Col1
FROM CTE
Similarly you can also define them in a CROSS APPLY that is sometimes a bit less verbose.
A silly example just to show the syntax is
SELECT A,
B,
CASE WHEN type='P' THEN A ELSE B END Col1
FROM master..spt_values
CROSS APPLY (SELECT CASE WHEN number %2 = 1 THEN 1 END,
CASE WHEN number %2 = 0 THEN 0 END) T(A,B)
Following your update you can replace the derived table with a CTE and nest CTEs as follows
;WITH x as
(
SELECT
ROW_NUMBER() OVER(PARTITION BY G.Value, C.Code, FR.Mod1 ORDER BY FR.Date DESC, FG.Date DESC, FC.Date DESC) ROW,
...<snip>
WHERE FR.Date = #VersionDate
),
x2 As
(
SELECT *,
CASE WHEN 1=1 THEN Work ELSE 1 END WANNABE
FROM x
)
SELECT 1 AS Region, 'Test',
CAST(Work AS NUMERIC(18,2)) Work,
Work + 2 AS Work2,
WANNABE
FROM x2
WHERE x2.Row = 1
AND RTRIM(LTRIM(x2.Col1)) IN ('', '2')
Yeah it is posible, but how is all your sql statement? You can use the case statement in the select statement as you are using it.
Something like this
SELECT SUM((CASE WHEN column1='1' THEN 10 ELSE 0 END)) AS A, SUM((CASE WHEN column1='2' THEN 10 ELSE 0 END)) AS B
FROM YourTable

Resources