Strange exec and sp_executesql different behavior - sql

I have a small stored procedure (SQl Server 2012).
IF I run it using:
exec spSelRegion #bOver21 = 1
I Get different results than if I run it using:
exec sp_executesql N'spSelRegion',N'#bOver21 bit',#bOver21=1
What is the difference?
ALTER PROCEDURE [dbo].[spSelRegion]
(
#ID int = NULL
,#Name varchar(128) = NULL
,#OrderBy varchar(16) = 'ID'
,#bOver21 bit = null
)
AS
SELECT distinct
r.[ID],
r.[Name],
r.[dInserted],
r.[sInserted],
r.[dUpdated],
r.[sUpdated],
r.[timestamp]
FROM
[dbo].[tblRegion] r
left outer join tblCountyRegion cr
on r.ID = cr.RegionNbr
WHERE
(r.[ID] = #ID or #ID is null)
AND (r.[Name] = #Name or #Name is null)
AND (#bOver21 is null and r.[ID] >20
OR (#bOver21 = 1 and r.[ID] > 20 and cr.IsActive=1)
OR (#bOver21 = 0 and r.[ID] >= 21 and r.id < 31))
I don't want to make this more complecated than it is . But after a microsoft update today , some stored procedures are now runnig using sp_executesql instead of Exec and this is source of the issue.

You would need to run your dynamic SQL with the parameter specified. Without that, it assumes a null value passed. So this:
exec sp_executesql N'spSelRegion #bOver21 = #bOver21',N'#bOver21 bit',#bOver21=1

Related

SQL Server SP error Msg 4145, Level 15, State 1

When I try to run individual query for select and/or insert, it works. When I put it in SP and run it for all DB, it errors out.
Msg 4145, Level 15, State 1, Line 45 An expression of non-boolean type
specified in a context where a condition is expected, near 'Addr'.
Declare #command varchar(MAX)
Select #command = '
USE [?]
IF DB_NAME() not like ''%_VER''
BEGIN RETURN END
ElSE
BEGIN
Insert Into [TEST_VER].[dbo].[TestTable]
(ClientName,ID,Type,Filled,FilledDate,Cancelled,CancelledDate,CancellationReason,Deleted,NumberOfPosition,Address,City,State,Country,Annual,AnnualMax,FeePercentage,FeeTotal,GrossProfit,NetProfit,Rate,OTRate,CRate,COTRate,GrossProfit,GrossMargin,ProfitMargin,RegularMarkup,OTMarkup)
select
DB_NAME() as ClientName,
ID,
Type.Description as Type,
Filled,
FilledDate,
Cancelled,
CancelledDate,
CancellationReason.Description as CancellationReason,
Item.Deleted,
NumberOfPosition,
Address.Description as Address,
City.Description as City,
ProvinceState.Description as State,
Country.Description as Country,
PayP.Annual,
PayP.AnnualMaximum,
PayP.FeePercentage,
PayP.FeeTotal,
PayP.GrossProfit,
PayP.NetProfit,
PayT.Rate,
PayT.OTRate,
PayT.CRate,
PayT.COTRate,
PayT.GrossProfit,
PayT.GrossMargin,
PayT.ProfitMargin,
PayT.RegularMarkup,
PayT.OTMarkup
from [Item]
left join [Type] on Item.TypeID = Type.TypeID AND Type.LanguageId = 1
left join [CancellationReason] on Item.CancellationReasonID = CancellationReason.CancellationReasonID AND CancellationReason.LanguageID = 1
left join [Address] on Item.LocationID = Address.AddressID
left join [City] on Address.CityId = City.CityID
left join [ProvinceState] on Address.ProvinceStateId = ProvinceState.ProvinceStateID
left join [Country] on Address.CountryId = Country.CountryID
left join [PayP] on (Item.PaymentID=PayP.ID and Item.TypeID = 1)
left join [PayT] on (Item.PaymentID=PayT.ID and Item.TypeID > 1)
END'
EXEC sp_MSforeachdb #command
The short response is that your sql command is too long.
This undocumented stored procedure sp_msforeachdb doesn't accept a varchar as long as varchar(max). In fact, on SQL SERVER 2019 as seen in the db -fiddle below it only accepts 2000 characters which results in part of your query being cut off. As a result your join expression is incomplete resulting in the error message Msg 4145, Level 15, State 1, Line 45 An expression of non-boolean type specified in a context where a condition is expected, near 'Addr'..
I would recommend a shorter query or creating a stored procedure with your query in a generally accessible db and then running the command from there or rewriting your query to run differently. I've also included a few suggestions at the end of this answer.
You may use the stored procedure as I have used below sp_helptext to find out more about the definition in your db version.
The code and reproducible sample fiddle to prove the above is shown below.
select ##version;
GO
| (No column name) |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Microsoft SQL Server 2019 (RTM-CU6) (KB4563110) - 15.0.4053.23 (X64) <br> Jul 25 2020 11:26:55 <br> Copyright (C) 2019 Microsoft Corporation<br> Express Edition (64-bit) on Windows Server 2019 Standard 10.0 <X64> (Build 17763: ) (Hypervisor)<br> |
exec sp_helptext 'sp_msforeachdb'
GO
| Text |
| :---------------------------------------------------------------------------------------------------------------------------------------------------
/*
* The following table definition will be created by SQLDMO at start of each connection.
* We don't create it here temporarily because we need it in Exec() or upgrade won't work.
*/
create proc sys.sp_MSforeachdb
#command1 nvarchar(2000), #replacechar nchar(1) = N'?', #command2 nvarchar(2000) = null, #command3 nvarchar(2000) = null,
#precommand nvarchar(2000) = null, #postcommand nvarchar(2000) = null
as
set deadlock_priority low
/* This proc returns one or more rows for each accessible db, with each db defaulting to its own result set */
/* #precommand and #postcommand may be used to force a single result set via a temp table. */
/* Preprocessor won't replace within quotes so have to use str(). */
declare #inaccessible nvarchar(12), #invalidlogin nvarchar(12), #dbinaccessible nvarchar(12)
select #inaccessible = ltrim(str(convert(int, 0x03e0), 11))
select #invalidlogin = ltrim(str(convert(int, 0x40000000), 11))
select #dbinaccessible = N'0x80000000' /* SQLDMODbUserProf_InaccessibleDb; the negative number doesn't work in convert() */
if (#precommand is not null)
exec(#precommand)
declare #origdb nvarchar(128)
select #origdb = db_name()
/* If it's a single user db and there's an entry for it in sysprocesses who isn't us, we can't use it. */
/* Create the select */
exec(N'declare hCForEachDatabase cursor global for select name from master.dbo.sysdatabases d ' +
N' where (d.status & ' + #inaccessible + N' = 0)' +
N' and (DATABASEPROPERTYEX(d.name, ''UserAccess'') <> ''SINGLE_USER'' and (has_dbaccess(d.name) = 1))' )
declare #retval int
select #retval = ##error
if (#retval = 0)
exec #retval = sys.sp_MSforeach_worker #command1, #replacechar, #command2, #command3, 1
if (#retval = 0 and #postcommand is not null)
exec(#postcommand)
declare #tempdb nvarchar(258)
SELECT #tempdb = REPLACE(#origdb, N']', N']]')
exec (N'use ' + N'[' + #tempdb + N']')
return #retval
Here I run the query with the 2000 max and you will see that part of the query is cut off
Declare #command varchar(2000)
Select #command = '
USE [?]
IF DB_NAME() not like ''%_VER''
BEGIN RETURN END
ElSE
BEGIN
Insert Into [TEST_VER].[dbo].[TestTable]
(ClientName,ID,Type,Filled,FilledDate,Cancelled,CancelledDate,CancellationReason,Deleted,NumberOfPosition,Address,City,State,Country,Annual,AnnualMax,FeePercentage,FeeTotal,GrossProfit,NetProfit,Rate,OTRate,CRate,COTRate,GrossProfit,GrossMargin,ProfitMargin,RegularMarkup,OTMarkup)
select
DB_NAME() as ClientName,
ID,
Type.Description as Type,
Filled,
FilledDate,
Cancelled,
CancelledDate,
CancellationReason.Description as CancellationReason,
Item.Deleted,
NumberOfPosition,
Address.Description as Address,
City.Description as City,
ProvinceState.Description as State,
Country.Description as Country,
PayP.Annual,
PayP.AnnualMaximum,
PayP.FeePercentage,
PayP.FeeTotal,
PayP.GrossProfit,
PayP.NetProfit,
PayT.Rate,
PayT.OTRate,
PayT.CRate,
PayT.COTRate,
PayT.GrossProfit,
PayT.GrossMargin,
PayT.ProfitMargin,
PayT.RegularMarkup,
PayT.OTMarkup
from [Item]
left join [Type] on Item.TypeID = Type.TypeID AND Type.LanguageId = 1
left join [CancellationReason] on Item.CancellationReasonID = CancellationReason.CancellationReasonID AND CancellationReason.LanguageID = 1
left join [Address] on Item.LocationID = Address.AddressID
left join [City] on Address.CityId = City.CityID
left join [ProvinceState] on Address.ProvinceStateId = ProvinceState.ProvinceStateID
left join [Country] on Address.CountryId = Country.CountryID
left join [PayP] on (Item.PaymentID=PayP.ID and Item.TypeID = 1)
left join [PayT] on (Item.PaymentID=PayT.ID and Item.TypeID > 1)
END'
select 1
select #command
GO
| (No column name) |
| ---------------: |
| 1 |
USE [?]
IF DB_NAME() not like '%_VER'
BEGIN RETURN END
ElSE
BEGIN
Insert Into [TEST_VER].[dbo].[TestTable]
(ClientName,ID,Type,Filled,FilledDate,Cancelled,CancelledDate,CancellationReason,Deleted,NumberOfPosition,Address,City,State,Country,Annual,AnnualMax,FeePercentage,FeeTotal,GrossProfit,NetProfit,Rate,OTRate,CRate,COTRate,GrossProfit,GrossMargin,ProfitMargin,RegularMarkup,OTMarkup)
select
DB_NAME() as ClientName,
ID,
Type.Description as Type,
Filled,
FilledDate,
Cancelled,
CancelledDate,
CancellationReason.Description as CancellationReason,
Item.Deleted,
NumberOfPosition,
Address.Description as Address,
City.Description as City,
ProvinceState.Description as State,
Country.Description as Country,
PayP.Annual,
PayP.AnnualMaximum,
PayP.FeePercentage,
PayP.FeeTotal,
PayP.GrossProfit,
PayP.NetProfit,
PayT.Rate,
PayT.OTRate,
PayT.CRate,
PayT.COTRate,
PayT.GrossProfit,
PayT.GrossMargin,
PayT.ProfitMargin,
PayT.RegularMarkup,
PayT.OTMarkup
from [Item]
left join [Type] on Item.TypeID = Type.TypeID AND Type.LanguageId = 1
left join [CancellationReason] on Item.CancellationReasonID = CancellationReason.CancellationReasonID AND CancellationReason.LanguageID = 1
left join [Address] on Item.LocationID = Address.AddressID
left join [City] on Address.CityId = City.CityID
left join [ProvinceState] on Address.ProvinceStateId = P
db<>fiddle here
#Larnu has also made some suggestions about alternatives such as option 1 and option 2 which you may consider in your own time.

Conditional query with a parameter stored procedure

I'm learning SQL so I don't know yet all the subtlety of the language,
I wrote the following stored procedure (simplified here):
ALTER PROCEDURE [dbo].[SelectAllIssues]
#Status nvarchar(1) = 0
AS
BEGIN
SET NOCOUNT ON;
IF #Status = 1 OR #Status = 2
BEGIN
SELECT IssueStatuses.Id AS 'StatusId'
FROM Issues
INNER JOIN IssueStatuses ON Issues.IssueStatusId = IssueStatuses.Id
WHERE Issues.IssueStatusId = #Status
ORDER BY Created
END
ELSE
BEGIN
SELECT IssueStatuses.Id AS 'StatusId'
FROM Issues
INNER JOIN IssueStatuses ON Issues.IssueStatusId = IssueStatuses.Id
ORDER BY Created
END
END
But it doesn't look like a natural way to do that and there is a lot of repeated code.
I want to avoid something like
EXEC sp_executesql #sqlStrComplet
But if it's the only way.
I don't know the correct tag but sqllocaldb info MSSQLLocalDB return
Version : 13.1.4001.0
and I use SQL Server Management Studio (SSMS)
Just:
select s.Id as StatusId
from issues i
inner join IssueStatuses s on i.IssueStatusId = s.Id
where (i.IssueStatusId = #Status and #status in (1, 2)) or #status not in (1, 2)
Order By created
The where clause can be simplified:
where i.IssueStatusId = #Status or #status not in (1, 2)

Using different set of WHERE clauses in stored procedure depending on Parameter value

I have 2 stored procedures which return the same columns that I am trying to merge into a single procedure. They both have a different set of parameters and both have different WHERE clauses, but they use the same tables and select the exact same rows.
WHERE clause 1: (uses #UIOID, and #Level)
WHERE ( #UIOID = CASE WHEN #Level = 'Single' THEN C.C_UIOID_PK
WHEN #Level = 'Children' THEN CLC.UIOL_P
WHEN #Level = 'Parent' THEN CLP.UIOL_C
END
OR ( #UIOID = '0'
AND #Level = 'All'
)
)
Where clause 2: (Uses #TeamCode, #Year, #IncludeQCodes)
WHERE C.C_IsChild = 0
AND C.C_MOA <> 'ADD'
AND #TeamCode = C.C_OffOrg
AND C.C_Active = 'Y'
AND ( #Year BETWEEN dbo.f_GetAcYearByDate(C.C_StartDate) AND dbo.f_GetAcYearByDate(C.C_EndDate)
OR #Year = 0 )
AND ( C.C_InstCode NOT LIKE 'Q%'
OR #IncludeQCodes = 1 )
Ideally I want to add a new parameter which basically tells it which of the two WHERE clauses to run, but I can't seem to recreate that with CASE statement because as far as I can tell, they only work for a single WHERE clause, not a whole set of different clauses
I want to do this without having to repeat the select statement again and putting the whole thing in IF statements, and i don't want to put the query into a string either. I just want one select statement ideally.
The problem with using temp tables is the query itself takes a while to run without any parameters and is used in a live website, so I don't want it to have to put all records in a temp table and then filter it.
The problem with using a CTE is you can't follow it with an IF statement, so that wouldn't work either.
Here is the sort of logic I am trying to achieve:
SELECT A
B
C
FROM X
IF #WhichOption = 1 THEN
WHERE ( #UIOID = CASE WHEN #Level = 'Single' THEN C.C_UIOID_PK
WHEN #Level = 'Children' THEN CLC.UIOL_P
WHEN #Level = 'Parent' THEN CLP.UIOL_C
END
OR ( #UIOID = '0'
AND #Level = 'All'
)
)
ELSE IF #WhichOption = 2 THEN
WHERE C.C_IsChild = 0
AND C.C_MOA <> 'ADD'
AND #TeamCode = C.C_OffOrg
AND C.C_Active = 'Y'
AND ( #Year BETWEEN dbo.f_GetAcYearByDate(C.C_StartDate) AND dbo.f_GetAcYearByDate(C.C_EndDate)
OR #Year = 0 )
AND ( C.C_InstCode NOT LIKE 'Q%'
OR #IncludeQCodes = 1 )
Save the following process in a procedure. You can also directly insert into a physical table.
declare #varTable Table (columns exactly as Procedures return)
if(condition is met)
begin
insert into #varTable
exec proc1
end
else
begin
insert into #varTable
exec proc2
end
Add the parameter that you said that it would indicate what filter apply :
select XXXXX
from XXXXX
where (#Mode = 1 and ( filter 1 ))
or
(#Mode = 2 and ( filter 2 ))
option(recompile)
If the #Mode parameter is 1 then it will evaluate the filter 1, otherwise it will evaluate the filter 2.
Add an option(recompile) at the end of the statement, so the SQL engine will replace the variables with their values, eliminate the filter that won't be evaluated, and generate an execution plant for just the filter that you want to apply.
PS: Please notice that although these catchall queries are very easy to code and maintain, and generate a perfectly functional and optimal execution, they are not advised for high-demand applications. The option(recompile) forces the engine to recompile and generate a new execution plan at every execution and that would have a noticeable effect on performance if your query needs to be executed hundreds of times per minute. But for the occasional use it's perfectly fine.
Try to use dynamic SQL:
DECLARE #sql NVARCHAR(max), #where NVARCHAR(max), #WhichOption INT = 1;
SET #sql = 'SELECT A
B
C
FROM X';
IF #WhichOption = 1
SET #where = 'WHERE ( #UIOID = CASE WHEN #Level = ''Single'' THEN C.C_UIOID_PK
WHEN #Level = ''Children'' THEN CLC.UIOL_P
WHEN #Level = ''Parent'' THEN CLP.UIOL_C
END
OR ( #UIOID = ''0''
AND #Level = ''All''
)
)';
ELSE IF #WhichOption = 2
SET #where = ' WHERE C.C_IsChild = 0
AND C.C_MOA <> ''ADD''
AND #TeamCode = C.C_OffOrg
AND C.C_Active = ''Y''
AND ( #Year BETWEEN dbo.f_GetAcYearByDate(C.C_StartDate)
AND dbo.f_GetAcYearByDate(C.C_EndDate)
OR #Year = 0 )
AND ( C.C_InstCode NOT LIKE ''Q%''
OR #IncludeQCodes = 1 ) ';
SET #sql = CONCAT(#sql,' ', #where)
PRINT #sql
EXECUTE sp_executesql #sql

Throwing conversion error while pass the value directly

This code throws an error when pass the value directly, but it doesn't show any error if pass the value by using parameter.
--It throws an Error
DECLARE #sql NVARCHAR(4000)
DECLARE #ID INT=1234
SET #sql = N'select
[count]
FROM dbo.Table_1 AS t
JOIN dbo.table_2 AS t2 ON t.store_number = t2.store_number
AND t2.[year] = 17
AND t2.week_of_year = 6
AND t2.day_of_week = 2
WHERE t.RC_ID = #ID'
EXEC sp_executesql #sql
-- It throws an error
select
[count]
FROM dbo.Table_1 AS t
JOIN dbo.table_2 AS t2 ON t.store_number = t2.store_number
AND t2.[year] = 17
AND t2.week_of_year = 6
AND t2.day_of_week = 2
WHERE t.ID = 1234
-- IT WORKS
DECLARE #sql
DECLARE #ID INT
SET #ID = 1234
select
[count]
FROM dbo.Table_1 AS t
JOIN dbo.table_2 AS t2 ON t.store_number = t2.store_number
AND t2.[year] = 17
AND t2.week_of_year = 6
AND t2.day_of_week = 2
WHERE t.ID = #ID
The Error is :
Msg 245, Level 16, State 1, Line 1 Conversion failed when converting
the varchar value 'TEST' to data type int.
But there is No data like 'Test' in the table.
One of your values that you are comparing as integers contains a bad value:
select t2.*
from table_2 t2
where try_convert(int, year) is null or try_convert(int, week_of_year) is null or
try_convert(int, day_of_week) or try_convert(int, id) is null;
Whether the error occurs depends on the execution plan.
from what i see you are trying to use a parameter(#id) into the sp_executeSQL without never passing it. A quick fix would be to do something like that
DECLARE #sql NVARCHAR(4000)
DECLARE #ID INT = 10
SET #sql = N'select
[count]
FROM dbo.Table_1 AS t
JOIN dbo.table_2 AS t2 ON t.store_number = t2.store_number
AND t2.[year] = 17
AND t2.week_of_year = 6
AND t2.day_of_week = 2
WHERE t.RC_ID = ' + cast(#ID as nvarchar(20))
EXEC sp_executesql #sql
Hope this helps
One of your table columns that you are filtering is not a numeric data type. When you do
WHERE VarcharColumn = 1
The SQL Server engine will always try to convert the most complex type to the simplest one, in this case "1" is a integer and VarcharColumn is VARCHAR, so the engine will try to convert all the values stored in VarcharColumn to integer before filtering by value 1. Since at least one value stored there is not an integer ("TEST") then the conversion fails and that message pops up.
You have 2 solutions:
Validate and fix all your values in those columns so they are actually numbers and alter the data type to the corresponding one.
Compare against the same type. WHERE Column = '1'
Of course always try to keep your data types in check.
Also in your dynamicSQL query, the declaration of the #ID must be inside your script (it's also missing an initial value).
DECLARE #sql NVARCHAR(4000)
SET #sql = N'
DECLARE #ID INT = 1
select
[count]
FROM dbo.Table_1 AS t
JOIN dbo.table_2 AS t2 ON t.store_number = t2.store_number
AND t2.[year] = 17
AND t2.week_of_year = 6
AND t2.day_of_week = 2
WHERE t.RC_ID = #ID'
EXEC sp_executesql #sql
The reason for the error poping up 'sometimes' is because the different forms of your statement are making the execution plan do things in different order. If it tries to convert the varchar value to int first, it will fail. If it tries to convert the int value to varchar (for example) then it won't fail.
To find the problem, try this:
SELECT
*
FROM
dbo.Table_1 AS T
WHERE
CONVERT(VARCHAR(200), T.store_number) = 'Test' OR
CONVERT(VARCHAR(200), T.ID) = 'Test'
SELECT
*
FROM
dbo.table_2 AS T
WHERE
CONVERT(VARCHAR(200), T.store_number) = 'Test' OR
CONVERT(VARCHAR(200), T.[year]) = 'Test' OR
CONVERT(VARCHAR(200), T.week_of_year) = 'Test' OR
CONVERT(VARCHAR(200), T.day_of_week) = 'Test'

calling a remote sproc from a function

I have a stored procedure SprocA resides on ServerA. SprocA takes 4 parameters, executes a dynamic sql and returns a record with 1 column (1 integer value).
I'd like to be able to call this from a function FnB on ServerB so that I can use it in a stored procedure SprocB on server ServerB to return a recordset.
For example, I'd like to have something like this
Create Function FnB
#CustomerId int
,#PartId varchar(30)
,#DateFrom datetime
,#DateTo datetime
Returns int
As
Begin
Declare #Ret int
Exec #Ret = LnkSrv.DB_History.dbo.SprocA(#CustomerId, #PartId, #DateFrom, #DateTo)
Return #Ret
End --FnB
Create Procedure SprocB
#RowId int
As
Begin
Select Partid, FnB(Customerid, Partid, DateFrom, DateTo) As TotalQtyShipped
, AskedPrice, AskedQty, AppvPrice, AppvQty
From Tbl_Header a
Inner Join Tbl_Detail b On a.RowID = b.RowID
Where a.RowID = #RowId
End --SprocB
Possible result:
PartID TotalQtyShipped AskedPrice AskedQty AppvPrice AppQty
pn1 1000 10 100 10 100
pn2 550 20 50 15 50
pn3 2000 5 2000 5 1500
Please help
TL
If your solution based on dynamic SQL (ServerA.SprocA) you can't use functions at all in the following call sequence because SQL Server treats functions as deterministic and you can't change SQL Server state in the function call.
if I were on your place I'd made that LnkSrv.DB_History.dbo.SprocA creates denormalized table (tbl_FnB) with following (see below) columns insted of returning scalar value
CustomerId PartId DateFrom DateTo TotalQtyShipped
then SprocB would look like this
Create Procedure SprocB
#RowId int
As
Begin
exec LnkSrv.DB_History.dbo.SprocA; -- creates table Tbl_FnB on its side
Select Partid, Tbl.TotalQtyShipped
, AskedPrice, AskedQty, AppvPrice, AppvQty
From Tbl_Header a
Inner Join Tbl_Detail b On a.RowID = b.RowID
Inner Join LnkSrv.DB_History.dbo.Tbl_FnB f On f.CustomerId = b.Customerid
and f.Partid = b.Partid
and f.DateFrom = b.DateFrom
and f.DateTo = b.DateTo
Where a.RowID = #RowId
End --SprocB
I assumed that fields CustomerId PartId DateFrom DateTo located in the Tbl_Detail table
There's no real issue with what you're asking for except that you cannot use execute inside a function as you have it;
You can however do this:
create proc [dbo].[GetRowCount] (#TblName NVARCHAR(25) , #Itemid INT,#RowCnt int = 0)
AS BEGIN
DECLARE #Sqlstring nvarchar(2000)
set #Sqlstring = 'SELECT #RowCnt = COUNT(*) FROM ['+ #TblName +']
WHERE Itemid = '+ convert(varchar(10),#Itemid)
EXEC sp_executesql #Sqlstring,N'#RowCnt int output',#RowCnt output
END
...
declare #RowCnt int
exec [GetRowCount] #TblName='TableName',#Itemid='ItemID',#RowCnt=#RowCnt output
select #RowCnt
you should be able to adapt this for your own situation.