Using a multi-valued parameter in SSRS with dynamic SQL - sql-server

I am trying to use a multi-valued parameter in SSRS within a dynamic SQL query. In a static query I would use
SELECT myField
FROM myTable
WHERE myField IN (#myParameter)
Using answers to this question (TSQL Passing MultiValued Reporting Services Parameter into Dynamic SQL) I have tried
-- SSRS requires the output field names to be static
CREATE TABLE #temp
(
myField VARCHAR(100)
)
DECLARE #myQuery VARCHAR(5000) = 'SELECT myField
INTO #temp
FROM myTable
WHERE CHARINDEX('','' + myField + '','', '',''+''' + #myParameter + '''+'','') > 0'
EXEC (#myQuery)
This approach should work if the query understood #myParameter to be a string in a CSV format, but it doesn't seem to (as suggested by the link above). For example
SELECT #myParameter
won't work if there is more than one value selected.
I've also tried moving the parameter into a temporary table:
SELECT myField
INTO #tempParameter
FROM #myParameter
-- SSRS requires the output field names to be static
CREATE TABLE #temp
(
myField VARCHAR(100)
)
DECLARE #myQuery VARCHAR(5000) = 'SELECT myField
INTO #temp
FROM myTable
WHERE myField IN (SELECT myField FROM #tempParameter)'
EXEC (#myQuery)
I have SSRS 2012 and SQL Server 2012. NB: I need to use dynamic SQL for other reasons.

You don't need dynamic SQL for this. SSRS will (much to my dislike) inject multi value parameters when using a hard coded SQL statement in the report. Therefore you can just do something like the following:
SELECT *
FROM MyTable
WHERE MyColumn IN (#MyParameter)
AND OtherCol > 0;
Before running the query, SSRS will remove #MyParameter and inject a delimited list of parameters.
The best guess, if you need to use dynamic SQL, is to use a string splitter and an SP (I use DelimitedSplit8K_LEAD here). SSRS will then pass the value of the parameter (#MultiParam) as a delimited string, and you can then split that in the dynamic statement:
CREATE PROC dbo.YourProc #MultiParam varchar(8000), #TableName sysname AS
BEGIN
DECLARE #SQL nvarchar(MAX);
SET #SQL = N'SELECT * FROM dbo.' + QUOTENAME(#TableName) + N' MT CROSS APPLY dbo.DelimitedSplit8K_LEAD (#MultiParam,'','') DS WHERE MT.MyColumn = DS.item;';
EXEC sp_executesql #SQL, N'#MultiParam varchar(8000)', #MultiParam;
END;
GO

As I mentioned in the comments, your parameter is coming from SSRS as a single comma separated string as such:
#myParameter = 'FirstValue, Second Value Selected, Third Val'
When you try to use the parameter in the IN clause, it is read as such:
select *
from my table
where my column in ('FirstValue, Second Value Selected, Third Val')
This is invalid. The correct syntax would be like below, with quotes around each value.
select *
from my table
where my column in ('FirstValue', 'Second Value Selected', 'Third Val')
So, you need to find a way to quote each value, which is hard because you don't know how many values there will be. So, the best thing to do is split that parameter into a table, and JOIN to it. Since we use a table-valued function in this example, we use CROSS APPLY.
First, create the function that Jeff Moden made, and so many people use. Or, use STRING_SPLIT if you are on 2016 onward, or make your own. However, anything that uses a recursive CTE, WHILE loop, cursor, etc will be far slower than the one below.
CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
Then, you simply call that with your function like so:
DB FIDDLE DEMO
create table mytable (Names varchar(64))
insert into mytable values ('Bob'),('Mary'),('Tom'),('Frank')
--this is your parameter from SSRS
declare #var varchar(4000) = 'Bob,Mary,Janice,Scarlett'
select distinct mytable.*
from mytable
cross apply dbo.DelimitedSplit8K(#var,',') spt
where spt.Item = mytable.Names

The split string solution didn't work for me either, and I tried to figure out what type the Multi parameter variable in SSRS actually is so I somehow could work with it. The multi parameter variable was #productIds and the type of the data field was UNIQUEIDENTIFIER. So I wrote the type to a log table
INSERT INTO DebugLog
SELECT CAST(SQL_VARIANT_PROPERTY(#productIds,'BaseType') AS VARCHAR(MAX))
When I selected one value the type was NVARCHAR, however when I selected two or more values I got an error
System.Data.SqlClient.SqlException: The sql_variant_property function requires 2 argument(s).
So I stopped trying to figure out what a multi parameter variable actually was, I already spent to much time.
Since I had the values in a table I could select the values from that table into a temp table and then join on that in the dynamic built query
SELECT p.Id
INTO #tempProdIds
FROM Products p WHERE p.Id IN #productIds
SET #query +='
....
JOIN #tempProdIds tpi on tl.Product = tpi.Id
....
'
EXEC(#query)

Related

Accumulating (concatenate) values in variable does not work

I'm trying to accumulate values into a variable in SQL Server>=2012.
It works in case 1 below, but in case 2 I get the answer ",CD" instead of the expected ",EF,AB,CD" Why?
In MMS:
USE MyDB
GO
-- Create a simple table
CREATE TABLE Tbl1 (Code VARCHAR(2), So TINYINT NULL)
INSERT INTO Tbl1 VALUES('AB', 10)
INSERT INTO Tbl1 VALUES('CD', NULL)
INSERT INTO Tbl1 VALUES('EF', 5)
GO
-- Case 1
DECLARE #MyVar VARCHAR(255) = ''
SELECT #MyVar=#MyVar + ',' + Code FROM Tbl1 ORDER BY So
SELECT #MyVar
GO
-- Case 2
DECLARE #MyVar VARCHAR(255) = ''
SELECT #MyVar=#MyVar + ',' + Code FROM Tbl1 ORDER BY ISNULL(So, 255)
SELECT #MyVar
GO
The explanation is in the documentation:
Don't use a variable in a SELECT statement to concatenate values (that
is, to compute aggregate values). Unexpected query results may occur.
Because, all expressions in the SELECT list (including assignments)
aren't necessarily run exactly once for each output row.
There are opinions (but not in the official docs), stating that without an ORDER BY clause (and/or a DISTINCT clause) the aggregation works as you expect.
If you are using SQL Server 2017+, you may use STRING_AGG() to build the expected output:
DECLARE #MyVar VARCHAR(255) = ''
SELECT #MyVar = STRING_AGG(Code, ',') WITHIN GROUP (ORDER BY ISNULL(So, 255))
FROM Tbl1
SELECT #MyVar

Substring is slow with while loop in SQL Server

One of my table column stores ~650,000 characters (each value of the column contains entire table). I know its bad design however, Client will not be able to change it.
I am tasked to convert the column into multiple columns.
I chose to use dbo.DelimitedSplit8K function
Unfortunately, it can only handle 8k characters at max.
So I decided to split the column into 81 8k batches using while loop and store the same in a variable table (temp or normal table made no improvement)
DECLARE #tab1 table ( serialnumber int, etext nvarchar(1000))
declare #scriptquan int = (select MAX(len (errortext)/8000) from mytable)
DECLARE #Counter INT
DECLARE #A bigint = 1
DECLARE #B bigint = 8000
SET #Counter=1
WHILE ( #Counter <= #scriptquan + 1)
BEGIN
insert into #tab1 select ItemNumber, Item from dbo.mytable cross apply dbo.DelimitedSplit8K(substring(errortext, #A, #B), CHAR(13)+CHAR(10))
SET #A = #A + 8000
SET #B = #B + 8000
SET #Counter = #Counter + 1
END
This followed by using below code
declare #tab2 table (Item nvarchar(max),itemnumber int, Colseq varchar(10)) -- declare table variable
;with cte as (
select [etext] ,ItemNumber, Item from #tab1 -- insert table name
cross apply dbo.DelimitedSplit8K(etext,' ')) -- insert table columns name that contains text
insert into #tab2 Select Item,itemnumber, 'a'+ cast (ItemNumber as varchar) colseq
from cte -- insert values to table variable
;WITH Tbl(item, colseq) AS(
select item, colseq from #tab2
),
CteRn AS(
SELECT item, colseq,
Rn = ROW_NUMBER() OVER(PARTITION BY colseq ORDER BY colseq)
FROM Tbl
)
SELECT
a1 Time,a2 Number,a3 Type,a4 Remarks
FROM CteRn r
PIVOT(
MAX(item)
FOR colseq IN(a1,a2,a3,a4)
)p
where a3 = 'error'
gives the desired output. However, just the loop takes 15 minutes to complete and overall query completes by 27 minutes. Is there any way I can make it faster? Total row count in my table is 2. So I don't think Index can help.
Client uses Azure SQL Database so I can't choose PowerShell or Python to accomplish this either.
Please let me know if more information is needed. I tried my best to mention everything I could.

Select rows with any member of list of substrings in string

In a Micrososft SQL Server table I have a column with a string.
Example:
'Servernamexyz.server.operationunit.otherstuff.icouldnt.predict.domain.domain2.domain3'
I also have a dynamic list of substrings
Example:
('icouldnt', 'stuff', 'banana')
I don't care for string manipulation. The substrings could also be called:
('%icouldnt%', '%stuff%', '%banana%')
What's the best way to find all rows where the string contains one of the substrings?
Solutions that are not possible:
multiple OR Statements in the WHERE clause, the list is dynamic
external Code to do a "for each", its a multi value parameter from the reportbuilder, so nothing useful here
changing the database, its the database of a tool a costumer is using and we can't change it, even if we would like... so much
I really cant believe how hard such a simple problem can turn out. It would need a "LIKE IN" command to do it in a way that looks ok. Right now I cant think of anything but a messy temp table.
One option is to use CHARINDEX
DECLARE #tab TABLE (Col1 NVARCHAR(200))
INSERT INTO #tab (Col1)
VALUES (N'Servernamexyz.server.operationunit.otherstuff.icouldnt.predict.domain.domain2.domain3' )
;WITH cteX
AS(
SELECT 'icouldnt' Strings
UNION ALL
SELECT 'stuff'
UNION ALL
SELECT 'banana'
)
SELECT
T.*, X.Strings
FROM #tab T
CROSS APPLY (SELECT X.Strings FROM cteX X) X
WHERE CHARINDEX(X.Strings, T.Col1) > 1
Output
EDIT - using an unknown dynamic string variable - #substrings
DECLARE #tab TABLE (Col1 NVARCHAR(200))
INSERT INTO #tab (Col1)
VALUES (N'Servernamexyz.server.operationunit.otherstuff.icouldnt.predict.domain.domain2.domain3' )
DECLARE #substrings NVARCHAR(200) = 'icouldnt,stuff,banana'
SELECT
T.*, X.Strings
FROM #tab T
CROSS APPLY
( --dynamically split the string
SELECT Strings = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#substrings, ',', '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
) X
WHERE CHARINDEX(X.Strings, T.Col1) > 1

How to pass an array of integer values from a table to a stored procedure?

I have a stored proc using dynamic sql that updates a few columns based on the value passed to it. I am trying to test it out for multiple values without having to enter those manually. These values are to be taken from a table. Is there a way to pass all these values in the table and have it go through the proc? Just like in your regular programming language where you would run through an array. I am doing this in sql server 2012.
Code is something like this
CREATE PROCEDURE sp1 #enteredvalue int
AS
BEGIN
UPDATE table1
SET column1 = 'some var char value',
column2 = 'some integer values'
WHERE xid = #enteredvalue
END
I want to enter the values for that integer parameter (#enteredvalue) from a table that has different values.
Perhaps a little more dynamic SQL will do the trick (along with a parser)
Declare #String varchar(max) = '1,25,659'
Declare #SQL varchar(max) = ''
Select #SQL = #SQL + concat('Exec [dbo].[sp1] ',Key_Value,';',char(13))
From (Select * from [dbo].[udf-Str-Parse-8K](#String,',')) A
Select #SQL
--Exec(#SQL)
Returns
Exec [dbo].[sp1] 1;
Exec [dbo].[sp1] 25;
Exec [dbo].[sp1] 659;
The UDF if needed (super fast!)
CREATE FUNCTION [dbo].[udf-Str-Parse-8K](#String varchar(8000), #Delimiter varchar(50))
Returns Table
As
--Usage: Select * from [dbo].[udf-Str-Parse-8K]('Dog,Cat,House,Car',',')
-- Select * from [dbo].[udf-Str-Parse-8K]('John||Cappelletti||was||here','||')
-- Select * from [dbo].[udf-Str-Parse-8K]('The quick brown fox',' ')
Return (
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 a, cte1 b, cte1 c, cte1 d) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter)) = #Delimiter),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter,#String,s.N),0)-S.N,8000) From cte3 S)
Select Key_PS = Row_Number() over (Order By A.N)
,Key_Value = Substring(#String, A.N, A.L)
,Key_Pos = A.N
From cte4 A
)
Another approach is (without Dynamic SQL):
1) Create a new SP where input parameter is a table
https://msdn.microsoft.com/en-us/library/bb510489.aspx
2) In that procedure, create a WHILE loop to go through each row and execute your existing SP for each individual row value
Example of WHILE loop is here:
SQL Call Stored Procedure for each Row without using a cursor
To pass a table into an SP, consider creating a User-Defined Table type. Example:
create type ArrayOfInt as table (IntVal int)
go
create proc SumArray(#IntArray ArrayOfInt readonly)
as
select sum(IntVal) from #IntArray
go
declare #IntArray ArrayOfInt
insert #IntArray values (1), (2), (3)
select * from #IntArray
exec SumArray #IntArray
drop proc SumArray
drop type ArrayOfInt

SQL Server: UPDATE a table by using ORDER BY

I would like to know if there is a way to use an order by clause when updating a table. I am updating a table and setting a consecutive number, that's why the order of the update is important. Using the following sql statement, I was able to solve it without using a cursor:
DECLARE #Number INT = 0
UPDATE Test
SET #Number = Number = #Number +1
now what I'd like to to do is an order by clause like so:
DECLARE #Number INT = 0
UPDATE Test
SET #Number = Number = #Number +1
ORDER BY Test.Id DESC
I've read: How to update and order by using ms sql The solutions to this question do not solve the ordering problem - they just filter the items on which the update is applied.
Take care,
Martin
No.
Not a documented 100% supported way. There is an approach sometimes used for calculating running totals called "quirky update" that suggests that it might update in order of clustered index if certain conditions are met but as far as I know this relies completely on empirical observation rather than any guarantee.
But what version of SQL Server are you on? If SQL2005+ you might be able to do something with row_number and a CTE (You can update the CTE)
With cte As
(
SELECT id,Number,
ROW_NUMBER() OVER (ORDER BY id DESC) AS RN
FROM Test
)
UPDATE cte SET Number=RN
You can not use ORDER BY as part of the UPDATE statement (you can use in sub-selects that are part of the update).
UPDATE Test
SET Number = rowNumber
FROM Test
INNER JOIN
(SELECT ID, row_number() OVER (ORDER BY ID DESC) as rowNumber
FROM Test) drRowNumbers ON drRowNumbers.ID = Test.ID
Edit
Following solution could have problems with clustered indexes involved as mentioned here. Thanks to Martin for pointing this out.
The answer is kept to educate those (like me) who don't know all side-effects or ins and outs of SQL Server.
Expanding on the answer gaven by Quassnoi in your link, following works
DECLARE #Test TABLE (Number INTEGER, AText VARCHAR(2), ID INTEGER)
DECLARE #Number INT
INSERT INTO #Test VALUES (1, 'A', 1)
INSERT INTO #Test VALUES (2, 'B', 2)
INSERT INTO #Test VALUES (1, 'E', 5)
INSERT INTO #Test VALUES (3, 'C', 3)
INSERT INTO #Test VALUES (2, 'D', 4)
SET #Number = 0
;WITH q AS (
SELECT TOP 1000000 *
FROM #Test
ORDER BY
ID
)
UPDATE q
SET #Number = Number = #Number + 1
The row_number() function would be the best approach to this problem.
UPDATE T
SET T.Number = R.rowNum
FROM Test T
JOIN (
SELECT T2.id,row_number() over (order by T2.Id desc) rowNum from Test T2
) R on T.id=R.id
update based on Ordering by the order of values in a SQL IN() clause
Solution:
DECLARE #counter int
SET #counter = 0
;WITH q AS
(
select * from Products WHERE ID in (SELECT TOP (10) ID FROM Products WHERE ID IN( 3,2,1)
ORDER BY ID DESC)
)
update q set Display= #counter, #counter = #counter + 1
This updates based on descending 3,2,1
Hope helps someone.
I had a similar problem and solved it using ROW_NUMBER() in combination with the OVER keyword. The task was to retrospectively populate a new TicketNo (integer) field in a simple table based on the original CreatedDate, and grouped by ModuleId - so that ticket numbers started at 1 within each Module group and incremented by date. The table already had a TicketID primary key (a GUID).
Here's the SQL:
UPDATE Tickets SET TicketNo=T2.RowNo
FROM Tickets
INNER JOIN
(select TicketID, TicketNo,
ROW_NUMBER() OVER (PARTITION BY ModuleId ORDER BY DateCreated) AS RowNo from Tickets)
AS T2 ON T2.TicketID = Tickets.TicketID
Worked a treat!
I ran into the same problem and was able to resolve it in very powerful way that allows unlimited sorting possibilities.
I created a View using (saving) 2 sort orders (*explanation on how to do so below).
After that I simply applied the update queries to the View created and it worked great.
Here are the 2 queries I used on the view:
1st Query:
Update MyView
Set SortID=0
2nd Query:
DECLARE #sortID int
SET #sortID = 0
UPDATE MyView
SET #sortID = sortID = #sortID + 1
*To be able to save the sorting on the View I put TOP into the SELECT statement. This very useful workaround allows the View results to be returned sorted as set when the View was created when the View is opened. In my case it looked like:
(NOTE: Using this workaround will place an big load on the server if using a large table and it is therefore recommended to include as few fields as possible in the view if working with large tables)
SELECT TOP (600000)
dbo.Items.ID, dbo.Items.Code, dbo.Items.SortID, dbo.Supplier.Date,
dbo.Supplier.Code AS Expr1
FROM dbo.Items INNER JOIN
dbo.Supplier ON dbo.Items.SupplierCode = dbo.Supplier.Code
ORDER BY dbo.Supplier.Date, dbo.Items.ID DESC
Running: SQL Server 2005 on a Windows Server 2003
Additional Keywords: How to Update a SQL column with Ascending or Descending Numbers - Numeric Values / how to set order in SQL update statement / how to save order by in sql view / increment sql update / auto autoincrement sql update / create sql field with ascending numbers
SET #pos := 0;
UPDATE TABLE_NAME SET Roll_No = ( SELECT #pos := #pos + 1 ) ORDER BY First_Name ASC;
In the above example query simply update the student Roll_No column depending on the student Frist_Name column. From 1 to No_of_records in the table. I hope it's clear now.
IF OBJECT_ID('tempdb..#TAB') IS NOT NULL
BEGIN
DROP TABLE #TAB
END
CREATE TABLE #TAB(CH1 INT,CH2 INT,CH3 INT)
DECLARE #CH2 INT = NULL , #CH3 INT=NULL,#SPID INT=NULL,#SQL NVARCHAR(4000)='', #ParmDefinition NVARCHAR(50)= '',
#RET_MESSAGE AS VARCHAR(8000)='',#RET_ERROR INT=0
SET #ParmDefinition='#SPID INT,#CH2 INT OUTPUT,#CH3 INT OUTPUT'
SET #SQL='UPDATE T
SET CH1=#SPID,#CH2= T.CH2,#CH3= T.CH3
FROM #TAB T WITH(ROWLOCK)
INNER JOIN (
SELECT TOP(1) CH1,CH2,CH3
FROM
#TAB WITH(NOLOCK)
WHERE CH1 IS NULL
ORDER BY CH2 DESC) V ON T.CH2= V.CH2 AND T.CH3= V.CH3'
INSERT INTO #TAB
(CH2 ,CH3 )
SELECT 1,2 UNION ALL
SELECT 2,3 UNION ALL
SELECT 3,4
BEGIN TRY
WHILE EXISTS(SELECT TOP 1 1 FROM #TAB WHERE CH1 IS NULL)
BEGIN
EXECUTE #RET_ERROR = sp_executesql #SQL, #ParmDefinition,#SPID =##SPID, #CH2=#CH2 OUTPUT,#CH3=#CH3 OUTPUT;
SELECT * FROM #TAB
SELECT #CH2,#CH3
END
END TRY
BEGIN CATCH
SET #RET_ERROR=ERROR_NUMBER()
SET #RET_MESSAGE = '#ERROR_NUMBER : ' + CAST(ERROR_NUMBER() AS VARCHAR(255)) + '#ERROR_SEVERITY :' + CAST( ERROR_SEVERITY() AS VARCHAR(255))
+ '#ERROR_STATE :' + CAST(ERROR_STATE() AS VARCHAR(255)) + '#ERROR_LINE :' + CAST( ERROR_LINE() AS VARCHAR(255))
+ '#ERROR_MESSAGE :' + ERROR_MESSAGE() ;
SELECT #RET_ERROR,#RET_MESSAGE;
END CATCH

Resources