Can you not raise errors within a case statement in T-SQL? I always have problems with SQL case statements :/
begin try
declare #i int
--set #i = (select COUNT(1) from table_name)
select Item_Num =
CASE (select COUNT(1) from table_name)
when 1 then (select Item_Num from table_name)
when 0 then (raiserror('No records in database', 0, 0))
ELSE (raiserror('Multiple records in database', 0, 0))
END
from table_name
end try
begin catch
declare #errormsg nvarchar(1024),
#severity int,
#errorstate int;
select #errormsg = error_message(),
#severity = error_severity(),
#errorstate = error_state();
raiserror(#errormsg, #severity, #errorstate);
end catch
Think of Case/When as operating on a single piece of data. If you think of it this way, a lot of your problems will go away.
If/Then is used to control the flow of logic.
Something like this should work for you.
declare #i int
set #i = (select COUNT(1) from table_name)
If #i = 1
Begin
Print "1 row"
End
Else If #i = 0
Begin
Print "no rows"
End
Else
Begin
Print "too many rows"
End
You can raise an error from the case expression by converting an error string to int.
select case (select count(*) from mytable)
when 1 then 100
when 2 then 200
else convert(int, 'ERROR')
end
This gives an error message like
Conversion failed when converting the varchar value 'ERROR' to data type int.
which is about as good as you're going to get.
Not all failed conversions give the input string in the error message. Conversions to datetime, for example, do not. So if your case expression returns a datetime, you still have to trigger the error with a string-to-integer conversion:
select case (select count(*) from mytable)
when 1 then getdate()
else convert(datetime, convert(int, 'ERROR'))
end
It gets worse: if you are returning a date, you can't explicitly convert that from int, so you have to resort to
convert(date, convert(char(1), convert(int, 'ERROR')))
It's pretty horrible, but in my opinion the only thing more important than clean code is informative error messages, so I live with it.
As I said in the comment, I think it would be easier to simply create a flag that you check outside the scope of the CASE statement. Something along the lines of:
--- code before the TRY...
BEGIN TRY
DECLARE #i int
-- declare a variable to act as a flag
DECLARE #my_flag as int
-- than change your statement to simply fill the value of the flag
CASE (SELECT COUNT(1) FROM table_name)
WHEN 1 THEN SET #my_flag = 1
WHEN 0 THEN SET #my_flag = 0
ELSE SET #my_flag = -1
END
IF (NOT #my_flag in (-1, 0))
BEGIN
SET #Item_Num = (SELECT Item_Num FROM table_name) -- consider a filter here
END
ELSE
BEGIN
IF (-1 = #my_flag) RAISERROR('too many records', 0, 0)
IF (0 = #my_flag) RAISERROR('no records', 0, 0)
END
END TRY
BEGIN CATCH
--- rest of the code goes here....
With #TempTable as
(
Select * from ...
-- record set that determines if I should error out
)
SELECT CASE WHEN COUNT(1) > 0
THEN 'Multiple records in database'
ELSE 0
END AS [Value]
FROM #TempTable
datatype mismatch will kill the method if you're trying to error out this whole call with TSQL. Worked in my case because it was a heavy process that I needed to know was transferred correctly. If My record set was >1 then I know I should fail this. Useful if you're using SSIS or multiple methods within a .NET environment
Related
I haven't touched SQL Server and SQL queries for over 5 years and now I need to write a new stored procedure today.
Many of these stored procedure parameters are optional thus there are potentially different execution plans and paths for every execution. I wonder if I don't do this right it will be horribly slow and likely an anti-pattern so I want to be careful here.
The following is pseudo-code of what I'm trying to accomplish.
ALTER PROCEDURE GetPatients
#Active BIT,
#PatientGender NVARCHAR(1),
#PatientName NVARCHAR(50),
#PatientDOB DATE
AS
BEGIN
SET NOCOUNT ON;
SELECT *
FROM PatientTable
WHERE Active = #Active
--#PatientGender could be M, F or B for both.
--If B then disregard the values in the PatientGender Column
AND (CASE #PatientGender
WHEN 'M' THEN PatientGender = 'M'
WHEN 'F' THEN PatientGender = 'F'
ELSE
END)
--#PatientName is a wildcard search
--#PatientName is null then disregard the values in the PatientName column
AND (CASE #PatientName
WHEN '' THEN --don't include this and branch
ELSE PatientName LIKE '%#PatientName%'
END)
--PatientDOB same as above. Not included here for brevity but if empty skip if
present add an AND branch to WHERE clause.
END
GO
So what I've come up with is these two solutions and I have a feeling neither of them are really any good.
First is an ugly branching stored procedure .....but it works.
ALTER PROCEDURE GetPatients
#Active BIT,
#PatientGender NVARCHAR(1),
#PatientName NVARCHAR(50),
#PatientDOB DATE
AS
BEGIN
SET NOCOUNT ON;
IF #PatientGender = 'M'
BEGIN
SELECT *
FROM PatientTable
WHERE Active = #Active
AND PatientGender = 'M'
END
IF #PatientGender = 'F'
BEGIN
SELECT *
FROM PatientTable
WHERE Active = #Active
AND PatientGender = 'F'
END
ELSE
BEGIN
SELECT *
FROM PatientTable
WHERE Active = #Active
END
--and so it keeps going till every condition is covered
--it's ugly
END
Second is a dynamic SQL and while this "looks" better but if I remember correctly you should avoid dynamic SQL unless no other option.
ALTER PROCEDURE GetPatients
#Active BIT,
#PatientGender NVARCHAR(1),
#PatientName NVARCHAR(50),
#PatientDOB DATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #SQL NVARCHAR(1000)
DECLARE #PatientGenderFilter NVARCHAR(500)
DECLARE #PatientNameFilter NVARCHAR(500)
DECLARE #PatientDOBFilter NVARCHAR(500)
SET #PatientGenderFilter = CASE
WHEN #PatientGender = 'M' THEN N'AND PatientGender = ''M'' '
WHEN #PatientGender = 'F' THEN N'AND PatientGender = ''F'' '
END
SET #PatientNameFilter = CASE #PatientName
WHEN '' THEN N''
ELSE N'AND PatientName LIKE ''%' + #PatientName + '%'' '
END
SET #SQL = N'SELECT * FROM PatientTable WHERE Active = ' + CAST(#Active AS CHAR(1)) + ' ' + #PatientGenderFilter + #PatientNameFilter
--PRINT #SQL
EXEC(#SQL)
END
GO
So my question is: I have two working queries but both seem sub-standard. I am seeking confirmation that this is as good as it gets, or that query improvements might be possible.
From your question, if your query parameters are optional from the stored procedure, you can try to use OR and AND let's judge your condition.
Other things
make sure you had added indexes for your query from your filter columns.
you might meet parameter-sniffing from your query which might let your query slow. so you can try to add OPTION(RECOMPILE)
query as below.
SELECT *
FROM PatientTable
WHERE Active = #Active
AND (#PatientGender IS NULL OR PatientGender = #PatientGender)
AND (#PatientName = '' OR PatientName LIKE CONCAT('%',#PatientName,'%'))
OPTION(RECOMPILE)
NOTE
using OPTION(RECOMPILE) will tell QO not to try to keep the execution plan that might let your CPU become busy if your query is often query.
I want to add a try catch to a while loop. The loop used while on ##ROWCOUNT > 0. In the while I have an update top (100) statement that works well without a try catch around it. When I add the try, the while ends in the first loop. What impact does the try have on ##ROWCOUNT that makes the while loop end even tough the update touched 100 records?
--do we have anything to process?
select top 1 * from SomeTable where processedFlag is null
WHILE(##ROWCOUNT > 0)
BEGIN
begin try
-- here I have an udpate top (100) statement that processes records with null flag in small batches
end try
begin catch
-- update ##ROWCOUNT so the while continues?
select top 1 * from SomeTable where processedFlag is null
end catch
END
I believe it because of
Statements such as USE, SET , DEALLOCATE CURSOR, CLOSE CURSOR,
BEGIN TRANSACTION or COMMIT TRANSACTION reset the ROWCOUNT value to 0.
May be END TRY is among them, but MSDN doesn't list all possible statements.
This will fix the problem:
DECLARE #i INT
SELECT #i = COUNT(*) FROM SomeTable WHERE processedFlag IS NULL
WHILE(#i > 0)
BEGIN
BEGIN TRY
UPDATE...
SET #i = ##ROWCOUNT
END TRY
BEGIN CATCH
SELECT #i = COUNT(*) FROM SomeTable WHERE processedFlag IS NULL
END CATCH
END
I have insert statements (simplified) in a SPROC like the following
SET ROWCOUNT 100
WHILE(1=1)
BEGIN
INSERT INTO table1
SELECT *
FROM table2
WHERE some_condition
-- EDIT: Realized forgot to include this following vital line that is causing issue
SET #var = #var + ##ROWCOUNT
-- ##ROWCOUNT now takes on a value of 1, which will cause the following IF check to fail even when no lines are inserted
IF(##ROWCOUNT = 0)
BEGIN
BREAK
END
END
But the issue is, after any operation even when no more rows fit my some_condition, ##ROWCOUNT is equal to 1, not 0.
How can I break that loop when there are 0 rows returned matching my some_condition?
The "set" statement creates a row count of 1. What you should do is immediately save ##ROWCOUNT into a #rowCount variable and use that var later on.
declare #rowCount int
WHILE(1=1)
BEGIN
INSERT INTO table1
SELECT *
FROM table2
WHERE some_condition
-- EDIT: Realized forgot to include this following vital line that is causing issue
SET #rowCount = ##ROWCOUNT
SET #var = #var + #rowCount
-- ##ROWCOUNT now takes on a value of 1, which will cause the following IF check to fail even when no lines are inserted
IF(#rowCount = 0)
BEGIN
BREAK
END
END
Also, you can simplify by setting #rowCount to -1 initially and changing the WHILE condition to #rowCount <> 0. The conditional BREAK will no longer be needed.
An alternative solution. This checks each iteration to see if the ID of the last inserted record has changed or not. If it hasn't changed, it indicates that no records were added that iteration.
SET ROWCOUNT 100
declare #id int;
WHILE(1=1)
INSERT INTO table1
SELECT *
FROM table2
WHERE some_condition
IF(#id= ##identity)
BEGIN
BREAK
END
set #id = ##identity;
END
Try this solutions:
1st solution
Using ##ROWCOUNT in loop's condition.
SET ROWCOUNT 100
INSERT INTO table1
SELECT *
FROM table2
WHERE some_condition
WHILE(##ROWCOUNT > 0)
BEGIN
INSERT INTO table1
SELECT *
FROM table2
WHERE some_condition
END
2nd solition
Using goto.
SET ROWCOUNT 100
WHILE(1=1)
BEGIN
INSERT INTO table1
SELECT *
FROM table2
WHERE some_condition
IF(##ROWCOUNT = 0)
BEGIN
goto label
END
END
label1:
print 'After lopp'
I think you should use select to get the ##rowcount into a variable. try this:
declare #number_of_rows int
SET ROWCOUNT 100
WHILE(1=1)
BEGIN
INSERT INTO table1
SELECT *
FROM table2
WHERE some_condition
SELECT #number_of_rows=##ROWCOUNT
IF (#number_of_rows = 0)
BEGIN
BREAK
END
END
Implemented solution similar to Moho, but used SELECT instead of SET to store ##ROWCOUNT.
I have 3 queries. If the first one returns empty set, I execute the second query, and if it returns empty set too, I give to the server the last chance and do the third:
SELECT ... INTO #Query1
IF EXISTS(SELECT * FROM #Query1)
SELECT * FROM #Query1
ELSE BEGIN
SELECT ... INTO #Query2
IF EXISTS(SELECT * FROM #Query2)
SELECT * FROM #Query2
ELSE BEGIN
SELECT ...
END
END
It worked well, but I've faced a new task - the query should be used as a subquery as well as other queries, within IF EXISTS([subquery]). Doing this, I see
Incorrect syntax near the keyword 'INTO'.
How should I modify the original query to meet this new condition?
Regards,
Are you looking for something like this:
create function dbo.Foo()
returns #Result table ( ThingId Int, Source Int )
as
begin
insert into #Result
select ThingId, 1 as Source from Things1
if ##ROWCOUNT = 0
insert into #Result
select ThingId, 2 as Source from Things2
if ##ROWCOUNT = 0
insert into #Result
select ThingId, 3 as Source from Things3
--...
return
end
go
select * from dbo.Foo()
select 42 as 'The Answer' where exists ( select * from dbo.Foo() where Source > 9 )
You can pass parameters into the function to use in WHERE clauses and the like. The Source column may be omitted if you don't care to know where the data originated.
I want to dynamically use TOP or not sort of like this...
SELECT #SomeNumber CASE WHERE 0 THEN TOP 5 COLUMNNAME
ELSE COLUMNNAME
END
FROM TABLE
I hope to have understood your problem: you want to select the TOP 5 rows if you pass #SomeNumber = 0 else select all th etable rows
As a first straight implementation you can do something like that
declare #SomeNumber as int
set #SomeNumber = 5
-- set #SomeNumber = 1
SELECT TOP (SELECT #SomeNumber) COLUMNNAME FROM MYTABLE
you can change the parameter value in order to have how many rows you want
Otherwise i suggest you to implement a stored procedure (and maybe you already did that, otherwise you can follow the next steps in order to do it)
CREATE procedure [dbo].[TOPCLAUSE]
-- clause parameter
#SomeNumber as integer
AS
IF #SomeNumber = 0
BEGIN
SELECT TOP 5 COLUMNNAME FROM MYTABLE
END
ELSE
BEGIN
SELECT COLUMNNAME FROM MYTABLE
END
GO
Then you can call
exec [dbo].[TOPCLAUSE] 0
exec [dbo].[TOPCLAUSE] 1
I probably not answered your question but let me know if it helped you
I don't think you can.
You could either use dynamic SQL:
Declare #int int
set #int = 10
exec ('Select top ' + #int + ' * From Customers')
Or you could set rowcount
if (#someNumber != 0)
begin
set rowcount 5
end
select * From Customers
set rowcount 0
I've just used something like this:-
Declare #SQL nvarchar(max), #Params nvarchar(max)
set #Params = N''
Set #SQL = N'SELECT ' + Cast(#SomeNumber as varchar) + ' CASE WHERE 0 THEN TOP 5 COLUMNNAME
ELSE COLUMNNAME
END
FROM TABLE'
exec sp_executesql #SQL, #Params
Short answer is no, not the way you have it.
You can however use IF to test and run a different query:
IF (#SomeNumber = 0)
BEGIN
SELECT TOP 5 ColumnName FROM Table
END
ELSE
BEGIN
SELECT ColumnName FROM Table
END
Two options: conditional SQL or dynamic SQL.
(1) Conditional:
IF #SomeNumber = 0
SELECT TOP 5 COLUMNAME FROM TABLE
ELSE
SELECT COLUMNAME FROM TABLE
(2) Dynamic: build up the query in a varchar() and pass it to sp_execute
Another loophole: make use of subquery's with row_number function
DECLARE #DoTopJN AS bit
SET #DoTopJN = 0 -- or 1
SELECT X.Sequence
X.COLUMNA
--etc
FROM (SELECT ROW_NUMBER() OVER (ORDER BY Y.Code) AS Sequence
,Y.COLUMNA
,Y.COLUMNB
-- etc.
FROM Y) X
WHERE ((#DoTopJN = 0) OR (X.Sequence = 1))
I don't think this is possible because TOP is applied on not just a column but the whole row. You would have to create two different select statements and put them in a IF ELSE construct.
To correct SPE109's code:
DECLARE #SomeNumber INT = 0
DECLARE #SQL nvarchar(max), #Params nvarchar(max)
set #Params = N''
SELECT #SQL = N'SELECT ' + CASE WHEN #SomeNumber = 0 THEN '' ELSE 'TOP ' + CAST(#SomeNumber as varchar) END + ' COLUMNNAME FROM TABLE'
exec sp_executesql #SQL, #Params