How do I execute an If statement inside a CTE - tsql

I need to create a cte based on the value of an SSRS report parameter. The If statement in the attached code executes just fine outside the cte, but errors inside the cte.
I and one of my coworkers have spent several hours unsuccessfully trying to solve this. I have tried using a CASE statement. I have tried placing the WITH cteIngnoreCommID AS inside both the IF and ELSE, but nothing works.
This cte will be joined in subsequent code that creates another cte. Hopefully someone can tell me what I need to do.
--Declare and set Report parameter for testing purposes only
Declare #IgnoreYear as varchar(4)='2010'
;
WITH cteIgnoreCommID as (
IF #IgnoreYear=''
-- create cte with one record have blank values
Select '' as IgnoreComm_ID
,'' as IgnoreYear
,0 as IngnoreBalFwd
ELSE
-- create cte with records obtained from multiple tables.
-- for proof of concept I am just creating one record.
Select '30710000' as IgnoreComm_ID
,#IgnoreYear as IgnoreYear
,5000 as IngnoreBalFwd
)
--The above code run just fine if you comment out the cte lines.
I get the correct results if run the IF/ELSE outside the cte. When run as written with the cte I get the following 2 errors:
Msg 156, Level 15, State 1, Line 5 Incorrect syntax near the keyword
'IF'. Msg 102, Level 15, State 1, Line 13 Incorrect syntax near ')'.

The easiest way would be to use CASE statements inside the CTE. You could modify your CTE to look like this and it should do what you want:
Declare #IgnoreYear as varchar(4)='2010'
;
WITH cteIgnoreCommID as (
Select CASE WHEN #IgnoreYear='' THEN ''
ELSE '30710000'
END AS IgnoreComm_ID
,CASE WHEN #IgnoreYear='' THEN ''
ELSE #IgnoreYear
END as IgnoreYear
,CASE WHEN #IgnoreYear='' THEN 0
ELSE 5000
END as IngnoreBalFwd
)

You can put the if outside the CTE and make the CTE declaration based on the conditional instead:
if 3 > 2
BEGIN
;WITH cte AS (
SELECT 1 AS val
)
SELECT *
FROM cte
END
ELSE
BEGIN
;WITH cte AS (
SELECT 2 AS val
)
SELECT *
FROM cte
END
Using your code:
Declare #IgnoreYear as varchar(4)='2010'
IF #IgnoreYear=''
BEGIN
;WITH cteIgnoreCommID AS (
-- create cte with one record have blank values
Select '' as IgnoreComm_ID
,'' as IgnoreYear
,0 as IngnoreBalFwd
)
SELECT *
FROM cteIgnoreCommID
END
ELSE
BEGIN
;WITH cteIgnoreCommID AS (
Select '30710000' as IgnoreComm_ID
,#IgnoreYear as IgnoreYear
,5000 as IngnoreBalFwd
)
SELECT *
FROM cteIgnoreCommID
END

You could do something like this:
Declare #IgnoreYear as varchar(4)='2010'
;
WITH cteIgnoreCommID as (
-- create cte with one record have blank values
Select '' as IgnoreComm_ID
,'' as IgnoreYear
,0 as IngnoreBalFwd WHERE #IgnoreYear = ''
UNION ALL
-- create cte with records obtained from multiple tables.
-- for proof of concept I am just creating one record.
Select '30710000' as IgnoreComm_ID
,#IgnoreYear as IgnoreYear
,5000 as IngnoreBalFwd WHERE (#IgnoreYear IS NULL OR #IgnoreYear <> '')
)
SELECT *
FROM cteIgnoreCommID
If #IgnoreYear is never NULL, then you can remove the "#IgnoreYear IS NULL OR" part.

I am fairly new to SQL but the answer to my problem ended up being to not create a CTE but a Temp Table. This handled my IF statement fine and I could join to it with the data as required by the report parameter. The code I used to create either a blank temp table or a filled temp table was:
create table #tmpIgnoreComm
( IgnoreComm_ID varchar(8)
,IgnoreYear varchar(4)
,IngnoreBalFwd numeric )
If #IgnoreYear<>''
insert into #tmpIgnoreComm
--(IgnoreComm_ID,IgnoreYear,IngnoreBalFwd)
Select *
From
( Select Distinct
-- Long code to retrieve data
) data
Thanks everyone for you input.

Related

How does one Create a Parameterized Recursive CTE to flatten a heirarchy within a Scalar Function?

I'm trying to create a scalar function to determine whether a user of a provided ID or any of their subordinates have orders under a collection of provided order IDs.
Note I am using my own User-Defined Table Type of IntegerIdTableType to take in the collection of OrderIds.
CREATE FUNCTION DoOrdersExistUnderUserOrUsersSubordinates
(
#orderIds dbo.IntegerIdTableType READONLY,
#userId INT
)
RETURNS BIT
AS
BEGIN
RETURN
(
WITH GetUserIds(ordinateUserId)
AS
(
SELECT ordinateUserId UserId
UNION ALL
SELECT GetUserIds(Subordinate.Id)
FROM UsersAccounts.Users Subordinates
WHERE Subordinates.SupervisorId = #ordinateUserId
)
SELECT CASE WHEN EXISTS
(
SELECT 1
FROM Orders
WHERE Orders.Id IN
(
SELECT Id
FROM #orderIds
)
AND Orders.UserId IN
(
SELECT UserId
FROM GetUserIds(#userId)
)
)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END
)
END
Here is some sample data for both my Orders and Users tables.
Users
Orders
Expected Results
When calling DoOrdersExistUnderUserOrUsersSubordinates with the following values, I expect the following results.
I'm having 2 issues with this function:
Syntax errors:
Incorrect syntax near the keyword 'WITH'.
Incorrect syntax near ')'.
'GetUserIds' is not a recognized built-in function name
The above seems to happen even without being wrapped in a function.
I don't know what the correct way to pass a parameter to a recursive CTE is but I have seen examples where the declaration of the CTE has a name in brackets which I assumed to be a parameter
I've tried putting a semi-colon immediately before the WITH even though it's the only statement in the function and I just get Incorrect syntax near ';'. instead of Incorrect syntax near the keyword 'WITH'.
I've also tried getting rid of the BEGIN and END and that gives me Incorrect syntax near 'RETURN'., plus Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon. If I don't include the redundant semi-colon.
How do I get around all of this?
Surely Recursive CTE's must be able to take in a parameter or what would they recurse on?
UPDATE:
After battling with Example F of the documentation linked by Zohar_Peled, I eventually figured out that parameters aren't passed into the CTE as such, but rather joined to it then persisted within it through the brackets of its declaration. Whatever is then defined in the corresponding SELECTs is output through the parameters to whatever called the CTE (in this case, either the outer SELECT Id FROM UserNodes statement or the CTE itself (for the recursion)).
I changed the SQL statement within the function to the following and it worked as expected outside of the function.
WITH UserNodes([Root User ID], Id, SupervisorId)
AS
(
SELECT Users.Id, Users.Id, Users.SupervisorId
FROM UsersAccounts.Users
WHERE Users.SupervisorId IS NULL
UNION ALL
SELECT [Root User ID],
Users.Id,
Users.SupervisorId
FROM UsersAccounts.Users
JOIN UserNodes [Subordinate Descendant Users] ON [Subordinate Descendant Users].Id = Users.SupervisorId
)
SELECT CASE WHEN EXISTS
(
SELECT 1
FROM Orders
WHERE Orders.Id IN
(
SELECT Id
FROM #orderIds
)
AND Orders.UserId IN
(
SELECT Id
FROM UserNodes
WHERE [Root User ID] = #userId
)
)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END
This works fine alone (with the required variables provided to substitute the missing function parameters) but as soon as I put it back into the CREATE FUNCTION block, I'm faced with the same syntax errors as before (excluding 2.).
As stated, I'm not able to test this, but this is what I'm suggesting you change:
CREATE FUNCTION DoOrdersExistUnderUserOrUsersSubordinates
(
#orderIds dbo.IntegerIdTableType READONLY,
#userId INT
)
RETURNS BIT
AS
BEGIN
declare #bln bit
;WITH UserNodes([Root User ID], Id, SupervisorId)
AS
(
SELECT Users.Id, Users.Id, Users.SupervisorId
FROM UsersAccounts.Users
WHERE Users.SupervisorId IS NULL
UNION ALL
SELECT [Root User ID],
Users.Id,
Users.SupervisorId
FROM UsersAccounts.Users
JOIN UserNodes [Subordinate Descendant Users] ON [Subordinate Descendant Users].Id = Users.SupervisorId
)
SELECT #bln = CASE WHEN EXISTS
(
SELECT 1
FROM Orders
WHERE Orders.Id IN
(
SELECT Id
FROM #orderIds
)
AND Orders.UserId IN
(
SELECT Id
FROM UserNodes
WHERE [Root User ID] = #userId
)
)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END
RETURN #bln
END
Let me know if it works...

T-SQL - Pivot/Crosstab - variable number of values

I have a simple data set that looks like this:
Name Code
A A-One
A A-Two
B B-One
C C-One
C C-Two
C C-Three
I want to output it so it looks like this:
Name Code1 Code2 Code3 Code4 Code...n ...
A A-One A-Two
B B-One
C C-One C-Two C-Three
For each of the 'Name' values, there can be an undetermined number of 'Code' values.
I have been looking at various examples of Pivot SQL [including simple Pivot sql and sql using the XML function?] but I have not been able to figure this out - or to understand if it is even possible.
I would appreciate any help or pointers.
Thanks!
Try it like this:
DECLARE #tbl TABLE([Name] VARCHAR(100),Code VARCHAR(100));
INSERT INTO #tbl VALUES
('A','A-One')
,('A','A-Two')
,('B','B-One')
,('C','C-One')
,('C','C-Two')
,('C','C-Three');
SELECT p.*
FROM
(
SELECT *
,CONCAT('Code',ROW_NUMBER() OVER(PARTITION BY [Name] ORDER BY Code)) AS ColumnName
FROM #tbl
)t
PIVOT
(
MAX(Code) FOR ColumnName IN (Code1,Code2,Code3,Code4,Code5 /*add as many as you need*/)
)p;
This line
,CONCAT('Code',ROW_NUMBER() OVER(PARTITION BY [Name] ORDER BY Code)) AS ColumnName
will use a partitioned ROW_NUMBER in order to create numbered column names per code. The rest is simple PIVOT...
UPDATE: A dynamic approach to reflect the max amount of codes per group
CREATE TABLE TblTest([Name] VARCHAR(100),Code VARCHAR(100));
INSERT INTO TblTest VALUES
('A','A-One')
,('A','A-Two')
,('B','B-One')
,('C','C-One')
,('C','C-Two')
,('C','C-Three');
DECLARE #cols VARCHAR(MAX);
WITH GetMaxCount(mc) AS(SELECT TOP 1 COUNT([Code]) FROM TblTest GROUP BY [Name] ORDER BY COUNT([Code]) DESC)
SELECT #cols=STUFF(
(
SELECT CONCAT(',Code',Nmbr)
FROM
(SELECT TOP((SELECT mc FROM GetMaxCount)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) t(Nmbr)
FOR XML PATH('')
),1,1,'');
DECLARE #sql VARCHAR(MAX)=
'SELECT p.*
FROM
(
SELECT *
,CONCAT(''Code'',ROW_NUMBER() OVER(PARTITION BY [Name] ORDER BY Code)) AS ColumnName
FROM TblTest
)t
PIVOT
(
MAX(Code) FOR ColumnName IN (' + #cols + ')
)p;';
EXEC(#sql);
GO
DROP TABLE TblTest;
As you can see, the only part which will change in order to reflect the actual amount of columns is the list in PIVOTs IN() clause.
You can create a string, which looks like Code1,Code2,Code3,...CodeN and build the statement dynamically. This can be triggered with EXEC().
I'd prefer the first approach. Dynamically created SQL is very mighty, but can be a pain in the neck too...

Removing all the Alphabets from a string using a single SQL Query [duplicate]

I'm currently doing a data conversion project and need to strip all alphabetical characters from a string. Unfortunately I can't create or use a function as we don't own the source machine making the methods I've found from searching for previous posts unusable.
What would be the best way to do this in a select statement? Speed isn't too much of an issue as this will only be running over 30,000 records or so and is a once off statement.
You can do this in a single statement. You're not really creating a statement with 200+ REPLACEs are you?!
update tbl
set S = U.clean
from tbl
cross apply
(
select Substring(tbl.S,v.number,1)
-- this table will cater for strings up to length 2047
from master..spt_values v
where v.type='P' and v.number between 1 and len(tbl.S)
and Substring(tbl.S,v.number,1) like '[0-9]'
order by v.number
for xml path ('')
) U(clean)
Working SQL Fiddle showing this query with sample data
Replicated below for posterity:
create table tbl (ID int identity, S varchar(500))
insert tbl select 'asdlfj;390312hr9fasd9uhf012 3or h239ur ' + char(13) + 'asdfasf'
insert tbl select '123'
insert tbl select ''
insert tbl select null
insert tbl select '123 a 124'
Results
ID S
1 390312990123239
2 123
3 (null)
4 (null)
5 123124
CTE comes for HELP here.
;WITH CTE AS
(
SELECT
[ProductNumber] AS OrigProductNumber
,CAST([ProductNumber] AS VARCHAR(100)) AS [ProductNumber]
FROM [AdventureWorks].[Production].[Product]
UNION ALL
SELECT OrigProductNumber
,CAST(STUFF([ProductNumber], PATINDEX('%[^0-9]%', [ProductNumber]), 1, '') AS VARCHAR(100) ) AS [ProductNumber]
FROM CTE WHERE PATINDEX('%[^0-9]%', [ProductNumber]) > 0
)
SELECT * FROM CTE
WHERE PATINDEX('%[^0-9]%', [ProductNumber]) = 0
OPTION (MAXRECURSION 0)
output:
OrigProductNumber ProductNumber
WB-H098 098
VE-C304-S 304
VE-C304-M 304
VE-C304-L 304
TT-T092 092
RichardTheKiwi's script in a function for use in selects without cross apply,
also added dot because in my case I use it for double and money values within a varchar field
CREATE FUNCTION dbo.ReplaceNonNumericChars (#string VARCHAR(5000))
RETURNS VARCHAR(1000)
AS
BEGIN
SET #string = REPLACE(#string, ',', '.')
SET #string = (SELECT SUBSTRING(#string, v.number, 1)
FROM master..spt_values v
WHERE v.type = 'P'
AND v.number BETWEEN 1 AND LEN(#string)
AND (SUBSTRING(#string, v.number, 1) LIKE '[0-9]'
OR SUBSTRING(#string, v.number, 1) LIKE '[.]')
ORDER BY v.number
FOR
XML PATH('')
)
RETURN #string
END
GO
Thanks RichardTheKiwi +1
Well if you really can't use a function, I suppose you could do something like this:
SELECT REPLACE(REPLACE(REPLACE(LOWER(col),'a',''),'b',''),'c','')
FROM dbo.table...
Obviously it would be a lot uglier than that, since I only handled the first three letters, but it should give the idea.

How to write a multi-parameter CTE script?

I am trying to write a TSQL script for an SSRS report that uses a CTE to select records based on the parameters chosen. I'm looking for the most efficient way to do this, either all in TSQL and/or SSRS. I have 4 parameters which can be set to NULL (All values) or one specific value. Then in my CTE, I have the following line:
ROW_NUMBER() over(partition by G.[program_providing_service],G.people_id
order by G.[actual_date] desc) as rowID
This above CTE is for the case when Program is NULL and People is not null. My 4 parameters are:
Program, Facility, Staff, and People.
So I only want to partition values when they are NULL. Currently I implement this by one CTE depending on the parameter values. For example, if they choose NULL for all parameters except People, then this CTE would look like:
ROW_NUMBER() over(partition by G.people_id
order by G.[actual_date] desc) as rowID
Or if all 5 parameters are null:
ROW_NUMBER() over(partition by G.[program_providing_service], G.[site_providing_service], G.staff_id, G.people_id
order by G.[actual_date] desc) as rowID
If they do not choose NULL for any of the 4 parameters, then I probably do not need to partition by any field since I just want the top 1 record ordered by actual_date descending. This is what my CTE looks like:
;with cte as
(
Select distinct
G.[actual_date],
G.[site_providing_service],
p.[program_name],
G.[staff_id],
G.program_providing_service,
ROW_NUMBER() over(partition by G.[program_providing_service],G.people_id
order by G.[actual_date] desc) as rowID
From
event_log_rv G With (NoLock)
WHERE
...
AND (#ClientID Is Null OR [people_id]=#ClientID)
AND (#StaffID Is Null OR [staff_id] = #StaffID)
AND (#FacilityID Is Null OR [site_providing_service] = #FacilityID)
AND (#ProgramID Is Null OR [program_providing_service] = #ProgramID)
and (#SupervisorID is NULL OR staff_id in (select staff_id from #supervisors))
)
SELECT
[actual_date],
[site_providing_service],
[program_name],
[staff_id],
program_providing_service,
people_id,
rowID
FROM cte WHERE rowid = 1
ORDER BY [Client_FullName]
where the ROW_NUMBER line varies depending on the parameters chosen. Currently I have 5 IF statements in this TSQL script that look like:
IF #ProgramID IS NOT NULL AND #ClientID IS NULL
BEGIN
...
END
with one CTE in each of these IF statements:
IF #FacilityID IS NOT NULL AND #ClientID IS NULL
BEGIN
...
END
IF #ProgramID IS NOT NULL AND #ClientID IS NULL
BEGIN
...
END
IF #StaffID IS NOT NULL AND #ClientID IS NULL
BEGIN
...
END
IF #ClientID IS NOT NULL
BEGIN
...
END
How can I code for all possible options, whether they choose NULL or else specific values?
OMG.... it took me long time to try to understand what you want to do. There is some contradiction in your description. Pleas revist your description. Like you said you only want to partition values when they are NULL; then you also said, when they choose NULL for all parameter except for people, then you partition on people....
No matter what way you want to achieve, partition on 'null' or 'not null', you can construct dynamic sql to achieve this, instead of adding a lot of [if...else]
Following code is pseudo, definitely not tested. Just give you a hint. The following code has one assumption, which is your parameters have priority in partition order, for example, if Program is not null (or null), Program is in the first location.
declare #sql varchar(max)
set #sql = '
;with cte as
(
Select distinct
G.[actual_date],
G.[site_providing_service],
p.[program_name],
G.[staff_id],
G.program_providing_service,
ROW_NUMBER() over(partition by
'
if(#progarm is null)
set #sql = #sql + 'G.[program_providing_service],'
if(#facility is null)
set #sql = #sql + 'G.[site_providing_service],'
if(#staff is null )
set #sql = #sql + 'G.staff_id,'
if(#people is null)
set #sql = #sql + 'G.people_id'
set #sql = #sql + '
order by G.[actual_date] desc) as rowID
From
event_log_rv G With (NoLock)
WHERE
...
AND (#ClientID Is Null OR [people_id]=#ClientID)
AND (#StaffID Is Null OR [staff_id] = #StaffID)
AND (#FacilityID Is Null OR [site_providing_service] = #FacilityID)
AND (#ProgramID Is Null OR [program_providing_service] = #ProgramID)
and (#SupervisorID is NULL OR staff_id in (select staff_id from #supervisors))
)
SELECT
[actual_date],
[site_providing_service],
[program_name],
[staff_id],
program_providing_service,
people_id,
rowID
FROM cte WHERE rowid = 1
ORDER BY [Client_FullName]
'
exec(#sql)

udf not returning data correctly in t-sql programming

HERE IS THE FUNCTION:
ALTER FUNCTION dbo.FN_GET_QUARTER
-- the parameters for the function here
(
#FN_Qtr_date datetime
)
RETURNS INT
AS
BEGIN
RETURN datepart(qq,#FN_Qtr_date)
END
HERE IS THE SQL REPORT:
IF(SELECT(OBJECT_ID('TEMPDB..#T1'))) IS NOT NULL DROP TABLE #T1
SELECT L_NUMBER, LAST_MAINTENANCE_DATE,
dbo.FN_FICO_BANDS (LAST_MAINTENANCE_DATE) AS FN_Qtr_date
INTO #T1
FROM OPENQUERY(SrvLink, '
SELECT LOAN_NUMBER,LAST_MAINTENANCE_DATE
FROM BDE.loan_V
FETCH FIRST 1000 ROWS ONLY WITH UR ')
GO
SELECT COUNT(*), FN_Qtr_date
FROM #T1
GROUP BY FN_Qtr_date
ORDER BY FN_Qtr_date
Results:
L count FN_Qtr_Date
150 Invalid
355 Invalid
I am not sure what I am doing wrong..
The first thing that jumps out at me is that you're calling dbo.FN_FICO_BANDS instead of dbo.FN_GET_QUARTER
dbo.FN_FICO_BANDS (LAST_MAINTENANCE_DATE) AS FN_Qtr_date
If you're going to use the function on LAST_MAINTENANCE_DATE, you need to use:
dbo.FN_Qtr_Date(LAST_MAINTENANCE_DATE) AS YourQuarter