WHERE clause in CASE - sql

I want to write SQL that if field #tank_id is null then not include this field in WHERE clause. Else include.
So here my sql:
DECLARE #dateTo AS DATE = '01.01.2018'
DECLARE #tank_Id int = null
SELECT sum(field_1 - field_2) FROM myTable
(CASE
WHEN #tank_id = 0 THEN (WHERE dt < #dateTo)
ELSE (WHERE dt < #dateTo AND tank_id = #tank_id)
END)
But I get error:
Error: Incorrect syntax near the keyword 'WHERE'.
SQLState: S1000
ErrorCode: 156

The code you provided does not match your text description.
In your description, you are asking how to not use the #tank_id if it's null, but in the code it looks like you want to not use it if it's 0 - so I'm going on the assumption that the text of the question is correct.
You can't use case as a flow control.
case is an expression that can only be used to return a single scalar value based on condition(s).
However, for this kind of query you don't need to use case, you can simply use a combination of and and or, like the following:
DECLARE #dateTo AS DATE = '01.01.2018'
DECLARE #tank_Id int = null
SELECT sum(field_1 - field_2)
FROM myTable
WHERE dt < #dateTo
AND (
tank_id = #tank_id
OR #tank_id IS NULL
)
BTW, you should always specify the RDBMS you are working with (oracle, mySql, postgreSQL, SQL Server etc`) in the tags, as well as add a tag for the specific version.

Try the following
DECLARE #dateTo AS DATE = '01.01.2018'
DECLARE #tank_Id int = null
SELECT sum(field_1 - field_2)
FROM myTable
WHERE dt < #dateTo
AND tank_id = (
case when #tank_id is null then tank_id else #tank_id end
)

No can do that.
A SELECT can be accompanied by at most one WHERE. That's how the syntax is defined.
That WHERE clause, if present, gets compiled (a.o. for the purpose of determining the physical data access strategy) so there is simply no means to make "what the WHERE clause consists of" in any way dependent upon information that can be known only at run-time (e.g. values in columns of the rows being processed).
So what you have to do is think about the cases you can run into and write your single WHERE clause such that it covers all cases. The comments and answers show you the correct way for the particular case you mentioned in your question.

Related

T-SQL get substring

I am looking to get an order number from a column named KEY_Ref, this ref column have various contents, but some rows look like this
LINE_NO=15^ORDER_NO=176572^RELEASE_NO=1^
Now I am interested in getting the value for ORDER_NO (176572 in this case)
How would I (In SQL Server) go about getting this (Or other) value from the main string
The logic is always
key1=value1^key2=value2^key3=value3^
You can use string_split():
select t.*, s.orderno
from t outer apply
(select stuff(s.value, 1, 9, '') as orderno
from string_split(t.key_ref, '^') s
where s.value like 'ORDER_NO=%'
) s;
Here is a db<>fiddle.
this is going to be a bit lengthy answer however if your SQL server version doesn't support string_split function you may use this.
declare #str varchar(100) = 'LINE_NO=15^ORDER_NO=176572^RELEASE_NO=1^'
declare #substr1 varchar(50) = substring(#str,charindex('^',#str)+1,len(#str))
declare #substr2 varchar(50) = substring(#substr1,charindex('=',#substr1)+1,charindex('^',#substr1)-charindex('=',#substr1)-1)
select #substr2 as 'order number'
the final variable will produce the desired value and you must merge the above queries to a single query that can fetch the value from the table in a single select statement.
this will work only if the pattern doesn't deviate from the one you've mentioned.

Is there a way to use in or = with a case statement in the where clause in sql?

I have a stored procedure that may or may not get a string list of int ids. When it doesn't get it the value is: ' '. Other wise its something like this: '500,507,908'
I'm trying to use it like this:
select ID as projectTeamId, Employee_ID, Supervisor_ID
from ProjectTeam
where Project_ID = #projectId and IsDeleted = 0 and
ID in (CASE #stringList WHEN '' THEN ID ELSE (SELECT * from TurnListStringIntoTable(#stringList)) END)
to get a result set but it errors out with this code when the string list comes in blank:
An error has occurred while processing Report 'MassReleaseHoursReport':
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
I know its an issue where id needs to = id instead of being in id. Any ideas on how I can have the case statement work with #stringList = '' then id = id else id in (SELECT * from TurnListStringIntoTable(#stringList))?
TurnListStringIntoTable returns a table from a string list which in my case is just the project Team ID
I would recommend boolean logic rather than a case expression:
where
Project_ID = #projectId
and IsDeleted = 0
and (
#stringList = ''
or id in (select * from TurnListStringIntoTable(#stringList))
)
Unrelated side note: if you are running SQL Server, as I suspect, and your version is 2016 or higher, you can use built-in function string_split() instead of your customer splitter.
Sure!
All you have to do is use the parameterless flavor of case:
select *
from my_table t
where t.col_1 = case
when #var in (1,2,3) then "foo"
when #var = 4 then "bar"
when #var in (5,6,7) then "baz"
else "bat"
end
One might note that the when expressions are not limited to looking at the same variable in any way. The only requirement is that they have to be a boolean expression. They're evaluated in order top to bottom. The first when clause that evaluates to true wins and it's then value is returned.
If there's no else and evaluation fails to find a match, the result value is null.
Your problem though, is that case expressions
return a single value, and
that value must be of the same type. Can have it returning a string in some cases and a table variable in another.
So... your where clause should look something like this:
where ...
and 'true' = case
when #stringList = '' then 'true'
when ID in ( select *
from TurnListStringIntoTable(#stringList)
) then 'true'
else 'false'
end
You'll probably find, too, that invoking a user-defined function to convert a comma-delimited string into a table variable within the where clause is probably a Bad Idea™ due to the performance impact that that will have.
You'd be better off to move your TurnListStringIntoTable call outside of the select statement, thus:
declare #list = TurnListStringIntoTable(#stringlist)
select ...
from ProjectTeam pt
where . . .
and #stringlist = ''
OR exists ( select * from #list x where x.ID = pt.ID )

Make SQL SERVER evaluate clauses in a certain order

Take the following table as an instance:
CREATE TABLE TBL_Names(Name VARCHAR(32))
INSERT INTO TBL_Names
VALUES ('Ken'),('1965'),('Karen'),('2541')
sqlfiddle
Executing following query throws an exception:
SELECT [name]
FROM dbo.tblNames AS tn
WHERE [name] IN ( SELECT [name]
FROM dbo.tblNames
WHERE ISNUMERIC([name]) = 1 )
AND [name] = 2541
Msg 245, Level 16, State 1, Line 1 Conversion failed when converting
the varchar value 'Ken' to data type int.
While the following query executes without error:
SELECT [name]
FROM dbo.tblNames AS tn
WHERE ISNUMERIC([name]) = 1
AND [name] = 2541
I know that this is because of SQL Server Query Optimizer's decision. but I am wondering if there is any way to make sql server evaluate clauses in a certain order. this way, in the first query,the first clause filters out those Names that are not numeric so that the second clause will not fail at converting to a number.
Update: As you may noticed, the above query is just an instance to exemplify the problem. I know the risks of that implicit conversion and appreciate those who tried to warn me of that. However my main question is how to change Optimizer's behavior of evaluating clauses in a certain order.
There is no "direct" way of telling the engine to perform operations in order. SQL isn't an imperative language where you have complete control of how to do things, you simply tell what you need and the server decides how to do it itself.
For this particular case, as long as you have [name] = 2541, you are risking a potential conversion failure since you are comparing a VARCHAR column against an INT. Even if you use a subquery/CTE there is still room for the optimizer to evaluate this expression first and try to convert all varchar values to int (thus failing).
You can evade this with workarounds:
Correctly comparing matching data types:
[name] = '2541'
Casting [name] to INT beforehand and only whenever possible and on a different statement, do the comparison.
DECLARE #tblNamesInt TABLE (nameInt INT)
INSERT INTO #tblNamesInt (
nameInt)
SELECT
[nameInt] = CONVERT(INT, [name])
FROM
dbo.tblNames
WHERE
TRY_CAST([name] AS INT) IS NOT NULL -- TRY_CAST better than ISNUMERIC for INT
SELECT
*
FROM
#tblNamesInt AS T
WHERE
T.nameInt = 2351 -- data types match
Even an index hint won't force the optimizer to use an index (that's why it's called a hint), so we have little control on how it gets stuff done.
There are a few mechanics that we know are evaluated in order and we can use to our advantage, such as the HAVING expressions will always be computed after grouping values, and the grouping always after WHERE conditions. So we can "safely" do the following grouping:
DECLARE #Table TABLE (IntsAsVarchar VARCHAR(100))
INSERT INTO #Table (IntsAsVarchar)
VALUES
('1'),
('2'),
('20'),
('25'),
('30'),
('A') -- Not an INT!
SELECT
CASE WHEN T.IntsAsVarchar < 15 THEN 15 ELSE 30 END,
COUNT(*)
FROM
#Table AS T
WHERE
TRY_CAST(T.IntsAsVarchar AS INT) IS NOT NULL -- Will filter out non-INT values first
GROUP BY
CASE WHEN T.IntsAsVarchar < 15 THEN 15 ELSE 30 END
But you should always avoid writing code that implies implicit conversions (like T.IntsAsVarchar < 15).
Try like this
SELECT [name]
FROM #TBL_Names AS tn
WHERE [name] IN ( SELECT [name]
FROM #TBL_Names
WHERE ISNUMERIC([name]) = 1 )
AND [name] = '2541'
2)
AND [name] = convert(varchar,2541 )
Since You are storing name as varchar(32) varchar will accept integer datatype values also called precedence value
What about:
SELECT *
FROM dbo.tblNames AS tn
WHERE [name] = convert(varchar, 2541)
Why do you need ISNUMERIC([name]) = 1) since you only care about the value '2541'?
You can try this
SELECT [name]
FROM dbo.TBL_Names AS tn
WHERE [name] IN ( SELECT [name]
FROM dbo.TBL_Names
WHERE ISNUMERIC([name]) = 1 )
AND [name] = '2541'
You need to just [name] = 2541 to [name] = '2541'. You are missing ' (single quote) with name in where condition.
You can find the live demo Here.
Honestly, I wouldn't apply the implicit cast to your column [name], it'll make the query non-SARGable. Instead, convert the value of your input (or pass it as a string)
SELECT [name]
FROM dbo.TBL_Names tn
WHERE [name] = CONVERT(varchar(32),2541);
If you "must", however, wrap [name] (and suffer performance degradation) then use TRY_CONVERT:
SELECT [name]
FROM dbo.TBL_Names tn
WHERE TRY_CONVERT(int,[name]) = 2541;

UDF Table UDV or Scalar UDF?

I will do my best to make this question better than my last fiasco. I am getting the dreaded >"cannot find either column "dbo" or the user-defined function or aggregate "dbo.PriMonthAvgPrice", or the name is ambiguous.<
I am attempting to find the avg sales price from the previous month. Here is my UDF:
USE [WoodProduction]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER FUNCTION [dbo].[PriMonthAvgPrice]
(
-- Add the parameters for the function here
#endofmonth datetime,
#begofmonth datetime,
#PlantCode varchar
)
RETURNS decimal (10,2)
AS
BEGIN
-- Declare the return variable here
DECLARE #MonthEndAvgPrice decimal (10,2)
-- Add the T-SQL statements to compute the return value here
SELECT #MonthEndAvgPrice =
(
select
sum(Actual_Sales_Dollars/Actual_Volume)
FROM
woodproduction.dbo.plywood_layup_sales pls
WHERE
Production_Date between #begofmonth and #endofmonth
and actual_volume <> 0
and #PlantCode = pls.Plant_Code
)
-- Return the result of the function
RETURN #MonthEndAvgPrice
END
This is my SELECT statement from my query:
SELECT
DISTINCT
P.[Plant_Number]
,p.plant_name
,pls.plant_code
,(pls.[Budget_Realization]) AS 'BR'
,(pls.[Actual_Volume] ) AS 'AV'
,(pls.[Budget_Volume]) AS 'BV'
--,sum (dpb.[Gross_Production_Per_Hr]) AS 'GPB'
,(p.Production_Volume) AS 'PV'
,CASE
WHEN coalesce (pls.[Actual_Volume],0) = 0 and
coalesce (pls.[Actual_Sales_Dollars],0) = 0
THEN 0
ELSE (pls.[Actual_Sales_Dollars]/pls.[Actual_Volume])
END
AS 'AP'
,pls.production_date
,[dbo].[PriMonthAvgPrice](#endofmonth,#begofmonth, pls.plant_code) AS 'PriMoAvgPrice'
My BASIC understanding is that I HAVE created a Scalar Function. From what I've been reading about my error however, This error returns on TVF's. Is this true? I created a SVF prior to this dealing with just determining a prior month end date so it wasn't as involved as this one where I create the query in the UDF.
Do I need to change this to a TVF? And if so, how do I incorporate SELECT * when I have to join multiple tables along with this?
Thanks in advance.
Aaron
You don't show the from clause, but is the database you created the function in part of it?
Does it work if you fully qualify the name (include the database)?
Have you independently tested the function with:
select [dbo].[PriMonthAvgPrice] ('01/01/2011', '02/01/2011', 'test')
Note: of course you would use some actual values that should return a result.
Please run this and tell us the values returned:
SELECT Actual_Sales_Dollars,Actual_Volume, pls.PLant_code
FROM woodproduction.dbo.plywood_layup_sales pls
WHERE Production_Date between '09-01-2011' and '09-30-2011'
and actual_volume <> 0

Stored Procedure with optional "WHERE" parameters

I have a form where users can specify various parameters to dig through some data (status, date etc.).
I can produce a query that is:
SELECT * FROM table WHERE:
status_id = 3
date = <some date>
other_parameter = <value>
etc. Each WHERE is optional (I can select all the rows with status = 3, or all the rows with date = 10/10/1980, or all the rows with status = 3 AND date = 10/10/1980 etc.).
Given a large number of parameters, all optional, what is the best way to make up a dynamic stored procedure?
I'm working on various DB, such as:
MySQL, Oracle and SQLServer.
One of the easiest ways to accomplish this:
SELECT * FROM table
WHERE ((#status_id is null) or (status_id = #status_id))
and ((#date is null) or ([date] = #date))
and ((#other_parameter is null) or (other_parameter = #other_parameter))
etc.
This completely eliminates dynamic sql and allows you to search on one or more fields. By eliminating dynamic sql you remove yet another security concern regarding sql injection.
Create your procedure like this:
CREATE PROCEDURE [dbo].[spXXX]
#fromDate datetime = null,
#toDate datetime = null,
#subCode int = null
as
begin
set NOCOUNT ON
/* NOCOUNT limits the server feedback on select results record count */
SELECT
fields...
FROM
source
WHERE
1=1
--Dynamic where clause for various parameters which may or may not be passed in.
and ( #fromDate is null or [dateField] >= #fromDate)
and ( #toDate is null or [dateField] <= #toDate)
and ( #subCode is null or subCode= #leaveTypeSubCode)
order by fields...
This will allow you to execute the procedure with 0 params, all params, or any # of params.
This is the style I use:
t-sql
SELECT *
FROM table
WHERE
status_id = isnull(#status_id ,status_id)
and date = isnull(#date ,date )
and other_parameter = isnull(#other_parameter,other_parameter)
oracle
SELECT *
FROM table
WHERE
status_id = nval(p_status_id ,status_id)
and date = nval(p_date ,date )
and other_parameter = nval(p_other_parameter,other_parameter)
A readable and maintainable way to do it (even usable with JOIN/APPLY) :
where
(#parameter1 IS NULL OR your_condition1)
and (#parameter2 IS NULL OR your_condition2)
-- etc
However it's a bad idea on most big tables (even more using JOIN/APPLY), since your execution plan will not ignore NULL values and generates massive performance loophole (ex : scaning all a table searching for NULL values).
A roundabout way in SQL Server is to use WITH(RECOMPILE) options in your query (available since SQL 2008 SP1 CU5 (10.0.2746)).
The best way to implements this (performance wise) is to use IF ... ELSE block, one for each combination possible. Maybe it's exhausting but you will have the best performances and it doesn't matter your database settings.
If you need more details, you can look for KM. answer here.
You can do something like
WHERE
(
ParameterA == 4 OR ParameterA IS NULL
)
AND
(
ParameterB == 12 OR ParameterB IS NULL
)
If you want to avoid dynamically building up SQL strings (which is often best avoided), you can do this in stored procs by comparing each critera in your where claused with a default value, which equates to "ignore". E.g.:
select * from Table where
(#Col1 IS NULL OR Col1 = #Col1) /*If you don't want to filter in #col, pass in NULL*/
AND
(#Col2 IS NULL OR Col2 = #Col2)